🌲🌲🌲 前言
说起来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的回调函数一定是在dom元素更新任务之后立即执行的,那么怎么把他的回调函数放在dom元素更新任务之后呐?这就要说到下一部分:事件循环「Event Loop」
⚙️⚙️⚙️ 事件循环
想要弄明白nextTick的原理,还是需要知道事件循环相关的知识。在这里不细说「毕竟说起来Event Loop就太多了,还涉及到node中事件循环的不同,掘金已经有很多大佬写过,这里就给大家放一张我自己总结的图」
然后重要说一下在事件循环中各种任务的执行顺序:
- 一开始整段脚本作为第一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
- 执行浏览器 UI 线程的渲染工作
- 检查是否有Web worker任务,有则执行
- 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
根据上面的执行顺序,如果想要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?