浅谈事件循环

Ryou Lv2
什么是事件循环

我们都知道主线程是最繁忙的,既然如此繁忙,那你是否思考过主线程是如何调度任务的?

比如:

  • 主线程正在执行一个 js 函数,执行到一半的时候用户点击了按钮,主线程该立即去执行点击事件的处理函数吗?
  • 主线程正在执行一个 js 函数,执行到一半的时候某个定时器时间到了,主线程该立即去执行它的回调没?
  • 用户点击按钮的同时某个定时器时间到了,又该先处理哪个?

答案是 排队

在最开始的时候,渲染主线程会进入无限循环,每一次循环都会检查消息队列中是否有任务存在,如果有就取出第一个任务执行,执行完一个后进入下一次循环;如果没有则进入休眠状态。

其它所有线程可以随时向消息队列添加任务。新任务会加到消息队列的末尾,在添加新任务时,如果主线程是休眠状态,则会将其唤醒继续循环拿取任务。

整个过程被称为事件循环(消息循环)。

什么是异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 定时器 setTimeout

  • 网络请求 Ajax

  • 用户的交互事件 addEventListener 。

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期【阻塞】的状态,从而导致浏览器【卡死】,下面是一个例子👇

1
2
3
4
setTimeout(function(){
console.log(1)
},3000)
console.log(2)

在上面的代码中,用 setTimeout 开了一个三秒的定时器,倒计时结束后会输出 1 ,最后一行代码输出 2 。

如果没有异步,主线程会阻塞三秒,在这三秒里,是无法进行其它操作的,如果这时候用户点击了按钮也毫无反应,而是要等倒计时结束后才会接着往后执行。

有了异步之后,当主线程再遇到类似于 setTimeout 这种情况就会采取别的办法,比如遇到 setTimeout 会开一个计时线程把它放到里面去执行,而主线程会处理其它任务,当定时器到时间后,会把 setTimeout 里的函数放到消息队列里排队,依次等待主线程拿取任务执行。

任务有优先级吗?

任务没有优先级,在消息队列中先进先出。

但消息队列是有优先级的。

根据 W3C 的解释:

在目前 Chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放定时器到达后的回调任务,优先级【中】。
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】。
  • 微队列:用户存放需要最快执行的任务,优先级【最高】

添加任务到微队列的方式主要使用 Promise 、MutationObserver ,例如 👇

1
2
//立即把一个函数添加到微队列
Promise.resolve().then(函数)
练习
1
2
3
4
5
6
7
8
9
10
11
console.log("1");

setTimeout(function() {
console.log("2");
}, 0);

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

console.log("4");

在上面的代码中首先,打印出 “1”,然后,遇到 setTimeout,会将其回调函数放入延时队列中,并继续执行下面的代码。接着,遇到 Promise.resolve().then(),会将其回调函数放入微队列中,但由于此时微任务队列优先级高于宏任务队列,所以会先执行微任务队列中的任务。接着执行同步代码打印出 “4”。然后执行微队列中的任务,打印出 “3”。最后,延时队列中的 setTimeout 的回调函数被执行,打印出 “2”。输出结果是 “1432”。

第二题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log("1");

setTimeout(function() {
console.log("2");
}, 0);

new Promise(function(resolve, reject) {
console.log("3");
setTimeout(function() {
console.log("4");
}, 0);
resolve();
}).then(function() {
console.log("5");
});

console.log("6");

首先输出同步代码 “1”,然后把 setTimeout 放到延时队列中,接着遇到 Promise ,但是Promise 的构造函数是同步执行的,所以这时候输出 “3”,再往后执行遇到了 setTimeout 将其放到了延迟队列,然后调用 resolve()改变 Promise 的状态,接着触发 .then 产生了一个新的Promise ,这时外层的 Promise 是已完成状态,所以会将.then 的回调函数加到微队列,然后输出 “6”。接着查看微队列中的任务,输出微队列中的 “5”,这时微队列是空的,接着取出延时队列的第一个任务输出 “2”,然后拿出第二个任务输出 “4”。输出结果是 “136524”。

评论
此页目录
浅谈事件循环