浅析Vue批量更新原理__Vue.js
发布于 3 年前 作者 banyungong 1614 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

)

首先看这个例子中,连续3次触发了mutation,那么watch中的cb会被执行几次呢?

答案是一次。

那么为什么会是一次呢?本文会围绕着这个问题的解释来粗浅地讨论一下Vue中批量更新的原理。

首先要知道,msg这个key,是通过Object.defineProperty被监听了的,Vue通过这个api实现在key被set的时候(也就是this.msg = xxx这种操作),触发所有订阅了这个key的Watcher的update方法。

这里引入了一个Watcher的概念,那么这个Watcher是什么呢?

从语义上理解,Wathcer其实类似于一个key的观察者,当key被set的时候,Watcher会调用自身的update方法。

这样说会有点抽象,在这个具体例子中的话就是msg被set了不同的值,那么watch这个option产生的Watcher就会执行自己原型上的update方法,这个方法最终的目的是执行实例上的cb,即msg对应的console.log(‘监听到mutation’)的这个方法。

只需知道对msg这个key进行set操作的时候可以拿到key的所有Watcher就可以了,Watcher是怎么生成的,为什么set操作的时候可以拿到所有的Watcher,不在这篇文章的讨论范围。

问题的关键在Watcher执行的update方法,这个方法前面说过是为了执行cb,在这个例子中就是console.log(‘监听到mutation’),但是首先思考一个问题:

我们可以监听到一个set引起的mutation就立即同步执行一次cb吗?

显然是不可以的。

举个例子:

for (let i = 0; i < 1000; i++) {
   this.msg = `循环了${i}次`;
}

假如对于这种操作,1000次里面每次Watcher都要响应执行一次update,那可能是有很大的性能开销的。像本文的这个例子中update其实就是一个console.log,但是如果cb是一个开销比较大的方法,那么就可能会引起性能问题了。

所以update操作一定是异步的。

虽然知道要通过异步来解决,但具体是如何解决的呢?Vue的做法是把调用cb放到了一个micro task或者macro task队列中,具体放到微任务队列还是宏任务队列要看当前的运行环境是否支持Promise、MutationObserver、setImmediate这几个相当于放入微任务队列的api,支持就会放在微任务队列,不支持则使用setTimeout这个api把调用cb放到宏任务队列里。

不管放到微任务队列还是宏任务队列,调用cb都会在所有的同步代码执行完毕后执行。这一点涉及到event loop的知识,因为总是先执行所有的同步代码,然后从微任务队列中按顺序执行,微任务队列空了才会从宏任务队列中取出一条执行。如果此时微任务队列还有任务,那么就会继续按照这个循环执行,这个就是event loop。

通俗地理解Vue的行为就是在监听到key的mutation之后key的Watcher都会触发update,想要调用自身的cb属性,但是Vue仅仅是答应会在未来的某个时刻执行Watcher的这个update请求也就是调用它的cb,并且在调用之前都不会再受理该Watcher的update的请求。

下面看看源码(P.S. 第一次看源码的话,vue-cli生成的项目,生效的源码就在node_modules/vue/dist/vue.runtime.esm.js):

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      // if already flushing, splice the watcher based on its id
      // 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);
    }
  }
}

首先queueWatcher就是Watcher执行update方法的时候做的事情,我们注意到有三个flag,has[id]、flushing和waiting,我们主要通过探究这三个flag的意义来理解这段源码。

我们对msg进行了3次set操作,对应三次Watcher的update方法,对应三次queueWatcher,而这三次排队每次传的参数,都是同一个Watcher实例,所以第一次has[id]才为null,第一次置为true之后,后面的两次就直接结束了。其实通俗地解释就是,第一次触发mutation,到Vue这就会告诉你,已经加入到队列了,后面就会处理,同一个Watcher的其他mutation就不再接待了。

那么首次mutation其实会把Wathcer实例加入到queue队列里,然后在未来的某个时间,会遍历queue并调用所有Watcher实例的cb。

关于flushing这个变量,其实我们看这个if else,在if分支watcher会被放到队列末尾,而else分支其实会按照watcher的id放到queue的相应位置,queue中的所有watcher是按照Id升序排列的,而如果这个id的watcher已经被执行过了,那么就会被放到队列的下一项,下个就执行该watcher的cb。

其实可以看出来flushing就是为了保证queue中的所有watcher的顺序永远都是按id升序的,为什么要这么做呢?Vue源码里给了三点注释:

  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.

大致说的是为了保证父子组件按照顺序执行update、用户创建的wathcer(像这个例子中在watch这个option中监听msg就会创建一个user wathcer)在render watcher之前执行、在父组件的watcher run的时候销毁了子组件,那么子组件的watcher就不会被执行了。

这里我觉得知道queue中所有watcher是按照id升序排列的,而id是创建watcher时自增的就够了,具体为什么这么干以后遇到具体场景就理解了。

最后是waiting这个变量,这个变量很简单,就是保证完成一次对queue的遍历之前不会开启新的遍历。

OK,源码大致看过了,再回到示例代码过一遍流程:

首先三次set,三次mutation,三次Watcher的update,一次往queue中push watcher,一次加入把flushSchedulerQueue加入到task队列,一次遍历queue调用watcher的cb,即一次输出"监听到mutation"。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: chenbj 原文链接:https://juejin.im/post/6844904198698434573

回到顶部