1000字带你掌握nextTick背后的原理
答:如果我们需要获取数据更新后的dom信息,比如动态获取dom的宽高、位置等,就需要使用nextTick。handleClick () {this.msg = ‘hello world’;this.$nextTick(() => {console.log(this.$refs.message.innerText) 
答:如果我们需要获取数据更新后的dom信息,比如动态获取dom的宽高、位置等,就需要使用nextTick
。
handleClick () {
this.msg = ‘hello world’;
this.$nextTick(() => {
console.log(this.$refs.message.innerText) // hello world
})
}
如vue官网的描述:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
以上出现了事件循环
的概念,其涉及到JS的运行机制,包括主线程的执行栈
、异步队列
、异步API
、事件循环
的协作,我们接下来先简单了解一下 JS 的运行机制。
二、JS 运行机制
JS 执行是单线程的,它是基于事件循环
的。事件循环大致分为以下几个步骤:
-
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
-
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
-
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
-
主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。消息队列中存放的是一个个的任务(task)。规范中规定 task 分为两大类,分别是 macro task
和 micro task
,并且每个 macro task
结束后,都要清空所有的 micro task
。执行顺序如下:
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
接下来,我们来了解一下macro task
和 micro task
的重要概念。
2.1 macro task
宏任务,称为task
-
macro task作用是为了让浏览器能够从内部获取javascript / dom的内容并确保执行栈能够顺序进行。
-
macro task调度是随处可见的,例如解析HTML,获得鼠标点击的事件回调等等。
2.2 micro task
微任务,也称job
-
micro task通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为做出反应,或者做出一些异步的任务,而不需要新建一个全新的task。
-
只要执行栈没有其他javascript在执行,在每个task结束时,micro task队列就会在回调后处理。在micro task期间排队的任何其他micro task将被添加到这个队列的末尾并进行处理。
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。
根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 micro task 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。
micro task的这一特性是做队列控制的最佳选择,vue进行DOM更新内部也是调用nextTick
来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个micro task后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行。
比如一段时间内,你无意中修改了最初代码片段中的 msg多次,其实只要最后一次修改后的值更新到DOM就可以了,假如是同步更新的,每次 msg 值发生变化,那么都要触发 setter->Dep->Watcher->update->patch
,这个过程非常消耗性能。
接下来我们就从源码分析vue中nextTick的实现。
三、nextTick源码解析及原理
/* @flow */
/* globals MutationObserver */
import { noop } from ‘shared/util’
import { handleError } from ‘./error’
import { isIE, isIOS, isNative } from ‘./env’
export let isUsingMicroTask = false
const callbacks = []
let pending = false
/**
* 对所有callback进行遍历,然后指向响应的回调函数
* 使用 callbacks 保证了可以在同一个tick内执行多次 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
*/
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copiesi
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times… so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
/**
* timerFunc 实现的就是根据当前环境判断使用哪种方式实现
* 就是按照 Promise.then和 MutationObserver以及setImmediate的优先级来判断,支持哪个就用哪个,如果执行环境不支持,会采用setTimeout(fn, 0)代替;
*/
// 判断是否支持原生 Promise
if (typeof Promise !== ‘undefined’ && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn’t completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn’t being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// “force” the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 不支持 Promise的话,再判断是否原生支持 MutationObserver
} else if (!isIE && typeof MutationObserver !== ‘undefined’ && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === ‘[object MutationObserverConstructor]’
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
// 新建一个 textNode的DOM对象,使用 MutationObserver 绑定该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
// 不支持的 MutationObserver 的话,再去判断是否原生支持 setImmediate
} else if (typeof setImmediate !== ‘undefined’ && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Promise,MutationObserver, setImmediate 都不支持的话,最后使用 setTimeout(fun, 0)
// Fallback to setTimeout.
tim 《大厂前端面试题解析+Web核心总结学习笔记+企业项目实战源码+最新高清讲解视频》无偿开源 徽信搜索公众号【编程进阶路】 erFunc = () => {
setTimeout(flushCallbacks, 0)
}
更多推荐
所有评论(0)