Promise自我修养之事件循环

2,887 阅读15分钟

事件循环

JS代码在执行时基于一种循环机制,我们称这种循环机制为事件循环!事件循环的实现至少应该包含一个用于宏任务的队列和一个用于微任务的队列。大部分的实现通常会有更多用于不同类型的宏任务和微任务的队列,这使得事件循环能够根据任务类型进行优先处理。

宏任务

宏任务的例子有很多,包括创建主文档对象、解析HTML、执行主线JavaScript代码,更改当前的URL、以及各种事件、setTimeout、setInterval等。

微任务

微任务是更小的任务,主要包括Promise的回调函数、DOM发生变化。微任务需要尽可能快的、通过异步的方式执行。

事件循环基于两个基本原则

  • 一次处理一个任务。
  • 一个任务开始后直到运行完成,不会被其他任务中断。

上图是一个简易的事件循环流程图,不过我们可以发现, 两类任务队列都是独立于事件循环的,这意味着任务队列的添加行为也发生在事件循环之外。 如果不这样设计,则会导致在执行JS代码时发生的任务事件都将被忽略!

最后“是否渲染”以及“更新渲染” 环节,是浏览器通常会尝试每秒渲染60次页面,已达到每秒60帧(60fps)的速度,在一轮的宏任务和微任务的完成之后,进入“是否需要渲染的决策环节”,本文对此暂不做过多讨论,后面的事件循环分析的过程也忽略此环节!

深入事件循环

下面我们通过一些例子深入的感受一下事件循环。

仅包含宏任务的实例

    console.log( "start" );
    
    function timer1Fn(){
       console.log( "我是timer1的回调函数!" );   
    }
    let timer1 = setTimeout( timer1Fn,1000);
    
    function timer2Fn(){
       console.log( "我是timer2的回调函数!" );   
    }
    let timer2 = setTimeout( timer2Fn,500);
    
    function timer3Fn(){
        console.log( "我是timer3的回调函数!" );   
        let timer4 = setTimeout( timer4Fn,200);
    }
    function timer4Fn(){
       console.log( "我是timer4的回调函数!" );   
    }
    let timer3 = setTimeout( timer3Fn,0);
    
    console.log( "end" );

打印结果如下:

start
end

我是timer3的回调函数!

我是timer4的回调函数!

我是timer2的回调函数!

我是timer1的回调函数!

通过这个例子,我们来分析一下事件循环的过程。
在此前我们先来说一下setTimeout,我们知道setTimeout是异步的,会放置到宏任务列表中,根据setTimeout设置的时间设置一个定时器,当定时器到时后,才会把setTimeout回调函数的放到宏任务列表中去。

但此时如果宏任务队列中已经有很多宏任务在等待执行,那么该回调函数也要老老实实等待前面的任务执行完成,才能轮到他。所以说setTimeout的精确度不是特别高,只能说它会在你设置的延迟时间之后尽快的执行,但一般会比你设置的延时时间再晚一些才会触发!

上面我们提到过,执行主线JS代码也是一个宏任务。
主线js开始执行的时候,首页打印出“start”,紧接着执行到timer1,timer2,timer3这个三个setTimeout,我们会把这个三个setTimeout按照延迟时间依次放到宏任务列表中(异步队列),继续执行下面的同步代码,打印出“end”。

“end”打印出之后,执行主线js代码这个宏任务基本结束了,从宏任务列中移除。然后去检测一下微任务列表中是否有待执行的任务,发现微任务列表是空,那么开始下一轮循环,宏任务列表的下一个任务被推到栈中执行。

假如执行整个主线js代码用了10ms,等到下一轮循环开始的时候,发现任务列表中timer3中的回调函数timer3Fn等待执行,timer3Fn发现了有一个定时器timer4。执行完timer3Fn之后,检查一下微任务列表依然是空,此时本轮事件循环又结束了。

接着过了大约200ms之后timer4的回调函数timer4Fn被添加到宏任务列表中等待执行,新一轮事件循环开始。 不停的重复这个过程,大概过了500ms、1000ms,timer2、timer1的回调函数也依次被加入到宏任务列表中,按照顺序执行。 。

仅包含微任务的实例

在说微任务之前,我们需要再强调一遍,本轮事件循环的产生的微任务,会在本轮事件循环的宏任务执行结束后执行,并且把微任务队列中的所有任务依次执行完成!

首先看两段代码:

//第一段代码
let first_promise = new Promise( (resolve, reject) => {
    resolve();
    console.log( "我是外部的promise,立即执行!");
}).then( res => {
    console.log( "我是外部的promise的第一个then回调!");
    return new Promise( (resolve, reject) => {
        resolve();
        console.log( "我是内部的promise,立即执行!");
    }).then( res => {
        console.log("我是内部的promise的第一个then回调!");
    }).then( res => {
        console.log("我是内部的promise的第二个then回调!");
    })
}).then( res => {
    console.log( "我是外部的promise的第二个then回调!");
})

//打印结果:
我是外部的promise,立即执行!
我是外部的promise的第一个then回调!
我是内部的promise,立即执行!
我是内部的promise的第一个then回调!

我是内部的promise的第二个then回调!
我是外部的promise的第二个then回调!
//第二段代码
let second_promise = new Promise( (resolve, reject) => {
    resolve();
    console.log( "我是外部的promise,立即执行!");
}).then( res => {
    console.log( "我是外部的promise的第一个then回调!");
    new Promise( (resolve, reject) => {
        resolve();
        console.log( "我是内部的promise,立即执行!");
    }).then( res => {
        console.log("我是内部的promise的第一个then回调!");
    }).then( res => {
        console.log("我是内部的promise的第二个then回调!");
    })
}).then( res => {
    console.log( "我是外部的promise的第二个then回调!");
})

//打印结果:
我是外部的promise,立即执行!
我是外部的promise的第一个then回调!
我是内部的promise,立即执行!
我是内部的promise的第一个then回调!

我是外部的promise的第二个then回调!
我是内部的promise的第二个then回调!

上面两段代码几乎完全相同,唯一的不同点,就是在第二段代码中,去掉了“外部promise的第一个then”中的return。然后就导致执行顺序发生了变化,因此造成了最后两句的打印顺序不同。

不知你是否想过这样一个问题,我们都知道promise的回调会产生一个微任务,那么这个微任务什么时候会添加到微任务队列?如果能深刻的理解产生的微任务什么时候被添加微任务队列中,以及添加的顺序,那么即使程序不运行,你对输出的结果也会了然于胸!

在理解“仅包含宏任务的实例”时,提到过setTimeout在倒计时结束时,在这样一个契机,它的回调才会被添加到宏任务队列中等待执行。同样,在发送ajax请求时,在请求返回响应之后,在这样一个契机,它的回调函数才会被添加到宏任务队列中,等待执行。

那么promise回调被添加到微任务队列的契机是什么呢?

我们都知道promise的状态,会由pending(进行中)变成fulfilled(已成功)rejected(已失败),当它的状态确定时(成功\失败),此时就是回调被添加到微任务队列的契机。

let promise = new Promise( (resolve, reject) => {
    resolve();
});

let promise1 =promise.then( function success(res){
    console.log("promise处于fulfilled(已成功)状态");
}, function error(){
    console.log("promise处于rejected(已失败)状态");
})

let promise2 = promise1.then( function success2(res){
    console.log( "promise1处于fulfilled(已成功)状态" );
}, function error2(){
    console.log( "promise1处于rejected(已失败)状态" );
})

对于上面的代码很好理解,因为promise的状态同步变成了fulfilled,所以回调会被立即添加到微任务队列中,等待执行。所以我们说即使是立即执行的promise,它的回调函数也总是异步执行的!

我们知道,promise.then()会产生一个新的Promise的实例,那么promise1就是一个全新的Promise的实例。因此,promise1.then的回调函数什么时候被添加到微任务队列,就取决于promise1什么时候会由pending(进行中)变成fulfilled(已成功)rejected(已失败)状态,这是一个复杂的过程。

promise1 的状态改变主要取决于promise的状态以及promise.then回调函数的执行。

比如promise状态处于pending,那么我们通过调用promise.then()得到的promise1,这个全新的Promise的实例,肯定会处于也只能处于pending状态。因而,由promise1.then()得到的promise2也就更只能处于pending状态了。

由于产生的回调函数时异步执行的,那么promise1promise2的状态也就只能是异步更新的!

如果此时promise状态变成了fulfilled(已成功)rejected(已失败),那么promise.then的回调函数,就会被添加到微任务队列中等待执行,当这个回调函数执行完成时,promise1的状态(成功或失败)也就随之确定下来。

promise1的状态已经确定时,promise1.then中的回调函数会被添加到微任务队列中,等待执行,当这个回调函数执行完成时,promise2的状态也就随之确定了下来。

现在回过头来看一下,再刚开始讲述微任务时给出的两段代码,为了便于理解,对下面的代码进行分解。

//第一段代码 分解
let first_promise = new Promise( (resolve, reject) => {
    resolve();
    console.log( "我是外部的promise,立即执行!");
});

let first_promise_1 = first_promise.then( res => {
    console.log( "我是外部的promise的第一个then回调!");
    
    let first_new_promise = new Promise( (resolve, reject) => {
        resolve();
        console.log( "我是内部的promise,立即执行!");
    });
    let first_new_1 = first_new_promise.then( res => {
        console.log("我是内部的promise的第一个then回调!");
    });
    let first_new_2 = first_new_1.then( res => {
        console.log("我是内部的promise的第二个then回调!");
    })
    
    return first_new_2;
});

let first_promise_2 = first_promise_1.then( first_res_2 => {
    console.log( "我是外部的promise的第二个then回调!");
});

//打印结果:
我是外部的promise,立即执行!
我是外部的promise的第一个then回调!
我是内部的promise,立即执行!
我是内部的promise的第一个then回调!

我是内部的promise的第二个then回调!
我是外部的promise的第二个then回调!

分析运行过程:
首先执行主线程代码,new Promise内部的代码是同步执行的,所以会率先打印我是外部的promise,立即执行!,同时first_promise的状态变成了fulfilled

紧接着执行first_promise.then(),得到first_promise_1,并把first_promise.then的回调(暂时称之为 micro_first_promise)添加到微任务队列中,此时的first_promise_1状态为pending,因为first_promise.then的回调还没执行,所以此时的first_promise_1的状态还无法确定。

然后执行first_promise_1.then()得到first_promise_2,因为此时的first_promise_1状态为pending,那么first_promise_1.then的回调并不会添加到微任务队列中,因此first_promise_2的状态也只能是pending

同步代码执行完成之后,依次执行微任务队列中的任务。此时的微任务队列只有micro_first_promise这么一个任务,压入执行栈中执行。

在执行的过程中,出现了一个新的Promise实例first_new_promise且状态是fulfilled,并return出去了first_new_2

我们知道,first_promise_2的状态只有在first_promise_1状态确定之后,才会发生变成fulfilledrejected,才能接收到传递下来的值(通过first_res_2接收)。而first_promise_1的状态以及传递出去的值,必须等到回调函数代码执行完成,并且拿到返回值之后,才能完全确定。

因此,会先把first_new_promise.then以及first_new_1.then的回调函数,在状态确定时依次加入到微任务队列,执行完成,确定了first_new_2的状态。此时才能确定first_promise_1的状态,也才会把first_promise_1.then回调添加到微任务队列,然后执行!

下面分析第二段代码:

//第二段代码 分解
let second_promise = new Promise(   (resolve, reject) => {
    resolve();
    console.log( "我是外部的promise,立即执行!");
})

let second_promise_1 = second_promise.then( res => {
    console.log( "我是外部的promise的第一个then回调!");
    
    let second_new_promise = new Promise( (resolve, reject) => {
        resolve();
        console.log( "我是内部的promise,立即执行!");
    });
    let second_new_1 = second_new_promise.then( res => {
        console.log("我是内部的promise的第一个then回调!");
    })
    let second_new_2 = second_new_1.then( res => {
        console.log("我是内部的promise的第二个then回调!");
    })
})

let second_promise_2 = second_promise_1.then( res => {
    console.log( "我是外部的promise的第二个then回调!");
})

//打印结果:
我是外部的promise,立即执行!
我是外部的promise的第一个then回调!
我是内部的promise,立即执行!
我是内部的promise的第一个then回调!

我是外部的promise的第二个then回调!
我是内部的promise的第二个then回调!

之前说过,两段代码唯一的区别就在retrun这个地方,前面的过程都一样,简单叙述一下: 执行同步代码,打印我是外部的promise,立即执行!,执行second_promise.then()得到second_promise_1且状态为pending,并把second_promise.then的回调(暂时称之为micro_second_promise)添加到微任务队列,等待执行。再执行second_promise_1.then()得到second_promise_2且状态为pending。同步代码执行完成!

现在从执行micro_second_promise这个微任务开始。

在执行micro_second_promise时,里面产生了一个新的Promise实例second_new_promise且状态为fulfilled,然后把执行second_new_promise.then()得到second_new_1状态为pending,并把second_new_promise.then的回调(暂时称之为micro_second_new)添加到微任务队列中。

因为micro_second_promise这个微任务已经在执行,因此被移出队列,所以此时的微任务队列中,只有micro_second_new这一个微任务等待执行。

接下来执行second_new_1.then()得到second_new_2状态为pending,内部的代码执行完成!

此时,micro_second_promise这个微任务已经执行完成,那么second_promise_1的状态也就确定了下来,因为没有显示的return,因此传递下去的值是undefinedsecond_promise_1的状态确定之后,那么就会把second_promise_1.then产生的微任务(暂时称之为micro_second_promise_1)添加到微任务队列中。

这个是时候,微任务队列中就有两个任务,micro_second_newmicro_second_promise_1。取出队列中的第一任务micro_second_new压入到执行栈中执行,当这个微任务执行完成之后,second_new_1的状态就确定了下来。那么就会把second_new_1.then产生的微任务暂时称之为micro_second_new_1,添加到微任务队列中。同样,因为此时micro_second_new已经在执行,所以会被移出微任务队列,因此微任务队列中包含micro_second_promise_1micro_second_new_1这两个微任务,依次执行!

宏任务与微任务共存的实例

    console.log('start');
    let timer1 = setTimeout(() => {
        console.log('timeout');
    }, 0);

    let promise1 = new Promise((resolve, reject) => {
        console.log( "我是promise1" );
        resolve( "我成功了!" );
    });
    promise1.then(val => {
        console.log(val);
        return "我是Promise1中第一个then的返回值!"
    }).then((val) => {
        console.log(val);
        let timer3 = setTimeout(() => {
            console.log('我是Promise1中第二个then中的setTimeout!');
        });
    });
 
    let promise2 = new Promise((resolve, reject) => {
        let timer2 = setTimeout(() => {
            console.log( "我是promise2" );
            resolve();
        });
    })
    promise2.then(() => {
        console.log('我是Promise2中第一个then!');
    }).then(() => {
        console.log('我是Promise2中第二个then!');
    });
    
    console.log('end');

打印结果如下:

start
我是promise1
end
我成功了!
我是Promise1中第一个then的返回值!

timeout

我是promise2
我是Promise2中第一个then!
我是Promise2中第二个then!

我是Promise1中第二个then中的setTimeout!

按照上面的分析过程,我们知道当执行主JS代码时,会率先打印出start我是promise1end
此时的宏任务列表中存在的任务分别是:timer1timer2的回调函数,微任务列表中仅存在promise1.then的回调。

当主JS代码执行完成后,会依次执行微任务列表中的任务。

执行promise1.then的回调打印出我成功了!,此时promise1.then()产生的新Promise实例的状态已经确定,因此把promise1.then.then的回调添加到微任务队列中。因为promise1.then的回调已经执行,所以被移出队列, 那么此时微任务列表中仅有这一个微任务,执行他打印出我是Promise1中第二个then!,并存把timer3的回调函数添加到宏任务列表中等待执行,至此微任务列表空了,本轮结束!

下一轮事件循环开始,从宏任务开始,执行timer1的回调打印出timeout,但本轮并没有产生新的的微任务,微任务仍为空,本轮结束。

进入到下一轮,从宏任务开始,执行timer2的回调,打印出我是promise2,此时promise2的状态才确定下来,把promise2.then的回调放到微任务列表中。timer2的回调这个宏任务执行完之后,紧接着依次取出微任务队列中的任务去执行,此时微任务队列中仅有promise2.then的回调这一个任务,执行打印我是Promise2中第一个then!。这个时候Promise2.then()得到Promise实例状态也随之确定了下来,因此会把promise2.then.then的回调添加到微任务列表中,执行打印出我是Promise2中第二个then!。微任务列表再次清空,本轮结束。

下一轮继续,执行宏任务timer3的回调,打印我是Promise1中第二个then中的setTimeout!,没有待执行的微任务,至此宏任务、微任务列表都为空,循环结束!