本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。
通过上一节分析我们了解了响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节我们来详细分析这个过程。
我们先来回顾一下 setter 部分的逻辑:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
...
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
setter 的逻辑有 2 个关键的点,一个是 childOb = !shallow && observe(newVal),如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者,这是本节的关键,接下来我会带大家完整的分析整个派发更新的过程。
过程分析
当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法, 它是 Dep 的一个实例方法,定义在 src/core/observer/dep.js 中:
class Dep {
// ...
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
这里的逻辑非常简单,遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法,它的定义在 src/core/observer/watcher.js 中:
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 的逻辑,queueWatcher 的定义在 src/core/observer/scheduler.js 中:
let has: { [key: number]: ?true } = {}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 主要是用于用户自定义的watch
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)
}
}
}
这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue。
也就是,当你在一个方法中修改了几个响应式对象的值,比如:
change() {
this.flag = true
this.msg = 'hello'
}
当执行到this.flag = true的时候,就去执行set方法,然后执行dep.notify(),接着就执行到queueWatcher,就走到了nextTick(flushSchedulerQueue),但是这个方法是异步的,所以程序会接着执行this.msg = 'hello',再走一遍上面的逻辑,但是此时waiting为true,所以不会再次执行nextTick(flushSchedulerQueue),当change函数执行完后,就开始执行nextTick(flushSchedulerQueue)。
这里有几个细节要注意一下,首先用 has 对象保证同一个 Watcher 只添加一次;接着对 flushing 的判断,else 部分的逻辑稍后我会讲;最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次,另外 nextTick 的实现之后会用一篇文章专门去讲,目前就可以理解它是在下一个 tick,也就是异步的去执行 flushSchedulerQueue。
接下来我们来看 flushSchedulerQueue 的实现,它的定义在 src/core/observer/scheduler.js 中。
let flushing = false
let index = 0
function flushSchedulerQueue () {
flushing = true
let watcher, id
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
...
}
...
resetSchedulerState()
}
这里有几个重要的逻辑要梳理一下,对于一些分支逻辑如 keep-alive 组件相关和之前提到过的 updated 钩子函数的执行会略过。
- 队列排序
queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
-
组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher的创建也是先父后子,执行顺序也应该保持先父后子。 -
用户的自定义
watcher要优先于渲染watcher执行;因为用户自定义watcher是在渲染watcher之前创建的。 -
如果一个组件在父组件的
watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。
- 队列遍历
在对
queue排序后,接着就是要对它做遍历,拿到对应的watcher,执行watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对queue.length求值,因为在watcher.run()的时候,很可能用户会再次添加新的watcher,比如,用户自定义的watch中,如果写的代码如下:
methods: {
change() {
this.msg = Math.random()
}
},
watch: {
msg() {
this.msg = Math.random()
}
}
当我触发change函数的时候,改变了msg,那么此时就会触发重新渲染,在渲染过程中执行watcher.run(),这个函数就会执行用户自定义的msg(),执行后又改变了msg,那么再一次触发set方法,因为此时flushing为true,就会执行下面的逻辑,继续往队列中插入watcher:
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
这样就形成一个死循环,不断的往queue里面添加,当数量达到一定程度,默认是100,会报错,报错信息如下:
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
- 状态恢复
这个过程就是执行 resetSchedulerState 函数,它的定义在 src/core/observer/scheduler.js 中。
onst queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
逻辑非常简单,就是把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。
接下来我们继续分析 watcher.run() 的逻辑,它的定义在 src/core/observer/watcher.js 中。
class Watcher {
run () {
if (this.active) {
// 如果是渲染watcher value为undefined,如果是自定义watcher,则有返回值
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
// 用户自定义watcher
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
对于渲染watcher而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同。
对于自定义watcher而言,执行 watcher 的回调,回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。
通过这一篇文章的分析,我们对 Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。nextTick 是 Vue 一个比较核心的实现了,下一篇文章我们来重点分析它的实现。