美文网首页
JavaScript 的事件循环

JavaScript 的事件循环

作者: LenHong | 来源:发表于2019-02-20 16:35 被阅读0次

    一道有关事件循环的前端面试题:

    //请写出输出内容
    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        console.log('async2');
    }
    
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    
    async1();
    
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    
    
    /*
    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
    */
    

    这道题主要考察的是事件循环中函数执行顺序的问题,其中包括asyncawaitsetTimeoutPromise函数。主要涉及到以下知识点。

    任务队列

    • JS分为同步任务和异步任务
    • 同步任务都在主线程上执行,形成一个执行栈
    • 主线程之外,事件触发线程管理着一个任务队列(包括 宏任务队列微任务队列),只要异步任务有了运行结果,就在任务队列之中放置一个事件。
    • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行


    宏任务

    task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

    浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

    task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

    微任务

    microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

    所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

    microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

    运行机制

    在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

    • 执行一个宏任务(栈中没有就从事件队列中获取)
    • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
    • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
    • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
    • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

    流程图如下:

    image
    简单的说就是按照程序顺序执行,遇到微任务就放到放到微任务队列,遇到宏任务就放到宏任务队列,执行栈执行完之后,再清空微任务队列,接着执行宏任务队列

    Promise和async中的立即执行

    我们知道Promise中的异步体现在thencatch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

    await做了什么

    很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

    由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    

    等价于

    async function async1() {
        console.log('async1 start');
        Promise.resolve(async2()).then(() => {
                    console.log('async1 end');
            })
    }
    

    看一个使用 await 的例子:

    let a = 0
    let b = async () => {
      a = a + await 10
      console.log('2', a) // -> '2' 10
    }
    b()
    a++
    console.log('1', a) // -> '1' 1
    

    对于以上代码你可能会有疑惑,让我来解释下原因

    • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
    • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
    • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

    下面的题目有助于加深对于javascript事件循环的理解

    变式一

    在第一个变式中我将async2中的函数也变成了Promise函数,代码如下:

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        //async2做出如下更改:
        new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
        });
    }
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    async1();
    
    new Promise(function(resolve) {
        console.log('promise3');
        resolve();
    }).then(function() {
        console.log('promise4');
    });
    
    console.log('script end');
    

    可以先自己看看输出顺序会是什么,下面来公布结果:

    script start
    async1 start
    promise1
    promise3
    script end
    promise2
    async1 end
    promise4
    setTimeout
    

    变式二

    在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:

    async function async1() {
        console.log('async1 start');
        await async2();
        //更改如下:
        setTimeout(function() {
            console.log('setTimeout1')
        },0)
    }
    async function async2() {
        //更改如下:
        setTimeout(function() {
            console.log('setTimeout2')
        },0)
    }
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout3');
    }, 0)
    async1();
    
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    

    可以先自己看看输出顺序会是什么,下面来公布结果:

    script start
    async1 start
    promise1
    script end
    promise2
    setTimeout3
    setTimeout2
    setTimeout1
    

    变式三

    变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:

    async function a1 () {
        console.log('a1 start')
        await a2()
        console.log('a1 end')
    }
    async function a2 () {
        console.log('a2')
    }
    
    console.log('script start')
    
    setTimeout(() => {
        console.log('setTimeout')
    }, 0)
    
    Promise.resolve().then(() => {
        console.log('promise1')
    })
    
    a1()
    
    let promise2 = new Promise((resolve) => {
        resolve('promise2.then')
        console.log('promise2')
    })
    
    promise2.then((res) => {
        console.log(res)
        Promise.resolve().then(() => {
            console.log('promise3')
        })
    })
    console.log('script end')
    

    无非是在微任务那块儿做点文章,前面的内容如果你都看懂了的话这道题一定没问题的,结果如下:

    script start
    a1 start
    a2
    promise2
    script end
    promise1
    a1 end
    promise2.then
    promise3
    setTimeout
    

    参考文章

    相关文章

      网友评论

          本文标题:JavaScript 的事件循环

          本文链接:https://www.haomeiwen.com/subject/oxxxsqtx.html