内容简介:先上一段简单的代码执行结果总是如下:为什么呢?为什么同样是异步,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循环嵌套
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Dive Into Python 3
Mark Pilgrim / Apress / 2009-11-6 / USD 44.99
Mark Pilgrim's Dive Into Python 3 is a hands-on guide to Python 3 (the latest version of the Python language) and its differences from Python 2. As in the original book, Dive Into Python, each chapter......一起来看看 《Dive Into Python 3》 这本书的介绍吧!
RGB转16进制工具
RGB HEX 互转工具
图片转BASE64编码
在线图片转Base64编码工具