1

参考:

  1. JavaScript 运行机制详解:再谈Event Loop
  2. 深入理解JavaScript的执行过程--单线程的JS
  3. 细说JavaScript单线程的一些事
  4. The JavaScript Event Loop: Explained
  5. 模拟Event Loop执行过程
  6. Node.js 事件循环一: 浅析
  7. 理解 Event Loop、Micro Task & Macro Task
  8. HTML系列:macrotask和microtask

1. js为啥是单线程执行

单线程是指只在一个线程里执行JS代码,但浏览器是多线程的。

1.1 单线程的好处

简单,处理DOM时不会出现并发竞争问题

2. 异步的必要性

让用户体验更流畅

3. 如何实现异步:Event Loop

见参考1,4,5
clipboard.png

3.1 调用栈(call Stack)

函数执行上下文。控制执行函数的调用执行。单线程只能有一个call statck, 并且每次只能执行一个任务。栈是有尺寸的,即在一次执行中函数的嵌套调用数量是有限的,如果超过这个限制就会报错。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());

3.2 回调队列(callback queue)

看参考图,生动的动画模拟可以见参考5。当call stack空的时候,主线程查看callback队列里是否有异步任务,如果有则取出执行,执行完后(即call stack变空了)再去查看call back队列是否有异步任务。这个是循环的过程,也叫事件循环(event loop)。

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

个人认为事件队列主要实现了异步回调功能。

3.3 非阻塞I/O

首先明确一点,JS本身就没有I/O(网络请求,磁盘读写,用户交互)的,所有的I/O操作都是由宿主执行的,宿主提供相关的API供JS调用。当JS调用IO API时并不会等待宿主的执行结果而是继续执行后面的代码,当IO执行完成后宿主再通知JS,即在eventQueue中插入回调task。
不过还有些I/O是同步的,比如同步XHR请求,alert。尽量避免使用。

3.4 阻塞eventLoop

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)或者放在服务端运算。

4. Macro-task & micro-task

event loop中除了 callback queue(macro-task)外还有个队列专门处理micro-task。micor-task队列为空时才去处理macro-task队列执行。
当打算以同步的方式处理异步回调(即执行栈为空时立马执行回调函数)时可以采用micro-task方式。

  • macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
  • microtasks: process.nextTick, Promises, Object.observe, MutationObserver

普拉斯强
2.7k 声望53 粉丝

Coder