在《响应式原理一:data 初始化》一文中,分析了 data
是如何将一个普通对象变成响应式对象,核心的实现是在函数 defineReactive
中采用 ES5 Object.defineProperty
定义了 get
和 set
函数,其作用是依赖收集和派发更新。那么,本文将分析派发更新的实现原理。
setter 实现原理
沿着主线将其实现逻辑整理成一张逻辑图如下:
当修改 data
属性值,会触发 setter
,更新视图;也就是说,会重新执行渲染逻辑,即 render
和 patch
过程,接着就一步一步地来分析该过程。setter
实现逻辑如下:
// src/core/observer/index.js
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #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
,如果有的话则执行;否则将新值赋值给旧值,即 val = newVal
。
然后,判断 shallow
的值,如果其值为 false
,则执行 observe
。
最后,调用实例 dep
方法 notify
通知所有订阅者做出相应的改变。
notify
实现原理
// src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
复制代码
变量 subs
是 watcher
数组,notity
作用是遍历数组 subs
,调用实例 watcher 方法 update
,通知已订阅的 watcher
做出变更。如果当前环境是开发环境,并且是同步执行的,则需要对 subs
进行排序,以确保它们的顺序。那么,来看 update
的实现逻辑:
// src/core/observer/watcher.js
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
复制代码
update
实现逻辑挺简单的,分为三种情况。如果 lazy
为 true
,则执行 this.dirty = true
,用于计算属性;如果 sync
为 true
,则执行实例 watcher
方法 run
,后续会分析;最后一种情况则执行函数 queueWatcher(this)
,即执行派发更新逻辑。
至于 lazy
和 sync
是如何来的呢?它们的初始化是发生在实例化 Watcher
,而对于 Watcher
,可分为渲染 Watcher
、Computed Watcher
、用户 Watcher
,用户 Watcher
比渲染 Watcher
先被初始化。
queueWatcher
实现原理
// src/core/observer/scheduler.js
/**
* 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)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
复制代码
Vue 框架在处理 watcher
时,并不是每当有数据变化时,就触发 watcher
的回调,而是会将其先加入到队列 queue
,然后等到下一个 tick
才执行队列 queue
里的 watcher
回调,即在 nextTick
后执行 flushSchedulerQueue
。
这里用对象 has
来保存 watcher
,以确保同一个 watcher
只被执行一次;然后通过 flushing
来控制逻辑的执行,如果其值为 false
时,则将 watcher
添加到队列 queue
;否则基于 id
和 index
,将 watcher
插入到队列中;最后通过 waiting
来保证 nextTick(flushSchedulerQueue)
只被执行一次。
先来分析 flushSchedulerQueue
的实现,下一节再来分析 nextTick
。
// src/core/observer/scheduler.js
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (b) {
watcher.before()
}
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
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
复制代码
函数的作用是刷新队列和执行 watcher
。那么,在刷新队列之前,先对队列做了排序,作用是为了确保:
- 组件的更新顺序是从父到子,因为组件的创建是先父后子;那么
watcher
的创建也是先父后子,执行顺序也理所当然的是先父后子; - 用户自定义
watcher
先于渲染watcher
,因为用户自定义watcher
先于渲染watcher
创建; - 如果一个组件在其父组件执行
watcher
期间被销毁,那么其watcher
的执行可以被跳过。
经过一番排序后,queue
队列所保存的 watcher
的顺序是从小到大。
接着,对 queue
队列进行遍历,获取到对应实例 watcher
,如果 watcher.before
不为空的话,则执行该回调,该回调是在初始化 watcher
时作为参数传进来的。
然后执行 watcher.run
,并且将对象 has
保存的 watcher
设置为空。在遍历的过程中,需要注意两点:
-
队列
queue
的长度没有被缓存下来,而是每次对其进行求值;原因是watcher.run
在执行的过程中,用户可能会添加新的watcher
,这样会再次执行到函数queueWatcher
(代码见上)。因为此时
flushing
为true
,从而执行else
逻辑,那么又是如何往队列queue
添加watcher
呢?插入逻辑是从队列后面往前找,找到第一个待插入
watcher
的id
比当前队列中watcher
的id
大的位置,把watcher
按照id
插入到队列中,从而使用queue
的长度发生变化。 -
在遍历的过程中,有一段
if
逻辑,即当在开发环境时,如果同一个watcher
执行的次数超过MAX_UPDATE_COUNT
,即 100 时,此时会在控制台抛出告警,提示watcher
的回调执行陷入死循环。那么这样的场景是如何触发的呢?export default { watcher: { message(oldValue, newValue) { this.message = Math.random() } } } 复制代码
那么 watcher.run
又是如何实现的呢?先来分析其逻辑,再回过头来分析剩下的逻辑。
// src/core/observer/watcher.js
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
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
) {
// set new value
const oldValue = this.value
this.value = value
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)
}
}
}
}
复制代码
首先通过 this.get()
获取到它当前的值,然后判断是否满足条件。如果满足新旧值不一样、value
其数据类型为对象、deep
为 true
中的任何一个,都会执行 if
逻辑。
如果当前是用户自定义 watcher
,则调用函数 invokeWithErrorHandling
;否则是渲染 watcher
或者 computed watcher
,通过 call
执行其回调函数。
对于渲染 watcher
而言,在执行 this.get()
的过程中,会触发其 getter
,即
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
复制代码
也就是说,当我们修改响应式数据时,会触发组件重新渲染,即执行 render
和 patch
。
那么来看下 invokeWithErrorHandling
是如何实现的?
// src/core/utils/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
复制代码
其实现逻辑也挺简单的,也就是执行用户在自定义 watcher
时传进来的回调函数,此时会传入 oldValue
和 newValue
。所以,我们在外部才能拿到这两个值。
至此,watcher.run
实现逻辑分析完,回到 flushSchedulerQueue
分析剩下的逻辑。
在恢复状态之前,分别对 activatedChildren
、queue
拷贝一份,保存到变量 activatedQueue
、updatedQueue
,然后再执行恢复状态逻辑,即 resetSchedulerState
:
// src/core/observer/scheduler.js
/**
* Reset the scheduler's state.
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
复制代码
其实现逻辑挺简单的,就是把一些控制状态重置,以及 watcher
清空。
最后,分别触发组件的 updated
和 activated
钩子函数。先来看下是如何触发组件 activated
钩子函数的?
// src/core/observer/scheduler.js
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
复制代码
遍历队列 queue
,先对每个 watcher
设置属性 _inactive
为 true
,再调用 activateChildComponent
,实现如下:
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
复制代码
最后来看下如何触发组件 updated
钩子函数?
// src/core/observer/scheduler.js
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
复制代码
至此,派发更新逻辑已分析完。