前言
我们通过响应式原理的分析得知,当数据改变之后,界面会跟着响应发生变化,Vue内部会再次执行_update方法,生成新的Vnode,比较新旧Vnode,然后进行更新dom的操作。
我们这里先定义同步更新 和 异步更新
同步更新:当数据发生变化后,立即执行_update方法,进行 生成新的vnode->dom更新的过程。
异步更新:数据发生变化后,不会立即响应数据的变化,进行 生成新的vnode->dom更新的过程。更新的过程会被推送到异步队列中,延迟执行。
为什么需要异步更新
我们思考一个问题,为什么需要异步更新机制呢?
我们以如下场景为例。
代码入下
<body>
<div id="app">
<p>{{sum}}</p>
<button @click="click">计算</button>
</div>
</body>
<script>
let data = {
sum: 0,
message: "hello vue",
}
var app = new Vue({
el: "#app",
data: data,
methods: {
click () {
this.sum += 10;
this.sum *= this.sum
}
},
})
</script>
如图所示
功能就是点击按钮,一开始sum = 0, 先计算 sum += 10; 再将sum放大十倍。
这里我们看到sum被重新赋值了两次,那么根据响应式原理分析得知,会调用两次sum的setter方法。
如何更新界面,就要分情况分析了。
如果是同步更新,那么就会调用两次更新操作,进行两次的 生成vnode->更新dom 的过程。
如果是异步更新,在第一次调用sum的setter方法的时候,把更新的过程推送到一个异步队列中,当同步代码执行完毕之后, 执行所有异步更新方法, 这时候sum已经等于100了,再根据这个数据去渲染界面,只会进行一次 生成vnode->更新dom 的过程。
上述示例说明了异步更新可以减少 重复生成vnode->dom更新的过程,上述事例只改变了两次数据影响不大,那如果用for循环是改变了千次万次数据,那么同步更新界面就会执行千次万次的 生成vnode->更新dom,异常消耗cpu资源导致界面卡顿等一系列问题。 所以综上分析得知,异步更新可以提升渲染效率。
vue异步更新实现
通过之前的数据响应式原理分析得知,当数据发生变化后,会调用setter方法,setter方法中执行了dep.notify()通知所有的Watcher(监听者,每个组件一个)更新界面。
Watcher的update方法,如果是同步更新,就会走run方法,如果是异步更新就会执行queueWatcher(this)方法,run方法没什么好说的,执行run方法会直接再次调用传入Watcher构造方法的_updater方法,由于是更新,所以就会进行 重新生成vnode -> dom diff -> dom更新 的过程。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 通常情况会走异步队列更新
queueWatcher(this)
}
}
接下来我们重点分析queueWatcher(this)方法。
通过源码分析可得知,queueWatcher(this)的主要逻辑就是将当前的Watcher保存到一个队列中,我们把这个队列queue称之为异步队列。
在方法的最后调用了nextTick(flushSchedulerQueue)方法
那么nextTick和flushSchedulerQueue是什么呢?
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 这里用一个has的哈希表存储已经存在与异步队列中等待下一次更新的Watcher(观察者)
// 如果队列中已经存在了,那就不重复添加了
if (has[id] == null) {
has[id] = true
// flushing是指是否正在执行异步队列中的更新方法
// 如果没有正在执行异步队列中的更新方法
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.
// 否则如果有正在执行的异步队列(每次是队列里的方法一批量执行的)
// 从后往前找到那个 同id的 Watcher将其替换掉
// (因为组件更新的过程中也会更改数据,将Watcher推到异步队列)
// 如果没找到,那么将会放到queue的末尾
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) {
// waitting表示正在更新
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 将flushSchedulerQueue加入到callbacks中
// 将更新的回调推送至callbacks中
nextTick(flushSchedulerQueue)
}
}
}
我们先看flushSchedulerQueue方法,源代码就不贴了
flushSchedulerQueue方法主要就是遍历queue中保存的Watcher,然后调用Watcher的run方法,run方法可以更新界面。
也就是是说nextTick(flushSchedulerQueue)传入参数flushSchedulerQueue是一个可以执行异步队列中所有Watcher的更新操作。
接下来我们分析一下nextTick方法,nextTick方法根据名称可以翻译为下一帧,我们暂且猜到nextTick方法可以将flushSchedulerQueue更新函数放到下一帧去执行。
精简化后的nextTick方法如下,可以看到,vue维护了一个callbacks全局变量,这个callbacks保存了等待执行的回掉方法,默认情况下保存了flushSchedulerQueue更新界面的方法。
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将cb保存至callbacks队列中
callbacks.push(() => {
cb.call(ctx)
})
// 如果当前空闲
if (!pending) {
// 当前忙
pending = true
// 执行callbacks中所有的回掉方法
timerFunc()
}
}
那么保存在callbacks中的方法是何时执行的呢?这就要分析timerFunc()这个函数了。
我们通过源码得知 timerFunc这个函数会经过一系列判断被赋值,如果当前 环境支持Promise那么timerFunc就是一个执行promise.then的操作,否则就是一个执行setTimeout的操作,调用的方法都是flushCallbacks
简化后的源码如下
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 异步执行flushCallbacks
// 就是将callback中保存的callback函数都执行掉
p.then(flushCallbacks)
}
isUsingMicroTask = true
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
flushCallbacks方法就是执行callbacks中保存的所有函数 ,callbacks中保存了flushSchedulerQueue刷新函数或者用户自定义的函数。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
通过以上分析得知 timerFunc方法执行以后,会将callback中保存的回掉函数推送到由浏览器维护的异步队列执行,因为promise.then中的方法将放到微任务队列中,settimeout中的方法会放到宏任务队列中,微任务和宏任务都是由执行环境维护的异步任务队列(如果对js异步执行的原理不明白的,请先了解js的同步和异步执行机制)
总之,我们的更新渲染函数都会被放到callbacks中等待同步代码执行完以后,再执行。
总结
当数据变化以后,监听数据的监听者Watcher会被保存在一个queue队列中, 此时callback[] 中有一个方法flushSchedulerQueue可以将保存在queue中全部的监听者Watcher都更新(dom更新)。但是数据改变以后,并不会立即调用callback[]中的方法,因为callback中方法的执行会在 promise.then 或者 settimeout 中。因为这些方法都是异步执行的。 所以当我们的数据发生变化以后, 并不会立即更新界面,而是等待同步代码执行完毕以后,才会异步的更新dom。 这也就是我们在同步代码中获取dom的时候,并不能获取到更新后的dom的原因,所以官方提供了$nextTick方法,在dom更新之后再 执行传入的回掉方法呢。