美文网首页Js面试大前端开发JavaScript
面试 | JS 事件循环 event loop 经典面试题含答案

面试 | JS 事件循环 event loop 经典面试题含答案

作者: 林一一呢 | 来源:发表于2021-04-14 10:23 被阅读0次

    原文地址

    掘金

    欢迎 github Star

    思维导图

    eventLoop.png

    一、JS异步编程基本概念

    JS 之所以是单线程的是因为浏览器(多线程)只分配一个线程来执行 JS 代码,之所以只分配一个线程试因为浏览器考虑到多线程操作会导致的一些问题,假设 JS 是多线程的,其中一个线程在 DOM 节点上添加内容,而另一个线程在这个节点上删除内容,那么浏览器该执行哪一个呢?所以 JS 的设计就是单线程的。但是单线程会造成很多的任务都需要等待执行,所以就引入了浏览器的事件循环机制。

    进程和线程 Tip

    进程中可以包括多个线程,比如打开一个页面,这个页面就占用了计算机的一个进程,页面加载时,浏览会分配一个线程去计算DOM树,一个去执行 JS 代码,其他的线程去加载资源文件等。

    二、event loop

    JS 主线程不断的循环往复的从任务队列中读取任务,执行任务,这种运行机制称为事件循环(event loop)。推荐看一个2分钟了解event loop

    宏任务和微任务

    浏览器的事件循环(event loop)中分成宏任务和微任务。JS 中任务分成同步任务和异步任务。

    1. 宏任务(macro task)

    JS 中主栈执行的大多数的任务,例如:定时器,事件绑定,ajax,回调函数,node中fs操作模块等就是宏任务

    2. 微任务(micro task)

    promise, async/await, process.nextTick等就是微任务。

    思考:为什么要引入微任务,只有宏任务可以吗?

    微任务的引入是为了解决异步回调的问题,假设只有宏任务,那么每一个宏任务执行完后回调函数也放入宏任务队列,这样会造成队列多长,回调的时间变长,这样会造成页面的卡顿,所以引入了微任务。

    思考,为什么 await 后面的代码会进入到promise队列中的微任务?

    async/await 只是操作 promise 的语法糖,最后的本质还是promise。举一个小栗子

    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')
        })
    }
    

    4. 宏任务和微任务的执行顺序(很重要)

    图解

    1. 主栈队列就是一个宏任务,每一个宏任务执行完就会执行宏任务中的微任务,直到微任务全部都执行完,才开始执行下一个宏任务。
    2. JS 中任务的执行顺序优先级是:主栈全局任务(宏任务) > 宏任务中的微任务 > 下一个宏任务。,所以 promise(微任务) 的执行顺序优先级高于setTimeout定时器。
    3. 不能满目的将 .then 的回调放入微任务队列;因为没有调用 resolve或者reject 之前是不算异步任务完成的, 所以不能将回调随意的放入微任务事件队列
    4. await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 micro task中这个微任务是 promise 队列中微任务,然后就会跳出整个 async 函数来继续执行后面的代码。
    5. process.nextTick 是一个独立于 eventLoop 的任务队列,主栈中的宏任务每一次结束后都是先执行 process.nextTick队列,在执行微任务 promise.then()
    6. 每一个宏任务和宏任务的微任务执行完后都会对页面 UI 进行渲染。

    热身1 先看一个小栗子

    // A 任务
    setTimeout(() => {
        console.log(1)
    }, 20)
    
    // B 任务
    setTimeout(() => {
        console.log(2)
    }, 0)
    
    // C 任务
    setTimeout(() => {
        console.log(3)
    }, 10)
    
    // D
    setTimeout(() => {
        console.log(5)
    }, 10)
    
    console.log(4)
    /* 输出
    *   4 -> 2-> 3 -> 5 -> 1
    */
    

    任务队列图解

    在主线程的主任务(宏任务)先自上而下执行,遇到 setTimeout 代码都是下一个(宏任务)。所以都会被加入到等待队列中,浏览器有专门监听等待队列中的代码,在主栈中的同步代码执行完成后,等待队列中的任务先到执行时间的就先执行,如果等待任务队列中有两个同时到执行时间的异步代码,那么先入队列的就先到主栈中执行。所以等待队列中 B 任务执行后到 C 任务到 D 任务 再到 A 任务。输出的结果就是4 -> 2-> 3 -> 1

    热身2 将上面的栗子改一下

    // A 任务
    setTimeout(() => {
        console.log(1)
    }, 20)
    
    // B 任务
    setTimeout(() => {
        console.log(2)
    }, 0)
    
    // C 任务
    setTimeout(() => {
        console.log(3)
    }, 30)
    
    console.log(4)
    /* 输出
    *   4 -> 2-> 1 -> 3
    */
    

    这题的原理和上面一样,任务A 的执行时间比 C 任务先到了就先输出了 1 后输出 3。

    三、思考题求输出顺序(chrome 浏览器为准)

    1. 来一道思考题,求输出结果

    let xhr = new XMLHttpRequest()
    xhr.open('post', 'api')
    xhr.onreadystatechange = () =>{
        if(xhr.readyState == 2){
            console.log(2)
        }
        if(xhr.readyState == 4){
            console.log(4)
        }
    }
    xhr.send()
    console.log(3)
    /* 输出
    *   3 2 4
    */
    

    2. 再来一道思考题,在同步请求中下面代码输出的是什么

    let xhr = new XMLHttpRequest()
    xhr.open('get', 'xxx', false)
    xhr.send()
    
    xhr.onreadystatechange = () => {
        console.log(xhr.readyState)
    }
    

    没有输出,上面的两道题在 面试 | Ajax,fetch,axios的超高频面试题 有解析。

    3. 一道 Ajax 异步思考题,求输出结果。

    let xhr = new XMLHttpRequest()
    xhr.open('post', 'api')
    xhr.onreadystatechange = () =>{
        console.log(xhr.readyState)
    }
    xhr.send()
    /* 输出
    *   2 -> 3 -> 4。
    */
    

    xhr.onreadystatechange 是异步的会加入到等待队列,主任务执行 xhr.send() 后 ajax 的状态码变成 1。主任务空闲等待任务中的 xhr.onreadystatechange 开始监听到状态码变化,知道状态码由2 -> 3 -> 4 后不再变化。如果不熟悉 ajax 状态码的可以看看 面试 | Ajax,fetch,axios的超高频面试题

    4. promise 热身题,求输出结果

    console.log(1)
    new Promise((resolve, reject) => {
        console.log(2)
        resolve()
    }).then(res => {
        console.log(3)
    })
    console.log(4)
    /* 输出
    * 1 -> 2 -> 4 ->3 
    */
    

    解答:第一轮宏任务就是主栈中的同步任务,先输出1,JS 代码执行到promise立即执行输出2resolve.then() 中的代码放入到微任务队列,宏任务结束后输出 4,最后执行微任务队列输出3

    4. setTimeout 和 Promise 的执行顺序

    setTimeout(function () {
        console.log(1)
    }, 0);
    
    new Promise(function (resolve, reject) {
        console.log(2);
        resolve();
    }).then(function () {
        console.log(3)
    }).then(function () {
        console.log(4)
    });
    
    console.log(6);
    // 2, 6, 3, 4, 1
    

    解答:先开始主栈中的宏任务,遇到setTimeout后丢入宏任务队列等待,遇到promise立即执行输出2resolve()异步的丢入微任务队列,最后输出6,第一个宏任务执行结束开始留下来的微任务,即 .then() 输出 3, 4。第一轮循环结束开始下一轮宏任务 setTimeout,输出1

    5. setTimeout 和 Promise 的执行顺序

    setTimeout(function () {
        console.log(1)
    }, 0);
    
    new Promise(function (resolve, reject) {
        console.log(2)
        for (var i = 0; i < 10000; i++) {
            if (i === 10) {
                console.log(10)
            }
            i == 9999 && resolve();
        }
        console.log(3)
    }).then(function () {
        console.log(4)
    })
    console.log(5);
    // 2, 10, 3, 5, 4, 1
    

    这道题的解法和上面相同,都需要区分宏任务和微任务。

    6.求输出结果

    console.log("start");
    setTimeout(() => {
        console.log("children2")
        Promise.resolve().then(() =>{
            console.log("children3")
        })
    }, 0)
    
    new Promise(function(resolve, reject){
        console.log("children4")
        setTimeout(function(){
            console.log("children5")
            resolve("children6")
        }, 0)
    }).then(res =>{         // flag
        console.log("children7")
        setTimeout(() =>{
            console.log(res)
        }, 0)
    })
    // start children4 children2 children3  children5  children7 children6
    
    1. 首先开始主任务中的第一轮宏任务,输出start,遇到 setTimeout 不需要等待 0s 而是直接丢入宏任务队列(有人说需要等待 0s再放入到任务队列是不对的,可以使用console.time/timeEnd来测试),遇到promise立即执行输出children4,又遇到一个setTimeout 直接又丢入到宏任务队列,第一轮宏任务执行完,且没有微任务。问:上面的 .then() (注释的flag处) 是第一轮宏任务循环的微任务吗?不是!因为resolve 都没有执行,promise 的状态都还没有从pending改变,就不是第一轮的微任务。
    2. 开始下一轮的宏任务执行第一个进入的 setTimeout,输出children2,第二轮宏任务结束,开始微任务执行promise 中的.then() 输出 children3。第二轮循环结束
    3. 接着又开始setTimeout 的宏任务,输出children5,微任务输出 children7。这里遇到一个宏任务 setTimeout,丢入宏任务队列。
    4. 又开始新 setTimeout 宏任务,输出 res children6

    7. (头条)请写出下面代码的运行结果(不同的环境下输出有差异,下面以最新的 Chromium 为准)

    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((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
    

    这道题的难点在于是 promise2还是 async1 end 先输出。从全局宏任务之上而下执行时 await async2() 后面的代码 console.log('async1 end') 先进入 promise 中的微任务队列,最后.then() 中的console.log('promise2') 再进入到 promise 中的微任务队列。所以再开始下一轮宏任务循环之前先输出了 async1 end 再输出了 promise2。全局中的微任务执行完成开始下一轮宏任务setTimeout 最后输出 setTimeout

    8. 将上一道题目变换一下,求输出(不同的环境下输出有差异,下面以最新的 Chromium 为准)

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function 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
    

    首先开始全局下的宏任务依次输出 script start, async1 start, promise1, promise3, script end。其中 await async2();async2().then()的代码先进入到promise的微任务队列,await async2(); 后面的代码再进入到promise的任务队列,console.log('promise4'); 最后进入到 promise 的任务队列。全局下的宏任务结束,开始全局下的微任务,promise 的微任务队列中按照队列的先进先出原则依次输出,promise2,async1 end,promise4。全局微任务结束,开始下一轮的宏任务setTimeout,最终输出 setTimeout

    9. 再来将上面的题目变换一下,求输出(不同的环境下输出有差异,下面以最新的 Chromium 为准)

    async function async1() {
        console.log('async1 start');
        await async2();
        setTimeout(function() {
            console.log('setTimeout1')  // 这一部分代码会放入到 promise 的微任务队列中。
        },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
    

    按照上面的解析,原理都是一样的,全局下的宏任务执行完成后,开始执行全局下的微任务.then() 中的代码,最后开始下一轮宏任务的执行,下一轮宏任务是 setTimeout3 先执行,因为是setTimeout3 先加入下一个宏任务队列中的,再依次加入setTimeout2, setTimeout1到宏任务队列。所以输出的结果是setTimeout3, setTimeout2, setTimeout1

    参考

    《Javascript 忍者秘籍》第二版,事件循环篇

    第 10 题:常见异步笔试题,请写出代码的运行结果

    结束

    js 异步队列的题目就先到这里,如果觉得不过瘾的可以看看这篇文章的面试 | JS 你不得不懂的 异步编程 | promise 篇超高频面试题面试题

    相关文章

      网友评论

        本文标题:面试 | JS 事件循环 event loop 经典面试题含答案

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