event loop 事件迴圈

Node.js event loop 事件迴圈

JS 是 单执行绪,所有同步性的工作,会一个个执行,但遇到非同步的操作就会先放到一个叫做 task queue 的地方,等到目前没有其他工作,就会到 task queue 看看有没有还没执行的任务,再把它拿出来执行。

one thread == one call stack == one thing at a time

事件

event loop 教学

JavaScript 执行顺序

Node.js 及 JavaScript 皆是按照以下优先权顺序执行程式,越前面的优先权会优先执行

1. 宏任务(MacroTask)

  • script 全部代码
  • I/O
  • UI Rendeting
  • MessageChannel
  • postMessage

2. 微任务(MicroTask)

  • Process.nextTick(Node.js)
  • Promise
  • Object.observe(废弃)
  • MutationObserver

3. 任务伫列(task queues) 非同步事件

  • click
  • ajax
  • setTimeout、setInterval、setImmediate

4. 绘製画面伫列(render queue) 绘製画面事件

若是 浏览器 执行 JavaScript 的话会有这个 render queue

如果画面是 60 fps(frame per second) 的话,大约是 16.67 ms 会执行一次画面重绘

所以如果中间有程序被 1, 2, 3 绘製画面的程序要执行就会超过 16.67 m 才会执行

所以中间如果有程式执行太慢的话,你会觉得画面好像卡住没办法动

Node.js 没有绘製画面的状况,但还是有 1, 2, 3 执行顺序及优先权的状况发生

// 执行顺序:1
console.log('Script Start');

// 执行顺序:4
setTimeout(() => {
    console.log('Set Timeout');
}, 0)

// 执行顺序:3
Promise.resolve()
    .then(() => {
        console.log('Promise 1');
    })
    .then(() => {
        console.log('Promise 2');
    });

// 执行顺序:2
console.log('Script End');

// Script Start
// Script End
// Promise 1
// Promise 2
// Set Timeout

图解 event loop 流程

  1. Stack 没工作,执行 task queue 工作,将工作加入到 Stack 中,最早加入的工作优先处理(Queue)
  2. task queue 没工作时,sleep 直到工作出现,然后执行步骤 1

事件

前往event loop 图解说明理解程式及事件处理流程

事件

Call Stack 情境说明

后进先出 Last in First out,LIFO

正常 Call Stack 流程

乘法程式

// 乘法
let multiply = (a, b) => {
    return a * b;
};
// 平方
let square = (n) => {
    return multiply(n, n);
}
// 列印平方
let printSquare = (n) => {
    let squared = square(n);
    console.log(squared);
}
// 执行列印平方
printSquare(3);

Stack 状况

Step 1. 进入程式
顺序 Stack
1 main()
Step 2. 执行 printSquare(3);

前面都是 function 定义,不会执行,直到遇到 printSquare(3); 执行程式,所以会把要执行的程式加入到 Stack 当中

顺序 Stack
1 main()
2 printSquare(3);
Step 3. 执行 square(n);

进到 printSquare(3); 函式中,执行 square(n);,将 square(n); 加入到 Stack

顺序 Stack
1 main()
2 printSquare(3);
3 square(n);
Step 4. 执行 multiply(n, n);

进到 square(n); 函式中,执行 multiply(n, n);,将 multiply(n, n); 加入到 Stack

顺序 Stack
1 main()
2 printSquare(3);
3 square(n);
4 multiply(n, n);
Step 5. 取得 multiply(n, n); 回传资料

multiply(n, n); 裡面会直接运算乘法,然后回传资料,所以回传后 multiply(n, n); 要执行的任务完成了

依照 Stack 后进先出 Last in First out,LIFO 的原则,会将 multiply(n, n); 移除 Stack

顺序 Stack
1 main()
2 printSquare(3);
3 square(n);
Step 6. 取得 square(n); 回传资料

square(n); 会取得 multiply(n, n); 的结果,然后直接回传资料,所以回传后 square(n); 要执行的任务完成了

依照 Stack 后进先出 Last in First out,LIFO 的原则,会将 square(n); 移除 Stack

顺序 Stack
1 main()
2 printSquare(3);
Step 7. 列印资料 console.log(squared);

printSquare(3); 裡面有新的任务是要列印变数资料,所以会把 console.log(squared); 加入 Stack

顺序 Stack
1 main()
2 printSquare(3);
3 console.log(squared);
Step 8. 列印资料完成 console.log(squared);

列印资料完成后,会将 console.log(squared); 移除 Stack

顺序 Stack
1 main()
2 printSquare(3);
Step 9. 完成 printSquare(3);

完成 printSquare(3); 没有更多的任务了,会将 printSquare(3); 移除 Stack,最后遇到 main() 结束整个程式

顺序 Stack
1 main()

setTimeout Stack 状况

console.log('Hi');

setTimeout(() => {
    console.log('setTimeout Hello');
}, 3000);

console.log('End Hi');

// Hi
// End Hi
// setTimeout Hello
Step 1. 进入程式
Stack 顺序 Stack
1 main()
Step 2. 执行 console.log(‘Hi’);

遇到 console.log('Hi'); 执行程式,所以会把要执行的程式加入到 Stack 当中

Stack 顺序 Stack
1 main()
2 console.log(‘Hi’);
Step 3. 执行完成 console.log(‘Hi’);

console.log('Hi'); 执行完成后,会将 console.log('Hi'); 移除 Stack

Stack 顺序 Stack
1 main()
Step 4. 执行 setTimeout();

遇到 setTimeout(callback()); 执行程式,所以会把要执行的程式加入到 Stack 当中

Stack 顺序 Stack
1 main()
2 setTimeout(callback());
Step 5. 执行完成 setTimeout();

setTimeout(callback()); 执行时,会将裡面的 callback() 放到管理 Timer 的 webapis

然后完成 setTimeout(callback());setTimeout(callback()); 移除 Stack

Stack 顺序 Stack webapis 顺序 webapis
1 main() 1 timer(callback())
Step 6. 执行 console.log(‘End Hi’);

遇到 console.log('End Hi'); 执行程式,所以会把要执行的程式加入到 Stack 当中

Stack 顺序 Stack webapis 顺序 webapis
1 main() 1 timer(callback())
2 console.log(‘End Hi’);
Step 7. 执行完成 console.log(‘End Hi’);

执行完成后,将 console.log('End Hi'); 移除 Stack

Stack 顺序 Stack webapis 顺序 webapis
1 main() 1 timer(callback())
Step 8. 执行完成所有程式

执行完成所有程式后,将 main(); 移除 Stack

Stack 顺序 Stack webapis 顺序 webapis
1 timer(callback())
Step 9. webapis 时间计时完成

webapis 计时完成后,会将 callback(); 放到 task queue 当中

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback();
Step 10. event loop 检查 Stack 并移动 task queue

这时候 event loop 会去检查 Stack 是否有其他任务,如果没有其他任务,才会将 task queue 的任务以 先进先出(FIFO First In First Out) 的方式,将任务移动到 Stack 执行

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback();
Step 11. 执行 callback 中的 console.log(‘setTimeout Hello’);

遇到 console.log('setTimeout Hello') 执行程式,所以会把要执行的程式加入到 Stack 当中

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback();
2 console.log(‘setTimeout Hello’)
Step 11. 完成执行 callback 中的 console.log(‘setTimeout Hello’);

执行完成所有程式后,将 console.log('setTimeout Hello'); 移除 Stack

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback();
Step 12. 完成执行 callback

执行完成所有程式后,将 callback(); 移除 Stack

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue

多个 setTimeout Stack 状况

setTimeout(() => {
    console.log('setTimeout Hello 1');
}, 0);

setTimeout(() => {
    console.log('setTimeout Hello 2');
}, 0);

setTimeout(() => {
    console.log('setTimeout Hello 3');
}, 0);

// setTimeout Hello 1
// setTimeout Hello 2
// setTimeout Hello 3
Step 1. 执行所有 setTimeout();

在连续设定 3 个 setTimeout 后,会直接将 3 个程序丢给 webapis 管理

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 timer(callback1())
2 timer(callback2())
3 timer(callback3())
Step 2. 执行完成 setTimeout();

webapis 计时完成后,会将 callback(); 放到 task queue 当中

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback1()
2 callback2()
3 callback3()
Step 3. 依序完成执行 callback 中的 console.log(‘setTimeout Hello’);

event loop 会检查 Stack 是否有工作,没有的话依序将 task queue 的工作放到 Stack 中去执行

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback() 1 callback2()
2 console.log(‘setTimeout Hello1’); 2 callback3()

即便所有的 setTimeout 设定的时间一样,前面的 callback1() 如果执行时间太久,会影响后面的 callback2()callback3() 执行的时间

所以 setTimeout 只能保证程式执行的时间一定在指定的秒数之后,而不保证在指定的时间一定会执行,所以 setTimeout 设定 1000 ms,则程式会在 1000 ms 后执行,可能是 1000ms1300ms1700ms 或是 1000ms + X ms 秒后执行

无穷迴圈 Stack 状况

无穷迴圈程式

let foo = () => {
    foo();
}
//  RangeError: Maximum call stack size exceeded
foo();

Stack 状况

顺序 Stack
1 main()
2 foo()
3 foo()
.. foo()
.. foo()
.. foo()
n foo()
over stack size foo()

执行特定事件太多次导致 task queue 阻塞

$.on('document', 'scroll', () => {
  console.log('scroll!');
});
Step 1. 绑定事件到 webapis 中

执行程式时,webapi 会监听事件,直到事件发生后,将事件需要执行的 callback() 放到 task queue

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 $.($.on(‘document’, ‘scroll’, callback())
Step 2. 触发很多 scroll 事件

当触发很多事件后,task queue 会有很多此次 callback() 要处理的函式

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 $.($.on(‘document’, ‘scroll’, callback()) 1 callback()
2 callback()
3 callback()
4 callback()
5 callback()
callback()
n callback()
Step 3. 处理 callback()

然后 task queue 会持续不断的检查 Stack 是否有在执行程序,没有的话再将 callback() 放到 Stack 中去执行,直到 依序task queue 工作消化完成

所以在这裡就会看到,整个 task queue 被太多 callback() 佔满的状况,若有其他较重要的 callback 要执行,也会被原先的程序卡住

Stack 顺序 Stack webapis 顺序 webapis task queue 顺序 task queue
1 callback() 1 $.($.on(‘document’, ‘scroll’, callback()) 1 callback()
2 console.log(‘scroll!’); 2 callback()
3 callback()
4 callback()
5 callback()
callback()
n callback()

所以要尽量避免使用 scroll 事件或者任何会导致 task queue 阻塞的事件,除非必要,不然会导致整个工作执行缓慢

议题状况

执行时间太长的事件函数

当遇到需要执行比较久的工作,可以使用 setTimeout 的方式,将秒数设定为 0,然后将工作依序拆分到各个小工作,这样可以减少 Queue 被卡住的状况

let i = 0;

let start = Date.now();

function count() {

    // do a heavy job
    for (let j = 0; j < 1e9; j++) {
        i++;
    }

    console.log("Done in " + (Date.now() - start) + 'ms');
}

count();

副作用

因为 setTimeout 的工作也是排入 Queue 去等待执行,所以若 Queue 塞满工作时,setTimeout 设定的时间可能会有延迟的状况发生

  • 若 stack 裡头有其他任务正在进行,setTimeout 的时间可能不会被正确触发。
  • 在 setTimeout 裡头执行过长的任务也会导致 UI blocking。

主线任务执行时间过长

执行这一段程式码,你会发现为什麽过了一秒,console.log 却还没出现

function fib(n) {
    if (n < 1) {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}
setTimeout(() => console.log('hello'), 1000);
fib(40);

这是因为 fib(40) 造成的递迴塞满了整个 stack,直到执行序消化完了之后,才赶紧将 task queue 的 setTimeout 拿出来。

setTimeout 任务执行时间过长

setTimeout 会先把任务放入 task queue 当中,再回到 stack 执行,若 stack 有其他 task 也会阻塞任务执行。

因此,fib(40) 执行后因为让 stack 持续有任务,导致 500ms 过后 console.log(‘hello’) 还没出现。

function fib(n) {
    if (n < 1) {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}
setTimeout(() => {
    console.log('fib(40)')
    fib(40);
}, 0);
setTimeout(() => {
    console.log('hello');
}, 500);

// fib(40)
// hello

Stack 与 Queue 比较

类型 说明
Stack 先进先出 (FIFO, First in first out)
Queue 后进先出 (LIFO, Last in First out)

事件 事件

参考资料