一篇文章理解JS事件循环机制(JS Event Loop)

554 阅读4分钟

世界上最高效的学习方法,是把你学到的知识,通过自己理解的方式,教会另一个人。共勉

 1. Event Loop

很多人都听说过 JS 是单线程的,但是线程是什么?进程又是什么?它们之间有什么关系?

  从本质上说,这两个名词都是关于 CPU 时间片的一个描述。进程描述了 CPU 在运行指令、加载及保存上下文所需时间。线程则是进程中更小的一个单位,描述执行一段指令所需时间。 这个概念放到浏览器上说,打开一个 Tab 页是一个进程,其中的 UI 渲染、JS 代码执行、DOM 加载、Http 请求等都是一个个线程;放到 JS 代码执行上来说,一个函数是一个进程,而函数内变量的定义、其他函数的调用等任务都是一个个线程。

  JS 执行是单线程的,意味着 JS 在执行代码的时候一次只能处理一个任务,必须按队列顺序逐个执行。JS 的主要功效是处理前端交互,其中就包括操作 DOM 节点。试想若 JS 是多线程,在处理网页交互时,一个线程需要删除 DOM 节点,另一个线程却是要操作同一个 DOM 节点,这样该如何判断先执行哪个线程?但若队列中存在多个任务,上一个任务的执行会阻塞下一个任务,导致代码执行效率低下。就像 AJAX 请求线程,发出请求后需要等待响应结果,期间 CPU 却是空闲的。对此,JS的事件循环机制(Event Loop)很好地解决了问题。

 2. 同步任务和异步任务

  JavaScript 将任务分为两种:同步任务和异步任务。

  • 同步任务:执行完后能立即得出结果的任务。同步任务在主线程中执行,在执行过程中产生堆栈,堆中存储复杂数据类型(Object),栈中存储基本数据类型(String、Number、Boolean、Null、Undefined、Symbol)。

  • 异步任务:执行后无法立即得出结果,需要等待一段时间获得相应的任务。其中又分为宏任务(Macrotask)和微任务(Microtask)

    宏任务:主程序ScriptsetTimeoutsetIntervalsetImmediateI/O操作(mouse click、keypress、network event)UI渲染requestAnimationTrame等。

    微任务:promiseMutationObserverprocess.nextTick()mutationObject.oberse等。

 3. 主线程、执行栈和队列

 4. Event Loop

  1. 把同步任务推入主线程,异步任务推入任务队列;
  2. 把主线程中的同步任务推入执行栈,开始执行;
  3. 执行栈为空后,读取任务队列中的微任务,推入执行栈;
  4. 循环执行微任务队列,直到微任务列表为空;
  5. 读取宏任务列表中的第一条宏任务,推入执行栈;
  6. 这时的宏任务又相当于一个线程,重复上述步骤1-5,直到任务队列为空。

 实例:

const a = 1;
const arr = [1, 2, 3];
console.log('1');
console.log(a, arr);

setTimeout(function () {
    console.log('2');
    process.nextTick(function () {
        console.log('3');
    });
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5');
    });
}, 100);

new Promise(function (resolve) {
    console.log('6');
    resolve();
}).then(function () {
    console.log('7');
});

process.nextTick(function () {
    console.log('8');
    setImmediate(() => {
        console.info('9');
    });
    new Promise(function (resolve) {
        console.log('10');
        resolve();
    }).then(function () {
        console.log('11');
    });
    setTimeout(function () {
        console.log('12');
        setImmediate(() => {
            console.info('13');
        });
        process.nextTick(function () {
            console.log('14');
        });
        new Promise(function (resolve) {
            console.log('15');
            resolve();
        }).then(function () {
            console.log('16');
        });
    }, 100);
    process.nextTick(function () {
        console.log('17');
    });
});

执行结果:1  1,[1,2,3]  6 7 8 10 17 11 9 2 4  5 3 12 15 16 13 14

 解析:

 上述代码就相当于一个进程,里面的函数执行即为一个个线程,一个个任务。

  1. 把同步任务推入主线程,异步任务推入任务队列;
  2. 把主线程中的同步任务推入执行栈,开始执行;得出结果1 1,[1,2,3]
  3. 执行栈为空后,读取任务队列中的微任务,推入执行栈;得出结果6 7
  4. 循环执行微任务队列,直到微任务列表为空;得到结果 8 10 17 11 9
  5. 读取宏任务列表中的第一条宏任务,推入执行栈; 得到结果 2 4 5 3
  6. 这时的宏任务又相当于一个线程,重复上述步骤1-5,直到任务队列为空。