这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
前言
JS事件循环和异步执行机制在 Vue 中的一个应用场景 —— $nextTick
源码总览
看看 next-tick.js 中主要做了什么事情:
- 定义了三个变量,一个函数 callbacks:Array pending:Boolean flushCallbacks:Function timerFunc:undefined
- 进行一堆
if..else if..判断 (是给 timerFunc 降级赋值的过程) - 抛出一个函数 nextTick
// next-tick.js
let callbacks = [];
let pending = false;
// flushCallbacks ---------------------------------
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// timerFunc ---------------------------------
let timerFunc;
// 1.优先考虑Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2.降级到MutationObserver
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)) {
// 3.降级到setImmediate(只有IE支持)
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4.以上都不支持,用setTimeout兜底
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// nextTick ---------------------------------
export function nextTick (cb) {
// 把传进来的回调函数放到callbacks队列里
callbacks.push(cb);
// pending代表一个等待状态 等这个tick执行
if (!pending) {
pending = true
timerFunc()
}
// 如果没传递回调 提供一个Promise化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
逐个瞅瞅
callbacks
存储任务队列。在 nextTick 中收集,在 flushCallbacks 中依次执行
pending
pending 代表一个等待状态
flushCallbacks 函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0) //Q1:为什么要拷贝?
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
循环遍历,按照队列数据结构 “先进先出” 的原则,逐一执行所有 callback 。
Q1:执行 callbacks 里的任务,为什么要拷贝出来遍历,而不是直接遍历? 如果某一个 callback 执行的时候,又一次调用了 nextTick,进而更新了 callbacks,那这个时候的执行就不是我们所期望的了。所以需要拷贝出来,并清空队列,这时再更新 callbacks 也不影响我们的循环和执行,符合预期。
timerFunc 函数
timerFunc 是在那堆 if..else if.. 中赋值的,主要是做了环境的判断和兼容处理。优先使用 Promise 异步任务,按环境逐步做降级赋值:
Promise -> MutationObserver -> setImmediate -> setTimeout
Promise 分支
- 判断环境是否支持
Promise并且Promise是否为原生- 使用
Promise异步调用flushCallbacks函数- 当执行环境是 iPhone 等,使用
setTimeout异步调用noop,iOS 中在一些异常的webview中,promise结束后任务队列并没有刷新所以强制执行setTimeout刷新任务队列- IE 不支持 Promise,其他现代浏览器最新版本已完全支持
- 了解更多:Promise
MutationObserver 分支
- 对非IE浏览器和是否可以使用 HTML5 新特性
MutationObserver进行判断- 实例一个
MutationObserver对象,这个对象主要是对浏览器 DOM 变化进行监听,当实例化MutationObserver对象并且执行对象observe,设置 DOM 节点发生改变时自动触发回调- 把
timerFunc赋值为一个改变 DOM 节点的方法,当 DOM 节点发生改变,触发flushCallbacks(这里其实就是想用利用MutationObserver的特性进行异步操作)- 了解更多:MutationObserver
setImmediate 分支
- 判断
setImmediate是否存在,目前只有最新版本的 IE 和Node.js 0.10+ 实现了该方法- 如果存在,传入 flushCallbacks 执行
setImmediate- 了解更多:window.setImmediate
setTimeout 分支
- 当以上所有分支异步 api 都不支持的时候,使用
macro task(宏任务)的setTimeout执行flushCallbacks
nextTick 函数
- 声明一个局部变量 _resolve
- 把所有回调函数压进 callbacks 中,以栈的形式的存储所有 callback
- 当 pending 为 false 时,执行 timerFunc 函数
- 当没有 callback 的时候,返回一个 Promise 的调用方式,可以用 .then 接收
简单的说,nextTick 就是先收集所有需要异步执行的任务(放到 callbacks中),然后在下一个 tick时执行 callbacks 的任务。
为什么 :为了更好的性能,将更新 DOM 操作存放在异步更新队列中,在下一个 tick 统一进行更新 DOM 操作。 如果我们每更新一次数据,Vue 就需要去更新一次 DOM 操作的话,得有多卡顿? 所以 Vue 中就采用了异步更新队列这种方式来进行优化,也就是依赖上边我们分析的 nextTick 所做的最核心的事情。
参考链接
从 Event Loop 角度解读 Vue NextTick 源码
Vue - The Good Parts: nextTick
Vue源码——nextTick实现原理