前言
上文还漏了一些重要的诸如异步更新、computed
等细节,本文补齐
正文
异步更新
上文讲到Watcher
里的this.sync
是来控制同步与异步触发依赖更新的。同步的话缺点很明显,试想一下如下例子
new Vue({
data: {
a: 1,
b: 2
},
template: `
{{a}}{{b}}
`,
methods: {
change() {
this.a = 2
this.b = 3
}
}
}).$mount('#app')
同时改动了this.a、this.b
,因为这俩属性都收集了renderWatcher
,若是同步的话那么就会执行俩遍渲染函数,这是不明智的,所以若是异步的话可以将其更新回调放入异步更新队列,就可以一次遍历触发,现在我们看看异步的逻辑,即
update() {
if (this.computed) {
// ...
} else if (this.sync) {
// ...
} else {
queueWatcher(this)
}
}
queueWatcher
方法在scheduler .js
里,具体看下文
computed
首先看看initComputed,从此我们可得知initComputed
就是劫持computed
,将其转化为响应式对象
计算属性其实就是惰性求值
watcher
,它观测get
里的响应式属性(若是如Date.now()
之类非响应式是不会触发变化的),一旦其变化这个get
就会触发(get
作为Watcher
的expOrFn
),如此一来该计算属性的值也就重新求值了
和普通
watcher
区别就是它不会立即求值只有在被引用触发其响应式属性get
才会求值,而普通watcher
一旦创建就会求值
new Vue({
data: {
a: 1
},
computed: {
b: {
get() {
return this.a + 1
},
set(val) {
this.a = val - 1
}
}
},
template: `
{{a}}{{b}}
`,
methods: {
change() {
this.a = 2
}
}
}).$mount('#app')
以此为例,转化结果如下
Object.defineProperty(target, key, {
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
},
set: function set(val) {
this.a = val - 1
}
})
从这可见只有get
被触发才会开始其依赖收集,这和普通watcher
创建就求值从而开始依赖收集是不一样的
依赖收集
如此例所示,
b
是依赖a
的,所以a的dep
得收集到b
的watcher
(这样子a
变化可以通知b
重新求值),模板渲染依赖b
,所以b的dep
得收集到renderWatcher
也就是当计算属性b
被读取(在此是模板引用{{ b }}
),该get
会被执行
首先其定义了watcher
变量来存储this._computedWatchers[key]
,通过前文我们知道在initComputed
里遍历vm.$options.computed
给每个都new Watcher
,所以该watcher
就是计算属性b
的观察者对象
watcher <==> new Watcher(vm, b.get, noop, { computed: true })
若该对象存在的话就会执行该对象的depend、evalute
首先我们看Watcher.depend
depend() {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
这个很简单,就是判断下this.dep && Dep.target
,存在的话就调用this.dep.depend()
。这个this.dep
就是计算属性的dep
,它初始化在Watcher constructor
constructor() {
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
我们可见非计算属性watcher
是直接执行this.get
来触发响应式属性的get
从而收集依赖,计算属性watcher
就是初始化了this.dep
也就是该响应式计算属性对应的dep
前者直接开始求值,后者只有在访问到的时候才求值
回到this.dep.depend()
方法,我们看上诉提到的a的dep
、b的dep
如何收集依赖
- 我们知道这个就是收集依赖,那么我们得知道
Dep.target
是什么,这个其实是renderWatcher
,因为计算属性b
被renderWatcher
依赖,也就是这b.get
是render
触发访问的,这就完成了b的dep
收集
watcher.depend()
完了之后还有return watcher.evaluate()
evaluate() {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
首先判断dirty
,我们之前就有说过true
为未求值、false
为已求值,这个就是computed
缓存来源
未求值的话就是执行this.get()
求值,其实这相当于执行b.get
。注意这里Dep.target
已经变成了计算属性b
的watcher
get() {
return this.a + 1
}
- 关键到了,这里访问了
this.a
就触发了a.get
,这样子就会导致a的dep
收集到计算属性b
的watcher
如此我们就完成了依赖收集
依赖触发
我们现在触发了例子里的change
函数,也就是修改this.a
的值(其实修改this.b
也一样内在还是修改this.a
)
我们知道
a.dep.subs <===> [renderWatcher, bWatcher]
那么修改a
就会触发这俩个watcher.update
update() {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
// 这里是个优化
queueWatcher(this)
}
}
这里除了computed
我们都有讲过,所以我们这里讲computed
this.dep <==> b.dep
this.dep.subs.length <==> b.dep.subs.length <==> [renderWatcher].length === 1
首先判断下这个this.dep.subs.length === 0
,我们知道dirty === true
是未求值,所以在该属性未被依赖的时候(未被引用)将dirty
置为true
(其实就是重置默认值),这样子当被依赖的时候(被引用)evaluate
就会重新求值(dirty === false
的话evaluate
不会重新求值)
此例来看this.dep.subs.length === 1
,所以走else
分支,调用getAndInvoke
方法重新求值设置新值之后执行this.dep.notify()
,通知订阅了b
变化的更新(也就是通知renderWatcher
更新)
scheduler.js
这个文件里存储的都是异步执行更新的相关方法
queueWatcher
就是个watcher
入队的方法
const queue: Array = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher(watcher: Watcher) {
const 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.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
就像这个注释所言该方法就是push
一个watcher
到观察者队列,总体来看就是入队列,然后调用nextTick
在下一个Tick
执行flushSchedulerQueue
,也就是在下一个Tick
之前会入队完毕,接下来我我们看如何入队的
首先就是获取这个入队的watcher.id
,我们定义了has
这个对象用于纪录入队的watcher
。先判断下这个watcher
是否已经入队,已入队的话就啥也不干,未入队的话就给此watcher
标记在has
上
然后就是判断下这个flushing
。它是用于判断是否执行更新中,也就是更新队列是否正在被执行,默认是false
。
- 若是未执行更新中自然就是简单入队即可
- 若是在执行更新中却有观察者要入队那么就得考虑好这个要入队的
watcher
插在哪,也就是得插入到正在执行的watcher
后面,假设已经有俩[{id: 1}, {id: 2}]
,假设已经循环到{id: 1}
这个watcher
,那么这时候index
还是0,我们要插入的位置也是{id: 1}
后面
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
很明显这个寻找插入点是倒序查找,这里判断queue[i].id > watcher.id
是因为flushSchedulerQueue
里对queue
做了针对id
的排序
最后就是判断下waiting
,这个就是个防止多次触发nextTick(flushSchedulerQueue)
的一个标志,算是个小技巧
这个方法包含俩部分:
- 观察者入队
- 下一个
tick
执行更新队列
flushSchedulerQueue
export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array = []
let circular: { [key: number]: number } = {}
function flushSchedulerQueue() {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
这个方法就是具体的执行更新队列的所在,首先就是将flushing
置为true
,然后就是将queue
队列按照watcher.id
从小到大排列这是有门道的主要是以下三点:
- 组件更新是父到子的,先创建父然后是子,所以需要父在前
-
userWatch
在renderWatch
之前,因为userWatch
定义的更早 - 若是一个组件在父组件的
watcher
执行期间被销毁,那么子组件的watcher
自然也得跳过,所以父组件的先执行
for (index = 0; index < queue.length; index++) {
// ...
}
这里就是存储执行更新时当前watcher
的索引index
的地方,这里有个点需要注意的是不存储queue.length
,因为在执行更新中queue
可能会变化
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
这里就是before
钩子所在
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */ )
就像这个renderWatcher
就有传入before
这就是
beforeUpdate
所在
然后就是给当前watcher
移出has
这个记录表,表示这个id
的watcher
已经处理了,可以继续入队,因为执行更新中也可能有watcher
入队,然后执行watcher.run()
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
这段很重要,就是在开发环境下对无限循环的一个提示。因为watcher.run
(watcher
回调)可能会导致has[id]
有值,如下所示:
new Vue({
data: {
a: 1
},
watch: {
'a': function aCallback(nVal, oVal) {
this.a = Math.random()
}
},
template: `
{{a}}
`,
methods: {
change() {
this.a = 2
}
}
}).$mount('#app')
在执行到flushSchedulerQueue
时,queue
会有俩个watcher
:a:userWatcher
、renderWatcher
首先是userWatcher
,在watcher.run()
(也就是aCallback
这个回调函数)之前has[id] = null
,然后执行了这个aCallback
又给this.a
赋值,这样子就是在执行更新中watcher
入队(set() -> dep.notify() -> watcher.update() -> queueWatcher(this)
)
如此一来执行到queue
下一个项其实还是当前这个userWatcher
,就没完没了了
所以这里定了个规矩,就是这同一个watcher
执行了MAX_UPDATE_COUNT
也就是100次那说明这个有问题,可能就是无限循环了。circular
就是这么个标识变量
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
这里我们先看updatedQueue
,它是queue
的浅拷贝对象。这是因为紧随其后调用了resetSchedulerState
,若不浅拷贝的话queue
就被置空了,这也杜绝了queue
被影响
// 重置scheduler状态
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
// 若是开发环境,那么就每轮更新执行之后置空这个无限循环检测标志
// 这是因为下面检测也是开发环境下检测的
// 也就是默认生存环境下不会出现这种糟糕的代码
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
他就是重置scheduler
里这么些方法所用到的标识变量
这里只在非生产环境重置了
circular
,这就代表生存环境下不会出现这种糟糕的代码
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
然后就是callUpdatedHooks
// 执行updated钩子
function callUpdatedHooks(queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
// 要是当前这个watcher是渲染watcher,而且已经挂载了,那么触发updated钩子
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
这里就是统一调用update
钩子,这个和before
非统一调用不一样,这里通过watcher
获取到vm
也就是当前的实例对象,这样子就可以取到vm. _watcher
也就是renderWatcher
,也可以取到vm._isMounted
。有了这俩个条件就可以调用生命周期update
了
这就是
updated
所在
if (devtools && config.devtools) {
devtools.emit('flush')
}
这里是开发者工具的事件传递flush
queueActivatedComponent
这个其实是入队激活的组件,类似queueWatcher
入队watcher
export function queueActivatedComponent(vm: Component) {
// setting _inactive to false here so that a render function can
// rely on checking whether it's in an inactive tree (e.g. router-view)
vm._inactive = false
activatedChildren.push(vm)
}
这里就是将入队的实例的激活状态(_inactive
)置为激活,然后入队到activatedChildren
以待后用
function callActivatedHooks(queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
这个其实就是统一遍历调用actibated
钩子,给每一项实例的激活状态(_inactive
)置为未激活(从未激活到激活)
注意这里activateChildComponent
传入的第二参数是true
,在讲到keep-alive
详解