Vue源码分析—响应式原理(三)
派发更新
响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,我们来详细分析这个过程。
我们先来回顾一下setter
部分的逻辑:
/**
* Define a reactive property on an Object.
*/
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,
// ...
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()
}
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 () {
// stabilize the subscriber list first
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.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)
}
}
}
这里对于Watcher
的不同状态,会执行不同的逻辑,在一般组件数据更新的场景,会走到最后一个queueWatcher(this)
的逻辑,queueWatcher
的定义在src/core/observer/scheduler.js
中:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* 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
nextTick(flushSchedulerQueue)
}
}
}
这里引入了一个队列的概念,这也是Vue
在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发watcher
的回调,而是把这些watcher
先添加到一个队列里,然后在nextTick
后执行flushSchedulerQueue
。
这里有几个细节要注意一下,首先用has
对象保证同一个Watcher
只添加一次;接着对flushing
的判断;最后通过waiting
保证对nextTick(flushSchedulerQueue)
的调用逻辑只有一次,nextTick
目前就可以理解它是在下一个tick
,也就是异步的去执行flushSchedulerQueue
。
接下来我们来看flushSchedulerQueue
的实现,它的定义在src/core/observer/scheduler.js
中。
let flushing = false
let index = 0
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
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 (watcher.before) {
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')
}
}
这里有几个重要的逻辑要梳理一下,对于一些分支逻辑如keep-alive
组件相关和之前提到过的 updated
钩子函数的执行会略过。
- 队列排序
queue.sort((a, b) => a.id - b.id)
对队列做了从小到大的排序,这么做主要有以下要确保以下几点:- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher
的创建也是先父后子,执行顺序也应该保持先父后子。
2.用户的自定义watcher
要优先于渲染watcher
执行;因为用户自定义watcher
是在渲染watcher
之前创建的。
3.如果一个组件在父组件的watcher
执行期间被销毁,那么它对应的watcher
执行都可以被跳过,所以父组件的watcher
应该先执行。
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
- 队列遍历
在对queue
排序后,接着就是要对它做遍历,拿到对应的watcher
,执行watcher.run()
。这里需要注意一个细节,在遍历的时候每次都会对queue.length
求值,因为在watcher.run()
的时候,很可能用户会再次添加新的watcher
,这样会再次执行到queueWatcher
,如下:
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)
}
// ...
}
}
可以看到,这时候flushing
为true
,就会执行到else
的逻辑,然后就会从后往前找,找到第一个待插入watcher
的id
比当前队列中watcher
的id
大的位置。把watcher
按照id
的插入到队列中,因此queue
的长度发生了变化。
- 状态恢复
这个过程就是执行resetSchedulerState
函数,它的定义在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
/**
* 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
队列清空。
接下来我们继续分析watcher.run()
的逻辑,它的定义在src/core/observer/watcher.js
中。
class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
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
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
}
run
函数实际上就是执行this.getAndInvoke
方法,并传入watcher
的回调函数。getAndInvoke
函数逻辑也很简单,先通过this.get()
得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep
模式任何一个条件,则执行watcher
的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值value
和旧值oldValue
,这就是当我们添加自定义watcher
的时候能在回调函数的参数中拿到新旧值的原因。
那么对于渲染watcher
而言,它在执行this.get()
方法求值的时候,会执行getter
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch
的过程,但它和首次渲染有所不同。
总结
Vue
数据修改派发更新的过程,实际上就是当数据发生变化的时候,触发 setter
逻辑,把在依赖过程中订阅的的所有观察者,也就是watcher
,都触发它们的update
过程,这个过程又利用了队列做了进一步优化,在nextTick
后执行所有watcher
的run
,最后执行它们的回调函数。nextTick
是Vue
一个比较核心的实现了,接下来我们重点分析它的实现。