前言
上文还漏了一些重要的诸如异步更新、computed等细节,本文补齐
正文
异步更新
上文讲到Watcher里的this.sync是来控制同步与异步触发依赖更新的。同步的话缺点很明显,试想一下如下例子
new Vue({
data: {
a: 1,
b: 2
},
template: `<div @click="change">
{{a}}{{b}}
</div>`,
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: `<div @click="change">
{{a}}{{b}}
</div>`,
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<Watcher> = []
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<Component> = []
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: `<div @click="change">
{{a}}
</div>`,
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详解











网友评论