Vue源码解读:更新策略
之前介绍过初始化时 Vue 对数据的响应式处理是利用了Object.defifineProperty()
,通过定义对象属性 getter
方法拦截对象属性的访问,进行依赖的收集,依赖收集的作用就是在数据变更的时候能通知到相关依赖进行更新。
通知更新
setter
当响应式数据发生变更时,会触发拦截的 setter 函数,先来看看 setter :
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
// 劫持修改操作
set: function reactiveSetter (newVal) {
// 旧的 obj[key]
const value = getter ? getter.call(obj) : val
// 如果新旧值一样,则直接 return,无需更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// setter 不存在说明该属性是一个只读属性,直接 return
if (getter && !setter) return
// 设置新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对新值进行观察,让新值也是响应式的
childOb = !shallow && observe(newVal)
// 依赖通知更新
dep.notify()
}
})
}
dep.notify()
// src/core/observer/dep.js
// 通知更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
遍历 dep
中存储的 watcher
,执行 watcher.update()
。
watcher.update()
// src/core/observer/watcher.js
export default class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
// 懒执行时走这里,比如 computed watcher
// 将 dirty 置为 true,计算属性的求值就会重新计算
this.dirty = true
} else if (this.sync) {
// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run方法进行更新
// 这个属性在官方文档中没有出现
this.run()
} else {
// 更新时一般都这里,将 watcher 放入 watcher 队列
queueWatcher(this)
}
}
}
queueWatcher
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* 将 watcher 放入 queue 队列
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已经存在,则跳过
if (has[id] == null) {
// 缓存 watcher.id,用于判断 watcher 是否已经入队
has[id] = true
if (!flushing) {
// 当前没有处于刷新队列状态,watcher 直接入队
queue.push(watcher)
} else {
// 正在刷新队列,这时用户可能添加新的 watcher,就会走到这里
// 从后往前找,找到第一个 watcher.id 比当前队列中 watcher.id 大的位置,然后将自己插入到该位置。保持队列是有序的。
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// waiting 保证了 nextTick 的调用只有一次
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 直接刷新调度队列
// 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
flushSchedulerQueue()
return
}
// nextTick => vm.$nextTick、Vue.nextTick
nextTick(flushSchedulerQueue)
}
}
}
nextTick
等会再看,它的作用主要就是把 flushSchedulerQueue
使用异步任务去执行,先尝试用微任务,不支持的情况再用宏任务去执行。
那么先看看 flushSchedulerQueue
的作用:
flushSchedulerQueue
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 对队列做了从小到大的排序,目的:
// 1. 组件的更新由父到子,因为父组件在子组件之前被创建,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
// 2. 一个组件的用户 watcher 先于渲染 watcher 执行,以为用户 watcher 创建先于渲染 watcher。
// 3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
queue.sort((a, b) => a.id - b.id)
// 在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 执行 beforeUpdate 生命周期钩子,在 mount 阶段创建 Watcher 时传入
if (watcher.before) {
watcher.before()
}
// 将缓存的 watcher 清除
id = watcher.id
has[id] = null
// 执行 watcher.run,最终触发更新函数
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 activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
//重置刷新队列状态
resetSchedulerState()
// keep-alive 组件相关
callActivatedHooks(activatedQueue)
// 执行 updated 生命周期钩子
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
/**
把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。
/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
/**
由子组件到父组件依次执行 updated 生命周期钩子
/
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')
}
}
}
上面代码可以看出 flushSchedulerQueue
的作用就是执行更新队列。通过 watcher.run()
触发最终的更新。
watcher.run()
// src/core/observer/watcher.js
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.cb = cb
}
run () {
if (this.active) {
// 调用 this.get 方法
const value = this.get()
if (
value !== this.value || // 新旧值不相等
isObject(value) || // 新值是对象
this.deep // deep模式
) {
// 更新旧值为新值
const oldValue = this.value
this.value = value
if (this.user) {
// 如果是用户 watcher
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 渲染 watcher,this.cb = noop,一个空函数
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
这里有两种情况,当 this.user
为 true
的时候代表用户 watcher
,在之前介绍过也就是 user watcher
, 否则执行渲染 watcher
的逻辑。
- user watcher
invokeWithErrorHandling
接收的第一个参数就是我们自定义侦听属性的回调函数,在初始化侦听属性 initWatch
方法过程中,实例化 new Watcher(vm, expOrFn, cb, options)
的时候传入。
第三个参数就是 [value, oldValue]
(新值和旧值),这也就是为什么在侦听属性的回调函数中能获得新值和旧值。
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
// 利用 try catch 做一些错误处理
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
如果是渲染 watcher
则执行 this.cb.call(this.vm, value, oldValue)
。渲染 Wather
的实例化是在挂载时 mountComponent
方法中执行的:
// src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
export function noop (a?: any, b?: any, c?: any) {}
是一个空函数,所以 this.cb.call(this.vm, value, oldValue)
,就是在执行一个空函数。
渲染 watcher
在执行 watcher.run
会调用 this.get()
,也就会执行 this.getter.call(vm, vm)
。this.getter
实际就是实例化时传入的第二个参数 updateComponent
。
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会进入 patch
的过程。
nextTick
前面介绍了 flushSchedulerQueue
的作用就是去执行更新队列,那么我们看看 queueWatcher
中的这段代码是怎么回事:
nextTick(flushSchedulerQueue)
nextTick
// src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用 callbacks 数组存储经过包装的 cb 函数
callbacks.push(() => {
if (cb) {
// 用 try catch 包装回调函数,便于错误捕获
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick
第一个参数是一个回调函数,这里的回调函数对应的就是 flushSchedulerQueue
了。通过 try catch
将回调函数包装,用于错误捕获,然后将其放入 callbacks
中。
这里使用 callbacks
而不是直接在 nextTick
中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick
,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
接下来当 pending
为 false
的时候执行 timerFunc
,pending
为 true
,表示正在将任务放入浏览器的任务队列中;pending
为 false
,表示任务已经放入浏览器任务队列中了。
最后,nextTick
在没有传入 cb
回调函数的时候,会返回 promise
,提供了一个 .then
的调用。
nextTick().then(() => {})
timerFunc
// src/core/util/next-tick.js
// 可以看到 timerFunc 的作用很简单,就是将 flushCallbacks 函数放入浏览器的异步任务队列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 首选 Promise
p.then(flushCallbacks)
/**
* 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
* 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
* 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
*/
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 然后使用 MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 然后 setImmediate,宏任务
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
flushCallbacks
// src/core/util/next-tick.js
/**
* 1、将 pending 置为 false
* 2、清空 callbacks 数组
* 3、执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)
*/
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
不管是全局 API
Vue.nextTick
,还是实例方法vm.$nextTick
,最后都是调用next-tick.js
中的nextTick
方法。
如果觉得还凑合的话,给个赞吧!!!也可以来我的 个人博客 逛逛!