JS是单线程的
JS是单线程的,也就是它一次只能执行一段代码。JS中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具备并行任务处理的特性,我们称之为“单线程”。
虽然JS运行在浏览器中是单线程的,但是浏览器是事件驱动的(Event driven),浏览器中很多行为是异步(Asynchronized)的,会创建事件并放入执行队列中。浏览器中很多异步行为都是由浏览器新开一个线程去完成,一个浏览器至少实现三个常驻线程:
- JS引擎线程
- GUI渲染线程
- 事件触发线程
JS引擎
JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,比如最出名的就是Chrome浏览器的V8引擎,如下图所示,JS引擎主要有两个组件构成:
- 堆-内存分配发生的地方
- 栈-函数调用时会形一个个栈帧(frame)
调用栈
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
- 调用一个函数时,返回地址(return address)、参数(arguments)、本地变量(local variables)等都会被推入栈中。当函数执行完毕弹出堆栈的时候,局部变量(简单数据类型)也会跟着弹出,复杂的数据类型的话则是弹出相应的指针。
- 只有简单的数据类型(Number,String,Boolean,Undefined,Null,Symbol)是存放在栈中,复杂的数据类型譬如对象,数组,只是把对应的指针存放在栈中,真正的值是存放在Heap中的,当这个对象没有用处的时候,由垃圾回收机制进行释放空间。
- 当一个函数嵌套另一个函数时,则这个函数的相关参数也会被推入栈顶。
事件循环与任务队列
事件循环可以简单描述为:
- 函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
- 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;
- 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。
- Event Loop是由javascript宿主环境(像浏览器)来实现的;
- WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
- JavaScript 的并发模型基于"事件循环";
- Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数
var start=new Date();
setTimeout(function cb(){
console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
- main()入栈,局部变量start初始化;
- setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
- while循环入栈,开始阻塞1000ms;
- 500ms过后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
- 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',出栈,cb()出栈
Microtasks和Macrotasks
macro-task(Task)包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task(Job)包括:process.nextTick, Promises.then(), Object.observe(已被废弃), MutationObserver
根据 WHATVG 的说明,在一个事件循环的周期(cycle)中一个 (macro)task 应该从 macrotask 队列开始执行。当这个 macrotask 结束后,所有的 microtasks 将在同一个 cycle 中执行。在 microtasks 执行时还可以加入更多的 microtask,然后一个一个的执行,直到 microtask 队列清空。
setTimeout(function cb() {
console.log(4);
}, 0);
new Promise(function executor (resolve) {
console.log(1);
for(var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(2);
}).then(function onFulfilled() {
console.log(5);
});
console.log(3);
//执行结果:1 2 3 5 4
或者可以简单写成这样:
setTimeout();
var promise = new Promise(executor);
promise.then(callback);
console.log(3);
- main()入栈;
- setTimeout入栈,出栈,丢给WebAPIs,开始定时0ms(实际上不一定是多少,总之大于0),到时之后,将回调函数cb()放入macrotask queue;
- Promise构造函数executor()入栈,log(1)入栈,输出1,出栈;
- for循环入栈,当i=9999时,resolve()入栈,Promise实例的状态变为fulfilled(完成),resolve()出栈。构造函数执行完后,我们得到了promise(它是resolved);
- promise.then入栈,onFulfilled(then方法绑定的resolved状态的回调函数)放入microtask queue;
- log(2)入栈,输出2,出栈;
- executor()出栈;
- log(3)入栈,输出3,出栈,main()出栈;
- 此时栈为空,microtask queue中的任务可以进栈了,onFulfilled()入栈,log(5)入栈,输出5,出栈;
- 此时Stack和microtask queue都为空,Event Loop,将macrotask queue中的cb()入栈,log(4)入栈,输出4,log(4)出栈,cb()出栈
- Promise构造函数是同步函数,该executor函数由Promise实现立即执行
- resolve函数由 JavaScript 引擎提供,不用自己部署。
参考资料:
- Tasks, microtasks, queues and schedules . https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1 . https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec
- How JavaScript works: an overview of the engine, the runtime, and the call stack . https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
- 理解事件循环一(浅析) . https://github.com/ccforward/cc/issues/47
- 理解事件循环二(macrotask和microtask . https://github.com/ccforward/cc/issues/48
- 从Promise来看JavaScript中的Event Loop、Tasks和Microtasks . https://github.com/creeperyang/blog/issues/21
- 理解 Node.js 事件循环 . http://www.zcfy.cc/article/node-js-at-scale-understanding-the-node-js-event-loop-risingstack-1652.html
- Philip Roberts: What the heck is the event loop anyway? . https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html
- http://latentflip.com/loupe
- Promises/A+ . https://promisesaplus.com/