vue异步更新流程梳理

前言

vue实例创建后,当我们重新赋值data中的数据时,视图就会更新,那么具体干了啥呢?本文用demo + 调试断点,一步步来研究一下具体流程。

demo代码




  
  
  
  Document


  
{{value}}

众所周知,vue在实例化的时候会对data的数据进行响应式设置,当我们赋值的时候就会触发对应的setter,所以把断点打到setter看一下,如下图:


image.png

我们传入的newVal 是 2,老的val 是1,后面直接val = newVal ,那么此时val就更新为2了。更新之后,最后还有一句代码

dep.notify();

这里就是对观察者们进行通知了。当我们的值变化时,dep就负责去通知watcher们,每一个watcher实例就会调用自己内部的update方法。咱门进入notify方法看看它是不是这样子:


image.png

看果然是这样子,subs这个数组就是专门存放watcher实例的,循环遍历watcher,调用每个watcher自己的update方法。
这里只存放了一个watcher实例,看看这个watcher是啥。这里先解释一下watcher都有哪些分类:

  1. 渲染watcher, 负责更新视图变化的,即一个vue实例对应一个渲染watcher
  2. 用户自定义watcher,用户通过watch:{value(val, oldVal){}}选项定义的,或者this.$watch()方法生成的。
  3. computed选项里面的计算属性也是watcher, 和第2点中的watcher的区别是它的watcher实例有dirty属性控制着watcher.value值的变化

打开右边的scope栏,找到Local下的subs[0]


image.png

再去查看watcher.vm._wather是否有值,有就代表是渲染watcher。同时找到源码Watcher的声明:


image.png

由此可见我们现在正在执行update的这个watcher就是一个渲染watcher。
继续找看update方法干了啥:

image.png

update就是执行queueWatcher(this),再看这里干了啥
image.png

其实猜也能猜到就维护一个queue:[watcher, ...]的队列,里面的watcher不重复;最后执行nextTick(flushSchedulerQueue); 看看flushSchedulerQueue干了啥,其实就是执行watcher.run方法。
image.png

来到这里我们大概就清晰了这个流程:
赋值操作this.value = 2就是把渲染watcher放到一个queue的队列中,并且通过nextTick在将来某个时刻把queue队列的watcher拿出来一个个去执行watcher.run()方法。

这里的nextTick就是利用事件循环,在未来某个时刻会执行flushSchedulerQueue方法 , flushSchedulerQueue 又是循环执行 watcher.run();继续回到watcher看run方法干了啥:


image.png

image.png

注意观察上面两张图, 在run方法中执行this.get(),视图就改变了值,从1变成2,一切的谜团就在这一行代码中,我们继续研究get()方法!

image.png

get里面调用了this.getter.call(vm, vm),找到this.getter,发现这是一个updateComponent的方法:


image.png

那么这个方法是怎么来的呢?


image.png

如上图,是watcher实例化的第二个参数: expOrFn,那么就打断点在watcher实例化过程中,看看这个updateComponent怎么来的。刷新页面:


image.png

根据上图中1、2步骤,在call stack执行栈中往下点,一个个方法进去看,很幸运,在下面的mountComponent方法的执行栈就看到了updateComponent的声明,在下图2处,就是updateComponent这个方法做的事情,从方法中我们可以知道,就是这行代码起了作用
 vm._update(vm._render(), hydrating);
image.png

总结

那么,至此我们再总结一下流程:
this.value = 2 触发 setter,
同步执行过程:
setter => dep.notify() => watcher.update() =>queueWatcher(this) =>nextTick(flushSchedulerQueue);

由于把flushSchedulerQueue放到了nextTick里面,那么接下来未来的某个时刻会执行flushSchedulerQueue,然后从queque队列中提取watcher出来循环执行watcher.run

watcher.run() => watcher.get() => watcher.getter() => updateComponent() => vm._update(vm._render())

到此为止我们就知道大概就是这么一个流程。
最后再看看_update()方法与_render()方法,
_render()方法很纯粹,就是返回一个虚拟dom: vnode对象。而_update()方法就是把虚拟dom: vnode去进行patch的过程,得到一个新的真实dom。

一些问题:

  • 为什么watcher放在queue队列中不直接去执行watcher.run呢,而要放到nextTick里面,等待未来某个时刻统一执行呢?
    答:其实还是为了性能,高效,如果你这么写代码:
this.value = 2
this.value = 3

没有nextTick就会走两次 vm._update(vm._render())了,而这里面的patch过程的diff就是一个比较复杂消耗性能的过程。

  • 为什么只有一个渲染watcher?
    答: 因为vue1.x就是因为采用了一个绑定值一个watcher的方式,虽然变化可以精确到绑定值的位置,但是这样子在大一点的项目就很多watcher,会消耗大量内存造成性能瓶颈,vue2采用了虚拟dom更新的方案,以组件为单位进行更新,一个组件实例对应一个渲染watcher。组件内的一个或者多个响应式属性更新 --> 触发渲染watcher.unpdate() --> 同一个渲染watcher只被送入一次queueWatcher队列 -->nextTick之后的回调里面触发watcher.run()。

你可能感兴趣的:(vue异步更新流程梳理)