Vue源码分析-Watcher和Dep实现监听数据变化视图跟随变化原理(终于自己写了一个超简版的)

1,709 阅读12分钟

终于自己写了一个超简版的gitHub地址.

Vue中的三种Watcher

1.computed Watcher

在initComputed的时候,在非服务端的渲染的情况下,会循环key in computed对象,为每一个key和其对应的value(函数/对象)创建watcher,

watcher的特点是创建时watcher.lazy=true,watcher.get()不会立马执行,watcher.value初始化时为undefined

2.user Watcher

在initWatch的时候,会循环key in watch对象,通过createWatcher(vm, key, handler)为每一个key和其对应的handler(函数/对象)创建watcher

user watcher的标志是watcher.user-true,user Watcher会在创建的时候直接执行this.get(),得到的值赋值给watcher.value

3. 渲染 Watcher

每个组件只有一个渲染Watcher,vm._watcher就是这个组件的渲染watcher

渲染watcher的创建是在执行mountComponent方法时,创建后会直接执行this.get方法

 <!--UpdateComponrnt方法主要是1.vm._render()生成Vnode;2.vm._update1中生成的虚拟Vnode转换为真实dom,这样可以实现组件的更新和重新渲染-->
 updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };

 new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* true说明是渲染watcher*/);

以一个最简单的页面开始

<!--App.vue-->
<template>
  <div>
    <div>oriValue的值: {{oriValue}}</div>
    <div>computed对象里oriValueAfterComputed的值(跟随oriValue变化):{{oriValueAfterComputed}}</div>
    <div>watch对象里oriValueWatch的值(跟随oriValue变化):{{oriValueWatch}}</div>
    <button @click="oriplus">oriValue加1</button>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                oriValue:1,
                oriValueWatch:""
            }
        },
        methods:{
            oriplus(){
                this.oriValue++
                console.log("this.oriValueAfterComputed",this.oriValueAfterComputed)
            }
        },
        computed:{
           oriValueAfterComputed:{
                get(){
                    return this.oriValue+"  Computed"
                },
                set(value){
                    this.oriValueAfterComputed = "setValue"+" oriValueAfterComputed :"+value
                }
            }
        },
        watch:{
            oriValue:{
                handler(newValue){
                    this.oriValueWatch  = newValue + "  Watch"
                }
            }
        }
    }
</script>

从这几个简单的属性,我们可以知道

oriValueAfterComputed随着oriValue变化

oriValueWatch随着oriValue变化

也就是说,当oriValue变化,需要通知computed watcher和user watcher 将oriValueAfterComputed,oriValueWatch更新,最后通过渲染watcher将页面更新渲染

Vue中实现数据监听的原理是

以需要监听oriValue为例,在oriValue对应的dep对象subs数组中放入与oriValue相关的watcher对象,然后在每个相关watcher的deps数组里面放入deps对象

当oriValue变化时,触发watcher.notify方法,将该watcher的deps数组里面的每个watcher都执行一次。deps数组的最后一个watcher是渲染watcher,前面的watcher实现值的更新获得最新的值,渲染watcher执行更新视图

通过debugger来调试代码

initData

为了讲解清晰,请只关注oriValue这个值

initState(vm)-> initData(vm) -> obserbve(data,true)=>ob = new Observer(value) =>this.walk(value)=>defineReactive?1(obj, keys[i]);

function defineReactive?1 (obj,key,val) {
    var dep = new Dep();
    var property = Object.getOwnPropertyDescriptor(obj, key);
    var getter = property && property.get;
    var setter = property && property.set;
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        dep.notify();
      }
    });
  }

avatar 在defineReactive?1这个方法中,我们设置了oriValue这个值的getter 和 setter方法,在读取oriValue这个值的时候,会触发getter函数,给oriValue赋值的时候,触发setter方法。 这里特别需要说明,defineReactive?1只是创建了属于这个key的dep对象以及定义了key的setter和getter函数,并没有执行这两个函数!!!

在这个在defineReactive方法里面,我们需要注意

var dep = new Dep() //这里创建了dep对象

可以有截图可以看到,在defineReactive?1方法的参数key为oriValue的时候,这个dep对象的id是6,在后面关于oriValue的dep操作,值的都是这个id为6的dep对象

avatar 同理,在后面关于oriValueWatch的dep操作,都是这个id为7的dep对象

initComputed

为了代码简单明了,将默认为 非服务端渲染 以及 使用缓存,以及 省略异常情况报警告的代码

var computedWatcherOptions = { lazy: true };

  function initComputed (vm, computed) {
    var watchers = vm._computedWatchers = Object.create(null);
    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      defineComputed(vm, key, userDef); 
    }
  }
  

上面的代码只要是创了建watchers对象,为computed对象的属性创建对应的watche对象,如下所示(本例子中computed对象只有oriValueAfterComputed这个属性值)

watchers["oriValueAfterComputed"] = new Watcher(vm:vm,
                                    getter: function(){ return this.oriValue+"  Computed"},
                                    noop,
                                    computedWatcherOptions:{ lazy: true };
                                    )

接着为computed的key定义getter和setter函数,上面的initState同理,取值是触发getter方法,赋值时触发setter方法。

从下面的defineComputed可以看到,getter方法是createComputedGetter方法执行的结果

 function defineComputed (
    target,
    key,
    userDef
  ) {
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = createComputedGetter(key);
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = createComputedGetter(key)
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

而createComputedGetter返回的是一个computedGetter函数,这个computedGetter函数做了什么呢? 首先,computedGetter先拿到key对应的watcher对象,在这个例子中就是

watchers["oriValueAfterComputed"] = new Watcher(vm:vm,
                                    getter: function(){ return this.oriValue+"  Computed"},
                                    noop,
                                    computedWatcherOptions:{ lazy: true };
                                    )

然后如果这个watcher的dirty属性为true的话,就重新执行watcher.evaluate(),也就是watcher.getter(), 在本例子中getter方法就是

function(){ 
    return this.oriValue+"  Computed"
}

上面这段代码,最后返回value值,也就是上述代码的执行结果,结果会存储在value属性中,dirty一直false的时候,getter函数不会重新执行(也就是缓存的概念),getter函数触发最后获得的结果就是该key在computed对象中定义的get()方法的执行结果

 function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
      //dirty一直false的时候,evaluate函数,也就是getter函数不会重新执行
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

createComputedGetter还有一个地方暂时没有分析,等getter方法被触发的时候我们再分析这里

  if (Dep.target) {
          watcher.depend();
        }

至于setter方法就没有过多的需要分析的了

sharedPropertyDefinition.set = userDef.set || noop;

setter方法就是该key在computed对象中定义的set()方法 avatar

和initData一样,defineComputed只是创建了属于computed对象中key的Watcher对象以及定义了key的setter和getter函数,并没有执行这两个函数!!!没有执行!!!所以初始化后这个key对应的watcher对象的value还是undefined

initWatch

在分析initwatch前先说明一下,和computed对象的key创建的watcher不一样,watch对象里的key对应的watcher是创建后会里面先执行一次,watch.value初始化后不为undefined

function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
      <!--handler可以对象,也可以是一个方法,或者是字符串-->
        createWatcher(vm, key, handler); //本例子中handler不为array
      }
    }
  }
function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
  <!--如果handler是一个对象,则handler是它的handler属性-->
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
     <!--如果handler是一个字符串,handler为vm[handler]-->
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }
Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options); //这里就是为每一个key创建一个watcher对象
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          // 省略...
        }
      }
      //省略...
    };
  }

initWatch方法总结来说就是做了这么几件事情

1.拿到watch对象中每个key对应的handler函数

2.创建对应key的watcher,将handler作为参数创建watcher对象

3.watcher内部执行this.get(),也就是执行handler方法,这里的get()可能会触发initData中的getter函数

new Watcher的过程

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    //省略无关代码
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
    //这样getter获得的
      this.getter = parsePath(expOrFn);
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

这段代码就是决定watcher的get方法是否在初始化后直接执行

computed对象中的属性的对应watcher没有初始化的时候执行,因为在创建的时候回传进了lazy:true

watch对象属性对应的watcher没有传入,默认lazy为undefined,所以会执行this.get()

 this.value = this.lazy
      ? undefined
      : this.get();
  };

那么get方法执行经历了什么,为什么和实现视图变化有莫大的关系呢?

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
     //省略错误处理代码
    } finally {
      //省略deep属性为true的处理方法
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

以我们本次的例子为例,下面是这次例子的watcher对象

watch:{
            oriValue:{
                handler(newValue){
                    this.oriValueWatch  = newValue + "  Watch"
                },
                immediate:true
            }
        }

1.pushTarget(this);

 function pushTarget (target) {
   targetStack.push(target);
   Dep.target = target;
 }

全局创建了一个targetStack数组,用来存放当前运行的watcher

在initWatcher中,var watcher = new Watcher(vm, expOrFn, cb, options);这里创建了一个watcher对象,并且watcher的get函数是立马执行的,所以当前的watcher是oriValue的watcher,将这个watcher压进targetStack这个栈内,并把Dep类的target设为这个Watcher对象

2.执行getter方法,读取oriValue的值,触发oriValue的getter方法,把oriValue对应的dep放进上面Dep.target的subs数组中,也就是当前的watcher,oriValue的Watcher

var vm = this.vm;
value = this.getter.call(vm, vm);

这里的getter函数读取了vm.orivalue的值,所以会触发在initData里面给oriValue定义的getter函数,看看下图 t7GSNn.md.png t7Gphq.png 还记得这个id为6的dep对象吗,就是在initData方法里的defineReactive方法中定义的那个dep对象,接下来就是执行dep.depend方法,

dep.depend方法简单来说,就是把dep对象放入目前Dep.target中的watcher对象的deps数组中,再把这个watcher放进dep对象中,他们大概就这这么个关系

t7thxP.png

这时候oriValue的dep对象里面就有对应的watcher了

3.popTarget(this);

function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}

最后,把当前的watcher出栈,如果栈为空,则Dep.target为undefined

initWatch到此完成,总结来说,这个方法就是为watch对象里面的key创建对应的watcher,并执行this.get()方法读取一次key的值,触发getter函数,让这个key的dep对象和watcher建立关系

mountComponent方法

前面的初始化完成后,会调用mountComponent,这里面主要更新组件的方法是updateComponent,UpdateComponent方法主要是执行组件的_render函数,生成一个虚拟dom,也叫做vnode,再调用_update方法将vnode转化成真实dom。这个方法在本文中不会过多分析,大家知道是做什么用的就好

updateComponent = function () {
     vm._update(vm._render(), hydrating);
   };

看看mountComponent方法是怎样创建渲染watcher的

function mountComponent (
 vm,
 el,
 hydrating
) {
//省略无关代码
 var updateComponent;
  //省略无关代码
 updateComponent = function () {
     vm._update(vm._render(), hydrating);
   };
 new Watcher(vm, updateComponent, noop, {
   before: function before () {
     if (vm._isMounted && !vm._isDestroyed) {
       callHook(vm, 'beforeUpdate');
     }
   }
 }, true /* isRenderWatcher,这个是是否为渲染watcher的标记 */);
  //省略无关代码
}

上面的代码非常明显地,创建了一个渲染watcher,

 Watcher = function Watcher (
   vm:vm,
   expOrFn:updateComponent,
   cb:noop,
   options,
   isRenderWatcher:true
 )

上面说了,options这个参数并没有传入{lazy:true},所以this.get()会执行,这里的this.get()

把目前这个渲染watcher压入栈

Dep.target为当前渲染watcher

执行updateComponent方法

渲染wather出栈

UpdateCompoent中有一步骤

vnode = render.call(vm._renderProxy, vm.$createElement);

render就是组件的渲染函数(本例子组件的render函数如下所示)

var render = function() {
 var _vm = this
 var _h = _vm.$createElement
 var _c = _vm._self._c || _h
 return _c("div", [
   _c("div", [_vm._v("oriValue的值: " + _vm._s(_vm.oriValue))]),
   _c("div", [
     _vm._v(
       "computed对象里oriValueAfterComputed的值(跟随oriValue变化):" +
         _vm._s(_vm.oriValueAfterComputed)
     )
   ]),
   _c("div", [
     _vm._v(
       "watch对象里oriValueWatch的值(跟随oriValue变化):" +
         _vm._s(_vm.oriValueWatch)
     )
   ]),
   _c("button", { on: { click: _vm.oriplus } }, [_vm._v("oriValue加1")])
 ])
}
var staticRenderFns = []
render._withStripped = true

export { render, staticRenderFns }

上面代码每行依次执行 当执行到

 _c("div", [_vm._v("oriValue的值: " + _vm._s(_vm.oriValue))]),

会触发oriValue的getter函数,就和initWatch一样,将oriValue的deps对象放进渲染watcher的deps数组内,将渲染watcher放进dep对象的subs数组内,**需要注意的是,这时候render还没执行完,updateComponent也没有执行完,所以Dp.target还是渲染watcher

oriValue的dep对象的subs数组有

[oriValue对应的userWatcher,组件渲染watcher]

当执行到

_vm._s(_vm.oriValueAfterComputed)

会触发oriValueAfterComputed的getter方法,这个getter方法在initComputed的时候被定义了

 function computedGetter () {
   var watcher = this._computedWatchers && this._computedWatchers[key];
   if (watcher) {
     if (watcher.dirty) {
       watcher.evaluate();
     }
     if (Dep.target) {
       watcher.depend();
     }
     return watcher.value
   }
 }
<!--这里就是获取initComputed中定义的计算watcher-->
var watcher = this._computedWatchers && this._computedWatchers[key];

还记得我们在initComputed里面new Watcher的时候传进去的options是{lazy:true} t76Er9.png t76NIP.png 所以这个watcher的dirty为true,会执行watcher.evalute()

evaluate方法也不过多分析,主要就是执行这个key在computed对象中定义的get方法,返回执行结果,将dirty设为false

Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };

这个函数里面也执行了this.get(),这是把这个computed Watcher压入栈,Dep.target就变成了这个computed watcher

this.get方法中this.getter.call(this)这一步会调用执行computed中该key对应的get方法

computed:{
    oriValueAfterComputed:{
        get(){
            return this.oriValue+"  Computed"
        },
        //·····
        },

可以看到上面函数的执行会读取oriValue的值

好了,相信都能猜到了,将oriValue的deps对象的deps数组放进computed watcher,将dep对象的subs数组放入computed watcher对象

oriValue的dep对象的subs数组有

[oriValue对应的userWatcher,组件渲染watcher,oriValue对应的computed watcher]

完成后将computed watcher出栈,现在Dep.target就变成了渲染watcher

最后computedGetter返回的是watcher.value,也就是执行结果,所以_vm.oriValueAfterComputed能computed方法执行后的值

computedGetter方法里面,可以看到在evaluate函数执行完后,在返回watch.value前还执行了这么一段代码的时候

if (Dep.target) {
    watcher.depend();
    }
Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };
  
Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

这个watch.depend其实就是把当前watcher(本例中是oriValueAfterComputed的computedWatcher)的dep数组里的每一个dep对象放进现在栈顶watcher对象(本例子中是组件渲染watcher)的deps数组中,再在这个deps数组的dep对象的subs数组中加入Dep.target自身 至于为什么有这么一步骤,我猜测是,当oriValue不作为页面元素渲染,只作为computed对象get函数的参数时,oriValued的dep对象里面是没有渲染watcher的,也就是说oriValueAfterComputed对应的watcher对象的deps数组也是没有渲染watcher的,那么当watcher.notify的时候,是不会更新的vnode,也就不会更新dom

当执行到

 _vm._s(_vm.oriValueWatch)

这时候读取了_vm.oriValueWatch,在前面initData的时候,给oriValueWatch定义了一个id为7的dep对象,现在Dep.target为渲染watcher,所以getter方法会 把id=7的dep对象放入Dep.target的watcher对象的deps 数组,把Dep.target放入这个dep对象subs数组中

现在oriValueWatch对应的dep对象的subs数组就有了 [ 组件渲染watcher ]

最后updatecomponent函数执行完,渲染watcher从targetStack出栈,Dep.target为undefined

现在我们来总结一下目前有的watcher对象和dep对象,梳理清楚他们的关系对接下来的watcher.notify和dep.update很有帮助 tqgclV.png

dep.notify()通知视图更新

好了,现在watcher们和dep们都准备好了,我们现在可以看看改变数据会发生什么事情了 tqRoM6.png 这个set函数会触发dep.notify(),例如现在是oriValue = oriValue+1,就会触发oriValue的setter方法

 Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();//这时候id=6的dep对象的subs数组遍历,每一个watcher依次执行update方法
  }
};

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    //不符合上面两者情况,会执行queueWatcher(this)方法,蚕食是当前watcher
    queueWatcher(this);
  }
};
<!--
 把watcher对象push到queue中
 重复的id将会被跳过,除非前一个同id的watcher已经执行完
-->
function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
    //如果queue正在执行,则按照id顺序插入到queue数组相应的位置
    //如果这个前一个同id的watcher已经会执行完了,则这个watcher会进入queue等待下一次flushing
      // if already past its id, it will be run next immediately.
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // queue the flush
    if (!waiting) {
      waiting = true;
      nextTick(flushSchedulerQueue);
    }
  }
}
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;
  <!--根据watcher的id从小到大来排序,因为渲染watcher是组件里最后创建的watcher,所以id最大,
  在queue数组中也是排到最后,最后执行-->
  queue.sort(function (a, b) { return a.id - b.id; });

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
  }
}

Watcher.prototype.run = function run () {
  if (this.active) {
    //新的value
    var value = this.get();
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
       //如果是user watcher,需要执行一次this.cb,也就是watcher里面对应key的handler函数
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};

所以当oriValue+1的时候,watcher更新流程如下 tqHYpF.png

最后回到上面的问题

tqHDk6.png 针对红框内的猜测,我在代码中vue.runtime.esm.js文件和App.vue中注释掉了这两段代码。 tqHzNV.png 现在页面只剩下computed的值oriValueAfterComputed渲染页面了(去掉oriValueWatch是因为oriValueWatch的dep也有渲染watcher) tqbShT.png 结果如下 tqb5r9.png 可以看到,上图中this.oriValueAfterComputed已经发生变化了,但视图并没有更新

把注释取消,视图随着oriValueAfterComputed的值更新了 tqb4KJ.png

本文作为个人学习记录,如果其中说得不妥当的地方,请路过的大佬们多多指出,谢谢各位!!!