JS 事件循环

98 阅读6分钟

事件循环

原理

任务分类

  • 同步任务:执行不需要等待,直接返回。在主线程上执行
  • 异步任务:不会在主线程上执行,而是由 Web APIs 模块处理,该模块会在任务完成后将回调函数送入任务队列。任务队列可以分为两类
    • 宏任务队列:保存宏任务,特点是需要较长时间完成,比如 setTimeoutsetIntervalI/OAjaxDom事件
    • 微任务队列:保存微任务,特点是能在较短时间完成,比如 Promise.thenasync/awaitnextTick

事件循环工作原理:

  1. 从上往下执行代码,遇到同步代码直接执行,遇到异步代码则交给 Web APIs 模块处理
  2. Web APIs 模块:由相应的处理线程(比如定时器线程、IO线程)处理异步逻辑,处理完后将回调函数包装为异步任务加入到任务队列
  3. 当所有同步代码执行完后,开始处理任务队列中的任务

一个示例

gPfQU2zHxJCiGDv.webp

注意点一:执行 new Promise(executor_fun).then(ok_fun, fail_fun) 时,这个 executor_fun 函数会立即同步执行

    <script>

        setTimeout(() => console.log("wait 0 s"), 0);//3

        new Promise((resolve, reject) => {
            console.log("promise");//1
            // resolve()
            reject()
        }).then(() => console.log("promise.then"), 
                () => console.log("promise.catch")//2
               );

    </script>

注意点二:优先处理微任务任务队列中的任务

    <script>
        console.log("hello");//1

        setTimeout(() => console.log("wait 0 s"), 0);//5

        new Promise((resolve, reject) => {
            console.log("promise");//2
            //调用resolve时才会将then回调放入任务队列
            resolve()
        }).then(() => console.log("promise.then"));//4

        console.log("hi");//3
    </script>

注意点三:如果在执行宏任务时产生了新的微任务,这些新的微任务不会立即执行,而是被放入微任务队列中,在当前宏任务执行完毕后被依次执行

<script>
    setTimeout(() => {
        console.log("here is timeout-1");//2
        new Promise((resolve, reject) => {
            console.log("promise");//3
            resolve()
        }).then(() => console.log("promise.then"));//4
    }, 0);

    setTimeout(() => {
        console.log("here is timeout-2");//5
    }, 0);

    console.log("start!");//1
</script>

总结:在每个宏任务处理完后都会检查微任务队列是否有任务,如果有则处理完所有微任务才会继续处理宏任务队列

注意点四:执行微任务过程中产生的微任务不会推迟到下一个循环中执行

<script>

    new Promise((resolve, reject) => {
        console.log("1.executor-1");
        resolve()
    }).then(() => {
        console.log("4.onfulfilled-1");
    });

    new Promise((resolve, reject) => {
        console.log("2.executor-2");
        resolve()
    }).then(() => {
        console.log("5.onfulfilled-2");

        setTimeout(() => {
            console.log("finally, setTimeout.callback");
        }, 0);

        new Promise((resolve, reject) => {
            console.log("6.executor-4");
            resolve()
        }).then(() => {
            console.log("8.onfulfilled-4");
            //执行微任务过程中产生的微任务不会推迟到下一个循环中执行
            //如果这里是死循环,即微任务队列中一直有任务,那么永远无法结束本轮执行,也就无法执行到最终 setTimeout 的回调
            for (let i = 0; i < 10; i++) {
                new Promise((resolve, reject) => {
                    console.log(`i.${i}.executor-${i}`);
                    resolve()
                }).then(() => {
                    console.log(`i.${i}.onfulfilled-${i}`);
                })
            }
        })
    });

    new Promise((resolve, reject) => {
        console.log("3.executor-3");
        resolve()
    }).then(() => {
        console.log("7.onfulfilled-3");
    });

</script>

image-20240424093214376.png

概念明晰

参考 Event Loop, Web APIs, (Micro)task Queue

Web APIs

Web APIs,是浏览器为 Javascript 运行时提供的功能合集,例如

Web APIs 本质上充当 JavaScript 运行时和浏览器功能之间的桥梁,使我们能够访问信息并使用超出 JavaScript 自身能力的功能。

有一些 API 允许我们启动异步任务,并将运行时间较长的任务交给浏览器处理。具体的,在调用这类 API 时需要为它们设置处理器(handlers),浏览器会在任务最终执行完成时应用这些处理器。

事件循环

事件循环:持续检查调用栈是否为空;如果为空,那么说明所有同步任务都执行完了,于是开始从任务队列中获取任务执行

微任务队列

微任务队列专门用于处理的回调:

  • Promise 处理器中的回调 (then(callback)catch(callback)finally(callback)
  • await 之后执行的 async 修饰的函数体
  • MutationObserver 回调
  • queueMicrotask 回调

每次执行完宏任务队列中的单个宏任务并且调用堆栈为空后,事件循环会处理微任务队列中的所有微任务,然后再次继续执行下一个宏任务。这可以确保立即处理与刚刚完成的宏任务相关的微任务,从而保持程序的响应能力和一致性。

微任务还可以调度其他微任务,所以可能会出现创建无限的微任务循环的情况,导致无限期地延迟宏任务队列并冻结程序的其余部分。

Promise

Promise 构造器签名

new <any>(
    executor: (
    	resolve: (value: any) => void, 
    	reject: (reason?: any) => void
    ) => void
) => Promise<any>

executor 函数的作用:进行异步操作(比如请求网络、读写文件),并在操作结束后的回调中调用 resolve 或 reject 以通知 Web APIs 模块异步操作已经完成(,然后 Web APIs 模块会将对应的回调函数包装成微任务送入微任务队列)。示例

new Promise(
    (resolve, reject) => {
        //异步逻辑
        fetch("https://baidu.com").then(res => resolve(res)).catch(err => reject('请求失败'))
	}
);

Promise 对象包含主要数据

  • 状态(挂起、填充和拒绝)
  • 数据
  • then 处理方法集合和 then 处理方法集合
  • 是否被处理

参考 JavaScript Visualized - Promise Execution - YouTube

Promise 对象的三个方法(重点:几个参数,每个参数的作用,参数是否可省略,返回值类型)

Promise<any>.then<void, never>(
    onfulfilled?: ((value: any) => void | PromiseLike<void>) | null | undefined, 
    onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined
): Promise<...>

Promise<any>.catch<void>(
    onrejected?: ((reason: any) => void | PromiseLike<void>) | null | undefined
): Promise<any>

Promise<any>.finally(
    onfinally?: (() => void) | null | undefined
): Promise<any>

一个示例

<script>
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('1');
            resolve('200');
        }, 500)
    });

    console.log(p);

    // promise.then
    p.then(res => {
        console.log('p.then', res);
    })

    p.then(res => {
        console.log('p.then.0', res);
    }, err => {
        console.log('p.then.1', err);
    })

    // promise.catch
    p.catch(err => {
        console.log('p.catch', err);
    })

    // promise.finally
    p.finally(() => {
        console.log('p.finally');
    })
</script>

async/await 语法是 Promise 的语法糖

async 修饰的函数返回类型是 Promise,函数体是 executor

<script>
    async function test() {
        console.log('1');
        return '200';
    }
    let p = test();

    //以上代码等于
    // let p = new Promise((resolve, reject) => {
    //     console.log('1');
    //     resolve('200')
    // })

    console.log(p);
    p.then(res => {
        console.log(res);
    })
</script>

await:用来将异步任务转化为同步任务;只能在 async 函数中使用 await 语法

<script>
    //模拟一个异步操作
    function a_secret_async_fn() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('ok');
            }, 500);
        });
    }

    //异步处理
    a_secret_async_fn().then(res => {
        console.log('ok' === res);
    }, err => {
        console.log('err', err);
    })

    //使用 await 同步处理
    async function test() {
        let res = await a_secret_async_fn();
        console.log('res', res);
    }

    test();
</script>

几个示例

<script>
    console.log('1')

    setTimeout(function (){
        console.log('2')
    }, 1000)

    console.log('3')
</script>

<script>
    console.log('1')

    setTimeout(function callback(){
        console.log('2')
    }, 1000)

    new Promise((resolve, reject) => {
        console.log('3')
        resolve()
    })
    .then(res => {
        console.log('4');
    })

    console.log('5')
</script>

<script>
    setTimeout(() => {
        console.log('1')
        Promise.resolve().then(() => {
            console.log('2')
        })
    },0)

    new Promise((resolve, reject) => {
        console.log('3')
        resolve()
    })
    .then(res => {
        console.log('4');
    })

    console.log('5')
</script>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    })

    setTimeout(function () {
        console.log('定时器setTimeout')//5
    }, 0);

    new Promise(function (resolve) {
        console.log('同步任务1');//1
        for (var i = 0; i < 10000; i++) {
            i == 99 && resolve();
        }
    }).then(function () {
        console.log('Promise.then')//3
    });

    app.$nextTick(() => {
        console.log('nextTick')//4
    })

    console.log('同步任务2');//2
</script>

<script>
    for (var i = 0; i < 5; i++) {
        setTimeout(function() {
          console.log('异步任务', i);
        }, 100);
    }

    console.log('同步任务', i);
</script>

<script>
    for (let i = 0; i < 5; i++) {
        setTimeout(function() {
          console.log('异步任务', i);
        }, 100);
    }

    console.log('同步任务', i);
</script>

示例:promise、async 和 await

<script>

    console.log('1.', new Promise((resolve, reject) => {
        resolve()
    }));
    console.log('2.', new Promise((resolve, reject) => {
    }));

    async function test() {
        return 1
    }

    console.log('3.', test());//3

    let p2 = test();
    let p3 = p2.then(res => console.log('6.', res));//6
    console.log('4,', p3);//4

    async function call_test() {
        console.log("5");
        let x = await test();
        console.log('7.', x);
    }
    call_test();
</script>

image-20240423153145203.png


源码

掘金文章

参考 事件循环模型全解析-哔哩哔哩_bilibili

参考 JS事件循环机制(Event Loop) - 掘金 (juejin.cn)