面试官:你能讲讲$nextTick吗?

233 阅读4分钟

面试官:你能讲讲$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,任务队列中存放的是一个个异步任务tasktask分为两类,分别是宏任务(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()