为什么使用nextTick
-
Vue的数据更新是采用
延迟异步更新的,就是说当我们修改了数据之后,页面并不会马上就更新,如果这个时候我们通过DOM操作来获取数据的话,获取的还是之前的旧的数据,这个时候我们就可以使用$nextTick方法,因为这个方法知道什么时候DOM更新完成。 -
原理:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
使用场景
在created 生命周期执行DOM操作
Vue中生命周期created函数执行时DOM其实并未进行渲染,这个时候可以将要进行DOM操作的代码放进this.$nextTick()的回调函数中,代码会在DOM节点更新完成后执行。总的来说,nextTick方法的作用就是让代码延迟执行
在数据变化后需要进行基于DOM结构的操作
在更新完数据后,如果还有操作要根据更新数据后的DOM结构进行,应当将这部分操作写入this.$nextTick()回调函数中
Vue.nextTick( [callback, context] )
-
参数
{Function}[callback]回调函数,不传参数时提供promise调用{Object}[context]回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上// 修改数据 vm.msg = 'Hello' // DOM 还没有更新 Vue.nextTick(function () { // DOM 更新了 }) // 作为一个 Promise 使用 Vue.nextTick() .then(function () { // DOM 更新了 })Vue实例方法
vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例
实现原理
通过源码知道,timeFunc这个函数起延迟执行的作用,它有四种实现的方式:
- Promise
- MutationObserver
- setImmediate
- setTimeout
// 执行flushCallbacks ()方法遍历callbacks数组,依次执行callbacks里的每个函数
function flushCallbacks () {
pending = false
// 拷贝
const copies = callbacks.slice(0)
// 清空
callbacks.length = 0
// 遍历执行
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 降级策略
// 判断使用哪种异步回调方式
// 先执行任务优先级高的微任务
// 首先尝试使用Promise.then()——微任务
// 其次尝试使用MutationObserver()回调——微任务
// 再次尝试使用setImmediate()回调——宏任务
// 最后尝试使用setTimeout()回调——宏任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// 标记当前nextTick使用的是微任务
isUsingMicroTask = true
// 不是IE环境并且支持MutationObserver,
// MutationObserver是一个用于监视DOM变动的接口,监听一个DOM对象上发生的子节点删除、属性修改、文本内容的修改
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// 实例化一个MutationObserver 类
const observer = new MutationObserver(flushCallbacks)
// 创建一个文本节点
const textNode = document.createTextNode(String(counter))
// 监听文本节点,当数据发生变化的时候执行flushCallbacks
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
// 数据更新
textNode.data = String(counter)
}
// 标记当前nextTick使用的是微任务
isUsingMicroTask = true
// 判断是否支持setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 如果都不支持则选择setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
每次调用nextTick函数时
- 把传入的回调函数
cb压入callbacks数组中 - 执行
timerFunc函数,延迟调用flushCallbacks函数 - 遍历执行
callbacks数组中的所有函数
/ 声明nextTick函数,接受一个回调函数和一个执行上下文为参数
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调函数存放到数组当中后面会遍历执行其中的回调
callbacks.push(() => {
if (cb) { // 对传入的回调进行try catch 错误捕获
try {
cb.call(ctx)
} catch (e) { // 进行统一的错误处理
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果没有pending的回调,就执行timerFunc函数选择当前的环境优先支持的异步方法
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
// 如果没有传入回调函数,并且当前环境也支持Promise,则返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
原理总结
以上就是VUE的nextTick方法的实现原理总结一下:
-
vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
-
MicroTask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
-
因为兼容性问题,vue不得不做了microtask向macrotask的降级方案