面试官:你能讲讲$nextTick吗?
我:将回调延迟到下次 DOM 更新循环之后执行。
面试官:没了吗?
我:没了!
2023年了,如果你像上面一样,面试个初、中级前端开发还行,面试高级前端开发,基本上就没戏啦!其实nextTick在Vue中还是非常重要的,下面就花一点时间,来学习一下它的原理吧。
用法
<template>
<div ref="myRef">年龄:{{age}}岁!</div>
</template>
<script>
export default {
data() {
return {
age: 18,
};
},
mounted() {
this.age = 19; //更改年龄为19岁
console.log(this.$refs['myRef'].innerHTML); //年龄:18岁!
this.$nextTick(()=>{
console.log(this.$refs['myRef'].innerHTML); //年龄:19岁!
})
},
};
</script>
在上面代码中,我们先更改了年龄为19,但是我们打印页面中的内容,还是未更新的,用$nextTick后,打印结果为正常。
这是因为Vue的DOM更新策略,Vue在更新DOM时是异步更新的,如果一个watcher多次更新,会被队列去重只更新一次,这样可以避免不必要的计算与更新。
那么,问题来了,
- Vue更新是异步的,到底是怎样的异步呢?
- nextTick是异步方法吗?
- 为什么nextTick一定能保证在Vue的DOM更新之后执行?
依次回答:Vue的DOM异步更新是通过nextTick执行的,nextTick是异步方法,所以我们在使用nextTick执行回调,一定会在Vue的DOM更新之后!说白了就是DOM更新时,利用了nextTick,而我们使用nextTick执行回调时,其实是往事件队列中添加事件,这个事件在DOM更新之后,所以一定能保证在Vue的DOM更新之后执行。
JS的运行机制
我们都知道JS是单线程的,而要实现单线程且非阻塞的方法就是事件循环机制,大致分为一下的几个步骤:
- 1.所有的同步任务都在主线程上执行,形成一个执行栈。
- 2.主线程之外,还存在一个任务队列。
- 3.执行栈中的所有同步任务执行完毕,就会依次读取任务队列,任务队列进去执行栈开始执行。
主线程的执行过程就是一个tick,任务队列中存放的是一个个异步任务task。task分为两类,分别是宏任务(macro task)和微任务(micro task),每当执行宏任务时,都要先清空该宏任务对应的微任务队列中所有的微任务。
常见的宏任务:
- setTimeout
- MessageChannel
- postMessage
- setImmediate
常见的微任务:
- MutationObsever
- Promise.then
MutationObsever
MutationObserver执行的函数会进入微任务队列,使用方式如下:
// 函数
function delayFuc() {
console.log('delayFuc执行了')
}
//微任务MutationObserver
const observer = new MutationObserver(delayFuc);
// 创建textNode
let textNode = document.createTextNode('1');
// 监听textNode, textNode改变就会调用delayFuc
observer.observe(textNode, { characterData: true })
textNode.data = '2'
console.log('改变textNode')
//改变textNode
//delayFuc执行了
nextTick
nextTick的源码分为两步:
- 能力检测
- 根据能力检测以不同的方式执行回调队列
根据能力检测,优先使用微任务,如果浏览器不支持,再使用宏任务。
能力检测
微任务优先宏任务 判断是否支持各个异步任务 优先级 Promise.resolve().then() > MutationObserver > setImmidate > setTimeout
// 定义执行异步任务的方式
let timerFunc;
// 要执行的任务队列
function flushCallBacks(){};
if (typeof Promise !== 'undefined') {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallBacks)
}
} else if (typeof MutationObserver !== 'undefined') {
let textNode = document.createTextNode('1');
let observer = new MutationObserver(flushCallBacks);
observer.observe(textNode, { characterData: true });
textNode.data = '2';
} else if (typeof setImmidate !== 'undefined') {
timerFunc = function () {
setImmidate(flushCallBacks)
}
} else {
timerFunc = function () {
setTimeout(flushCallBacks, 0)
}
}
上面代码的意思是:按照优先级,判断浏览器是否支持这些异步方法,列如:支持Promise的话,就使用Promise.resolve().then()微任务来执行回调任务队列。
当然,Vue源码中,还包含了其他能力检测,如MessageChannel等,大致的意思是一样的
执行回调队列
// 定义控制执行
let pending = false;
// 要执行的事件队列
let callBacks = [];
// 依次执行callBacks中添加的函数
function flushCallBacks() {
//依次执行
callBacks.forEach(cb => {
cb();
})
// 重置状态 进行下一个tick
pending = false;
callBacks = [];
}
function nextTick(cb) {
// 将执行函数,添加到事件集合中
callBacks.push(cb)
if (!pending) {
pending = true;
timerFunc()
}
}
上面代码中,nextTick函数功能为往callBacks事件队列中添加要执行的事件cb,初始时,pending = false,一旦调用了nextTick后,就将pending = true,等待执行异步任务timerFunc,在执行异步任务前,多次调用nextTick,继续往callBacks中添加事件,是早于timerFunc执行的,等事件添加完毕,开始执行timerFunc。timerFunc会执行回调函数flushCallBacks,将事件队列中的事件依次执行,然后将pengding置为false,清空事件队列。
完整代码
// 定义控制执行
let pending = false;
// 定义要执行的函数集合
let callBacks = [];
// 定义执行异步任务的方式
let timerFunc;
// 清空 callBacks中添加的函数
function flushCallBacks() {
//依次执行
callBacks.forEach(cb => {
cb();
})
// 重置状态 进行下一个tick
pending = false;
callBacks = [];
}
// 微任务优先宏任务 判断是否支持各个异步任务 优先级 Promise.resolve().then() > MutationObserver > setImmidate > setTimeout
if (typeof Promise !== 'undefined') {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallBacks)
}
} else if (typeof MutationObserver !== 'undefined') {
let textNode = document.createTextNode('1');
let observer = new MutationObserver(flushCallBacks);
observer.observe(textNode, { characterData: true });
textNode.data = '2';
} else if (typeof setImmidate !== 'undefined') {
timerFunc = function () {
setImmidate(flushCallBacks)
}
} else {
timerFunc = function () {
setTimeout(flushCallBacks, 0)
}
}
function nextTick(cb) {
// 将执行函数,添加到事件集合中
callBacks.push(cb)
// 第一次执行时 为真
if (!pending) {
pending = true;
timerFunc()
}
}
function a() {
console.log('a');
}
function b() {
console.log('b');
}
function c() {
console.log('c');
}
nextTick(a)
nextTick(b)
nextTick(c)
function d() {
console.log('d');
}
d()