前言:去年刷知乎的时候,看到一篇面经里有个问题是问vue的nextTick原理。猛然想到,我虽然经常使用这个方法,但一直知其然不知其所以然,于是扒了一下源码并着手写这篇文章作为记录。不过,因工作和生活上接连有事,一直到今天才修改整理出来。如有错漏,欢迎在评论区指正
一、 nextTick的作用
this.$nextTick(cb)将回调延迟到下次 DOM 更新之后执行。
首先,我们知道vue的一大特点是响应式,它的更新方式是异步进行dom更新,如果一个 watcher 被多次触发,只有最后一次有效。这种方式可以去除重复数据和不必要的dom操作,本是一个很好的优化,但某些时候如果我们需要获取修改后的数据或获取更新后的dom时就会出现一些问题。比如前几天群里有个同学问:“为什么用refs取不到dom?”。截图只有一行代码,信息极少,但如果代码不出错,大概率就是上述原因导致。遂推荐其套个nextTick,解决。
二、 nextTick的原理
nextTick源码较为简单,这里按源码结构一步步分析。
nextTick.js文件主要成员有六个,其中三个数据:isUsingMicroTask ,callbacks ,pending ,三个方法:flushCallbacks,timerFunc,nextTick 。
布尔值isUsingMicroTask 作为导出数据这里只修改未使用;数组callbacks 顾名思义,就是回调函数的集合;布尔值pending 为等待状态。
flushCallbacks为刷新执行所有回调函数的方法。
function flushCallbacks () {
pending = false //放掉等待状态
const copies = callbacks.slice(0) //复制回调数组
callbacks.length = 0 //重置回调数组
for (let i = 0; i < copies.length; i++) { //循环执行回调
copies[i]()
}
}
timerFunc为异步延迟包装方法。定义触发flushCallbacks方法的任务方式(根据运行环境选择宏任务或者微任务)。如果当前环境支持微任务Promise或者MutationObserver ,优先使用微任务。如果不支持则改用宏任务setImmediate 或setTimeout实现。这与event loop的运行机制有关,在我的理解中,event loop的每轮tick结束后(微任务队列执行完),浏览器即执行render,然后开始下一个宏任务。如果使用宏任务,才真正代表着“在dom更新后”。
最核心的nextTIck方法
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve //定义一个resolve指针
callbacks.push(() => {
if (cb) { //如果有回调,则使用ctx上下文执行回调
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) { //如果没有回调且当前环境可使用promise,
_resolve(ctx) //则执行resolve指针
}
})
if (!pending) { //如果当前不处于等待状态,
pending = true //挂起状态并执行timerFunc
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') { //如果没有回调且当前环境可使用promise,
return new Promise(resolve => { //返回一个执行promise,并保存resolve
_resolve = resolve
})
}
}
第一次看的时候我很奇怪,为什么已经在timerFunc里推微任务了,还要在最后没有cb的时候返回一个promise?
直到我看到了nextTick的另一种使用方法:await this.$nextTick()。