前言
$nextTick
是个很常用的API,简单来讲其作用是让函数延后执行。
来看下官方的描述
在深入响应式原理的文章中也有介绍到
上面官方的描述,其实也解答了,nextTick就是使用Promise、setTimeout等异步函数实现的。
分析
基本用法
<section id="app">
<div id="count">{{ count }}</div>
<button @click="plus">+1</button>
</section>
new Vue({
name: 'SimpleDemoAPI',
data() {
return {
count: 0
}
},
methods: {
plus() {
this.count += 1;
// 未更新前的值
console.log('alan->count sync', document.getElementById('count').innerText)
this.$nextTick(() => {
// 更新后的值
console.log('alan->count $nextTick', document.getElementById('count').innerText)
})
}
}
})
实现原理
源码分析
$nextTick
实际上是nextTick函数,在初始化Vue的时候,把nextTick赋值给了$nextTick
// src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
源码位置:src/core/util/next-tick.js
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 收集回调函数的队列
const callbacks = []
// 定义状态
let pending = false
// 清空回调队列的函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() // 把回调队列中的函数取出来执行
}
}
// 定义定时器函数,后面赋值
let timerFunc
// 如果运行环境支持Promise,则使用Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => { // 定义行数,赋值给timerFunc
p.then(flushCallbacks) // 将flushCallbacks作为resolve的回调函数,执行完回调队列中的函数
// ios中有特殊情况
// 在有问题的UIWebViews中,Promise.then不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。
// 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true // 标记使用微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 当运行环境不支持Promise时,判断下是否支持MutationObserver,如支持则使用MutationObserver
let counter = 1
// 用flushCallbacks作为MutationObserver的回调函数
const observer = new MutationObserver(flushCallbacks)
// 创建临时的文本节点,用MutationObserver观测它的变化,以触发new MutationObserver(flushCallbacks)执行
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => { // 将更改DOM的函数赋值给timerFunc
// 当执行nextTick时,会执行timerFunc,这里改变textNode的值,每次+1
// 触发new MutationObserver(flushCallbacks)执行
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 当运行环境不支持Promise、MutationObserver时,判断下是否支持setImmediate,如支持则使用setImmediate
// setImmediate这个API只在node环境下可用
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 如果上面的方式都不行,则使用setTimeout
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 调用$nextTick时,执行该函数
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve // 缓存Promise的resolve
// 收集回调函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 执行Promise的resolve回调
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 执行定时器函数,核心逻辑
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
实现逻辑:
-
初始化模块文件
-
定义
callbacks
-
定义状态
pending = false
-
定义冲刷队列函数
flushCallbacks()
-
定义定时器函数
timerFunc()
- 如果有Promise,直接执行
Promise.resolve(flushCallbacks)
- 如果有MutationObserver,就
new MutationObserver(flushCallbacks)
,然后创建一个文本节点,用observer观测它。在timerFunc()
里改变文本节点的值textNode.data = String((counter + 1) % 2)
,当执行timerFunc()
时,改变文本节点,变化一次就触发一次MutationObserver,就会执行flushCallbacks()
- 如果有setImmediate,就执行
setImmediate(flushCallbacks)
- 如果上面都不行,就使用setTimeout,执行
setTimeout(flushCallbacks, 0)
- 如果有Promise,直接执行
-
把回调函数cb,放入一个callbacks队列里
-
标记执行状态
pending = true
-
执行定时器函数
timerFunc()
,走timerFunc里面的逻辑
调用链路
一些疑问
ios下为什么要执行一下setTimeout?
在有问题的UIWebViews中,Promise.then
不会完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
为什么要用MutationObserver呢?
在不支持Promise的地方使用,例如:PhantomJS, iOS7, Android 4.4
降级到定时器,为什么优先选择setImmediate?
从技术上讲,它利用了(宏)任务队列,但它仍然是比setTimeout
更好的选择。
总结
nextTick其实挺简单的,底层就是使用了微任务/宏任务来实现,Promise -> MutationObserver -> setImmediate -> setTimeout
,将回调函数存放到队列中,然后利用事件循环的特点,每次循环结束前,先将微任务、宏任务清空。