# Vue中的三种Watcher `Vue`可以说存在三种`watcher`,第一种是在定义`data`函数时定义数据的`render watcher`,这种形式最为常见。第二种是`computed watcher`,是`computed`函数在自身内部维护的一个`watcher`,配合其内部的属性`dirty`开关来决定`computed`的值是需要重新计算还是直接复用之前的值。第三种就是`watcher api`了,就是用户自定义的`export`导出对象的`watch`属性,当然实际上他们都是通过`class Watcher`类来实现的。 ## 概述 `Vue.js`的数据响应式,通常有以下的的场景: * 数据变`->`使用数据的视图变。 * 数据变`->`使用数据的计算属性变`->`使用计算属性的视图变。 * 数据变`->`开发者主动注册的`watch`回调函数执行。 三个场景,对应三种`watcher`: * 负责视图更新的`render watcher`。 * 执行计算属性更新的`computed watcher`。 * 用户注册的普通`watcher api`。 ### Render Watcher 在`render watcher`中,响应式就意味着,当数据中的值改变时,在视图上的渲染内容也需要跟着改变,在这里就需要一个视图渲染与属性值之间的联系,`Vue`中的响应式,简单点来说分为以下三个部分: * `Observer`: 这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的`Watcher`。 * `Watcher`: 观察者,当监听的数据值修改时,执行响应的回调函数,在`Vue`里面的更新模板内容。 * `Dep`: 链接`Observer`和`Watcher`的桥梁,每一个`Observer`对应一个`Dep`,它内部维护一个数组,保存与该`Observer`相关的`Watcher`。 根据上面的三部分实现一个功能非常简单的`Demo`,实际`Vue`中的数据在页面的更新是异步的,且存在大量优化,实际非常复杂。 首先实现`Dep`方法,这是链接`Observer`和`Watcher`的桥梁,简单来说,就是一个监听者模式的事件总线,负责接收`watcher`并保存。其中`subscribers`数组用以保存将要触发的事件,`addSub`方法用以添加事件,`notify`方法用以触发事件。 ```javascript function __dep(){ this.subscribers = []; this.addSub = function(watcher){ if(__dep.target && !this.subscribers.includes(__dep.target) ) this.subscribers.push(watcher); } this.notifyAll = function(){ this.subscribers.forEach( watcher => watcher.update()); } } ``` `Observer`方法就是将数据进行劫持,使用`Object.defineProperty`对属性进行重定义,注意一个属性描述符只能是数据描述符和存取描述符这两者其中之一,不能同时是两者,所以在这个小`Demo`中使用`getter`与`setter`操作的的是定义的`value`局部变量,主要是利用了`let`的块级作用域定义`value`局部变量并利用闭包的原理实现了`getter`与`setter`操作`value`,对于每个数据绑定时都有一个自己的`dep`实例,利用这个总线来保存关于这个属性的`Watcher`,并在`set`更新数据的时候触发。 ```javascript function __observe(obj){ for(let item in obj){ let dep = new __dep(); let value = obj[item]; if (Object.prototype.toString.call(value) === "[object Object]") __observe(value); Object.defineProperty(obj, item, { configurable: true, enumerable: true, get: function reactiveGetter() { if(__dep.target) dep.addSub(__dep.target); return value; }, set: function reactiveSetter(newVal) { if (value === newVal) return value; value = newVal; dep.notifyAll(); } }); } return obj; } ``` `Watcher`方法传入一个回调函数,用以执行数据变更后的操作,一般是用来进行模板的渲染,`update`方法就是在数据变更后执行的方法,`activeRun`是首次进行绑定时执行的操作,关于这个操作中的`__dep.target`,他的主要目的是将执行回调函数相关的数据进行`sub`,例如在回调函数中用到了`msg`,那么在执行这个`activeRun`的时候`__dep.target`就会指向`this`,然后执行`fn()`的时候会取得`msg`,此时就会触发`msg`的`get()`,而`get`中会判断这个`__dep.target`是不是空,此时这个`__dep.target`不为空。 上文提到了每个属性都会有一个自己的`dep`实例,此时这个`__dep.target`便加入自身实例的`subscribers`,在执行完之后,便将`__dep.target`设置为`null`,重复这个过程将所有的相关属性与`watcher`进行了绑定,在相关属性进行`set`时,就会触发各个`watcher`的`update`然后执行渲染等操作。 ```javascript function __watcher(fn){ this.update = function(){ fn(); } this.activeRun = function(){ __dep.target = this; fn(); __dep.target = null; } this.activeRun(); } ``` 这是上述的小`Demo`的代码示例,其中上文没有提到的`__proxy`函数主要是为了将`vm.$data`中的属性直接代理到`vm`对象上,两个`watcher`中第一个是为了打印并查看数据,第二个是之前做的一个非常简单的模板引擎的渲染,为了演示数据变更使得页面数据重新渲染。在这个`Demo`下打开控制台,输入`vm.msg = 11;`即可触发页面的数据更改,也可以通过在`40`行添加一行`console.log(dep);`来查看每个属性的`dep`绑定的`watcher`。 ```html