为何js引擎是单线程的?
假如js引擎为多线程,DOM操作可能很容易会出现混乱错误的情况:比如在某个时刻a,a线程要操作a节点时,线程b在时刻a之前已将a节点删除了,这时便会出问题,影响到程序运行。
异步可以避免主线程阻塞,所以对于耗时/不确定的操作,使用异步是很好的选择。常见的有:处理ajax请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程等。
异步线程执行完毕后,会通知主线程执行相应的回调函数,这个通知机制的实现,如下:
消息(任务)队列与事件循环(event loop)
- 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
- 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
将逻辑用代码表示:
while(true) {
var message = queue.get();
execute(message);
}
消息队列中的消息具体是什么呢?消息的具体结构是与具体的实现相关的,但是为了简单起见,我们可以认为:
消息就是注册异步任务时添加的回调函数。(可能是不对的)
从生产者与消费者的角度看,异步过程是这样的:
- 异步线程是生产者,主线程是消费者(只有一个消费者)。异步线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。
同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。
主线程、宏任务、微任务执行优先级
主线程 > 当前宏任务 > 当前宏任务中的微任务 > 下一个宏任务
其中涉及到的元素有:执行栈、宏任务队列、微任务队列,执行栈会按优先级,从宏/微队列中选择task去执行。一次loop可以理解成one go-around,遇到宏微任务互相嵌套的情况,可按这个优先级去判断执行顺序,例如如下代码:
console.log("begins");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("promise 1 resolve dot then");
});
}, 0);
new Promise(function (resolve, reject) {
console.log("promise 2");
setTimeout(function () {
console.log("setTimeout 2");
resolve("promise 2 resolve");
}, 0);
}).then((res) => {
console.log("promise 2 resolve dot then");
setTimeout(() => {
console.log(res);
}, 0);
});
执行结果为:
"begins";
"promise 2";
"setTimeout 1";
"promise 1 resolve dot then";
"setTimeout 2";
"promise 2 resolve dot then";
"promise 2 resolve";