这篇文章分析了Vue更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习,加深对Vue更新机制的理解
先看看下面的例子:
<div id="app">
<div id="div" v-if="isShow">被隐藏的内容</div>
<input @click="getDiv" value="按钮" type="button">
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
//控制是否显示#div
isShow: false
},
methods:{
getDiv: function () {
this.isShow=true
var content = document.getElementById('div').innerHTML;
console.log('content',content)
}
}
})
</script>
但是实际执行的结果确是,div可以显示出来,但是打印结果的时候会报错,错误原因就是innerHTML为null,也就是div不存在。
只有当我们再次点击按钮的时候才会打印出div里面的内容。这就是Vue的异步更新队列的结果
Vue的dom更新是异步的,当数据发生变化时Vue不是立刻去更新dom,而是开启一个队列,并缓冲在同一个事件中循环发生的所有数据变化。
在缓冲时,会去除重复的数据,避免多余的计算和dom操作。在下一个事件循环tick中,刷新队列并执行已去重的工作。
所以上面的代码报错是因为当执行this.isShow=true时,div还未被创建出来,知道下次Vue事件循环时才开始创建
查重机制降低了Vue的开销
异步更新队列实现的选择:由于浏览器的差异,Vue会根据当前环境选择Promise.then或者MuMutationObserver,如果两者都不支持,则会用setImmediate或者setTimeout代替
通过之前对Vue数据响应式的分析我们知道,当Vue数据发生变化时,会触发dep的notify()方法,该方法通知观察者watcher去更新dom,我们先看一下这的源码
//直接看核心代码
notify () {
//这是Dep的notify方法,Vue的会对data数据进行数据劫持,该方法被放到data数据的set方法中最后执行
//也就是通知更新操作
// stabilize the subscriber list first
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++) {
// !!!核心:通知watcher进行数据更新
//这里的subs[i]其实是Dep维护的一个watcher数组,所以我们下面是执行的watcher中的update方法
subs[i].update()
}
}
//这里只展示部分核心代码
//watcher的update方法
update () {
/* istanbul ignore else */
//判断是否存在lazy和sync属性
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
//核心:将当前的watcher放到一个队列中
queueWatcher(this)
}
}
下面看看queueWatcher的逻辑 from src/core/observer/scheduler.js
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)
}
}
}
from src/core/util/next-tick.js
//cb:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
//callbacks:这个方法维护了一个回调函数的数组,将回调函数添家进数组
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
})
}
from src/core/util/next-tick.js
/**这部分逻辑就是根据环境来判断timerFunc到底是使用什么样的异步队列**/
let timerFunc
//首选微任务执行异步操作:Promise、MutationObserver
//次选setImmediate最后选择setTimeout
// 根据当前浏览器环境选择用什么方法来执行异步任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//如果当前环境支持Promise,则使用Promise执行异步任务
const p = Promise.resolve()
timerFunc = () => {
//最终是执行的flushCallbacks方法
p.then(flushCallbacks)
//如果是IOS则回退,因为IOS不支持Promise
if (isIOS) setTimeout(noop)
}
//当前使用微任务执行
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
//如果当前浏览器支持MutationObserver则使用MutationObserver
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)) {
//如果支持setImmediate,则使用setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//如果上面的条件都不满足,那么最后选择setTimeout方法来完成异步更新队列
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
from src/core/util/next-tick.js
function flushCallbacks () {
pending = false
//拷贝callbacks数组内容
const copies = callbacks.slice(0)
//清空callbacks
callbacks.length = 0
//遍历执行
for (let i = 0; i < copies.length; i++) {
//执行回调方法
copies[i]()
}
}
上面这几段代码其实都是watcher的异步队列更新中的入队操作,通过queueWatcher方法中调用的nextTick(flushSchedulerQueue),我们知道,其实是将flushSchedulerQueue这个方法入队
所以下面我们看一下flushSchedulerQueue这个方法到底执行了什么操作
from src/core/observer/scheduler.js
/**我们这里只粘贴跟本次异步队列更新相关的核心代码**/
//具体的更新操作
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
//重新排列queue数组,是为了确保:
//更新顺序是从父组件到子组件
//用户的watcher先于render 的watcher执行(因为用户watcher先于render watcher创建)
//当子组件的watcher在父组件的watcher执行时被销毁,则跳过该子组件的watcher
queue.sort((a, b) => a.id - b.id)
//queue数组维护的一个watcher数组
//遍历queue数组,在queueWatcher方法中我们将传入的watcher实例push到了该数组中
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
//清空has对象里面的"id"属性(这个id属性之前在queueWatcher方法里面查重的时候用到了)
has[id] = null
//核心:最终执行的其实是watcher的run方法
watcher.run()
//下面是一些警告提示,可以先忽略
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
}
}
}
//调用组件updated生命周期钩子相关,先跳过
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
下面我们来继续看一下watcher.run方法,到底执行了什么操作
from src/core/observer/watcher.js
/**
* Scheduler job interface.
* Will be called by the scheduler.
* 上面这段英文注释 是官方注释,从这我们看出该方法最终会被scheduler调用
*/
run () {
if (this.active) {
//这里调用了watcher的get方法
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) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${
this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
至此,Vue异步更新队列的核心代码我们就分析完了,为了便于理清思路,我们来一张图总结一下
我们都知道 . n e x t T i c k 方 法 , 其 实 这 个 ∗ ∗ .nextTick方法,其实这个 ** .nextTick方法,其实这个∗∗nextTick** 方法就是直接调用的上面的nextTick方法
from src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
注意,$nextTick()是会将我们传入的函数加入到异步更新队列中的,但是这里有个问题,如果我们想获得dom更新后的数据,我们应该把该逻辑放到更新操作之后
因为加入异步队列先后的问题,如果我们在更新数据之前入队的话 ,是获取不到更新之后的数据的
总结起来就是,当触发数据更新通知时,dep通知watcher进行数据更新,这时watcher会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。
这样就达到了多次更新同时完成,提升了用户体验,减少了浏览器的开销,增强了性能。