事件循环

207 阅读4分钟

JS为什么是单线程?

JS最初设计是应用在浏览器中运行,假如JS是多线程机制,则可能存在这样的情况:

现有两个线程Process1和Process2,它们同时对同一个Dom节点进行操作,其中Process1删除该Dom节点,而Process2编辑该Dom节点。这是两个矛盾的命令,浏览器将无法运行。

因此,JS需要被设计成单线程

JS为什么需要异步?

由于JS的单线程机制,决定了其运行顺序自上而下。如果不存在异步,则后边的程序必须等待前边的程序执行完成才可以进行开始运行。如果前边的程序程序运行时间过长,则导致线程阻塞,浏览器可能处在长时间无响应的状态。因此需要异步执行。

JS如何实现异步?

JS是通过事件循环(event loop)来实现异步的,event loop的机制代表了JS的执行机制。

举个例子:

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);

以上程序的运行结果是:1,3,2。

也就是说,setTimeout里的函数并没有立即执行,而是延迟了一段时间,在满足一定的条件之后才会执行。这样的代码称为异步代码,反之称为同步代码。

在此可以简单概括JS的运行机制如下(event loop(1)):

  • 首先判断JS是同步还是异步, 同步任务立即进入主线程,异步任务则进入到event table
  • 异步任务在event table中注册函数,当满足触发条件之后,该任务被推入到event queue
  • 同步任务会在主线程上一直执行,直到主线程处于空闲状态,此时,主线程会到event quene中查看是否有可执行的任务,如有,则将该任务推入主线程中继续执行

如此反复,称为事件循环

在此对以上例子进行解析:

console.log(1); //任务1,同步任务,进入到主线程里
setTimeout(() => { //任务2,异步任务,进入到event table注册函数,0秒之后被推入event queue中
    console.log(2);
}, 0); 
console.log(3); //任务3,同步程序,进入到主线程里

主线程在完成了任务1、任务3后,检查event queue是否存在可执行函数,执行setTimeout里的函数。

因此最终的输出结果是1-3-2。

在此需要注意的是,异步任务的执行需要两个条件:

  • 满足触发条件
  • 主线程空闲

因此,将函数体setTimeout(() => fn(), 3000)解释为“定时器在3秒之后执行fn”并不准确,准确的解释应该是:

3秒后,fn被推入到event queue,当主线程空闲时,fn从event quene推入到主线程中执行

正因如此,我们并不能完全依赖setTimeout作为一个定时器,对于setTimeout(() => fn(), 3000),如果主线程需要运行10秒,则fn实际上是13秒后才开始运行。

再看一个例子:

setTimeout(() => console.log(1), 0);

new Promise(resolve => {
    resolve(2);
    console.log(3);
}).then((res) => {
    console.log(res);
});

console.log(4);

假如我们利用之前的知识去分析:

setTimeout(() => console.log(1), 0); //任务1,异步任务,进入event table注册,0秒后进入event queue

new Promise(resolve => { //任务2,同步任务,其中包含
    resolve(2);
    console.log(3); //任务3,同步任务
}).then((res) => { //任务4,异步任务,在event table注册后进入event queue,排在任务1之后
    console.log(res); 
});

console.log(4); //任务5,同步任务

根据此分析,最终的输出结果是:3-4-1-2

这是正确的输出结果吗?程序执行之后,得到的最终结果应该是:3-4-2-1

是否因为异步任务的执行顺序不是前后顺序而另有规定,导致输出结果与我们预知的不一样?

事实上,单纯的按照异步和同步的划分方式,并不准确。

准确的划分方式是:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise, process.nextTick

MacDown Screenshot

按照这样的分类方式,JS的执行机制是(event loop(2)):

  • 执行一个宏任务,过程中如果遇到微任务,就将其放在微任务的【事件队列】里
  • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将其中的全部微任务执行完成

重复以上2步骤,结合event loop(1)和event loop(2),就可以得到更准确的JS执行机制。

此时我们再去分析刚刚出错的列子:

1.首先执行script下的宏任务,遇到setTimeout,将其放在宏任务的【队列】里
2.遇到 new Promise直接执行,里边的同步任务cosnole.log(3)立即触发
3.遇到 then 方法,是微任务,将其放在微任务的【队列】里
4.遇到console.log(4)直接执行。
5.当主线程完成cosnole.log(3)和console.log(4)后,会去检查微任务的【队列】,发现其中的任务 then,于是执行console.log(res),此处的res === 2;
6.当微任务完成之后,会去检查宏任务【队列】,发现setTimeout,并执行