今天做了一道笔试题觉得很有意义分享给大家, 题目如下:
- setTimeout(()=>{
- console.log('A');
- },0);
- var obj={
- func:function () {
- setTimeout(function () {
- console.log('B')
- },0);
- return new Promise(function (resolve) {
- console.log('C');
- resolve();
- })
- }
- };
- obj.func().then(function () {
- console.log('D')
- });
console.log('E');
JavaScript 都知道它是一门单线程的语言, 这也就意味着 JS 无法进行多线程编程, 但是 JS 当中却有着无处不在的异步概念 . 要完全理解异步, 就需要了解 JS 的运行核心 -- 事件循环(event loop).
一, 什么是事件队列?
首先来看一个小小的 demo
- console.log('start');
- setTimeout(()=>{
- console.log('A');
- },1000);
- console.log('end');
- //start
- //end
- //A
JS 执行之后, 程序输出'start' 和'end', 在大约 1s 之后输出了'A' . 那我们就有疑问了? 为什么 A 不在 end 之前执行呢?
这是因为 setTimeout 是一个异步的函数. 意思也就是说当我们设置一个延迟函数的时候, 当前脚本并不会阻塞, 它只是会在浏览器的事件表中进行记录, 程序会继续向下执行. 当延迟的时间结束之后, 事件表会将回调函数添加至事件队列 (task queue) 中, 事件队列拿到了任务过后便将任务压入执行栈 (stack) 当中, 执行栈执行任务, 输出'A'.
事件队列是一个存储着待执行任务的队列, 其中的任务严格按照时间先后顺序执行, 排在队头的任务将会率先执行, 而排在队尾的任务会最后执行. 事件队列每次仅执行一个任务, 在该任务执行完毕之后, 再执行下一个任务. 执行栈则是一个类似于函数调用栈的运行容器, 当执行栈为空时, JS 引擎便检查事件队列, 如果不为空的话, 事件队列便将第一个任务压入执行栈中运行.
那么我将这个例子做一个小小的改动看一看:
- console.log('start');
- setTimeout(()=>{
- console.log('A');
- },0);
- console.log('end');
- //start
- //end
- //A
可以看出, 我们将 settimeout 第二个参数设置为 0 后,'A' 也总是会在'end' 之后输出. 所以究竟发生了什么? 这是因为 setTimeout 的回调函数只是会被添加至事件队列, 而不是立即执行. 由于当前的任务没有执行结束, 所以 setTimeout 任务不会执行, 直到输出了'end' 之后, 当前任务执行完毕, 执行栈为空, 这时事件队列才会把 setTimeout 回调函数压入执行栈执行.
二, Promise 的含义和基本用法?
所谓 Promise, 简单说就是一个容器, 里面保存着某个未来才会结束的事件 (通常是一个异步操作) 的结果. 从语法上说, Promise 是一个对象, 从它可以获取异步操作的消息. Promise 提供统一的 API, 各种异步操作都可以用同样的方法进行处理.
写一个小 demo 看一下 Promise 的运行机制:
- let promise = new Promise(function(resolve, reject) {
- console.log('Promise');
- resolve();
- });
- promise.then(function() {
- console.log('resolved.');
- });
- console.log('Hi!');
- // Promise
- // Hi!
- // resolved
上面代码中, Promise 新建后立即执行, 所以首先输出的是 Promise. 然后, then 方法指定的回调函数, 将在当前脚本所有同步任务执行完才会执行, 所以 resolved 最后输出.
三, Macrotasks 和 Microtasks
Macrotasks 和 Microtasks 都属于上述的异步任务中的一种, 他们分别有如下 API:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver
setTimeout 的 macrotask, 和 Promise 的 microtask 有哪些不同, 先来看下代码如下:
- console.log(1);
- setTimeout(function(){
- console.log(2);
- }, 0);
- Promise.resolve().then(function(){
- console.log(3);
- }).then(function(){
- console.log(4);
- });
- //1
- //3
- //4
- //2
如上代码可以看到, Promise 的函数代码的异步任务会优先于 setTimeout 的延时为 0 的任务先执行.
原因是任务队列分为 macrotasks 和 microtasks, 而 promise 中的 then 方法的函数会被推入到 microtasks 队列中, 而 setTimeout 函数会被推入到 macrotasks
任务队列中, 在每一次事件循环中, macrotask 只会提取一个执行, 而 microtask 会一直提取, 直到 microsoft 队列为空为止.
也就是说如果某个 microtask 任务被推入到执行中, 那么当主线程任务执行完成后, 会循环调用该队列任务中的下一个任务来执行, 直到该任务队列到最后一个任务为止.
而事件循环每次只会入栈一个 macrotask, 主线程执行完成该任务后又会检查 microtasks 队列并完成里面的所有任务后再执行 macrotask 的任务.
四, 分析本题目
- setTimeout(()=>{
- console.log('A');
- },0);
- var obj={
- func:function () {
- setTimeout(function () {
- console.log('B')
- },0);
- return new Promise(function (resolve) {
- console.log('C');
- resolve();
- })
- }
- };
- obj.func().then(function () {
- console.log('D')
- });
console.log('E');
1, 首先 setTimeoutA 被加入到事件队列中 ==> 此时 macrotasks 中有['A'];
2,obj.func()执行时, setTimeout B 被加入到事件队列中 ==> 此时 macrotasks 中有['A','B'];
3, 接着 return 一个 Promise 对象, Promise 新建后立即执行执行 console.log('C'); 控制台首次打印'C';
4, 然后, then 方法指定的回调函数, 被加入到 microtasks 队列, 将在当前脚本所有同步任务执行完才会执行. ==> 此时 microtasks 中有['D'];
5, 然后继续执行当前脚本的同步任务, 故控制台第二次输出'E';
6, 此时所有同步任务执行完毕, 如上所述先检查 microtasks 队列, 完成其中所有任务, 故控制台第三次输出'D';
7, 最后再执行 macrotask 的任务, 并且按照入队列的时间顺序, 控制台第四次输出'A', 控制台第五次输出'B'.
五, 执行 JS 代码
分析与实际符合, NICE!
来源: https://juejin.im/post/5bac87b6f265da0a906f78d8