Open
Description
JavaScript is a single-threaded non-blocking asynchronous concurrent language
一、js为啥是单线程执行
1.1 单线程
单线程是指只在一个线程里执行JS代码,但浏览器是多线程的。
1.2 单线程的好处
简单,处理DOM时不会出现并发竞争问题。这估计也是WebWorker无法操作DOM的原因。
1.3 异步的必要性
让用户体验更流畅,相对于CPU的处理速度,I/O是很慢的。
二、如何实现异步:Event Loop
2.1 相关概念
- 同步任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步任务
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
- 消息队列
可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。
- 回调函数
那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
- 所谓“绑定事件”其实就是告诉浏览器如果触发了某个事件就执行相关的“回调函数”。
- 当浏览器监听到触发了指定事件时,就向“任务队列”插一条消息,告诉JS主线程某个“回调函数”可以执行了。
- 调用栈(call Stack)
单线程只能有一个call stack
, 并且每次只能执行一个任务。
栈是有尺寸的,即在一次执行中函数的嵌套调用数量是有限的,如果超过这个限制就会报错。JS执行时遇到这种错误"Uncaught RangeError: Maximum call stack size exceeded"就是说明call statck溢出了。回调函数上限数量取决于statck本身的大小以及statck元素的大小->见参考:
function computeMaxCallStackSize1() {
try {
return 1 + computeMaxCallStackSize1();
} catch (e) {
// Call stack overflow
return 1;
}
}
// 多了形参p,call stack元素就多占了内存
function computeMaxCallStackSize2(p) {
try {
return 1 + computeMaxCallStackSize2();
} catch (e) {
// Call stack overflow
return 1;
}
}
console.log(computeMaxCallStackSize1()); // 两个输出结果不一样
console.log(computeMaxCallStackSize2());
2.2 Event Loop
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
2.3 阻塞 Event Loop
event loop实现了异步回调,但回调只能一个一个的执行,但前面的一个回调非常耗时时就会阻塞后面的回调执行,用户交互也可能出现卡死。
function sleep(ms) {
var curr = Date.now();
while(Date.now() - curr < ms){};
}
console.log(1)
setTimeout(function(){ // 回调依旧被阻塞
console.log(2)
}, 0)
sleep(2000)
最好开启新线程执行耗时的运算(使用web worker)或者放在服务端运算。