本篇文章,我将详细介绍Vue的更新策略。
首先,让我们来思考一下,对于Vue的更新环节,我们应该从哪里入手开始阅读呢?仔细想一想,Vue中由数据影响视图,是不是当我们修改了响应式的数据时,Vue就会启动更新机制呢?而在前面的几节中,我们也得知了数据的响应式,得益于我们对属性描述符 get,set 操作的拦截,更新环节也从这里开始进入讲解。
defineReactive
// src/core/observer/index.js
export function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
...
Object.defineProperty(obj, key, {
...
set: function reactiveSetter (newVal) {
...
dep.notify()
}
})
}
dep.notify
读源码可知,在defineReactive中的set操作中,调用了dep的notify方法,而更新流程由此开始。
class Dep {
subs: Array<Watcher>
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher.update
当调用了notify,我们首先拷贝与当前数据相关的所有watcher,然后调用它们的update方法。
class Watcher {
update () {
if (this.lazy) {
// computed缓存相关
this.dirty = true
} else if (this.sync) {
// 如果设置了watcher的sync为true,直接同步执行
this.run()
} else {
queueWatcher(this)
}
}
}
queueWatcher
正常的情况下,我们会调用queueWatcher方法
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 只处理还没有入队列的watcher
if (has[id] == null) {
// 入队
has[id] = true
// flushing表示正常冲刷,也就是正在更新的意思
if (!flushing) {
// 没有在更新时,直接推入队列即可
queue.push(watcher)
} else {
let i = queue.length - 1
// index为正在更新的watcher的id
while (i > index && queue[i].id > watcher.id) {
i--
}
// 用splice的方式将该watcher推入队列,需要注意的是,如果当前更新的watcher的id比该watcher的要小,意味着该watcher将会进入下一次的更新队列中
queue.splice(i + 1, 0, watcher)
}
// 如果没有开启队列
if (!waiting) {
// 开启队列
waiting = true
// 调用nextTick方法,将flushSchedulerQueue传入,这里的nextTick等同于我们使用的this.$nextTick
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher做了两件事儿,一个是将当前的watcher传入队列,第二个就是打开了更新队列,但是需要注意的,此时并没有开始更新。
nextTick
我们传入了一个flushSchedulerQueue函数给nextTick,接下来,我们来看看这之中又发生了什么。
export function nextTick(cb, ctx) {
let _resolve
callbacks.push(() => {
if (cb) {
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主要实现了两个功能,一个是向callbacks数组存入该flushSchedulerQueue回调函数,第二个是添加异步任务到事件循环队列之中。添加的关键就在于timerFunc函数。
timerFunc
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
timerFunc就是一个更新策略的选择,也可以说是对异步任务的兼容操作。具体实现的优先级为Promise > MutationObserver > setImmediate > setTimeout,关键点在于,我们总会以一个异步的操作去执行flushCallbacks函数,也是从这里开始,后面的任务执行都是异步下进行的啦。
flushCallbacks
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
该函数的操作很简单,执行callbacks中的所有函数,其中的函数又是什么呢,其实就是我们前面存入的flushSchedulerQueue
flushSchedulerQueue
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 需要提前进行排序,有三点原因:第一是需要从父组件到子组件更新,第二是用户级watcher先于系统的watcher,第三是当组件在父组件的watcher还在进行run操作时被销毁了,则会跳过不进行更新。
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// watcher的run方法才是更新的关键
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
// 重置更新队列
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
watcher.run / get
class Watcher {
run () {
if (this.active) {
// 调用了watcher的get方法
const value = this.get()
// 以下的内容主要是对于用户级watcher的处理,不在本节的考虑范畴中
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
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)
}
}
}
}
get () {
// 结合之前的内容,代表以下内容是需要进行响应式的
pushTarget(this)
let value
const vm = this.vm
try {
// 调用watcher.getter
value = this.getter.call(vm, vm)
} catch (e) {
// 错误处理
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
// 递归的遍历并且转换它
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
从上面可以看出,最终当我们调用了传入的getter方法,就会更新所有内容了。那么又有一个问题来了,我们说了这么多,但是并没有提到,watcher是哪里开始调用的,其实这个也很简单,在我们了解到整个Vue到机制之后,我们可以很快速的得出一个结论:在Vue挂载的时候,我们会进行数据响应式处理。挂载这个词是不是非常的熟悉,我们在前几章中,有提到过这个词的哦,不记得的小伙伴可以回去看一看。
$mount
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
挂载的实质就是调用了mountComponent的方法
export function mountComponent (vm, el, hydrating) {
vm.$el = el
// 我们在之前已经进行了render的添加处理,如果此时还是没有render,那么就代表出错了
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 调用钩子
callHook(vm, 'beforeMount')
// updateComponent的实质就是Vue的更新函数
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 传入的第二个参数就是前文所说的watcher.getter
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
以上可得,在组件挂载时,我们会进行new Watcher的操作,执行并传入更新函数updateComponent,这就是watcher.getter,而在后续的数据更新时,会再次调用它,完成组件更新操作,而在updateComponent中,vm._render()实际上更多的是对template的处理,返回一个当前的虚拟DOM,而vm._update()则进行的是Vue的patch操作,进行节点的更新。
至此,本篇的更新流程都已讲述完毕,下一节我会从updateComponent出发,讲述一下patch函数究竟做了什么事儿。