JavaScript—异步、事件循环面试题

因为对知识点的理解不到位,导致文章大改。以后不想写博客了。

一、基础知识

1.1 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。

  1. Promise是一个构造函数
    1. 可以创建Promise的实例,const p = new Promise()
    2. new出来的Promise实例,代表一个异步操作,该异步操作立即执行
  2. Promise.prototype原型对象上包含一个.then()方法
    1. 每一次new Promise()构造函数得到的实例对象,都可以通过原型链的方式访问到.then(),例如p.then()
  3. .then()方法用来预先指定成功和失败的回调函数
    1. p.then(成功的回调函数,失败的回调函数)
    2. 成功的回调函数是必选的,失败的回调函数式可选的
  4. 在.then()方法返回一个新的promise,可继续.then()
    1. 相当于在这个新的promise上继续设置回调函数
  5. 如果在promise的链式操作中发生了错误,可以使用Promise.prototype.catch方法进行捕获和处理。如果不希望前面的错误导致后续的.then无法正常执行,则可以将.catch的调用提前。

1.2 同步任务与异步任务

JS任务分为两类:同步任务与异步任务

  • 同步任务(非耗时任务):在主线程排队执行的任务
  • 异步任务(耗时任务):委托给宿主环境执行的任务
    • 宏任务:异步Ajax、setTimeout、setInterval、文件操作等
    • 微任务:Promise.then、Promise.catch等

 1.3 事件循环机制

JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。

  • 每一个宏任务执行完之后,都会检查是否存在待执行的微任务
    • 如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
    • 没有,则执行下一个宏任务。

Promise.then加入微任务队列的时机:(非常重要)

参考:promise then 的回调函数是在什么时候进入微任务队列的? - SegmentFault 思否

异步的 Promise的 then 方法的回调是何时被添加到microtasks queue中的? - 知乎

如果在调用then时,如果promise已经resolve了,那么就是fullfilled状态,会将其任务加入微任务队列;如果在调用then时,promise没有resolve或reject,处于pending状态,会将其任务缓存在一个结构里,直到promise被接受或者拒绝时,加入微任务队列。

二、面试题

1、实现sleep()函数

setTimout((), 1000);
console.log("finished");

通常来说,第一反应是直接用setTimout()作为sleep()函数,但上述方法不可行,因为setTimeout会直接被加入宏任务队列,而下面的console.log是同步任务,所以会直接先输出。

setTimout(()=>console.log("finished"), 1000);

这种方法可以实现1s后执行回调函数里的内容,但使用了回调函数的实现方式,可读性和维护性差

function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms))
}
sleep(1000).then( ()=> console.log("finished"));

可以使用Promise优雅的构建sleep方法,避免使用回调函数。


async await实际上只是语法糖,提供同步编程方式实现异步调用。(也就是看上去是同步的,休眠了1s之后做其他的事情,但是实际上是通过异步来实现的。

function sleep(ms){
    return new Promise((resolve)=>setTimeout((resolve), ms));
}
//立即执行函数
(async function (){
    await sleep(1000);
    console.log("fin");
    //需要执行的其他的代码
})();

实现每隔1s输出一个数字:

function sleep(ms){
    return new Promise((resolve)=>setTimeout((resolve), ms));
}
(async function (){
    for(let i = 0; i < 5; i++){
        await sleep(1000);
        console.log(i);
    }
})();

2、执行顺序-1

let promise = new Promise(function(resolve, reject){
    console.log("AAA");
    resolve();
});
promise.then(() => console.log("BBB"));
console.log("CCC")

// AAA
// CCC
// BBB

Promise新建后会立即执行,所以首先输出AAA。然后,promise调用resolve(),将.then()方法加入微任务队列,继续执行后面的同步任务,输出CCC,最后清空微任务队列,最后输出BBB

3、执行顺序-2

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <link rel="shortcut icon" href="#">
</head>
<script>
    setTimeout(function () {
        console.log('setTimeout', document.body);
    }, 0);
    new Promise((resolve) => { 
        console.log("hi"); 
        resolve(); 
    }).then(()=>{ console.log('promise', document.body);});
    console.log(document.body);
    //输出
    //hi
    //null
    //promise null
    //setTimout hello
</script>
<body>
    <div>hello</div>
</body>

</html>

考点一:Script脚本的加载:脚本比先一步页面加载,如何解决上述的问题?

  1. 将所有需要的<script>标签都放在</body>之前,确保脚本执行之前完成页面渲染而不会造成页面堵塞问题。
  2. 合并JS代码,尽可能少的使用script标签
  3. 通过给script标签增加 defer属性或者是 async 属性来实现,asyncdefer不同之处是async加载完成后会自动执行脚本,defer加载完成后需要等待页面也加载完成才会执行代码。<script src="file.js" defer></script>
  4. 动态创建script来加载
function loadJS( url, callback ){
    var script = document.createElement('script'),
        fn = callback || function(){};
    script.type = 'text/javascript';
    //IE
    if(script.readyState){
        script.onreadystatechange = function(){
            if( script.readyState == 'loaded' || script.readyState == 'complete' ){
                script.onreadystatechange = null;
                fn();
            }
        };
    }else{
    //其他浏览器
        script.onload = function(){ fn(); };
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}
//用法
loadJS('file.js', function(){ alert(1);});

考点二:微任务队列,宏任务队列

  1. setTimeout会将任务加入宏任务队列
  2. Promise新建后会立即执行,所以会先输出"hi",此时promise的状态为fullfilled,.then中的回调函数加入微任务队列。
  3. 输出document.body,但因为脚本的加载阻塞的页面的加载,所以输出"null"
  4. 清空微任务队列,输出"promise null"
  5. 此时页面的加载已经完成,输出宏任务队列的内容,"setTimeout hello"

4、执行顺序-3

const setDelay = (millisecond) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("hi");
            resolve(`我延迟了${millisecond}毫秒后输出的`)
        }, millisecond)
    })
}
const setDelaySecond = (seconds) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`我延迟了${seconds}秒后输出的,是第二个函数`)
        }, seconds * 1000)
    })
}

setDelay(2000)
    .then((result) => {
        console.log(result)
        console.log('我进行到第一步的');
        return setDelaySecond(3)
    })
    .then((result) => {
        console.log('我进行到第二步的');
        console.log(result);
    })

//输出:
//hi
//setDelay:延迟了2000毫秒后输出
//我进行到第一步的
//我进行到第二步的
//setDelaySecond:延迟了3秒后输出

考点:promise回调函数加入微任务队列的时机 

如果在调用then时,如果promise已经resolve了,那么就是fullfilled状态,会将其任务加入微任务队列;如果在调用then时,promise没有resolve或reject,处于pending状态,会将其任务缓存在一个结构里,直到promise被接受或者拒绝时,加入微任务队列。

这里的"hi"和"setDelay:延迟了2000毫秒后输出"是一起输出的,因为它们同时加入微任务队列。

5、执行顺序-4

基础知识:async声明的函数的返回本质上是一个Promise,在await表达式之后的代码可以被认为是存在在链式调用的then回调方法中。

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('illegalscript start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('illegalscript end');

输出顺序:

illegalscript start
async1 start
async2
promise1
illegalscript end
async1 end
promise2

setTimeout

理解:执行到await async2的时候,相当于new了一个promise,因此直接console.log('async2');,而console.log('async1 end');相当于promise的.then()方法,被加入到微任务队列里。后面的console.log('promise2');也被加入到微任务队列里,所以会在同步任务执行完成之后,输出微任务队列里的两个console.log。

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2 start');
    new Promise((resolve, reject) => {
        resolve();
        console.log('async2 promise');
    })
}

console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
}).then(function () {
    console.log('promise3');
});
console.log('script end');

输出顺序:

script start
async1 start
async2 start
async2 promise
promise1
script end
async1 end
promise2
promise3

setTimeout

理解:执行到await async2的时候,相当于new了一个promise,因此直接输出async2 start,且async2函数内的promise也是立刻执行的,于是马上输出async2 promise,此时async1 end被加入到微任务队列。

如果将async2函数内的内容改为:

async function async2() {
    console.log('async2 start');
    return new Promise((resolve, reject) => {
        resolve();
        console.log('async2 promise');
    })//new promise 会立即执行, then会分发到微任务
}

输出顺序:

script start
async1 start
async2 start
async2 promise 
promise1
script end
promise2
promise3
async1 end 

setTimeout

(依旧不知道为什么async1 end会在最后输出,只能猜测await fn在resolve之后.then()里的东西,放在一个介于微任务队列和宏任务队列的容器中,等到微任务队列中的东西清空后执行)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值