0%

Javascript 事件循环机制

JavaScript的执行顺序是单线程执行,上一个任务执行完毕后执行下一个,由事件循环机制处理程序中多个代码块的执行。

需要注意的是,“异步”和“并行”他们的意义完全不同。“异步”指的是关于现在将来的时间间隙,而“并行”是能够同时发生。

并行计算最常见的工具是“进程”和“线程”。进程和线程独立运行,并可能同时运行。甚至在一些计算机上,多个线程能桐乡耽搁进程的内存。

与之相对的是,事件循环把自身的工作分为一个个任务并顺序执行,不运行对共享内存并行访问和修改。通过分立想成中彼此合作的事件循环,并行和顺序执行可以共存。

由于JavaScript的单线程特性,代码块的执行具有原子性。也就是说,一旦代码块被执行,那么必须得等他执行完毕才可以执行下一个代码块,这成为完整运行的特性。

在ES6中,有一个新的概念建立在事件循环队列之上,叫做任务队列。

任务队列

单线程意味着任务需要排队,一个任务执行完毕后才可以执行下一个任务。但这样遇到需要耗时间的任务(比如http请求或者setTimeout),这时候等着就很不合适了。可以将需要等待的任务挂起来等待,这边先继续往下执行任务。等执行完毕了再回头去处理挂起来的任务。

ps:这里说下Web Worker,Web Worker允许创建多个子线程来执行JavaScript脚本,但是由于子线程受主线程控制,并且无法操作DOM,所以这并没有改变JavaScript单线程的本质。

任务有两种:

  • 同步任务(synchronous) 主线程上排队执行的任务,一个个顺序执行。
  • 异步任务(asynchronous) 不进入主线程,而进入任务队列(task queue)的任务。

只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体执行逻辑:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,存在一个任务队列,异步任务都会挂起,放在Event Table并注册回调函数。当等待的事情完成时,Event Table会将这个任务的回调函数移入到任务队列中,等待执行。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,对应的异步任务结束等待状态,就进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

逻辑如图所示:
js event loop

异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

定时器和任务队列的关系

定时器功能主要由setTimeoutsetInterval这两个函数完成。他们内部的运行机制完全一样,区别在于setTimeout是一次性执行,而setInterval是反复的执行。

setTimeout

setTimeout的第一个参数是回调函数,第二个是推迟执行的毫秒数,之后的参数是附加参数,当定时器到期的时候传递给回调函数的参数。

如果setTimeout的第二个参数设置为0(或者不设置),就表示当前代码执行完毕(执行栈清空)后,立即执行回调函数。

HTML5标准规定setTimeout的第二个参数毫秒值不可低于4毫秒,如果低于这个值,就会自动增加。老版本的浏览器都将最短间隔设置为10毫秒。对于DOM的变动(涉及到页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次,所以使用requestAnimationFrame()的效果比使用setTimeout()效果好。

如果setTimeout到时了,回调函数也进入到了主线程,但前面排队了20多个任务,那么也需要等待其他任务执行完毕才可以。所以setTimeout定时器精度可能不高,只能确保在定时的时间之后运行。

异步任务的优先级

异步任务之间存在一个执行优先级,按照优先级执行。任务分为“宏任务(macro task)”和“微任务(micro task)”。

在挂起任务时,js引擎会将所有任务按照类别分到这两个队列中,首先在宏任务队列中(task queue)取出第一个任务,执行完毕后取出微任务队列中的所有任务顺序执行,之后再取宏任务,周而复始,直至两个队列的任务都取完。

ps:注意,是从宏任务中取一个,从微任务中取所有。。。😂

微任务

微任务包括:promiseObject.observeMutationObserver

宏任务

宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

任务的优先级:process.nextTick > promise.then > setTimeout > setImmediate

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setImmediate(function () {
console.log(1);
}, 0);
setTimeout(function () {
console.log(2);
}, 0);
new Promise(function (resolve) {
console.log(3);
resolve();
console.log(4);
}).then(function () {
console.log(5);
});
console.log(6);
process.nextTick(function () {
console.log(7);
});
console.log(8);
// 输出顺序为:3, 4, 6, 8, 7, 5, 2, 1

总而言之,浏览器中事件循环的执行为:当前执行栈执行完成时,立即优先处理微任务,再去处理宏任务,同一次事件循环中,微任务先于宏任务执行。

参考:JavaScript 运行机制详解:再谈Event Loop