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中的,当这个对象没有用处的时候,由垃圾回收机制进行释放空间。
- 当一个函数嵌套另一个函数时,则这个函数的相关参数也会被推入栈顶。
事件循环与任务队列
事件循环可以简单描述为:
1、函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
2、在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;
3、当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。

- Event Loop是由javascript宿主环境(像浏览器)来实现的;
- WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
- JavaScript 的并发模型基于"事件循环";
Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数
setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。
setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f,0),那么不会立刻执行
setTimeout(f,0)将第二个参数设为0,作用是让f在现有的任务(脚本的同步任务和“任务队列”中已有的事件)一结束就立刻执行。也就是说,setTimeout(f,0)的作用是,尽可能早地执行指定的任务。
看两个例子更好理解
var flag = true;
setTimeout(function(){
flag = false;
},0)
while(flag){}
console.log(flag);
以上这段代码给人一种感觉setTimeout(f,0)就是立即执行,运行没有什么问题啊 ,其实不然,看运行图所示我们知道 setTimeout(f,0)会被放到异步操作线程里面,等同步执行函数完成之后在接着执行。具体执行顺序流程实例:
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 引擎提供,不用自己部署。
