JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。JavaScript可以作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript语言的设计者意识到这个问题,将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
setTimeout(function(){
console.log("a");
},3000);
let _exit = Date.now() + 5 * 1000;
while(Date.now() < _exit){}
console.log("b");
// 先打印 a 还是先打印 b,相差时间是多少?b应很容易,但是两次打印的时间间隔是多少呢?结果是只有几毫秒,为什么不是3000毫秒呢?这就涉及到js的运行机制问题了。js的运行机制。JavaScript 的运行机制
- 主线程不断循环;
- 对于同步任务,创建执行上下文(Execution Context),按顺序进入执行栈;
- 对于异步任务:先同步执行这段代码;将相应的任务添加到
Event Loop(事件循环)的任务队列;由其他线程来执行具体的异步操作。其他线程是指:尽管 JavaScript 是单线程的,但浏览器内核是多线程的,它会将 GUI 渲染、定时器触发、HTTP 请求等工作交给专门的线程来处理; - 当主线程执行完当前执行栈中的所有任务,就会去读取
Event Loop的任务队列,取出并执行任务; - 重复以上步骤;
代码执行时,遇到定时器,是异步任务,所以会将定时器中的任务添加到
Event Loop的任务队列中,主线程继续向下执行代码。下面的
while循环是同步的,主线程会在这里停顿5000毫秒,5000毫秒后继续向下执行。打印b。此时主线程执行完了,会去读取
Event Loop的任务队列,由于是任务队列中任务是异步执行的,所有时间已经超过3000毫秒,里面的任务会执行,打印a。setTimeout(() => {
console.log(1);
}, 0);
new Promise((res, rej) => {
console.log(4);
res();
}).then(() => {
console.log(5);
});
console.log(2);
let _exit = Date.now() + 5 * 1000;
while (Date.now() < _exit) { };
console.log(3);这里的打印结果会是什么呢?4
2
3
5
1JavaScript的Event Loop机制了。Event Loop会维护自己的任务队列。任务队列又分为Macrotask Queue(宏任务队列)和Microtask Queue(微任务队列)两种。Macrotask Queue(宏任务队列)
几种常见的Macrotask Source:
鼠标、键盘事件;
AJAX;
setTimeout、setInterval;
数据库操作;
但对于不同的Macrotask,浏览器会进行调度,允许优先执行来自特定Macrotask Source(宏任务源)的Macrotask。例如,鼠标、键盘事件和网络请求都有各自的Macrotask Queue,当两者同时存在时,浏览器可以优先从用户交互相关的 Macrotask Queue中挑选Macrotask并执行,比如鼠标、键盘事件,从而保证流畅的用户体验。
Microtask Queue(微任务队列)
Microtask Queue与Macrotask Queue类似,也是一个有序列表。不同之处在于,一个Event Loop只有一个 Microtask Queue。
Microtask Source,通常认为有以下几种:
MutationObserver(监听DOM结构变化)
process.nextTick(node.js)
Event Loop 处理模型
Event Loop也不同。浏览器和node中的Event Loop是有区别的。浏览器:
Event Loop的每一次循环过程: 1、执行
Microtasks:遍历Microtask Queue并执行所有Microtask,如果没有Microtask,跳过;2、执行
Macrotask:从Macrotask Queue中取出最老的一个Macrotask并执行;如果没有Macrotask,跳过;3、进入
Update the rendering(更新渲染)阶段。浏览器会根据各种因素判断是否要跳过本次更新,当浏览器确认继续本次更新后,处理更新渲染相关工作。Node.js:
Node.js 中Event loop的Macrotask Queue(任务队列)分为6个阶段,它们会按照顺序反复运行,分别如下:1、timers:执行
setTimeout() 和setInterval()中到期的callback。2、I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行。
3、idle, prepare:队列的移动,仅内部使用。
4、poll:最为重要的阶段,执行I/O
callback,在适当的条件下会阻塞在这个阶段。5、check:执行
setImmediate的callback。6、close callbacks:执行
close事件的callback,例如socket.on("close",func)。 和浏览器不同的是,在Macrotask Queue的每个阶段完成后,都会执行一次microTask Queue。
Promise是Microtask,setTimeOut是Macrotask,所以Promise执行的优先级会高于setTimeOut。setTimeout(()=>{
console.log(1);
},0);
new Promise((res,rej)=>{
console.log(7);
res();
}).then(()=>{
console.log(8);
setTimeout(()=>{
console.log(9);
},0);
});
new Promise((res,rej)=>{
console.log(2);
res();
}).then(()=>{
console.log(3);
setTimeout(()=>{
console.log(6);
},0)
;});
console.log(4);
let _exit = Date.now()+5*1000;
while(Date.now() < _exit){};
console.log(5); 如果已经理解了js的Event Loop机制,得到下面的答案应该不会很难。
7
2
4
5
8
3
1
9
6如果大家有什么观点、看法可以在评论区和我们聊聊。也可以关注我们的微信公众号,和我们一起讨论前端问题。