深入了解Vue中nexTick源码和原理
nextTick
是Vue的一个核心实现,也是开发者经常用到的方法之一
nextTick
与$nextTick
的区别
`nextTick`是当数据发生变化,更新后执行回调,全局的方法(一般不用)。
`$nextTick`是当dom发生变化,更新后执行的回调,回调的 this 自动绑定到调用它的实例上(用者居多)。
定义与理解
* 定义:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM
所以就衍生出来了这个获取更新后的DOM的Vue放。所以放在Vue.nextTick()回调函数中执行的应该是会对DOM进行操作的jsdaim
* 理解: nextTick(),是将回到函数延迟在下一次DOM更新数据后调用,简单的理解就是:当数据更新了,在DOM中渲染后,自动执行该函数
关于nextTick
我们比较常见的使用方法
this.$nextTick(() => {
console.log('DOM更新后开始回调')
})
挂载
此方法是在Vue
的原型链上挂载上去的,只有一句话指向nextTick
,第一个参数指的是要回调的函数,第二个参数为当前的实例对象
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
//...代码有忽略
}
Vue
在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务。
源码解
let timerFunc;
/* 优先检测微任务(micro task) */
// 检测浏览器是否原生支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
}
// 如果不支持Promise,则检测是否支持原生的 MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
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
}
/* 对于宏任务(macro task) */
// 检测是否支持原生 setImmediate(高版本 IE 和 Edge 支持)
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
// 以上都不支持的情况下,使用setTimeout
else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
以上代码中,Vue
会优先使用微任务,即会优先检测当前浏览器是否支持原生 Promise
,如果不支持的话再去检测是否支持原生的 MutationObserver
,如果还不支持,则表明当前浏览器不支持微任务,那么就使用宏任务。对于宏任务会优先检测是否支持原生 setImmediate
,如果不支持的话就使用 setTimeout 0
。
执行回调队列
核心源码解
// 执行队列中的每一个回调
function flushCallbacks () {
pending = false // 重置异步锁
// 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份并清空回调函数队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行回调函数队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
export function nextTick (cb?: Function, ctx?: Object) {
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()
}
// 如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
总结
通过以上就是对 nextTick
的源码分析,我们了解到数据的变化到 DOM
的重新渲染是一个异步过程,发生在下一个 tick。当我们在实际开发中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick
后执行