JS面试——宏任务与微任务

2023-11-13


在这里插入图片描述

1.问题的由来

    我相信,你也曾遇到了如下疑惑:
在这里插入图片描述
    当时,我是直接背下来,先执行console.log再执行setTimeout函数内部代码。如今,觉得在前端混了一些时间了,不太懂的问题还是得搞懂,拖得越久和大佬们的差距也就越大。
    其实这个本质是由于JavaScript采用单线程的设计模式,代码执行有一个潜在的执行顺序。之所以采用单线程主要是为了方式类似于同时修改和删除dom树等操作的出现。因此代码都按照同步执行,依次执行一个任务。但是,当需要执行的任务中递归任务和循环任务比较多,浏览器需要花费大量时间去解析,浏览器可能会出现假死
    因此,浏览器还存在异步操作以缓解这种压力。主线程用来同步执行代码,当主线程执行时遇到了异步事件,会将异步事件加入到异步队列中,等主线程同步代码执行完毕后再来执行异步代码。
   想要从根本上明白代码的执行顺序,只了解setTimeout还远远不够深入,需要进一步了解宏任务和微任务两大概念。

2.JavaScript的宏任务与微任务

    宏任务主要包括 setTimeOut,setInterval,DOM事件,Ajax请求;微任务主要包括Promise,async/await。微任务的执行时机要比宏任务早。宏任务包含的内容是我们再熟悉不过得了,下面主要说一下微任务里面的三种函数的使用方法。

2.1promise的使用方法

    这个问题其实困扰了我有一段时间了,讲道理我不是很懂这个,但是,最近因为要总结这些知识点,不得不迎难而上,最终,在Bilibili的UP主——技术蛋老师找到了合适的答案:
在这里插入图片描述
    讲的是这么一个道理,情人节一对情侣决定要一个孩子,但是对于女生是否怀孕也只有女生自己知道,如果女生怀孕了他会给她的男朋友说“孩子他爹”,如果女生没有怀孕则会称呼她的男朋友为“老公”,无论结果如何,最后两人在情人节后还是会结婚,基于promise方法实现如下所示:

 const isPregnant = false;
        let p = new Promise((resolve, reject) => {
            if (isPregnant) resolve("孩子他爹");
            else reject("老公");
        }).then((data) => {
            console.log("应该对男友的称呼:", data);
        }).catch((data) => {
            console.log("应该对男友的称呼", data);
        }).finally(() => {
            console.log("最后,男女朋友结婚了");
        });

    通过上述实例,我们可以得到,Promise对象拥有两个参数,其中第一个参数是表示函数成功时执行的函数,第二个参数表示promise对象失败时执行的函数,这里的then和catch一样,分别对应着上面的resolve与reject。并且resolve与reject函数的参数会传递到对应的then和catch函数里面。无论执行了resolve与reject那个成功了finally都会被执行。
    提到Promise函数,我又想起了之前的一个很牛逼的概念——回调地狱,顺道把这个概念也处理了。所谓的回调地狱,说起的其实是为了让request1,request2,request3这三个异步函数能够按照顺序执行,因此采用嵌套的方式执行,但是由于嵌套的重数可能比较多导致代码可读性下降,举例如下(借一下大佬的例子):
在这里插入图片描述
    如果想要解决这个问题,可以使用如下方法实现(由于实际嵌套调用的时候使用的都是函数名,代码的可读性会依然保留):
在这里插入图片描述

2.2async与await的使用方法

    再讲async和await之前,需要介绍一个概念——语法糖,语法糖是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言:操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。ES6中载入了很多很多的语法糖,会比如比较常用的写法:

//用在匿名函数中
let test=()=>{
    console.log("语法糖举例");
}

    async和await的本质也是一种语法糖,async是用来返回promise对象的语法糖,await其实就是.then处理的语法糖(await只能在async函数里面使用)。

 //原生写法
let p = new Promise((resolve) => {
     resolve(1);
 });
 console.log(p);
 //async语法糖
 async function fun() {
     return 1;
 }
 console.log(fun());
//await语法糖
let p = new Promise((resolve) => {
    resolve(1);
}).then((data) => {
    console.log("所传入的参数为:", data);
});
async function fun() {
    await p;
};

在这里插入图片描述

3.面试题实战分析——程序执行顺序

    如果你觉得你看懂了上面的知识点,那么接下来,我们利用一道面试题,了解了解代码的执行顺序。请看下题,要求给出数组的输出顺序:

console.log("1");
setTimeout(function () {
    console.log("2");
    new Promise(function (resolve) {
        console.log("3");
        resolve();
    }).then(function () {
        console.log("4");
    });
});
new Promise(function (resolve) {
    console.log("5");
    resolve();
}).then(function () {
    console.log("6");
});
setTimeout(function () {
    console.log("7");
});
setTimeout(function () {
    console.log("8");
    new Promise(function (resolve) {
        console.log("9");
        resolve();
    }).then(function () {
        console.log("10");
    });
});
new Promise(function (resolve) {
    console.log("11");
    resolve();
}).then(function () {
    console.log("12");
});
console.log("13");

    为了弄清楚这道题,需要明白JavaScript再执行时遵循的规则:
(1)先执行同步代码,然后执行异步代码;
(2)Promise本身是同步的,但是其回调是异步的;
(3)微任务要比宏任务先执行;


    参考分析思路如下:
(1)首先由于Promise本身是同步的,promise属于微任务,遇到promise的时候还是会进入Promise对象,但是其回调将会存入回调任务序列队列中,因此第一波分析可以得到的顺序是:1-5-11-13;
(2)接下来,由于微任务优先于宏任务,释放回调队列,第二波分析得到的顺序是:6-12;
(3)然后,再来考虑每一个定时器,第一个定时器,显然有2-3,那么问题是这个定时器里面的4呢,回到微任务与宏任务上来,不同的定时器就是不同的宏任务,因此,4应该在下一个计时器进入前输出,即2-3-4;
(4)最后依次分析,得到最终结果如下:

1-5-11-13-6-12-2-3-4-7-8-9-10;

    最后,真心感谢各位大佬的博客,感觉的确收获了很多。
在这里插入图片描述

参考文献

    (1)JavaScript 运行机制解析

    (2)回调地狱以及用promise怎么解决回调地狱

    (3)什么是语法糖?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JS面试——宏任务与微任务 的相关文章

随机推荐