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
按照这样的分类方式,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,并执行