(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
复制代码

执行结果1,2,3,5,4

为什么执行这样的结果?

1、创建Promise实例是同步执行的。所以先输出1,2,3,这三行代码都是同步执行。

2、promise.then和setTimeout都是异步执行,会先执行谁呢? setTimeout异步会放到异步队列中等待执行。 promise.then异步会放到microtaskqueue中。microtask队列中的内容经常是为了需要直接在当前脚本执行完后立即发生的事,所以当同步脚本执行完之后,就调用microtask队列中的内容,然后把异步队列中的setTimeout放入执行栈中执行,所以最终结果是先执行promise.then异步,然后再执行setTimeout异步。

注意:目前microtask队列中常用的就是promise.then。

Promise.then异步函数会在本次消息循环结尾执行,setTimeout会在下次消息循环执行。(这个也是一种说法)

1. setTimeout

console.log('script start')	//1. 打印 script start
setTimeout(function(){
    console.log('settimeout')	// 4. 打印 settimeout
})	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')	//3. 打印 script start
// 输出顺序:script start->script end->settimeout
复制代码

2. Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
复制代码
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout 当JS主线程执行到Promise对象时,
promise1.then() 的回调就是一个 task
promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

3. async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}

console.log('script start');
async1();
console.log('script end')
复制代码

// 输出顺序:script start->async1 start->async2->script end->async1 end async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

理解事件循环

事件循环与任务队列是JS中比较重要的两个概念。这两个概念在ES5和ES6两个标准中有不同的实现。尤其在ES6标准中,清楚的区分宏观任务队列和微观任务队列才能解释Promise一些看似奇怪的表现。

事件循环

事件循环是什么?为什么要有事件循环这个东西?我们都知道JS是单线程的,但是像Ajax,或是DOM事件这种很耗时的操作,需要用并发处理,否则单线程会长时间等待,什么也做不了。而单线程循环就是并发的一种形式,一个线程中只有一个事件循环。而任务队列是用来配合事件循环完成操作的,一个线程可以拥有多个任务队列。

任务队列

任务队列是什么?故名思意,排着任务的队列。所谓任务是WebAPIs返回的一个个通知,让JS主线程在读取任务队列的时候得知这个异步任务已经完成,下一步该执行这个任务的回调函数了。主线程拥有多个任务队列,不同的任务队列用来排列来自不同任务源的任务。任务源是什么?像setTimeout/Promise/DOM事件等都是任务源,来自同类任务源的任务我们称它们是同源的,比如setTimeout与setInterval就是同源的。在ES6标准中任务队列又分为宏观任务队列和微观任务队列,我们后边再详细讨论。

下面先通俗的讲述一下ES5中事件循环到底是怎么循环的,如图



  • 函数调用栈:即执行栈。
  • WebAPIs:浏览器的接口。比如一个Ajax操作,主线程会把收发Ajax交给浏览器的API,之后就继续做别的事情,浏览器在接收到Ajax返回的数据之后,会把一个Ajax完成的事件排到相应的任务队列后边。
  • 任务队列们:主线程中有多个任务队列,同源的任务排在属于自己的任务队列。

一个具体点的栗子。比如现在打开了一个页面,里边有一段script,其中有Ajax,DOM操作等等。这段JS是在浏览器提供的全局环境(浏览器中是window)里执行的,执行中遇到函数调用时会压入执行栈。

主线程在遇到Ajax或是setTimeout这种异步操作时会交给浏览器的WebAPIs,然后继续执行后边的代码,直到最后执行栈为空。

  • 浏览器会在不确定的时间将完成的任务返回,排到相应的任务队列后。
  • 执行栈为空后,主线程会到任务队列中去取任务,这些任务会告诉下一步应该执行哪些回调函数。任务队列是具有优先级的,按照优先级决定访问的先后顺序。而优先级在不同的环境中会有所不同,所以不能给出一个固定的优先级。
  • 每访问一个队列,执行栈会执行完这个任务队列的所有的代码,然后再取下一个任务队列需要执行的的代码。如果在执行中遇到了当前属于任务队列的异步任务时。此次任务的返回不会直接排到当前任务队列之后。因为这属于两次不同的事件循环,会被区分开来。

就这样循环执行,直到三大块全为空,这称为事件循环

微观任务队列

ES6标准中任务队列存在两种类型,一种就是上边提到的一些队列,比如setTimeout、网络请求Ajax、用户I\O等都属于宏观任务队列(macrotask queue),另一种是微观任务队列(microtask queue),Promise就属于微观任务队列。

添加了微观任务队列之后事件循环有什么变化呢?在执行栈执行的过程中会把属于微观任务队列的任务分配到相应的微观任务队列中去。而在调用栈执行空之后,主线程读取任务队列时,会先读取所有微观任务队列,然后读取一个宏观任务队列,再读取所有的微观任务队列。如图:



setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
    console.log(1)
    for( var i=0 ; i<10000 ; i++ ){
        i==9999 && resolve()
    }
    console.log(2)
}).then(function(){
    console.log(5)
});
console.log(3);
复制代码