Vue响应式原理
1、课程目标
模拟一个最小版本的Vue
响应式原理在面试的常问问题
实际项目中出现问题的原理层面的解决
     给Vue实例新增一个成员是否是响应式的?
    给属性重新赋值成对象,是否是响应式的?
为学习Vue源码做铺垫。
2、数据驱动
在实现整个Vue响应式代码之前,我们先来了解几个概念。
第一个:数据驱动
第二个:响应式的核心原理
第三个:发布订阅模式和观察这模式
我们先来看一下数据驱动的内容:
数据响应式,双向绑定,数据驱动(我们经常看到这几个词)
数据响应式:数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了频繁的DOM操作,提高开发效率,这与Jquery不一样,Jquery是频繁的操作Dom
双向绑定:
   数据改变,视图改变,视图改变,数据也随之改变( 通过这句话,我们可以看到在双向绑定中是包含了数据响应式的内容)
    我们可以使用v-model 在表单元素上创建双向数据绑定
数据驱动是Vue最独特的特性之一
     开发过程中仅仅需要关注数据本身,不需要关心数据是如何渲染到视图中的。主流的MVVM框架都已经实现了数据响应式与双向绑定,所以可以将数据绑定到DOM上。
3、响应式的核心原理
3.1 Vue2.x响应式原理
关于Vue2.x的响应式原理在官方文档中也有介绍。
https://cn.vuejs.org/v2/guide/reactivity.html
在该文档中,我们注意如下一段内容:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
   | 
 
通过以上的文字,我们可以看到,在Vue2.x中响应式的实现是通过Object.defineProperty来完成的,注意该属性无法降级(shim)处理,所以Vue不支持IE8以及更低版本的浏览器的原因。
下面我们来看一下Object.defineProperty基本使用
修改data对象中的msg属性的值,实现视图的更新.(这也就是我们所说的响应式)
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>defineProperty</title>   </head>   <body>     <div id="app">hello</div>     <script>              let data = {         msg: "hello",       };              let vm = {};              Object.defineProperty(vm, "msg", {                  enumerable: true,                  configurable: true,                  get() {           console.log("get:", data.msg);           return data.msg;         },                  set(newValue) {           console.log("set:", newValue);                      if (newValue === data.msg) {             return;           }           data.msg = newValue;                      document.querySelector("#app").textContent = data.msg;         },       });                     vm.msg = "abc";              console.log(vm.msg);     </script>   </body> </html>
 
   | 
 
在进行测试的时候,可以在浏览器的控制台中,输入vm.msg进行测试。
在上面的代码中,我们是将一个对象中的属性转换成了getter/setter的形式,那么这里我们还有一个问题:
如果有一个对象中多个属性需要转换getter/setter,那么应该如何处理?
我们可以通过循环遍历的方式,将对象中的多个属性转换成getter/setter
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>defineProperty多个属性</title>   </head>   <body>     <div id="app">hello</div>     <script>              let data = {         msg: "hello",         count: 10,       };              let vm = {};       proxyData(data);       function proxyData(data) {                  Object.keys(data).forEach((key) => {                      Object.defineProperty(vm, key, {             enumerable: true,             configurable: true,             get() {               console.log("get", key, data[key]);               return data[key];             },             set(newValue) {               console.log("set:", key, newValue);               if (newValue === data[key]) {                 return;               }               data[key] = newValue;               document.querySelector("#app").textContent = data[key];             },           });         });       }       vm.msg = "hello world";       console.log(vm.msg);     </script>   </body> </html>
   | 
 
在上面的代码中,我们通过循环的方式给data对象中的每个属性添加了getter/setter.
这里我们只是在视图中展示了msg属性的值,如果想展示count属性的值,可以在浏览器的控制台中,通过vm.count=20这种形式来展示,当然,在后期的课程中我们会分别展示出msg与count属性的值,
3.2 Vue3响应式原理
Vue3的响应式原理是通过Proxy来完成的。
Proxy直接监听对象,而非属性,所以将多个属性转换成getter/setter的时候,不需要使用循环。
Proxy是ES6课程中新增的,IE不支持
Proxy实现响应式的基本代码如下(该代码的功能与上面所讲解的是一样的):
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Proxy</title>   </head>   <body>     <div id="app">hello</div>     <script>              let data = {         msg: "hello",         count: 0,       };                     let vm = new Proxy(data, {                           get(target, key) {           console.log("get key:", key, target[key]);           return target[key];         },                  set(target, key, newValue) {           console.log("set key:", key, newValue);           if (target[key] === newValue) {             return;           }           target[key] = newValue;           document.querySelector("#app").textContent = target[key];         },       });              vm.msg = "aaaa";       console.log(vm.msg);     </script>   </body> </html>
 
   | 
 
通过以上的代码我们发现使用Proxy的代码是给对象中所有属性添加getter/setter,而不需要通过循环的方式来实现,所以代码更加的简洁。
4、发布订阅模式
发布订阅模式:订阅者,发布者,信号中心
我们假定,存在一个“信号中心”,某个任务执行完成,就向信号中心"发布"(publish)一个信号,其它任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)
   | 
 
家长向学生所在的班级订阅了获取学生考试成绩的事件,当老师公布学生的成绩后,就会自动通知学生的家长。
在整个案例中,学生所在的班级为信号中心,老师为发布者,家长为订阅者
Vue 的自定义事件就是基于发布订阅模式来实现的。
下面通过Vue中兄弟组件通信过程,来理解发布订阅模式
  let eventHub=new Vue()
  addTodo:function(){          eventHub.$emit('add-todo',{text:this.newTodoText})     this.newTodoText='' }
 
  created:function(){          eventHub.$on('add-todo',this.addTodo) }
 
  | 
 
通过以上代码,我们可以理解发布订阅模式中的核心概念。
下面我们模拟Vue中的自定义事件的实现
下面我们先来做一个基本的分析:
先来看如下代码:
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Vue 自定义事件</title>   </head>   <body>     <script src="./js/vue.js"></script>     <script>              let vm = new Vue();              vm.$on("dataChange", () => {         console.log("dataChange");       });       vm.$on("dataChange", () => {         console.log("dataChange");       });              vm.$emit("dataChange");     </script>   </body> </html>
   | 
 
通过上面的代码,我们可以看到$on实现事件的注册,而且可以注册多个事件,那么我们可以推测在其内部有一个对象来存储注册的事件,对象的格式为:
{'click':[fn1,fn2],'change':[fn]}
  | 
 
以上格式说明了,我们注册了两个事件,分别为click与change.
下面我们根据以上的分析过程,来模拟实现自定义事件。
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>发布订阅模式</title>   </head>   <body>     <script>       class EventEmitter {         constructor() {                                 this.subs = {};         }                                             $on(eventType, fn) {                      if (!this.subs[eventType]) {             this.subs[eventType] = [];           }           this.subs[eventType].push(fn);         }                  $emit(eventType) {           if (this.subs[eventType]) {             this.subs[eventType].forEach((handler) => {               handler();             });           }         }       }              let em = new EventEmitter();       em.$on("click", () => {         console.log("click1");       });       em.$on("click", () => {         console.log("click2");       });       em.$emit("click");     </script>   </body> </html>
 
   | 
 
5、观察者模式
Vue的响应式机制使用了观察者模式,所以我们首先要先了解一下观察者模式
观察者模式与发布订阅模式的区别是,观察者模式中没有事件中心,只有发布者与订阅者,并且发布者需要知道订阅者的存在。
观察者(订阅者)—Watcher
     update(): 当事件发生时,具体要做的事情。
目标(发布者)–Dep
    subs 数组:存储所有的观察者
    addSub() 添加观察者,将其保存到subs数组中
   notify(): 当事件发生后,调用所有观察者的update() 方法。
没事事件中心
具体代码实现如下:
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>观察者模式</title>   </head>   <body>     <script>              class Dep {         constructor() {                      this.subs = [];         }                  addSub(sub) {                      if (sub && sub.update) {             this.subs.push(sub);           }         }                  notify() {                      this.subs.forEach((sub) => {             sub.update();           });         }       }              class Watcher {                  update() {           console.log("update something");         }       }              let dep = new Dep();       let watcher = new Watcher();       dep.addSub(watcher);       dep.notify();     </script>   </body> </html>
 
   | 
 
下面我们看一下观察者模式与发布订阅模式的区别。
观察者模式:是由具体目标调度的,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
发布订阅模式:由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
6、模拟Vue响应式原理–Vue
当我们在使用Vue的时候,首先会根据Vue类来创建Vue的实例。
那么Vue类主要的功能如下:
- 负责接收初始化的参数(选项)
 
- 负责把
data中的属性注入到Vue实例,转换成getter/setter(可以通过this来访问data中的属性) 
- 负责调用
observer监听data中所有属性的变化(当属性值发生变化后更新视图) 
- 负责调用
compiler解析指令/差值表达式 
结构
Vue中包含了_proxyData这个私有方法,该方法的作用就是将data中的属性转换成getter/setter并且注入到Vue的实例中。
模拟Vue/js/vue.js
基本代码实现如下:
class Vue {   constructor(options) {               this.$options = options || {};          this.$data = options.data || {};     this.$el =       typeof options.el === "string"         ? document.querySelector(options.el)         : options.el;                   this._proxyData(this.$data);             }   _proxyData(data) {          Object.keys(data).forEach((key) => {                     Object.defineProperty(this, key, {         enumerable: true,         configurable: true,         get() {           return data[key];         },         set(newValue) {           if (newValue === data[key]) {             return;           }           data[key] = newValue;         },       });     });   } }
  | 
 
在Vue类中,我们主要实现四项内容:
1、通过属性保存选项的数据
2、把data中的成员转换成getter和setter,注入到vue实例中.
3、调用observer对象,监听数据的变化
4、调用compiler对象,解析指令和差值表达式
在上面的代码中,我们首先实现了前两项内容。
下面进行测试。
index.html的代码如下:
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>模拟Vue</title>   </head>   <body>     <div id="app">       <h1>差值表达式</h1>       <h3>{{msg}}</h3>       <h3>{{count}}</h3>       <h1>v-text</h1>       <div v-text="msg"></div>       <h1>v-model</h1>       <input type="text" v-model="msg" />       <input type="text" v-model="count" />     </div>     <script src="./js/vue.js"></script>     <script>       let vm = new Vue({         el: "#app",         data: {           msg: "Hello World",           count: 12,         },       });     </script>   </body> </html>
 
   | 
 
在模板中添加了差值表达式,v-text,v-model内容,同时导入了我们自己创建的vue,并且创建了Vue的实例。
在浏览器的控制台中查看对应效果

7、Observer
Observer的功能
-    负责把
data选项中的属性转换成响应式数据 
-   
data中的某个属性也是对象,把该属性转换成响应式数据(例如data中的某个属性为Student对象,也要将Student对象中的属性转换成响应式) 
-   数据变化发送通知
 
observer.js文件中的基本代码如下:
class Observer {   constructor(data) {     this.walk(data);   }   walk(data) {          if (!data || typeof data !== "object") {       return;     }          Object.keys(data).forEach((key) => {       this.defineReactive(data, key, data[key]);     });   }
    defineReactive(obj, key, val) {     Object.defineProperty(obj, key, {       enumerable: true,       configurable: true,       get() {         return val;       },       set(newVal) {         if (newVal === val) {           return;         }         val = newVal;                },     });   } }
 
  | 
 
下面对以上代码进行测试。
class Vue {   constructor(options) {               this.$options = options || {};          this.$data = options.data || {};          this.$el =       typeof options.el === "string"         ? document.querySelector(options.el)         : options.el;          this._proxyData(this.$data);          new Observer(this.$data);        }  }
  | 
 
在Vue类的构造方法中的第三部,创建Observer的实例,同时传递data数据。
在index.html文件中,导入observer.js文件
<script src="./js/observer.js"></script> <script src="./js/vue.js"></script>
   | 
 
注意:由于在vue.js文件中使用了Observer对象,所以这里先导入observer.js文件。
下面我们修改一下代码,看一下效果:
<script>      let vm = new Vue({        el: "#app",        data: {          msg: "Hello World",          count: 12,        },      });      console.log(vm.msg);    </script>
   | 
 
在index.html中,我们打印输出了vm中的msg的值,
这时候,会执行vue.js文件中的get方法,也会执行observer.js 文件中的get方法。
如果将observer.js文件中的get方法修改成如下形式
get() {       return obj[key];           },
  | 
 
会出现如下错误:

以上错误信息的含义为:堆栈溢出
为什么会出现以上错误呢?
因为obj就是data对象,而通过obj[key]的方式来获取值,还是会执行get方法,所以这里形成了死循环。
8、完善defineReactive方法
如果,我们在data中添加一个对象,那么对象中的属性是否为响应式的呢?

在浏览器的控制台中,输出的person对象是响应式的,但是其内部属性并不是响应式的,下面处理一下这块内容。
而Vue中的对象是响应式的,对象中的属性也是响应式的。
关于这个问题的解决,非常的简单。
在observer.js文件中的defineReactive方法中,调用一次walk方法就可以了。如下代码所示:
defineReactive(obj, key, val) {    this.walk(val);    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get() {                return val;      },      set(newVal) {        if (newVal === val) {          return;        }        val = newVal;              },    });  }
  | 
 
在上面的代码中,首先调用了this.walk(val)方法,同时传递了val这个参数。
这样在所调用walk方法的内部,会先判断传递过来的参数的类型,如果不是对象,就停止执行walk方法总的循环,而这时候会Object.defineProperty,但是
如果传递过来的参数就是一个对象,那么会进行循环遍历,取出每一个属性,为其添加getter/setter
下面,我们在看另外一个问题,现在对index.html中vue对象中data中的msg属性重新赋值,并且赋值为一个对象,那么新赋值的这个对象的成员是否为响应式的呢?下面我们来测试一下:
<script>       let vm = new Vue({         el: "#app",         data: {           msg: "Hello World",           count: 12,           person: {             name: "zs",           },         },       });       console.log(vm.$data.msg);   	  vm.msg={text:'abc'}     </script>
   | 
 
在浏览器的控制台中,打印vm,看一下对应的效果

通过上图,可以发现新赋值给msg属性的对象中的属性并不是响应式的,所以接下来,我们需要为其改造成响应式的。
当我们给msg属性赋值的时候,就会执行observer.js文件中的defineReactive方法中的set操作,在这里我们可以将传递过来的值再次调用walk方法,
这样又会对传递过来的值,进行判断是否为对象,然后进行遍历,同时为其属性添加getter/setter
defineReactive(obj, key, val) {           let that = this;     this.walk(val);     Object.defineProperty(obj, key, {       enumerable: true,       configurable: true,       get() {                  return val;       },       set(newVal) {                      if (newVal === val) {           return;         }         val = newVal;         that.walk(newVal);                },     });
  | 
 
通过上面的代码可以看到,在defineReactive方法中的set操作中,又调用了walk方法,但是要注意的就是,这里需要处理this指向的问题。
9、Compiler
功能
-    负责编译模板,解析指令/差值表达式
 
-   负责页面的首次渲染
 
-  当数据变化后重新渲染视图
 
通过以上功能的描述,可以总结出Compiler主要就是对Dom进行操作。
在js目录下面创建compiler.js文件,实现代码如下:
class Compiler {   constructor(vm) {     this.el = vm.$el;     this.vm = vm;   }      compile(el) {}      compileElement(node) {}      compileText(node) {}      isDirective(attrName) {          return attrName.startsWith("v-");   }      isElementNode(node) {          return node.nodeType === 1;   }      isTextNode(node) {     return node.nodeType === 3;   } }
 
  | 
 
9.1  compile方法实现
在调用compile方法的时候传递过来的参数el就是模板,也就是index.html中的<div id="app"></div>
中的内容。
所以我们在compile方法中要遍历模板中的所有节点。
   compile(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);       }              if (node.childNodes && node.childNodes.length) {         this.compile(node);       }     });   }
 
  | 
 
以上就是compile方法的基本实现.
9.2 compileText方法实现
compileText方法的作用就是对对插值表达式进行解析.
在编写compileText方法之前,我们先测试一下前面写的代码。
首先在compiler.js文件中的构造方法中,调用compile方法。
class Compiler {   constructor(vm) {     this.el = vm.$el;     this.vm = vm;            this.compile(this.el);   } }
  | 
 
在vue.js文件中创建Compiler类的实例,传递的是Vue的实例。
class Vue {   constructor(options) {               this.$options = options || {};          this.$data = options.data || {};          this.$el =       typeof options.el === "string"         ? document.querySelector(options.el)         : options.el;          this._proxyData(this.$data);          new Observer(this.$data);          new Compiler(this);   } }
  | 
 
在第四步中,创建了Comiler类的实例。
同时需要在index.html文件中引入comiler.js文件。
<script src="./js/compiler.js"></script>    <script src="./js/observer.js"></script>    <script src="./js/vue.js"></script>
   | 
 
注意导入的顺序。
在compiler.js文件中的comileText方法中可以先打印一下文本节点,看一下具体的文本节点。
  compileText(node) {    console.dir(node);  }
 
  | 
 
下面完善一下compileText方法的实现如下:
 compileText(node) {            let reg = /\{\{(.+)\}\}/;      let value = node.textContent;
       if (reg.test(value)) {          let key = RegExp.$1.trim();          node.textContent = value.replace(reg, this.vm[key]);   } }
 
  | 
 
这时刷新浏览器,就可以看到对应效果。
9.3  compileElement方法实现
 compileElement方法,就是完成指令的解析。
在这里我们重点解析的指令为v-text与v-model
<div v-text="msg"></div>   <input type="text" v-model="msg" />
   | 
 
这些指令本身就是html标签的属性。
   compileElement(node) {                         Array.from(node.attributes).forEach((attr) => {              let attrName = attr.name;              if (this.isDirective(attrName)) {                                             attrName = attrName.substr(2);                  let key = attr.value;         this.update(node, key, attrName);       }     });   }   update(node, key, attrName) {          let updateFn = this[attrName + "Updater"];     updateFn && updateFn(node, this.vm[key]);    }      textUpdater(node, value) {     node.textContent = value;   }      modelUpdater(node, value) {          node.value = value;   }
 
  | 
 
通过以上的代码,我们可以看到,如果想以后在处理其它的指令,只需要添加方法就可以了,方法的名字后缀一定要有Updater.
这比写很多的判断语句方便多了。
compiler.js文件完整代码
class Compiler {   constructor(vm) {     this.el = vm.$el;     this.vm = vm;     this.compile(this.el);   }      compile(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);       }              if (node.childNodes && node.childNodes.length) {         this.compile(node);       }     });   }      compileElement(node) {                         Array.from(node.attributes).forEach((attr) => {              let attrName = attr.name;              if (this.isDirective(attrName)) {                                             attrName = attrName.substr(2);                  let key = attr.value;         this.update(node, key, attrName);       }     });   }   update(node, key, attrName) {          let updateFn = this[attrName + "Updater"];     updateFn && updateFn(node, this.vm[key]);    }      textUpdater(node, value) {     node.textContent = value;   }      modelUpdater(node, value) {          node.value = value;   }      compileText(node) {                    let reg = /\{\{(.+)\}\}/;          let value = node.textContent;
           if (reg.test(value)) {              let key = RegExp.$1.trim();              node.textContent = value.replace(reg, this.vm[key]);     }   }      isDirective(attrName) {          return attrName.startsWith("v-");   }      isElementNode(node) {          return node.nodeType === 1;   }      isTextNode(node) {     return node.nodeType === 3;   } }
 
  | 
 
当页面首次渲染的时候,把数据更新到视图的功能,我们已经完成了,但是还没有实现对应的响应式,也就是数据更改后,视图也要进行更新。
下面我们就来实现对应的响应式机制。
10、Dep类
下面我们先来实现Dep这个类。
该类的功能:
     收集依赖,添加观察者(`watcher`)
 
     通知所有观察值
什么时候收集依赖呢?
也就是在getter中收集依赖,添加观察者
什么时候通知观察者呢?
在setter中通知依赖,通知观察者
在dep.js文件中编写如下代码:
class Dep {   constructor() {          this.subs = [];   }      addSub(sub) {          if (sub && sub.update) {       this.subs.push(sub);     }   }      notify() {     this.subs.forEach((sub) => {       sub.update();     });   } }
  | 
 
修改Observer类中的代码
defineReactive(obj, key, val) {          let that = this;          let dep = new Dep();     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;         that.walk(newVal);                  dep.notify();       },     });   }
  | 
 
首先针对每一个响应式数据添加了一个Dep对象(发布者),然后在set方法中,当数据发生了变化后,会调用dep中的notify方法,完成更新视图的操作。
在set方法中添加依赖,也就是将watcher观察者添加到了Dep中的subs数组中。
以上代码无法进行测试,完成Watcher类可以进行测试
11、Watcher类
11.1 Watcher类创建
在编写Watcher类之前,我们先来看一张图,理解一下Dep与Watcher的关系
通过前面的学习,我们知道在Observer类中为每一个响应式的数据创建了Dep对象,而且在getter 中会收集依赖,所谓收集依赖就是将watcher观察者添加到subs数组中.
而在setter中会触发依赖,其实就是调用Dep对象中notify方法,该方法会获取subs数组中的所有的watcher,然后执行watcher中的update方法来更新对应的视图。
Watcher 类的代码如下:
class Watcher {   constructor(vm, key, cb) {     this.vm = vm;          this.key = key;          this.cb = cb;          this.oldValue = vm[key];   }      update() {          let newValue = this.vm[this.key];     if (newValue === this.oldValue) {       return;     }          this.cb(newValue);   } }
  | 
 
接下来还有一件事情需要处理一下:
当创建了·Watcher对象后,需要将当前创建的Watcher对象添加到Dep中的subs数组中。
我们可以查看Observer类,在get方法中已经写过将Watcher对象添加到Dep中的subs数组中了( Dep.target && dep.addSub(Dep.target);),但是
问题是,我们并没有创建target属性,所以下面我们创建一下target属性。
下面在Watcher类的构造方法中,添加给Dep添加target属性,用来保存Watcher的实例。
class Watcher {   constructor(vm, key, cb) {     this.vm = vm;          this.key = key;          this.cb = cb;          Dep.target = this;               this.oldValue = vm[key];     Dep.target = null;    } }
  | 
 
以上内容需要重点去体会.
11.2 创建Watcher对象
下面来看一下关于Watcher对象的创建。
   compileText(node) {                    let reg = /\{\{(.+)\}\}/;          let value = node.textContent;
           if (reg.test(value)) {              let key = RegExp.$1.trim();              node.textContent = value.replace(reg, this.vm[key]);              new Watcher(this.vm, key, (newValue) => {                    node.textContent = newValue;       });     }   }
 
  | 
 
下面要在index.html文件中导入相关的js文件。
<script src="./js/dep.js"></script>     <script src="./js/watcher.js"></script>     <script src="./js/compiler.js"></script>     <script src="./js/observer.js"></script>     <script src="./js/vue.js"></script>
   | 
 
注意:以上导入文件的顺序,由于在watcher.js文件中使用了dep.js文件中的内容,所以先导入dep,同样在compiler.js文件中使用了watcher.js文件中内容,所以先导入了watcher.js.
下面可以进行测试了。
先将index.html文件中的,如下语句注释掉:
vm.msg = { text: "abc" };
  | 
 
然后,打开浏览器的控制台,输入如下内容
对应的页面视图中的内容也发生了变化。这也就实现了响应式机制,所谓响应式就是当数据变化了,对应的视图也会进行更新。
所以需要在textUpdater和modelUpdater方法中完成Watcher对象的创建。
  textUpdater(node, value, key) {    node.textContent = value;    new Watcher(this.vm, key, (newValue) => {      node.textContent = newValue;    });  }    modelUpdater(node, value,key) {        node.value = value;    new Watcher(this.vm, key, (newValue) => {      node.value = newValue;    });  }
 
  | 
 
update(node, key, attrName) {      let updateFn = this[attrName + "Updater"];   updateFn && updateFn.call(this, node, this.vm[key], key);  }
  | 
 
12、双向数据绑定
这一小节,我们看一下Vue的双向数据绑定。
双向数据绑定包含两部分内容,数据变化更新视图,视图变化更新数据。
怎样实现双向绑定呢?
基本的思路就是,我们可以给文本框(第一个文本框)添加一个input事件,在输入完数据后触发该事件,同时将用户在文本框中输入的数据赋值给data中的属性(视图变化,更新数据,而当数据变化后,会执行行observer.js中的set方法,更新视图,也就是触发了响应式的机制)。
那么我们应该在哪实现数据的双向绑定呢?
我们知道,这里是对文本框的操作,所以需要compiler.js文件中的modelUpdater方法中,实现双向绑定。因为modelUpdater方法就是处理v-model.
   modelUpdater(node, value, key) {          node.value = value;     new Watcher(this.vm, key, (newValue) => {       node.value = newValue;     });          node.addEventListener("input", () => {       this.vm[key] = node.value;     });   }
 
  | 
 
在上面的代码中,我们为当前的文本框节点添加了input事件,当在文本框中输入内容的时候会触发该事件,同时,将用户在文本框节点中输入的值重新赋值给了data中对应的属性。
下面我们可以进行测试,在文本框中输入值,对应的差值表达式和v-text中的内容都会发生改变。同时在控制台中输出vm.msg的值会发现数据也发生了变化。
而我们知道,当给data中的属性赋值后,会执行observer.js中的set方法,更新视图,也就是触发了响应式的机制。
现在整个Vue的模拟实现,我们就完成了。
当然,我们这里只是模拟了最核心的内容也就是数据响应式与双向绑定。
13、总结
首先我们先来看一下最开始提出的问题。
第一个:给属性重新赋值成对象,是否是响应式的?答案:是响应式的。
应当我们给data中的属性进行重新赋值的时候,会执行Observer类中的defineReactive方法的set方法
在set方法中,调用了walk方法,该方法中判断重新给data属性中赋的值是否为对象,如果是对象,会将对象中的每个属性都修改成响应式的。
第二个问题:给Vue实例新增一个成员是否是响应式的?
例如如下代码:
  <script>       let vm = new Vue({         el: "#app",         data: {           msg: "Hello World",           count: 12,           person: {             name: "zs",           },         },       });       console.log(vm.$data.msg);       
        vm.test = "abc";     </script>
   | 
 
在index.html文件中,创建了Vue的实例后,给Vue实例后新增了test的属性,那么这个test属性是否为
响应式的呢?
答案:不是响应式的。
因为,我们所有的操作都是在创建Vue的实例的时候完成的,也就是在Vue类的构造函数中完成的。
在Vue类的构造函数中,创建了Observer的实例,完成了监听数据的变化。
所以当Vue的实例创建完成后,在为其添加属性,该属性并不是一个响应式的。
当然,为了解决这个问题,Vue中也给出了相应的解决方案,可以查看官方的文档: