「Vue系列」之面试官问NextTick是想考察什么?

6,409 阅读5分钟

🌲🌲🌲 前言

    说起来nextTick相信大家也都耳熟能详,虽然在业务开发中用到的次数不是很多,但是在面试题汇总中出现的频率可是不低。那么nextTick到底能考察我们什么知识,我们一块来分析分析。「如果对你有帮助,点赞是对我最大的鼓励哦,如果理解有误的地方,希望大佬指出,不胜感激。❤️」

🌴🌴🌴 铺垫

    在看nextTick之前,我们先铺垫一点相关的前置知识。

🧩🧩🧩 异步更新

Vue响应式更新并不是数据变化之后Dom立即发生变化,而是按照一定策略进行更新的

    正是因为Vue是异步更新Dom,所以当我们修改数据之后,Dom节点的内容不会立即修改,我们这样获取Dom节点的新内容的时候,获取的还是旧的内容。
Tipes:为什么Vue要使用异步更新Dom?避免不必要的计算和 DOM 操作,优化性能。

「一定策略:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。」

const text = ref(null);
let a = ref('0');
const testFun = async () => {
  a.value = '111111';
  console.log(text.value.innerHTML, 'beforeNextTick');
  await nextTick();
  console.log(text.value.innerHTML, 'afterNextTick');
};

控制台输出:

nextTick测试输出.png
    那么由此可见,nextTick的回调函数一定是在dom元素更新任务之后立即执行的,那么怎么把他的回调函数放在dom元素更新任务之后呐?这就要说到下一部分:事件循环「Event Loop」

⚙️⚙️⚙️ 事件循环

    想要弄明白nextTick的原理,还是需要知道事件循环相关的知识。在这里不细说「毕竟说起来Event Loop就太多了,还涉及到node中事件循环的不同,掘金已经有很多大佬写过,这里就给大家放一张我自己总结的图」

事件循环.png 然后重要说一下在事件循环中各种任务的执行顺序:

  1. 一开始整段脚本作为第一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  4. 执行浏览器 UI 线程的渲染工作
  5. 检查是否有Web worker任务,有则执行
  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

事件循环.png

    根据上面的执行顺序,如果想要nextTick的回调函数一定是在dom元素更新任务之后立即执行的,那么就需要在更新DOM的那个任务后追加nextTick的回调函数,下面就从源码的角度去分析一下是怎么实现追加的。

🗂🗂🗂 深入

有了上面的铺垫,我们直接去看源码:
nextTick 的源码位于src/core/util/next-tick.js
nextTick源码主要分为两块:

  • 环境兼容
  • 不同方式执行回调队列 Tipes:在github上看源码的时候将github改为github1s,github秒变vscode,观码体验不要太爽哦
// 环境兼容
// 优先微任务检测
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 检测浏览器是否原生支持 Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // 基于typeOf检测一个没有被声明的变量,不会报错
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 检测是否支持原生的 MutationObserver
  // MutationObserver:是HTML5中的API,是一个用于监视DOM变动
  // 它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等
  // 回调是放在微任务队列中执行的
  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
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 最后使用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

Tipes:为什么优先使用微任务:按照上面事件循环的执行顺序,执行下一次宏任务之前会执行一次ui渲染,等待时长比微任务要多很多。所以在能使用微任务的时候优先使用微任务,不能使用微任务的时候才使用宏任务,优雅降级。

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]()
  }
}

...环境兼容...

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)
    }
  })
  // 说明本次循环没有执行timerFunc,遍历执行回调
  if (!pending) {
    pending = true
    // 遍历执行
    timerFunc()
  }
  // 处理不传回调函数的情况
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Tipes: nextTick只能获取执行顺序在他前面的dom更改,如果在nextTick后面再次修改,则获取不到。

📖📖📖总结

    从上面涉及到的内容可以总结出,当面试官问道nextTick原理的时候,其实想要考察的有「当然这些都是我瞎猜的哈哈」:

    1.vue的Dom异步更新策略
    2.事件循环相关的知识

    相比于其他Vue的Api来说,nextTick的原理还是比较简单的,而且源码的行数也比较少,所以看看就懂了,哈哈。
「有帮助记得帮我点点赞哦」
最后祝各位大佬学习进步,事业有成!🎆🎆🎆

Tipes:往期内容
# 面试的时候面试官是这样问我Js基础的,角度真刁钻
# 「算法基础」之二叉树的遍历和搜索
# 「vue3系列」使用Teleport封装一个弹框组件
# 「vue3系列」为什么用Proxy取代Object.defineProperty?

🔗🔗🔗链接

1.图解Event Loop
2.nextTick源码