<-Home

JS Event Loop 事件循环♻️

JS 是一门单线程运行时的脚本语言,这是因历史原因设计者将其设计成了单线程语言,意在保证代码逻辑的顺序性,尤其是DOM操作问题(如果多线程执行DOM操作不加锁会导致意想不到的问题); 当在浏览器中新开一个Tab页时,就相当于新增了一个进程的运行,这个进程内部是多线程机制,其中包含的进程有:

  • GUI 渲染线程
  • 定时器触发线程
  • 事件触发线程
  • 异步http请求线程
  • 以及上面所说的js引擎线程

关于进程与线程相关的可以look这里

以上的几个线程虽然是各自独立的,甚至GUI 渲染线程和JS引擎线程是互斥,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行;由以上的这些机制就产生了Event Loop 事件循环;

JS 引擎在运行时会提供了两个数据结构作为代码运行支撑:

  1. Heap:用于变量存放等分配内存的作用;
  2. Call Stack: 所有的代码段(Task)运行前被push入栈,运行完成之后pop出栈;

event-loop-one

针对第2点,所有的代码段(Task)并非都是在第一时间push入栈,而是放置与特殊的队列中等待合适的时机被动push入栈,包括如下操作(对应的callback):

  1. setTimeout,setInterval,setImmediate【nodejs|IE】,requestAnimationFrame,I/O,window.postMessage, UI rendering

  2. process.nextTick【nodejs】,Promise.then,queueMicrotask,MutationObserver

在这里分为两类是有原因的,上面说的到的特殊队列可以分为两类,分别对应:

  • Macro Task Queue-宏任务队列(requestAnimationFrame(callback)会单独建立宏任务队列且优先级高于其他宏任务队列)
  • Micro Task Queue-微任务队列

事件循环对于两种队列中的Task处理机制是不尽相同的,下面具体介绍事件循环机制:

首先明确以下几点:

  • 一个Event Loop 里面有一个或者多任务队列(其实任务队列就是宏任务队列)
  • 一个 Event Loop 里面有一个微任务队列
  • 任务队列(task queue) = 宏任务队列(macrotask queue) != 微任务队列(microtask queue)
  • 一个任务(task)可能会被放入宏任务队列或者是微任务队列
  • 当一个任务被放入队列中的时候,表示准备工作已经完成,任务随时可以执行
  • 任务队列严格意义来说其实是一个集合而非队列,因为事件循环模式处理的第一步是选择第一个可执行的任务,而非第一个任务;
  • script 标签包裹的一份完整的js代码其实也是一个任务(宏任务)

事件循环遵循以下执行模型:

当任务调用栈(Call Stack)为空的时候:

  1. 在任务队列里选择等待最久且可执行的那个任务(task A)
  2. 如果task A 是 null (意味着任务队列(macrotask)是空的),直接跳转第6步
  3. 将 task A 作为当前正在执行的任务
  4. 执行 task A(这里其实就是之前说的到callback)
  5. 将当前正在执行的任务置为null,移除 task A
  6. 执行微任务队列(microtask)
    • (a).在队列里选择可执行任务(task x)
    • (b).如果 task x 是 null(说明微任务队列是空的),直接跳转 (g)步骤
    • (c).将task x 置为正在执行中的任务
    • (d).执行 task x
    • (e).将当前正在执行的任务置为null,移除 task x
    • (f).在队列里选可执行任务(task x),跳转到(b)步骤
    • (g).完成微任务队列的执行
  7. 跳转到步骤 1

以上模型可以简化成:

  1. 在任务队列里选择等待最久且可执行的那个任务执行然后移除
  2. 接着执行所有微任务队列里面所有可执行的任务
  3. 跳转 第1步

以上⬆️模型中说到的执行都是放入到调用栈(Call Stack)中执行,本文章所讲的事件循环模型只涉及浏览器事件模型,nodeJs事件循环模型见参考

Talk is cheap, show me the code;

/**
 * 1. 
 * 整个脚本会作为宏任务
 * callStack:[]
 * macrotask:[script]
 * microtask: []
 *     ⬇️
 * callStack:[script]
 * macrotask:[]
 * microtask: []
 */
function fn() {

    /** 
     * 2. 
     * callStack:[script, console.log]
     * macrotask:[]
     * microtask: []
    */
    console.log('1 start');
    
    /** 
     * 3. 
     * callStack:[script, setTimeout]
     * macrotask:[setTimeout-cb-1]
     * microtask: []
    */
    // setTimeout-cb-1
    setTimeout(function() {

        /** 
         * 20. 
         * callStack:[setTimeout-cb-1,queueMicrotask]
         * macrotask:[setTimeout-cb-2]
         * microtask: [then-cb-1, queueMicrotask-cb]
        */

        queueMicrotask(() => {
            /** 
             * 24. 
             * callStack:[setTimeout-cb-1,queueMicrotask-cb,console.log]
             * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
             * microtask: [then-cb-1]
            */
            console.log('8 microtask');
        });
        /** 
         * 25. 
         * callStack:[setTimeout-cb-1]
         * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
         * microtask: [then-cb-1]
        */

        /** 
         * 21. 
         * callStack:[setTimeout-cb-1,setTimeout]
         * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
         * microtask: [then-cb-1, queueMicrotask-cb]
        */
        // setTimeout-cb-3
        setTimeout(() => {
            /** 
             * 32. 
             * callStack:[setTimeout-cb-3, console.log]
             * macrotask:[]
             * microtask: []
            */
            console.log('10 timeout')
            
            /** 
             * 32. 
             * callStack:[setTimeout-cb-3]
             * macrotask:[]
             * microtask: []
            */
        }, 0);
        /** 
         * 34. 
         * callStack:[]
         * macrotask:[]
         * microtask: []
        */


        /** 
         * 22. 
         * callStack:[setTimeout-cb-1,console.log]
         * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
         * microtask: [then-cb-1, queueMicrotask-cb]
        */
        console.log('7 timeout');

        /** 
         * 23. 
         * callStack:[setTimeout-cb-1]
         * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
         * microtask: [then-cb-1, queueMicrotask-cb] then-cb-1 还是不可执行
        */


    /** 
     * 26. 
     * callStack:[]
     * macrotask:[setTimeout-cb-2, setTimeout-cb-3]
     * microtask: [then-cb-1]
    */
    });

    /** 
     * 4. 
     * callStack:[script, queueMicrotask]
     * macrotask:[setTimeout-cb-1]
     * microtask: [queueMicrotask-cb]
    */
    queueMicrotask(function() {

        /** 
         * 10. 
         * callStack:[queueMicrotask-cb,requestAnimationFrame]
         * macrotask: [requestAnimationFrame-cb]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [then-cb-1]
        */
        requestAnimationFrame(() => {
            /** 
             * 16. 
             * callStack:[requestAnimationFrame-cb,console.log]
             * macrotask: []
             * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
             * microtask: [then-cb-1]
            */
            console.log('5 animation');

            /** 
             * 17. 
             * callStack:[requestAnimationFrame-cb,queueMicrotask]
             * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
             * microtask: [then-cb-1, queueMicrotask-cb]
            */
            queueMicrotask(() => {
                /** 
                 * 18. 
                 * callStack:[requestAnimationFrame-cb,queueMicrotask,queueMicrotask-cb, console.log]
                 * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
                 * microtask: [then-cb-1]
                */
                console.log('6 microtask');
            });

            /** 
             * 19. 
             * callStack:[requestAnimationFrame-cb]
             * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
             * microtask: [then-cb-1]
            */
        });

        /** 
         * 20. 
         * callStack:[]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [then-cb-1] then-cb-1 不可执行
        */

        /** 
         * 11. 
         * callStack:[queueMicrotask-cb, console.log]
         * macrotask: [requestAnimationFrame-cb]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [then-cb-1]
        */
        console.log('3 microtask');

        /** 
         * 12. 
         * callStack:[queueMicrotask-cb, Promise.resolve, then-body]
         * macrotask: [requestAnimationFrame-cb]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [then-cb-1, then-cb-2]
        */
        // then-cb-2
        Promise.resolve().then(() => {
            /** 
             * 13. 
             * callStack:[queueMicrotask-cb,then-cb-2, console.log]
             * macrotask: [requestAnimationFrame-cb]
             * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
             * microtask: [then-cb-1]
            */
            console.log('4 microtask');
        }
        /** 
         * 14. 
         * callStack:[queueMicrotask-cb]
         * macrotask: [requestAnimationFrame-cb]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [then-cb-1]
        */
        );

    /** 
     * 12-1. 
     * callStack:[queueMicrotask-cb]
     * macrotask: [requestAnimationFrame-cb]
     * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
     * microtask: [then-cb-1, then-cb-2]
     * 此时微任务队列还是不为空,需要继续执行,这里看到then-cb-1还不是resolved状态,不可执行,则会执行 then-cb-2
    */

   /** 
     * 15. 
     * callStack:[]
     * macrotask: [requestAnimationFrame-cb]
     * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
     * microtask: [then-cb-1] 虽然微任务队列不为空,但是then-cb-1还是不可执行,进入下一轮 loop了
    */
    });

    /** 
     * 5. 
     * callStack:[script, Promise-body]
     * macrotask:[setTimeout-cb-1]
     * microtask: [queueMicrotask-cb]
    */
    new Promise((res, rej) => {

        /** 
         * 6. 
         * callStack:[script, Promise-body, setTimeout]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [queueMicrotask-cb]
        */
        // setTimeout-cb-2
        setTimeout(() => {
            /** 
             * 27. 
             * callStack:[setTimeout-cb-2, queueMicrotask]
             * macrotask:[setTimeout-cb-3]
             * microtask: [then-cb-1, queueMicrotask-cb]
            */
            queueMicrotask(() => {
                /** 
                 * 28. 
                 * callStack:[setTimeout-cb-2,queueMicrotask-cb]
                 * macrotask:[setTimeout-cb-3]
                 * microtask: [then-cb-1]
                */
                res('9 resolve');
            })
        }, 0);

        /** 
         * 29. 
         * callStack:[]
         * macrotask:[setTimeout-cb-3]
         * microtask: [then-cb-1]
         * 这时候then-cb-1对应的promise 状态是 resolved 可执行
        */

        /** 
         * 7. 
         * callStack:[script, Promise-body, setTimeout, console.log]
         * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
         * microtask: [queueMicrotask-cb]
        */
        console.log('2 promise directly');

    /** 
     * 8. 
     * callStack:[script, Promise-body, setTimeout, then-body]
     * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
     * microtask: [queueMicrotask-cb, then-cb-1]
    */
    // then-cb-1
    }).then(res => {
        /** 
         * 30. 
         * callStack:[then-cb-1, console.log]
         * macrotask:[setTimeout-cb-3]
         * microtask: []
        */
        console.log(res);
    });

    /** 
     * 31. 
     * callStack:[]
     * macrotask:[setTimeout-cb-3]
     * microtask: []
    */

    /** 
     * 9. 
     * callStack:[]
     * macrotask:[setTimeout-cb-1,setTimeout-cb-2]
     * microtask: [queueMicrotask-cb, then-cb-1]
     * script 最后执行完毕,但是此时微任务不是空,需要继续执行
    */
}

fn();

devtool-png

⚠️注意:requestAnimationFrame 对应的 callback 如果在debugger 模式下, devtool 与页面不在同一屏显示(如上图),则不会执行,因为这时候浏览器并未捕获到帧

参考: