上把打了个酱油快速走读了一下首次渲染流程,这把我们玩个核心,来看看 Vue 中最经典的部分:响应式数据原理。啥都不说,我们先上图:

定义响应式数据
我们在初始化 Vue 的时候,会执行 initState(vm)
1 | export function initState(vm: Component) { |
这里最重要的是 initData 这个方法:
1 | function initData(vm: Component) { |
这里也没什么特别的,关键是 observe(data, true /* asRootData */) 这一句:
1 | export function observe(value: any, asRootData: ?boolean): Observer | void { |
这里也只是通过传进来的 value 实例化了一个 Observer 对象:
1 | export class Observer { |
这里分数组和对象分别进行了处理,我们先来看看 defineReactive:
1 | export function defineReactive( |
这里分别给数据定义了 get 和 set 方法,执行 get 方法的时候会进行依赖收集。
依赖收集
我们看看依赖收集做了什么:
1 | depend () { |
这里的 Dep.target 又是哪来的呢,其实它就是 Watcher 的实例。
1 |
|
上一章说了,在组件 $mount 的时候会初始化一个 Wathcer,
在 Watcher 初始化的时候会执行 this.get, pushTarget 将前Dep.target 设置为当前的 Watcher。同时,执行 this.get 的时候,还会调用 this.getter.call(vm, vm),这个 getter 就是构造函数中传递进来的 expOrFn,它是啥呢,它就是:
1 | updateComponent = () => { |
其中 vm._render 执行的时候会对数据进行取值操作,从而触发 defineProperty 中的 get 方法。
触发更新
当数据被赋值的时候会触发 set 方法,调用 dep.notify(),最后会调用 Watcher 类中的:
1 | update () { |
queueWatcher:
1 | export function queueWatcher(watcher: Watcher) { |
我们先看看 flushSchedulerQueue:
1 | function flushSchedulerQueue() { |
其他更多细节可以参考我们自己 DIY 的响应式系统。
这里我们看看数组是怎么处理的。
数组处理
1 | if (Array.isArray(value)) { |
我们先看看 protoAugment:
1 | function protoAugment(target, src: Object) { |
看来还得看看 arrayMethods:
1 | import {def} from '../util/index' |
我们举例来说说这一串的操作是干了个啥:
1 | [1, 2, 3] --__proto__--> arrayMethods --__proto__--> Array.prototype |
其中 arrayMethods 对 7 个数组方法进行了包装,使得在对数进行操作的时候可以通知 Watcher 进行更新。这里的 ob.dep 是个啥呢。比如说 data 中有这样一个属性 arr: [1,2],在对 arr 进行依赖收集的时候有如下代码,这里的 childOb.dep 和上文的 ob.dep 是同一个对象。
1 | ... |
具体来说,这里的 childOb 就是 [1,2] 这个对象经过 observe 返回后的 Observer 实例,它有 dep 属性。从这里可以看到它和 arr 拥有一样的依赖,即下面这两个操作都会触发通知相同的 Watcher 去更新:
1 | // 1 |
computed 的处理
1 | const computedWatcherOptions = {lazy: true} |
这里遍历了 computed 里面的每一个属性,并且为每一个属性初始化了一个 Watcher 对象。这样,当我们在 computed 里面访问 data 里面的属性时,就可以收集到依赖了。注意到这里传入了 { lazy: true },我们看看会有什么效果:
1 | this.dirty = this.lazy // for lazy watchers |
该属性仅仅是标记了当前数据是 “脏的”,并且不会立即求值。所谓 “脏的” 指的是当前值已经脏了,需要重新求值了,这个后面会再提到。
然后我们看看 defineComputed 做了啥:
1 | export function defineComputed( |
这里跑到了 createComputedGetter 这个方法:
1 | function createComputedGetter(key) { |
当我们第一次访问计算属性的时候会触发 get,由于 dirty 为 true,所以这里会走 watcher.evaluate 进行求值,并将 this.dirty 置为 false,这样下次再对 computed 进行求值的时候就不会执行 watcher.evaluate() 了,这样就实现了缓存功能。
1 | evaluate () { |
而当 computed 依赖的数据变化的时候,会触发 Watch 的 update:
1 | update () { |
这里仅仅是把 dirty 又重置为了 true 以使得下次对 computed 进行求值的时候重新执行 watcher.evaluate()。
缓存部分说完了,我们来看看下面这一段代码做了什么:
1 | if (Dep.target) { |
1 | depend () { |
这里有点难理解,我们用一个例子来说明:

首次渲染的时候组件会实例化一个 Watcher 对象,会触发对 description 的求值,这里又会实例化一个 Watcher,而 description 中对 fullName 进行求值,又会实例化一个 Watcher。这样就形成了一个依赖栈,靠近栈底的元素会依赖其上面的元素。
当执行 fullName 的时候,由于其依赖了 firstName 和 secondName,所以它会被添加进两者的 dep 中。收集完后会执行 popTarget(),此时 Dep.target 指向 description 的 Watcher,然后会执行 watcher.depend() 。注意这里的 watcher 还是 fullName 的,即 fullName 依赖啥,其他依赖 fullName 的也需要跟我有同样的依赖。举个例子:儿子依赖老爸,老爸是个啃老族依赖父母,所以孙子也间接依赖了爷爷奶奶。整下的就举一反三了。
我们调试下代码,发现跟我们的分析是相符的:
