通过上一节分析我们了解了响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节来详细分析这个过程。
我们看下 defineReactive 中 setter 的逻辑:
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 新旧值相同,或都为 NaN 的情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// ...
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
setter 的逻辑有 2 个关键的点
childOb = !shallow && observe(newVal):对新设置的值变成一个响应式对象;dep.notify():通知所有的订阅者
1. 触发setter
当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法, 它是 Dep 的一个实例方法,定义在 src/core/observer/dep.js 中:
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice() // 浅拷贝返回一个新数组
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
- 首先浅拷贝
subs返回一个新的数组 - 遍历所有的
subs,也就是Watcher的实例数组,然后调用每一个watcher的update方法
class Watcher {
// ...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
在这里我们只关心 update 函数其实就是调用了 queueWatcher(this)
2. queueWatcher
下面来看下 queueWatcher 的实现:
// src/core/observer/scheduler.js
const queue: Array<Watcher> = [] // watcher 队列
let has: { [key: number]: ?true } = {} // watcher id map
let waiting = false // 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
let flushing = false // 是否正在调用 flushSchedulerQueue
let index = 0 // 当前正在作业的 watcher 在 queue 数组的下标
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // push watcher
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher) // 插入watcher
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
Vue并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到一个队列里,然后在nextTick后执行flushSchedulerQueue。has对象保证同一个Watcher只添加一次,但是当执行flushSchedulerQueue的过程中,watcher是可以被添加进队列的,因为在flushSchedulerQueue遍历queue的时候会执行has[id] = null- 接着对
flushing的判断:- 为
false表示还没有调用flushSchedulerQueue,此时将watcher推入queue队列。 else部分的逻辑稍后再说。
- 为
waiting:保证对nextTick(flushSchedulerQueue)的调用逻辑只有一次。nextTick:在之后章节会介绍,目前就可以理解它是在下一个tick,也就是异步的去执行flushSchedulerQueue。
Push a watcher into the watcher queue. Jobs with duplicate IDs will be skipped unless it's pushed when the queue is being flushed.
3. flushSchedulerQueue
接下来我们来看 flushSchedulerQueue 的实现,它的定义在 src/core/observer/scheduler.js 中。
// src/core/observer/scheduler.js
const queue: Array<Watcher> = [] // watcher 队列
let has: { [key: number]: ?true } = {} // watcher id map
let waiting = false // 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
let flushing = false // 是否正在调用 flushSchedulerQueue
let index = 0 // 当前正在作业的 watcher 在 queue 数组的下标
let circular: { [key: number]: number } = {}
export const MAX_UPDATE_COUNT = 100
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 排序
queue.sort((a, b) => a.id - b.id)
// 每次遍历都需要重新计算 length
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 执行 beforeUpdate 钩子
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
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 updatedQueue = queue.slice()
// 状态恢复
resetSchedulerState()
// 调用 updated 钩子
callUpdatedHooks(updatedQueue)
// ...
}
3.1 队列排序
queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
-
组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher的创建也是先父后子,执行顺序也应该保持先父后子。 -
用户的自定义
watcher要优先于渲染watcher执行;因为用户自定义watcher是在渲染watcher之前创建的。 -
如果一个组件在父组件的
watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。
3.2 队列遍历
在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()。
这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher:修改数据值从而触发 setter,这样会再次执行到 queueWatcher,如下:
// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // push watcher
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher) // 插入watcher
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
可以看到,这时候 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。因此 queue 的长度发生了变化。
3.2.1 watcher.run()
下面来看看当执行 watcher.run() 时发生了什么:
class Watcher {
// ...
run () {
// active: true
if (this.active) {
// 渲染watcher始终返回undefined
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
// 简化后的
this.cb.call(this.vm, value, oldValue)
}
}
}
}
通过 this.get() 得到它当前的值,然后做判断,如果满足以下条件之一:
- 新旧值不等
- 新值是对象类型
deep模式
则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。
那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 this.getter 方法,也就是 updateComponent:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同,在之后的章节会介绍。
3.2.2 循环判断
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
}
}
每次遍历 queue 时,用 circular[id] 来记录 watcher 对象的循环次数,当大于 MAX_UPDATE_COUNT 时认为是死循环
比如有以下例子:
<script>
export default {
data() {
return {
msg: 1
}
},
watch: {
msg() {
this.msg++
}
},
mounted() {
this.msg++
}
}
</script>
会产生如下报错:

3.3 状态恢复
这个过程就是执行 resetSchedulerState 函数,它的定义在 src/core/observer/scheduler.js 中。
// src/core/observer/scheduler.js
const 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
}
3.4 调用 updated 钩子
在 flushSchedulerQueue 函数的执行中还会执行 beforeUpdate 和 updated 钩子,这部分之前章节已经介绍过了,这里就不多说了。