1. 知识储备
在阅读源代码之前请按顺序阅读这些文章 / 视频:
vue.js: 异步更新队列 https://cn.vuejs.org/v2/guide/reactivity.html#异步更新队列
从浏览器多进程到 JS 单线程, JS 运行机制最全面的一次梳理 https://segmentfault.com/a/1190000012925872
Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014 https://www.youtube.com/watch?v=8aGhZQkoFbQ
- MDN:MutationObserver https://developer.mozilla.org/zh-CN/docs/web/API/MutationObserver
- MDN:MessageChannel https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
Tasks, microtasks, queues and schedules https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
vue.js 升级踩坑小记(可省略, 但是这篇文章给我的收获还是很大的) https://juejin.im/post/5a1af88f5188254a701ec230
2. 知识点小结:
这里只做一个最简单粗暴的知识点小结, 不包含任何解释.
宏任务(macrotask)
主代码块, setTimeout,setInterval, setImmediate,MessageChannel,postMessage
微任务(microtask)
promise,MutationObserver
任务执行顺序以及渲染的执行
macrotask -> microtask -> 渲染 -> macrotask -> microtask -> 渲染 -> ......
3. Vue 如何实现 .nextTick()
Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel, 如果执行环境不支持, 会采用 setTimeout(fn, 0) 代替.
异步队列很明显提高了性能, 但是如果我们想要在 DOM 更新之后做点什么, 可能就有点麻烦了 (详情看这 https://cn.vuejs.org/v2/guide/reactivity.html#异步更新队列 ), 因为主代码块在微任务列表之前, 而 Vue 是在微任务或者下一个宏任务中才更新 DOM 的, 这时候就需要使用. nextTick() 了.
Vue.js 2.5 之前, 几乎都是用 microtask 来模拟 Node.js 的. nextTick():
浏览器是否支持 Promise? 是则使用 Promise, 否则进行下一步
浏览器是否支持 MutationObserver, 是则使用 MutationObserver, 否则进行下一步
setTimeout (此时是一个 macrotask)
Vue.js 2.5 之后, 默认使用 microtask , 在 DOM 事件强制使用 macrotask:
先确定使用 macrotask 时用哪个 API, 优先级为:
setImmediate -> MessageChannel ->setTimeout
确定使用 microtask 时用哪个 API, 优先级为: Promise -> macroTimerFunc(和 macrotask 一致)
判断是否使用 macrotask , 是则调用 macroTimerFunc, 否则调用 microTimerFunc
DOM 事件默认会包裹一层函数来强制其使用 macrotask
4. 正题
Vue.js 2.5 之前,.nextTick()放在 env.js 中, 使用 Promise, MutationObserver, setTimeout 来实现异步队列:
- // env.js
- /* @flow */
- /* globals MutationObserver */
- import { noop } from 'shared/util'
- // 能否使用 __proto__?
- export const hasProto = '__proto__' in {}
- // 浏览器环境检测, 和本文无关, 可忽略
- export const inBrowser = typeof window !== 'undefined'
- export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
- export const isIE = UA && /msie|trident/.test(UA)
- export const isIE9 = UA && UA.indexOf('msie 9.0')> 0
- export const isEdge = UA && UA.indexOf('edge/')> 0
- export const isAndroid = UA && UA.indexOf('android')> 0
- export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)
- // this needs to be lazy-evaled because vue may be required before
- // vue-server-renderer can set VUE_ENV
- let _isServer
- export const isServerRendering = () => {
- if (_isServer === undefined) {
- /* istanbul ignore if */
- if (!inBrowser && typeof global !== 'undefined') {
- // detect presence of vue-server-renderer and avoid
- // Webpack shimming the process
- _isServer = global['process'].env.VUE_ENV === 'server'
- } else {
- _isServer = false
- }
- }
- return _isServer
- }
- export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
- /* istanbul ignore next */
- function isNative (Ctor: Function): boolean {
- return /native code/.test(Ctor.toString())
- }
- // 相关代码在这里
- export const nextTick = (function () {
- const callbacks = [] // 存放回调函数
- let pending = false // 是否有异步队列 (callbacks) 正在等待执行
- let timerFunc // 处理异步队列的函数(Promise,MutationObserver,setTimeout)
- function nextTickHandler () { // 清空 callbacks 列表, 执行 callback 列表中的函数
- pending = false // 表示没有异步队列在等待了
- const copies = callbacks.slice(0)
- callbacks.length = 0
- for (let i = 0; i <copies.length; i++) {
- copies[i]()
- }
- }
- // nextTick 利用了微任务队列, 微任务队列可以用原生的 Promise 或者 MutationObserver 来实现
- // MutationObserver 被广泛支持, 但是在 iOS>= 9.3.3 上会有严重的 bug.
- // 因此优先使用 Promise
- /* istanbul ignore if */
- if (typeof Promise !== 'undefined' && isNative(Promise)) { // 用 Promise 把回调函数推入微任务队列
- var p = Promise.resolve()
- var logError = err => { console.error(err) }
- timerFunc = () => {
- p.then(nextTickHandler).catch(logError)
- // 在 UIWebViews 中虽然 Promise.then 没有完全 break, 但是会陷入一个很奇怪的状态
- // 回调函数都被推入微任务队列中, 但是在浏览器处理别的任务 (比如 timer) 之前队列不会被清空.
- // 因此添加一个空的 timer 来强制清空微任务队列.
- if (isIOS) setTimeout(noop)
- }
- } else if (typeof MutationObserver !== 'undefined' && (
- isNative(MutationObserver) ||
- // PhantomJS and iOS 7.x
- MutationObserver.toString() === '[object MutationObserverConstructor]'
- )) {
- // Promise 不能用则用 MutationObserver,MutationObserver 也属于微任务
- // e.g. PhantomJS IE11, iOS7, Android 4.4
- var counter = 1
- var observer = new MutationObserver(nextTickHandler)
- var textNode = document.createTextNode(String(counter)) // 创建一个看不见的文本节点, 让 MutationObserver 来监听它
- observer.observe(textNode, {
- characterData: true
- })
- timerFunc = () => {
- counter = (counter + 1) % 2
- textNode.data = String(counter)
- }
- } else { // Promise 和 MutationObserver 都不能用
- // 用 setTimeout 代替, setTimeout 为宏任务
- /* istanbul ignore next */
- timerFunc = () => {
- setTimeout(nextTickHandler, 0)
- }
- }
- return function queueNextTick (cb?: Function, ctx?: Object) { // 添加回调函数, 调用 VUe.nextTick 即调用这个函数, 注意到可以传入一个对象做为该函数的上下文!
- let _resolve
- callbacks.push(() => { // 包裹传入的函数, 绑定其上下文, 并 push 到 callbacks 中
- if (cb) cb.call(ctx)
- if (_resolve) _resolve(ctx)
- })
- if (!pending) { // 如果没有异步队列在等待执行, 那么处理当前的异步队列
- pending = true
- timerFunc()
- }
- if (!cb && typeof Promise !== 'undefined') {
- return new Promise(resolve => {
- _resolve = resolve
- })
- }
- }
- })()
- let _Set
- /* istanbul ignore if */
- if (typeof Set !== 'undefined' && isNative(Set)) { // 浏览器支持 Set
- // use native Set when available.
- _Set = Set
- } else { // 浏览器不支持 Set
- // a non-standard Set polyfill that only works with primitive keys.
- _Set = class Set {
- set: Object;
- constructor () {
- this.set = Object.create(null)
- }
- has (key: string | number) { // set[key]是否存在
- return this.set[key] === true
- }
- add (key: string | number) { // 添加一个元素
- this.set[key] = true
- }
- clear () { // 清空对象内所有元素
- this.set = Object.create(null)
- }
- }
- }
- export { _Set }
Vue.js 2.5 + 把 nestTick 单独成一个文件了: next-tick.js:
- // next-tick.js
- /* @flow */
- /* globals MessageChannel */
- import { noop } from 'shared/util'
- import { handleError } from './error'
- import { isIOS, isNative } from './env'
- const callbacks = [] // 存储回调函数
- let pending = false // 当前是否有异步队列在等待执行?
- function flushCallbacks () { // 执行任务队列中的回调函数
- pending = false // pending 为 false, 表示异步队列已经被清空
- const copies = callbacks.slice(0)
- callbacks.length = 0
- for (let i = 0; i <copies.length; i++) {
- copies[i]()
- }
- }
- // 在 < 2.4 的版本, nextTick 几乎都是使用 microtasks 来实现
- // 但这会导致一些问题(下面讲)
- // 所以 2.5+ 默认使用 microtasks, 但某些场景下会强制使用 macrotasks(比如, v-on 绑定的事件)
- let microTimerFunc
- let macroTimerFunc
- let useMacroTask = false // 是否使用 macrotask 来处理 nextTick? 默认为否
- // 决定 macrotask 的实现.
- // 在技术上 setImmediate 是最理想的, 但是它只能在 IE 中使用.
- // 让回调函数始终排队在 同一个事件循环中触发的 DOM 事件 之后的唯一 polyfill 就是使用 MessageChannel
- /* istanbul ignore if */
- if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 优先使用 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 { // 不支持 setImmediate 和 MessageChannel 时用 setTimeout 代替
- /* istanbul ignore next */
- macroTimerFunc = () => {
- setTimeout(flushCallbacks, 0)
- }
- }
- // 决定 MicroTask 的实现.
- /* istanbul ignore next, $flow-disable-line */
- if (typeof Promise !== 'undefined' && isNative(Promise)) {
- const p = Promise.resolve()
- microTimerFunc = () => {
- p.then(flushCallbacks)
- // 在 UIWebViews 中虽然 Promise.then 没有完全 break, 但是会陷入一个很奇怪的状态
- // 回调函数都被推入微任务队列中, 但是在浏览器处理别的任务 (比如 timer) 之前不会执行这些任务
- // 因此添加一个空的 timer 来强制清空微任务队列.
- if (isIOS) setTimeout(noop)
- }
- } else {
- // 不支持 Promise 则用 macrotask 代替
- // MutationObeserver 因为兼容性问题被抛弃了
- microTimerFunc = macroTimerFunc
- }
- // 包裹一个函数, 强制其使用 macrotask
- // 默认会给每一个 DOM 事件的回调函数调用 withMacroTask
- export function withMacroTask (fn: Function): Function {
- return fn._withTask || (fn._withTask = function () {
- useMacroTask = true // 使用 macrotask
- const res = fn.apply(null, arguments)
- useMacroTask = false // 状态重新设置为 false, 不然其他回调函数也会用 macrotask
- return res
- })
- }
- 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
- if (useMacroTask) {
- macroTimerFunc()
- } else {
- microTimerFunc()
- }
- }
- // $flow-disable-line
- if (!cb && typeof Promise !== 'undefined') {
- return new Promise(resolve => {
- _resolve = resolve
- })
- }
- }
5. 为什么默认使用 microtask 的一点补充
(摘自知乎, 原链接: Vue 中如何使用 MutationObserver 做批量处理? https://www.zhihu.com/question/55364497 )
根据 HTML Standard https://link.zhihu.com/?target=https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model , 在每个 task 运行完以后, UI 都会重渲染(知识点小结那块有说到任务执行顺序以及什么时候渲染), 那么在 microtask 中就完成数据更新, 当前 task 结束就可以得到最新的 UI 了. 反之如果新建一个 task 来做数据更新, 那么渲染就会进行两次.(当然, 浏览器实现有不少不一致的地方, 上面 Jake 那篇文章里已经有提到.)
来源: http://www.jianshu.com/p/668fcef7374b