官方说明:在下次 DOM 更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM。html
既然涉及到执行顺序,首先仍是简要的说下 JS 的执行机制数组
Event Loop 浏览器
阮一峰性能优化
全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。bash
主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。app
一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。dom
主线程不断重复上面的第三步。异步
浏览器环境下常见的函数
macro-task(宏任务): script, setImmediate, MessageChannel, setTimeout, postMessage,I/Ooop
micro-task(微任务): Promise.then, MutationObserver
这里只是个简单的转述,学习具体内容请点击上面的连接。
export const nextTick = (function () {
// 存储须要执行的回调函数
var callbacks = []
// 标识是否有 timerFunc 被推入了任务队列
var pending = false
// 函数指针
var timerFunc
// 下一个 tick 时循环 callbacks, 依次取出回调函数执行,清空 callbacks 数组
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 检测 MutationObserver 是否可用
// 当执行 timerFunc 时,改变监听值,触发观测者将 nextTickHandler 推入任务队列
if (typeof MutationObserver !== 'undefined') {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}
} else {
// 若是 MutationObserver 不可用
// timerFunc 指向 setImmediate 或者 setTimeout
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
}
// 返回的函数接受两个参数,回调函数和传给回调函数的参数
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
// 将构造的回调函数压入 callbacks 中
callbacks.push(func)
// 防止 timerFunc 被重复推入任务队列
if (pending) return
pending = true
// 执行 timerFunc
timerFunc(nextTickHandler, 0)
}
})()
复制代码
初版的 nextTick 实现 timerFunc 顺序为 MutationObserver, setImmediate,setTimeout
nextTick 最开始在 util/env.js 文件中,2.5.2版本迁移到 util/next-tick.js 中维护。
初版到2.5.2版本之间,nextTick 修改了屡次,修改的内容主要是 timerFunc 的实现。
第一次修改是将 MutationObserver 替换为 postMessage, 给出的理由是 MutationObserver 在 UIWebView (iOS >= 9.3.3) 中不可靠(如今是否有问题不清楚)。后面版本中又恢复了 MutationObserver 的使用,同时对 MutationObserver 使用作了检测, 非IE环境下且是原生 MutationObserver。
第二次改动是恢复了微任务的优先使用,timerFunc 检测顺序变为 Promise, MutationObserver, setTimeout. 在使用 Promise 时,针对 IOS 作了特殊处理,添加空的计时器强制刷新微任务队列。 同时这一版中还有个小的改动, nextTickHandler 方法中对 callbacks 数组重置修改成
callbacks.length = 0
复制代码
一个小的性能优化,减少空间消耗。
然而这个方案并无持续多久就迎来来一次‘大’改动,微任务所有裁撤,timerFunc 检测顺序变为 setImmediate, MessageChannel, setTimeout. 缘由是微任务优先级过高了,其中一个 issues 编号为 #6566, 状况以下:
<div class="header" v-if="expand">
<i @click="expand = false, countA++">Expand is True</i>
</div>
<div class="expand" v-if="!expand" @click="expand = true, countB++">
<i>Expand is False</i>
</div>
复制代码
上面代码想完成的效果很容易理解,点击切换 div。可是实际效果如上图所示,偏离预期,当点击一下时,彷佛两个 click 事件都被触发了,什么状况,一脸懵逼....
这里给个连接有兴趣能够去点点看 点我点我
尤大对此给出了回复,简而言之,点击 div.header, 触发标签 i 上绑定的事件,执行事件后
expand = false
countA = 1
复制代码
而后由于微任务优先级过高,在事件冒泡到外层 div 时就已经触发,更新期间,click listener 加到了外层div, 由于 dom 结构一致,div 和 i 标签都被重用,而后 click 事件冒泡到 div, 触发了第二次更新
expand = true
countB = 1
复制代码
因此出现了如图所示的尴尬结果。若是对这块想了解的更多,能够去找一下这个issue: #6566. 这里又要提到 JS 的事件机制了,task 依次执行, UI Render 可能在 task 之间执行, 微任务在 JS 执行栈为空时会清空队列。
以后又作了次小改动,在 MessageChannel 后添加了 Promise 处理 non-DOM envirment.
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
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 microTimerFunc
let macroTimerFunc
let useMacroTask = false
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb?: Function, ctx?: Object): ?Promise {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// 若是不传入回调函数就直接返回一个 Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
这一版抽到单独文件维护,而且引入 microTimerFunc, macroTimerFunc 分别对应微任务,宏任务。 macroTimerFunc 检测顺序为 setImmediate, Messagechannel, setTimeout, 微任务首先检测 Promise, 若是不支持 Promise 就直接指向 macroTimerFunc. 对外暴露了两个方法 nextTick 和 withMacroTask. nextTick 和以前逻辑变化不大,withMacroTask 对传入的函数作一层包装,保证函数内部代码触发状态变化,执行 nextTick 的时候强制走 macroTimerFunc。
此次修改的一个主要缘由是在任何地方都使用宏任务会产生一些很奇妙的问题,其中表明 issue:#6813。 点我点我
从上图能够看到列表的 display 有两个控制:
初始状态:
当快速拖动网页边框缩小页面宽度时,会先显示第一张图,而后快速的隐藏
这个过程也比较好理解,以前优先使用宏任务,在两个 task 之间,会进行 UI Render ,这时,li 的行内框设置失效,展现为块级框,在以后的 task 运行了 watcher.run 更新状态,再一次 UI Render 时,ul 的 display 的值切换为 none,列表隐藏。
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
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) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
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)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
惊奇的发现彷佛又回到了第一版,以前由于微任务优先级过高,太快的执行致使了非预期的问题,而此次的回归主要缘由是由于宏任务执行时间太靠后致使一些没法规避的问题,而微任务高优先级致使的问题是有变通的方法的,权衡以后,决定改回高优先级的微任务。
<div @click>
<i @click>Test</i>
</div>
复制代码
给出相似的 DOM 结构,点击 i 标签,触发回调事件,事件中对组件状态作了修改,当前 task 执行完成,检查微任务队列并所有执行,其中就会执行 flushSchedulerQueue 方法,flushSchedulerQueue 会执行全部收集到的 watcher 的 run 方法(这里涉及到响应式原理)以更新 DOM。而后 UI 从新渲染。而后取出下一个 task 执行,假设就是冒泡到 div 的click事件, 以后流程和上面的执行过程基本一致。因此就致使一次点击更新两次。
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)
}
复制代码
写法很朴素,感受很亲切。执行 timerFunc 让 textNode 的值在 0/1 变换,每次变化触发 observe 回调,在当前微任务队列后面添加一个 microtask 。 microtask 在执行过程当中产生的微任务会添加到当前队列后面等待执行,以前看过一篇文章说这个限制大约是1000,但暂时没有找到相关规范。