您现在的位置是:亿华云 > 系统运维

浅析Vue响应系统原理与搭建Vue2.x迷你版

亿华云2025-10-02 14:51:11【系统运维】3人已围观

简介Vue2.x响应式原理怎么实现的?Vue 最独特的特性之一,是其非侵入性的响应式系统。那么什么是响应式原理?数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐

Vue2.x响应式原理怎么实现的浅析?

Vue 最独特的特性之一,是应系其非侵入性的响应式系统。那么什么是统原响应式原理?

数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,理搭视图会进行更新,浅析避免了繁琐的应系DOM操作,提高开发效率。统原简言之,理搭在改变数据的浅析时候,视图会跟着更新。应系

了解概念之后,统原那么它是理搭怎么实现的呢?

其实是利用Object.defineProperty()中的getter 和setter方法和设计模式中的观察者模式。

那么,浅析我们先来看下Object.defineProperty()。应系MDN中它是统原这样解释它的:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。服务器租用

let data = {   msg:hello }; let vm = { }; Object.defineProperty(vm, msg, {          enumerable: true, // 可枚举(可遍历)         configurable: true, // 可配置(可以使用delete 删除,可以通过defineProperty重新定义)         // 当获取值的时候执行         get() {              console.log(get, data.msg);             return data.msg         },         // 当设置值的时候执行         set(newVal) {              if (newVal === data.msg) {                  return             }             data.msg = newVal;             console.log(set, data.msg);         } }) // 测试 console.log(vm.msg); /*  > "get" "hello" > "hello" */ vm.msg = world; // > "set" "world" 

简单介绍Object.defineProperty()之后,接着就是了解观察者模式,看到它,你可能会想起发布-订阅模式。其实它们的本质是相同的,但是也存在一定的区别。

我们不妨先来看下发布-订阅模式。

发布-订阅者模式里面包含了三个模块,发布者,订阅者和统一调度中心。这里统一调度中心相当于报刊办事大厅。发布者相当与某个杂志负责人,他来中心这注册一个的杂志,而订阅者相当于用户,我在中心订阅了这分杂志。每当发布者发布了一期杂志,办事大厅就会通知订阅者来拿新杂志。发布-订阅者模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

下面,我们将通过一个实现Vue自定义事件的例子来更进一步了解发布-订阅模式。云服务器提供商

function EventEmitter(){      // 初始化统一调度中心     this.subs = Object.create(null); // { click:[fn1,fn2]} } // 注册事件 EventEmitter.prototype.$on = function (eventType,handler){          console.log(this);         this.subs[eventType]= this.subs[eventType]||[];         this.subs[eventType].push(handler); } // 触发事件 EventEmitter.prototype.$emit = function (eventType,data){          if(this.subs[eventType]){                  this.subs[eventType].forEach(handler => {                      handler(data);                 });         } } // 测试 const em = new EventEmitter(); //订阅者 em.$on(click1,(data)=>{      console.log(data); }) // 发布者 em.$emit(click1,maomin) //maomin 

这种自定义事件广泛应用于Vue同级组件传值。

接下来,我们来介绍观察者模式。

观察者模式是由目标调度,比如当事件触发时,目标就会调用观察者的方法,所以观察者模式的订阅者(观察者)与发布者(目标)之间存在依赖。

// 发布者(目标) function Dep(){      this.subs = []; } Dep.prototype.addSub = function (sub){      if(sub&&sub.update){              this.subs.push(sub);     } } Dep.prototype.notify = function (data){          this.subs.forEach(sub=>{              sub.update(data);         }) } // 订阅者(观察者) function Watcher(){ }     Watcher.prototype.update=function(data){      console.log(data); } // 测试 let dep = new Dep(); let watcher = new Watcher(); // 收集依赖 dep.addSub(watcher); // 发送通知 dep.notify(1); dep.notify(2); 

下图是区分两种模式。

实现Vue2.x迷你版本

为什么要实现一个Vue迷你版本,目的就是加深对Vue响应式原理以及其中一些API的理解。首先我们先来分析Vue2.x 响应式原理的整体结构。

如下图所示:

我们接下来,将根据这幅图片描述的流程来实现一款迷你版Vue。Vue2.x采用了Virtual DOM,但是因为这里只需要实现一个迷你版,所以我们这里做了简化,我们这里就是直接操作DOM。

下面,我们来看下我是如何搭建一款Vue mini的。

第一步

页面结构如下,香港云服务器我们可以先引入Vue2.x完整版本,看下实现效果。

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>Vue2.x Reactive</title> </head> <body>     <div id="app">         <h2>文本节点</h2>         <div>{ { msg}}</div>         <div>{ { count}}</div>         <div>{ { obj.name}}</div>         <div>{ { arr[0]}}</div>         <div>{ { obj.inner.age}}</div>         <div>{ { obj.inner.name}}</div>         <h2>v-text</h2>         <div v-text="msg"></div>         <h2>v-model</h2>         <input type="text" v-model="msg">         <input type="text" v-model="count">         <h2>v-html</h2>         <div v-html="html"></div>         <h2>v-show</h2>         <div v-show="isShow">{ { isShow}}</div>         <h2>v-on</h2>         <button v-on:click="handler">handler</button>         <button @click="onClick">onClick</button>         <h2>v-if</h2>         <div>             <p v-if="isIf">{ { isIf}}</p>         </div>     </div>     <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>     <script>         const vm = new Vue({                  el: #app,                 data() {                      return {                          msg: maomin,                         count: 1,                         obj: {                              name: hello,                             inner: {                                  age: 17                             }                         },                         arr: [string1],                         html: <div>{ { msg}}</div>,                         isShow: false,                         isIf:true                     }                 },                 methods: {                      handler() {                          // this.count = 2;                         this.isIf = !this.isIf;                     },                     onClick() {                          this.obj.inner.age = 18;                         // console.log(this.obj.inner.age);                     }                 }             });     </script> </body> </html> 

 经过测试,Vue2.x完整版搭载的页面显示如下。我们将使用Vue迷你版本同样实现以下页面效果。

第二步

我们将根据整体结构图和页面结构来搭建这个Vue迷你版本,我们姑且将这个版本叫做vuemini.js。

通过整体结构图我们发现,一共有Vue、Observer、Compiler、Dep、Watcher这几个构造函数。我们首先创建这几个构造函数,这里不使用class类来定义是因为Vue源码大部分也使用构造函数,另外,相对也好拓展。

Vue

// 实例。 function Vue(options) {      this.$options = options || { };     this._data = typeof options.data === function ? options.data() : options.data || { };     this.$el = typeof options.el === string ? document.querySelector(options.el) : options.el;     // 负责把data中的属性注入到Vue实例,转换成getter/setter     this._proxyData(this._data);     this.initMethods(this, options.methods || { })     // 负责调用observer监听data中所有属性的变化     new Observer(this._data);     // 负责调用compiler解析指令/插值表达式     new Compiler(this); } // 将data中的属性挂载到this上 Vue.prototype._proxyData = function (data) {      Object.keys(data).forEach(key => {          Object.defineProperty(this, key, {              configurable: true,             enumerable: true,             get() {                  return data[key]             },             set(newVal) {                  if (newVal === data[key]) {                      return                 }                 data[key] = newVal;             }         })     }) } function noop(a, b, c) {  } function polyfillBind(fn, ctx) {      function boundFn(a) {          var l = arguments.length;         return l             ? l > 1                 ? fn.apply(ctx, arguments)                 : fn.call(ctx, a)             : fn.call(ctx)     }     boundFn._length = fn.length;     return boundFn } function nativeBind(fn, ctx) {      return fn.bind(ctx) } const bind = Function.prototype.bind     ? nativeBind     : polyfillBind; // 初始化methods属性 Vue.prototype.initMethods = function (vm, methods) {      for (var key in methods) {          {              if (typeof methods[key] !== function) {                  warn(                     "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +                     "Did you reference the function correctly?",                     vm                 );             }         }         vm[key] = typeof methods[key] !== function ? noop : bind(methods[key], vm);     } } 

Observer

// 数据劫持。 // 负责把data(_data)选项中的属性转换成响应式数据。 function Observer(data) {      this.walk(data); } Observer.prototype.walk = function (data) {      if (!data || typeof data !== object) {          return     }     Object.keys(data).forEach(key => {          this.defineReactive(data, key, data[key]);     }) } Observer.prototype.defineReactive = function (obj, key, val) {      let that = this;     // 负责收集依赖     let dep = new Dep();     // 如果val是对象,把val内部的属性转换成响应式数据     this.walk(val);     Object.defineProperty(obj, key, {          enumerable: true,         configurable: true,         get() {              // 收集依赖             Dep.target && dep.addSub(Dep.target)             return val         },         set(newVal) {              if (newVal === val) {                  return             }             val = newVal;             // data内属性重新赋值后,使其转化为响应式数据。             that.walk(newVal);             // 发送通知             dep.notify();         }     }) } 

Compiler

// 编译模板,解析指令/插值表达式 // 负责页面的首次渲染 // 当数据变化时重新渲染视图 function Compiler(vm) {      this.el = vm.$el;     this.vm = vm;     // 立即编译模板     this.compile(this.el); } // 编译模板,处理文本节点和元素节点 Compiler.prototype.compile = function (el) {      let childNodes = el.childNodes;     Array.from(childNodes).forEach(node => {          // 处理文本节点         if (this.isTextNode(node)) {              this.compileText(node);         }         // 处理元素节点          else if (this.isElementNode(node)) {              this.compileElement(node);         }         // 判断node节点,是否有子节点,如果有子节点,要递归调用compile方法         if (node.childNodes && node.childNodes.length) {              this.compile(node);         }     }) } // 编译文本节点,处理插值表达式 Compiler.prototype.compileText = function (node) {      // console.dir(node);     let reg = /\{ \{ (.+?)\}\}/;     let value = node.textContent;     if (reg.test(value)) {          let key = RegExp.$1.trim();         if (this.vm.hasOwnProperty(key)) {              node.textContent = value.replace(reg, typeof this.vm[key] === object ? JSON.stringify(this.vm[key]) : this.vm[key]);             // 创建watcher对象,当数据改变更新视图             new Watcher(this.vm, key, (newVal) => {                  node.textContent = newVal;             })         } else {              const str = `this.vm.${ key}`;             node.textContent = value.replace(reg, eval(str));             // 创建watcher对象,当数据改变更新视图             new Watcher(this.vm, key, () => {                  const strw = `this.vm.${ key}`;                 node.textContent = value.replace(reg, eval(strw));             })         }     } } // 判断节点是否是文本节点 Compiler.prototype.isTextNode = function (node) {      return node.nodeType === 3; } // 判断节点是否是元素节点 Compiler.prototype.isElementNode = function (node) {      return node.nodeType === 1; } // 编译元素节点,处理指令 Compiler.prototype.compileElement = function (node) {      // console.log(node.attributes);     // 遍历所有的属性节点     Array.from(node.attributes).forEach(attr => {          let attrName = attr.name;         // console.log(attrName);         // 判断是否是指令         if (this.isDirective(attrName)) {              // 判断:如v-on:click             let eventName;             if (attrName.indexOf(:) !== -1) {                  const strArr = attrName.substr(2).split(:);                 attrName = strArr[0];                 eventName = strArr[1];             } else if (attrName.indexOf(@) !== -1) {                  eventName = attrName.substr(1);                 attrName = on;             } else {                  attrName = attrName.substr(2);             }             let key = attr.value;             this.update(node, key, attrName, eventName);         }     }) } // 判断元素属性是否是指令 Compiler.prototype.isDirective = function (attrName) {      return attrName.startsWith(v-) || attrName.startsWith(@); } // 指令辅助函数 Compiler.prototype.update = function (node, key, attrName, eventName) {      let updateFn = this[attrName + Updater];     updateFn && updateFn.call(this, node, this.vm[key], key, eventName); } // 处理v-text指令 Compiler.prototype.textUpdater = function (node, value, key) {      node.textContent = value;     new Watcher(this.vm, key, (newVal) => {          node.textContent = newVal;     }) } // 处理v-html指令 Compiler.prototype.htmlUpdater = function (node, value, key) {      node.insertAdjacentHTML(beforeend, value);     new Watcher(this.vm, key, (newVal) => {          node.insertAdjacentHTML(beforeend, newVal);     }) } // 处理v-show指令 Compiler.prototype.showUpdater = function (node, value, key) {      !value ? node.style.display = none : node.style.display = block     new Watcher(this.vm, key, (newVal) => {          !newVal ? node.style.display = none : node.style.display = block;     }) } // 处理v-if指令 Compiler.prototype.ifUpdater = function (node, value, key) {      const nodew = node;     const nodep = node.parentNode;     if (!value) {          node.parentNode.removeChild(node)     }     new Watcher(this.vm, key, (newVal) => {          console.log(newVal);         !newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);     }) } // 处理v-on指令 Compiler.prototype.onUpdater = function (node, value, key, eventName) {      if (eventName) {          const handler = this.vm.$options.methods[key].bind(this.vm);         node.addEventListener(eventName, handler);     } } // 处理v-model指令 Compiler.prototype.modelUpdater = function (node, value, key) {      node.value = value;     new Watcher(this.vm, key, (newVal) => {          node.value = newVal;     })     // 双向绑定,视图变化更新数据     node.addEventListener(input, () => {          this.vm[key] = node.value;     }) } 

Dep

// 发布者。 // 收集依赖,添加所有的观察者(watcher)。通知所有的观察者。 function Dep() {      // 存储所有的观察者watcher     this.subs = []; } // 添加观察者 Dep.prototype.addSub = function (sub) {      if (sub && sub.update) {          this.subs.push(sub);     } } // 发送通知 Dep.prototype.notify = function () {      this.subs.forEach(sub => {          sub.update();     }) } 

Watcher

function Watcher(vm, key, cb) {      this.vm = vm;     this.key = key;     this.cb = cb;     // 把当前watcher对象记录到Dep类的静态属性target     Dep.target = this;     if (vm.hasOwnProperty(key)) {          this.oldVal = vm[key];     } else {          const str = `vm.${ key}`;         this.oldVal = eval(str);     }     Dep.target = null; } // 当数据发生变化的时候更新视图 Watcher.prototype.update = function () {      let newVal;     if (this.vm.hasOwnProperty(this.key)) {          newVal = this.vm[this.key];     } else {          const str = `this.vm.${ this.key}`;         newVal = eval(str);     }     this.cb(newVal); } 

以上这几个构造函数就实现了我们所说的迷你版本,将它们整合成一个文件vuemini.js。在上面所提示的页面引入,查看效果。

另外,我在data中绑定了一个html属性,值为一个<div>{ { msg}}</div>,与之前完整版相比,图中的v-html下方的maomin文本也被渲染出来。

尤大开发的Vue2.x迷你版本

下面,我们将看下尤大开发的迷你版本,这个版本引入了Virtual DOM,但是主要是针对响应式式原理的,可以根据尤大的迷你版本与上面的版本作个比较,可以看下有哪些相似之处。

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>vue2mini</title> </head> <body>     <div id="app"></div>     <script>         // reactivity ---         let activeEffect         class Dep {              subscribers = new Set()             depend() {                  if (activeEffect) {                      this.subscribers.add(activeEffect)                 }             }             notify() {                  this.subscribers.forEach(effect => effect())             }         }         function watchEffect(effect) {              activeEffect = effect             effect()             activeEffect = null         }         function reactive(raw) {              // use Object.defineProperty             // 1. iterate over the existing keys             Object.keys(raw).forEach(key => {                  // 2. for each key: create a corresponding dep                 const dep = new Dep()                 // 3. rewrite the property into getter/setter                 let realValue = raw[key]                 Object.defineProperty(raw, key, {                      get() {                          // 4. call dep methods inside getter/setter                         dep.depend()                         return realValue                     },                     set(newValue) {                          realValue = newValue                         dep.notify()                     }                 })             })             return raw         }         // vdom ---         function h(tag, props, children) {              return {  tag, props, children };         }         function mount(vnode, container, anchor) {              const el = document.createElement(vnode.tag);             vnode.el = el;             // props             if (vnode.props) {                  for (const key in vnode.props) {                      if (key.startsWith(on)) {                          el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])                     } else {                          el.setAttribute(key, vnode.props[key]);                     }                 }             }             if (vnode.children) {                  if (typeof vnode.children === "string") {                      el.textContent = vnode.children;                 } else {                      vnode.children.forEach(child => {                          mount(child, el);                     });                 }             }             if (anchor) {                  container.insertBefore(el, anchor)             } else {                  container.appendChild(el);             }         }         function patch(n1, n2) {              // Implement this             // 1. check if n1 and n2 are of the same type             if (n1.tag !== n2.tag) {                  // 2. if not, replace                 const parent = n1.el.parentNode                 const anchor = n1.el.nextSibling                 parent.removeChild(n1.el)                 mount(n2, parent, anchor)                 return             }             const el = n2.el = n1.el             // 3. if yes             // 3.1 diff props             const oldProps = n1.props || { }             const newProps = n2.props || { }             for (const key in newProps) {                  const newValue = newProps[key]                 const oldValue = oldProps[key]                 if (newValue !== oldValue) {                      if (newValue != null) {                          el.setAttribute(key, newValue)                     } else {                          el.removeAttribute(key)                     }                 }             }             for (const key in oldProps) {                  if (!(key in newProps)) {                      el.removeAttribute(key)                 }             }             // 3.2 diff children             const oc = n1.children             const nc = n2.children             if (typeof nc === string) {                  if (nc !== oc) {                      el.textContent = nc                 }             } else if (Array.isArray(nc)) {                  if (Array.isArray(oc)) {                      // array diff                     const commonLength = Math.min(oc.length, nc.length)                     for (let i = 0; i < commonLength; i++) {                          patch(oc[i], nc[i])                     }                     if (nc.length > oc.length) {                          nc.slice(oc.length).forEach(c => mount(c, el))                     } else if (oc.length > nc.length) {                          oc.slice(nc.length).forEach(c => {                              el.removeChild(c.el)                         })                     }                 } else {                      el.innerHTML =                      nc.forEach(c => mount(c, el))                 }             }         }         // paste all previous code from Codepen         const app = {              data: reactive({                  count: 0             }),             render() {                  return h(div, {                      onClick: () => {                          app.data.count++                     }                 }, String(app.data.count))             }         }         function mountApp(component, selector) {              let isMounted = false             let oldTree             watchEffect(() => {                  if (!isMounted) {                      mount(oldTree = component.render(), document.querySelector(selector))                     isMounted = true                 } else {                      const newTree = component.render()                     patch(oldTree, newTree)                     oldTree = newTree                 }             })         }         mountApp(app, #app)     </script> </body> </html> 

 【编辑推荐】

中国程序员开发的远程桌面火了!Mac可用,仅9MB,支持自建中继器 3分钟带你彻底搞懂 Kafka 抖音服务器带宽有多大,才能供上亿人同时刷? 火爆Github!这个号称后现代编辑能超越Vim么? 2021年最危险的七大攻击技术

很赞哦!(926)