前言
vue实例创建后,当我们重新赋值data中的数据时,视图就会更新,那么具体干了啥呢?本文用demo + 调试断点,一步步来研究一下具体流程。
demo代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">{{value}}</div>
<script src="../dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
value: 1
}
},
mounted () {
this.value = 2
}
})
</script>
</body>
</html>
众所周知,vue在实例化的时候会对data的数据进行响应式设置,当我们赋值的时候就会触发对应的setter,所以把断点打到setter看一下,如下图:
我们传入的newVal 是 2,老的val 是1,后面直接val = newVal ,那么此时val就更新为2了。更新之后,最后还有一句代码
dep.notify();
这里就是对观察者们进行通知了。当我们的值变化时,dep就负责去通知watcher们,每一个watcher实例就会调用自己内部的update方法。咱门进入notify方法看看它是不是这样子:
看果然是这样子,subs这个数组就是专门存放watcher实例的,循环遍历watcher,调用每个watcher自己的update方法。
这里只存放了一个watcher实例,看看这个watcher是啥。这里先解释一下watcher都有哪些分类:
- 渲染watcher, 负责更新视图变化的,即一个vue实例对应一个渲染watcher
- 用户自定义watcher,用户通过
watch:{value(val, oldVal){}}
选项定义的,或者this.$watch()方法生成的。 - computed选项里面的计算属性也是watcher, 和第2点中的watcher的区别是它的watcher实例有dirty属性控制着watcher.value值的变化
打开右边的scope栏,找到Local下的subs[0]
再去查看watcher.vm._wather是否有值,有就代表是渲染watcher。同时找到源码Watcher的声明:
由此可见我们现在正在执行update的这个watcher就是一个渲染watcher。
继续找看update方法干了啥:
update就是执行
queueWatcher(this)
,再看这里干了啥其实猜也能猜到就维护一个queue:[watcher, ...]的队列,里面的watcher不重复;最后执行
nextTick(flushSchedulerQueue);
看看flushSchedulerQueue干了啥,其实就是执行watcher.run方法。来到这里我们大概就清晰了这个流程:
赋值操作this.value = 2
就是把渲染watcher放到一个queue的队列中,并且通过nextTick在将来某个时刻把queue队列的watcher拿出来一个个去执行watcher.run()方法。
这里的nextTick就是利用事件循环,在未来某个时刻会执行flushSchedulerQueue方法 , flushSchedulerQueue 又是循环执行 watcher.run();继续回到watcher看run方法干了啥:
注意观察上面两张图, 在run方法中执行this.get(),视图就改变了值,从1变成2,一切的谜团就在这一行代码中,我们继续研究get()方法!
get里面调用了this.getter.call(vm, vm),找到this.getter,发现这是一个updateComponent的方法:
那么这个方法是怎么来的呢?
如上图,是watcher实例化的第二个参数: expOrFn,那么就打断点在watcher实例化过程中,看看这个updateComponent怎么来的。刷新页面:
根据上图中1、2步骤,在call stack执行栈中往下点,一个个方法进去看,很幸运,在下面的mountComponent方法的执行栈就看到了updateComponent的声明,在下图2处,就是updateComponent这个方法做的事情,从方法中我们可以知道,就是这行代码起了作用
vm._update(vm._render(), hydrating);
总结
那么,至此我们再总结一下流程:
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()。