背景
当我们对某项数据进行频繁更新时,会引起DOM的更新,会出现很严重的性能问题,vue中使用nextTick优化这个问题。
简单理解:每次数据变化之后不是立刻去执行DOM更新,而是把数据变化的动作缓存起来,在合适的时机只执行一次DOM更新操作,就需要设置一个合适的时间间隔。
理解nextTick的原理前需要理解两块前置知识
- Vue响应式原理
- Object.defineProperty
- 浏览器事件循环机制
- 宏任务,微任务
定义
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用该方法,获取更新后的DOM。
简单理解为:当页面中的数据发生改变,就会把该任务放到一个异步队列中,只有在当前任务空闲时才会进行DOM渲染,当DOM渲染完成以后,该函数就会自动执行。
解析:
在外层定义了三个变量,callbacks,pending,timerFunc,callbacks,其实就是队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。
callbacks新增回调函数后又执行了timerFunc函数,pending用来标识同一个时间只能执行一次。 timerFunc函数的定义:
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持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]'
)) {
//判断2:是否原生支持MutationObserver
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)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
解析:
上述出现了好几个isNative函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false。
代码其实是做了四个判断,对当前环境进行不断的降级处理, 尝试使用原生的Promise.then、MutationObserver和setImmediate,上述三个都不支持最后使用setTimeout;
降级处理的目的都是将flushCallbacks函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。
MutationObserver是Html5的一个新特性,用来监听目标DOM结构是否改变,也就是代码中新建的textNode;如果改变了就执行MutationObserver构造函数中的回调函数,不过是它是在微任务中执行的。
flushCallbacks;nextTick不顾一切的要把它放入微任务或者宏任务中去执行。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
对源码的解析
源码主要分为两部分:
判断当前环境能使用的最合适的API并保存异步函数
调用异步函数执行回调队列
环境判断
主要判断用哪个宏任务或微任务。
判断顺序如下
- Promise
- MutationObserver
- setImmediate
- setTimeout
export let isUsingMicroTask = false // 是否启用微任务开关
const callbacks = [] // 回调队列
let pending = false // 异步控制开关,标记是否正在执行回调函数
// 该方法负责执行队列中的全部回调
function flushCallbacks () {
// 重置异步开关
pending = false
// 防止nextTick里有nextTick出现的问题
// 所以执行之前先备份并清空回调队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行任务队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 保存一个异步任务
const p = Promise.resolve()
timerFunc = () => {
// 执行回调函数
p.then(flushCallbacks)
// ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
// 所以用一个空的计时器来强制刷新任务队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
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,虽然也是宏任务,但是比setTimeout更好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 以上都不支持的情况下,使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
环境判断结束就会得到一个延迟回调函数timerFunc
nextTick()
主要逻辑:
- 把传入的回调函数放进回调队列callbacks
- 执行保存的异步任务timeFunc,就会遍历callbacks执行相应的回调函数
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
})
}
}
vue.nextTick的应用场景
- created生命周期中操作DOM
- 修改数据,获取DOM值
- v-show/v-if由隐藏变为显示
Vue3中的nextTick的原理
nextTick接受一个函数为参数,同时会创建一个微任务,把参数fn赋值给p.then(fn),在队列的任务完成后,fn就执行了。
几个维护队列的方法,执行顺序是这样的: queueJob-->queueFlush-->flushJobs-->nextTick参数的fn
vue.nextTick(callback)、vue.$nextTick(callback)的区别
- vue.nextTick(callback)当数据发生变化,更新后执行回调
- vue.$nextTick(callback)当dom发生变化,更新后执行的回调