目录
阅读本文前题是,已经对vue数据绑定机制中的的observer和watcher原理有个大致的了解,只要知道一个watcher去get一个data时,就会建立一个互相绑定的关系,data的会把watcher存人dep.subs数组,知道都有谁在watch它,以便发生变化时通知,而watcher也会把data的dep放入自己的deps数据,知道自己依赖哪些数据。
在此基础上,本文进一步讲解一下computed的原理,整体流程图如下,可对照此图理解后面的内容:
1. computed初始化
在初始化组件创建组件实例时,会调用到initComputed函数,这里会初始化组件里定义的computed属性,代码如下:
// vue/src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
在for循环中,针对每一个computed属性,会做两件事:创建watcher,以及compuetdGetter(即vm.sum的getter)
1.1. 创建watcher
...
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
...
getter值即为用户定义的computed属性,如本示例中的sum:
// myproject/src/views/myview.js
computed: {
sum() {
return this.a + this.b
}
}
options的lazy值为true,这个值会同时传给watcher的dirty,这两个值初始化为true,决定了computed watcher和render watcher (负责监听数据变化,并更新组件视图的watcher) 在数据发生变化时,响应执行过程的不同,这个后面会讲到。
1.2. 创建computedGetter
在vm实例上为sum创建一个computedGetter,即vm.sum的getter,在initComputed中通过如下调用步骤进入到computedGetter的定义:
defineComputed -> createComputedGetter
// vue/src/core/instance/state.js
...
function initComputed (vm: Component, computed: Object) {
...
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
...
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
...
createComputedGetter(key)
...
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
以上computed初始化工作就完成了,此时这个function computedGetter () {…}里的内容还并未执行,这时watcher.value还并没有被计算得到值,而没有去计算值,也就没有去调用data的getter去取data,所以此时computed watcher和data也还没有绑定依赖,这个值的变化要关注下面两个过程:
- 当data发生变化时,watcher.value依然不会马上计算出值,只会去设置watcher.dirty为true,标识watcher.value的是个脏值,已经过时了,别人再来取我时,我需要重新计算。前面讲到computed watcher初始化时dirty为true,所以初始化完成后,watcher.value就已经是一个脏值,因为还没有执行过计算得到值。
- 组件mount时,创建render watcher去初次get vm.sum这个computed属性时,执行computedGetter的代码,才会通过watcher.evaluate()去计算得到watcher.value的值,并把dirty设置为false,而evaluate的过程会去取data,就会让computed watcher和所需的data绑定依赖。
这里还要关注一个问题,就是data的变化会通知computed watcher去设置dirty,就是上面第1步,但是render watcher是怎么知道数据变化,并去取vm.sum,即上面第2步。
这是借助上面computedGetter函数里的watcher.depend(),以computed watcher为桥梁,让render watcher和data之间做了一个依赖绑定。
接下来就详细讲解这几个过程在代码里是怎么做的。
2. computed初次计算及依赖绑定
2.1 组件mount创建render watcher触发computedGetter
组件mount时,会创建一个render watcher,传入的expOrFn参数是一个更新组件的函数updateComponent:
// vue/src/core/instance/lifecycle.js
export function mountComponent (
...
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...
}
render watcher的构造函数中,this.getter值得到的就是updateComponent这个函数,然后会主动去调用一自己的get函数,最终进入到updateComponent函数。
// vue/src/core/observer/watcher.js
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
...
// render watcher lazy 为 false, computed watcher 为 true
this.value = this.lazy ? undefined : this.get()
...
}
...
get() {
value = this.getter.call(vm, vm)
}
}
进入到updateComponent后,会调用vm._render函数,然后进入组件编译生成的render函数:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", [_vm._v(_vm._s(_vm.sum) + " ")])
}
因为组件模板中用到了{{ sum }},所以render函数中会引用到vm.sum这个值:
便会调用到vm.sum的computedGetter函数。
// vue/src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
此时dirty还是初始的赋值true,就会去调evaluate
2.2. computed初次计算及computed watcher的依赖绑定
// vue/src/core/observer/watcher.js
export default class Watcher {
...
evaluate () {
this.value = this.get()
this.dirty = false
}
...
get() {
...
value = this.getter.call(vm, vm)
...
return value
}
}
evaluate函数里,会调用computed watcher的getter,前面讲到,getter就是那this.a + this.b函数,进而就会去调到a和b的getter函数,这里就把computed watcher和a,b做了依赖绑定。
接下来把计算的值给了this.value,这便是computed的缓存值,并把dirty设置为false,如果下次数据没有更新,dirty为false,就会不调用evaluate,而是直接取这个值。
2.3. render watcher的依赖绑定
evaluate结束之后,接下来会去调用vm.sum的computed watcher的depend()
// vue/src/core/observer/watcher.js
export default class Watcher {
...
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
...
}
此时的Dep.target是render watcher,这里的this.deps,就是vm.sum的computed watcher刚绑定的依赖data.a和data.b的dep。
computed watcher的depend函数就实现了以自已为桥梁,让render watcher和data.a,data.b建立了绑定。
前面那个问题为什么render watcher没有直接引用data,却能知道data的变化,答案就在这里。
至此vm.sum的computed watcher已计算出了初始值,computed watcher和所依赖的data之间,render watcher和所依赖的data之间,也都进行了绑定,并且computed watcher的dirty值设为了false,watcher.value缓存了计算结果,下次取vm.sum的值不再evaluate计算,直接返回watcher.value值,除非data变化了。
3. data 变化通知 watcher 更新
通过上一节我们知道了,computed watcher和render watcher都绑定了对应的data,当data变化时,便会通知两个watcher去更新。
// vue/src/core/observer/watcher.js
export default class Watcher {
...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
...
}
3.1. computed watcher 更新
由于computed watcher由于lazy值为true,所以update过程很简单,就是将dirty设置为true,vm.sum再被get时,就会执行第2节中的一系列过程,重新走一遍计算新值和绑定依赖。
3.2. render watcher 更新
render watcher的更新则是一个异步操作,先通过queueWatcher,将自己添加到队列中:
// vue/src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
...
queue.push(watcher)
...
if (!waiting) {
waiting = true
...
nextTick(flushSchedulerQueue)
}
}
...
然后异步的在下一个时钟nextTick里执行flushSchedulerQueue,这就保证了,computed watcher的this.dirty = true的本时钟内同步赋值,会先于render watcher异步更新,而如果render watcher先更新,computed watcher的dirty还没有改为true,那么render watcher访问vm.sum时的computedGetter时,拿到的就是缓存的旧值。
nextTick会将flushSchedulerQueue放到一个callbacks数据组里,通过一个异步方法timerFunc,依次执行这些callbacks,这个异步过程的实现有4种,按照如下顺序依次检测哪种可用就用哪种:
- Promise
- MutationObserver
- setImmediate
- setTimeout
在下一个时钟,执行flushSchekulerQueue过程如下:
// vue/src/core/observer/scheduler.js
function flushSchedulerQueue () {
...
for (index = 0; index < queue.length; index++) {
...
watcher.run()
...
}
}
// vue/src/core/observer/watcher.js
export default class Watcher {
...
run () {
...
const value = this.get()
...
}
...
}
这里便又会去执行get过程,把第2节中的内容再次走一遍。