【笔记】复习Vue双向绑定和Scheduler

714 阅读4分钟

前言

前几天又复习了一下Vue2响应式原理,记下了一些笔记,对于正在看Vue2源码的同学应该有一些帮助。

内容主要包括两部分:双向绑定和Scheduler

双向绑定

  1. vue组件【A】始化,并初始化一个render Watcher,赋值给Dep.target。
  2. defineProperty一下data对象,首先会初始化一个dep = new Dep(),另外会代理setter、getter,返回一个observal的对象
    • 也就是说,每次调用defineProperty就会生成一个dep对象。也就是说,data、computed里面,有一个属性就会有一个dep对象。
  3. 这个observal的对象被【A】、【B】两个组件引用,并调用了observal对象的name属性
  4. 调用observal对象的name属性,触发getter函数,其执行dep.depend()
  5. dep有个subs的属性,它里面会存放watcher对象。而dep.depend()的代码如下:
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

它会用到Dep.target,而这个值取决于当前的Vue组件初始化的阶段。如果处于render阶段,那么这个Dep.target就是render Watcher。如果处于watch属性这种,那么Dep.target就是user Watcher。

  • render Watcher其实就是这个watcher.run的时候的回调函数是去更新组件
  • user Watcher这个watcher.run的时候其实是执行用户自定义的watch属性的回调函数

继续往下分析,这里Dep.target.addDep(this),他会做两件事:

  1. 把dep.id存到Dep.target这个watcher内部,好让watcher知道自己关联哪些dep了。即如果发现watcher已经关联某个dep了,就不会继续往下走了。
  2. 如果继续往下走,dep本身还会把watcher加入到dep.subs这个数组里

所以,当observal的某个属性被get,然后dep.depend()执行后,这个属性就会跟watcher绑定起来。

总结:

  1. watcher分为render watcher和user watcher;render watcher的回调函数用来更新组件;user watcher的回调函数用来执行watch属性的回调函数。
  2. 一个Vue组件至少有一个watcher
  3. 一个Vue组件的data和computed中的每一个属性,都对应一个dep对象,当getter调用时,通过dep.depend(),让dep对象关联上一系列watcher对象
    • 一个dep可以对应多个watcher
    • 一个watcher可以关联多个dep.id

注意,getter对象调用,可能是被本组件调用,也可能是被其他组件调用,所以dep.subs里面寸的watcher可能属于多个vue组件

  1. setter调用时,执行dep.notify(),它会遍历subs,依次执行watcher.update()

watcher属于各个vue组件,所以setter后,接下来就会执行各个vue组件的render callback或者user callback。

  1. 这个时候会有sheduler机制,在非sync模式下,watcher的callback不会立即执行,而是通过scheduler包下的queueWatcher来调度执行

调度Scheduler

调度的主要作用:

  1. 合并setter:当同一个dep.id,也就是同一个属性,被多次setter时,watcher的callback只有一个可以执行。

具体来说,当同一个dep.id被多次setter时,只有第一个watcher会被加入到任务队列。但是又因为watcher是一个mutable的对象,其实多个setter对应的都是同一个watcher引用。所以即使只是把第一个watcher加入到任务队列,也能拿到最新的数据。

  1. 任务队列排序,queueWatcher的时机不一定是按照watcher.id的顺序来的,而vdom里面,组件是父组件先渲染,然后子组件在渲染。所以,会把任务队列按照watcher.id升序排列,然后再去依次执行。以保证组件更新以预期的顺序执行。

调度的原理:

基于事件循环,从微观上看,就是在nextTick时flush掉任务队列;从宏观上看,就是用户在一个微任务里触发了多次setter,然后再nextTick时真正去执行watcher的回调函数。

具体看代码把:github.com/vuejs/vue/b…

NextTick原理:

对于Promise.resolve来说,每次执行相当于往微任务队列里加一个任务。 而对于nextTick,每次执行,会先把callback存到队列里,然后把清空队列的这个任务加入到微任务队列。

所以区别就是,nextTick会把多个任务合并到一个微任务中。

为什么nextTick的时候才能观测到视图变化。

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
      // 修改数据
      this.message = 'changed'
      // DOM 还没有更新
      this.$nextTick(function () {
        // DOM 现在更新了
        // `this` 绑定到当前实例
        this.doSomethingElse()
      })
    }
  }
})

在上述代码里,微任务队列会依次加入两个任务:

  1. queueWatcher内部的flushQueue这个任务
  2. this.doSomethingElse()这个任务

微任务队列中的任务是依次执行的,第一个任务是flushQueue,flushQueue就是会调用watcher.run,run会同步去更新组件。也即是说,在下一个微任务开始前,flushQueue已经同步的把该更新的组件都更新了。