学数学的程序猿
数学 / 前端开发 / 算法About me
由代码的输出顺序引出事件循环
前端
2023-02-20 17:15:02

看看以下代码会在控制台打印出什么:

function a(){
    console.log(1);

    setTimeout(()=>{
        console.log(2);
    })
    Promise.resolve().then(function (){
        console.log(3);
    });
}

setTimeout(function (){
    console.log(4);
    Promise.resolve().then(a);
}, 0)

Promise.resolve().then((function (){
    console.log(5);
}))

console.log(6);

【浏览器进程模型】

浏览器启动后会开启浏览器进程、网络进程、渲染进程等多个进行。

其中,每启动一个标签页的时候,会启动一个渲染进程,渲染进程中会有一个渲染主线程,用于解析html/css,处理图层,执行js,绘制页面,执行计时器和用户操作的回调函数等。


【单线程】

js是单线程的语言,因为它运行在浏览器的渲染主线程中,而渲主线程只有一个。

因为是单线程的,所有有些比较耗时或者不是第一时间需要执行的操作,比如网络请求,计时器,用户点击等,就不能采用同步的方式去执行。

所以需要采用异步的方式来处理这些任务,具体做法就是在主线程中当计时器、网络请求、事件监听等任务发生时,将其交由其他线程去处理,主线程立即结束任务的执行,转而执行后续的代码。

当其他线程执行完毕后,将事先传递的回调函数包装成一个任务,加入到消息队列的末尾,等待主线程去调度执行。


【事件循环/消息循环】

最开始的时候,浏览器的渲染主线程会进入一个无线循环

每一次循环的时候,会去检查消息队列中有没有待执行的任务

如果有的话就取出第一个任务去执行,执行完后进入下一次循环

如果没有的话就进入等待状态,等待队列中有任务的时候被唤醒

其他线程可以向队列中添加任务,新添加的任务会排在队列的末尾,等待主线程来执行,

如果添加的时候主线程是等待状态,则唤醒主线程来拿取任务

在一个浏览器中,会有很多个队列,除了微队列的优先级最高以外,浏览器可以自行决定从其他的几个队列中取第一个来执行


【微队列】

W3C规定每个浏览器必须要有一个微队列,微队列中的任务优先级最高,如果微队列中有未执行的任务,浏览器必须优先从微队列中取出任务来执行。

在chrome浏览器中,至少包含了以下几个常用队列:

  1. 延时队列,用来存放计时器结束后需要执行的任务,优先级【中】,比如:setTimeout setInterval
  2. 交互队列,用来存放用户操作后的事件处理任务,优先级【高】,比如:addEventListener
  3. 微队列,用来存放需要最快执行的任务,优先级【最高】
    添加任务到微队列的主要方式:Promise、MutationObserver,例如:Promise.resolve().then(fn)

【简单图解】

根据以上的结论,再来看代码:

function a(){
    console.log(1);  //第四个输出,因为在第三个输出后又添加了一个微队列任务,就是执行函数a

    setTimeout(()=>{
        console.log(2);  //第六个输出,在第五个输出完毕后,此为第一个待执行的任务,所以再第五个输出后执行
    })
    Promise.resolve().then(function (){
        console.log(3);  //第五个输出,因为第4个输出后,向队列中添加了两个任务,一个是延时任务,一个微队列任务,此微队列任务优先执行
    });
}

setTimeout(function (){
    console.log(4);  //第三个输出,因为微队列任务执行完毕后,setTimeout添加的任务在队列第一个
    Promise.resolve().then(a);
}, 0)

Promise.resolve().then((function (){
    console.log(5);  //第二个输出,因为Promise.resolve().then(fn)添加的任务在微队列中,优先级最高
}))

console.log(6);   //第一个输出,因为它是渲染主线程中的第一个执行的输出

综上注释,以上代码的输出结果为:

6
5
4
1
3
2