内容简介:先上一段简单的代码执行结果总是如下:为什么呢?为什么同样是异步,Promise.then 就是 比 setTimeout 先执行呢。
先上一段简单的代码
console.log('aa'); setTimeout(() => { console.log('bb')}, 0); Promise.resolve().then(() => console.log('cc')); 复制代码
执行结果总是如下:
aa cc bb 复制代码
为什么呢?为什么同样是异步,Promise.then 就是 比 setTimeout 先执行呢。
这就涉及到浏览器事件循环机制了。
-
以前浏览器只有一类事件循环,都是基于当前执行环境上下文, 官方用语叫
browsing-context
链接在此。我们可以理解为一个window
就是一个执行环境上下文,如果有iframe
, 那么iframe
内就是另一个执行环境了。 - 2017年新版的HTML规范新增了一个事件循环,就是web workers。这个暂时先不讨论。
事件循环机制涉及到两个知识点 macroTask
和 microTask
,一般我们会称之为 宏任务
和 微任务
。不管是 macroTask
还是 microTask
,他们都是以一种 任务队列
的形式存在。
macroTask
script
(整体代码), setTimeout
, setInterval
, setImmediate
(仅IE支持), I/O
, UI-rendering
注:此处的 I/O 是一个抽象的概念,并不是说一定指输入/输出,应该包括DOM事件的触发,例如click事件,mouseover事件等等。这是我的理解,如果有误,还请指出。
microTask
包括: Promises
, process.nextTick
, Object.observe
(已废弃), MutationObserver
( 监听DOM改变
)
以下内容摘抄于知乎何幻的回答
一个浏览器环境(unit of related similar-origin browsing contexts.)只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。
相同任务源的任务,只能放到一个任务队列中。
不同任务源的任务,可以放到不同任务队列中。
对上面的几句话进行总结:事件循环只有一个,围绕着调用栈, macroTask
, microTask
。 macroTask
和 microTask
是一个大的任务容器,里面可以有多个任务队列。不同的 任务源
,任务会被放置到 不同的任务队列
。那任务源是什么呢,比如 setTimeout
, setInterval
, setImmediate
,这都是不同的任务源,虽然都是在 macroTask
中,但肯定是放置在不同的任务队列中的。
最后,具体浏览器内部怎么对不同任务源的任务队列进行 排序 和取数,这个目前我还不清楚,如果正在看文章的你知道的话,请告诉下我。
接下来我们继续分析 macroTask
和 microTask
的执行顺序,这两个队列的行为与浏览器具体的实现有关,这里只讨论被业界广泛认同和接受的队列执行行为。
macroTask
和 microTask
的循环顺序如下:
注意: 整体代码算一个 macroTask
-
先执行一个
macroTask
任务(例如执行整个js文件内的代码) -
执行完
macroTask
任务后,找到microTask
队列内的所有
任务,按先后顺序取出并执行 -
执行完
microTask
内的所有任务后,再从macroTask
取出一个
任务,执行。 - 重复:2,3 步骤。
现在,我们来解释文章开始时的那串代码,为什么 Promise
总是优先于 setTimeout
console.log('aa'); setTimeout(() => { console.log('bb')}, 0); Promise.resolve().then(() => console.log('cc')); 复制代码
-
浏览器加载整体代码并执行算一个
macroTask
-
在执行这段代码的过程中,解析到
setTimeout
时,会将setTimeout内的代码
添加到macroTask
队列中。 -
接下来,又解析到
Promise
, 于是将Promise.then()内的代码
添加到microTask
队列中。 -
代码执行完毕,也就是第一个
macroTask
完成后,去microTask
任务队列中,找出所有任务并执行, 此时执行了console.log('cc')
; -
microTask
任务队列执行完毕后,又取出下一个macroTask
任务并执行,也就是执行setTimeout
内的代码console.log('bb')
可以这样理解: 一个 宏任务
执行完后,会执行完所有的 微任务
,再又执行一个 宏任务
。依此循环,这也就是事件循环。
如果对事件循环机制还是不怎么理解的话,可以看下这篇文章,图文并茂,讲的挺细的。
Vue nextTick函数的实现
首先我们运用 nextTick
的方式有两种
// 第一种,Vue全局方法调用 Vue.nextTick(fn, context); // 第二种,在实例化vue时,内部调用 this.$nextTick(fn); 复制代码
其实这两种方式都是调用的 Vue 内部提供的一个nextTick 方法,Vue内部对这个方法做了些简单的封装
// src/core/instance/render.js --- line 57 // 这里调用 nextTick 时自动把当前vue实例对象作为第二个参数传入,所以我们调用 this.$nextTick时,不需要传第二个参数 Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this) }; // src/core/global-api/index.js --- line 45 // 直接将 nextTick 暴露出去,作为Vue全局方法 Vue.nextTick = nextTick; 复制代码
也就是说,这两种调用方式,都是执行的Vue内部提供的 nextTick
方法。这个 nextTick
方法,Vue用了一个单独的文件维护。如下
代码来源:vue项目下 src/core/util/next-tick.js
首先文件头部,定义了一个触发回调的函数
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]() } } 复制代码
这部分代码的意思,就是依次触发 callbacks
内的函数。那么 callbacks
数组是存放什么的?其实就是存放我们调用 this.$nextTick(fn)
是传入的 fn
,只不过对它做了一层作用域包装和异常捕获。
nextTick 函数 定义在文件的末尾,代码如下。注意看我加的注释。
export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 将传入的函数包装一层,绑定作用域,并try-catch捕获错误 // 如果没传入函数,且浏览器原生支持 Promise 的情况下,让 Promise resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // pending 是一个开关,每次执行 flushCallbacks 后,会将 pending 重置为 fasle if (!pending) { pending = true if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line // 这里返回一个 Promise, 所以我们可以这样调用,$this.nextTick().then(xxx) if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } 复制代码
上面的代码的 pending
有点意思, 它是为什么处理同时调用多个 nextTick 的业务场景, 例如
new Vue({ // 省略 created() { // 执行第一个时 , pending 为 false, 所以会进入 if (!pending),然后 pending 被设为false this.$nextTick(fn1); // this.$nextTick(fn2); this.$nextTick(fn3); } }) 复制代码
如果是这样调用, 那么Vue会怎么做呢,
看到这里的同学估计会有个疑问点, useMacroTask
是什么, macroTimerFunc
是什么, microTimerFunc
又是什么。接下来会一一解开。
// Here we have async deferring wrappers using both microtasks and (macro) tasks. // In < 2.4 we used microtasks everywhere, but there are some scenarios where // microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690) or even between bubbling of the same // event (#6566). However, using (macro) tasks everywhere also has subtle problems // when state is changed right before repaint (e.g. #6813, out-in transitions). // Here we use microtask by default, but expose a way to force (macro) task when // needed (e.g. in event handlers attached by v-on). let microTimerFunc let macroTimerFunc let useMacroTask = false 复制代码
接下来, macroTimerFunc
的定义
// Determine (macro) task defer implementation. // Technically setImmediate should be the ideal choice, but it's only available // in IE. The only polyfill that consistently queues the callback after all DOM // events triggered in the same loop is by using MessageChannel. /* istanbul ignore if */ 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 { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } 复制代码
再是 microTimerFunc
的定义
// Determine microtask defer implementation. /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { 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) } } else { // fallback to macro microTimerFunc = macroTimerFunc } 复制代码
最后上一段代码,出自Google 2018GDD大会,欢迎探讨并说出原因。
button.addEventListener('click', () => { Promise.resolve().then(() => console.log('microtask 1')) console.log('listener 1') }) button.addEventListener('click', () => { Promise.resolve().then(() => console.log('microtask 2')) console.log('listener 2') }) 1. 手动点击,输出结果 2. 用测试代码 button.click() 触发,输出结果 复制代码
答案在这篇文章
参考并推荐几篇好文:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 浏览器和Node中的事件循环机制
- 理解事件循环(从浏览器端到node端)
- JS异步详解 - 浏览器/Node/事件循环/消息队列/宏任务/微任务
- 008.Python循环for循环
- 006.Python循环语句while循环
- 007.Python循环语句while循环嵌套
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。