JS事件循环和经典异步面试题

32 篇文章 4 订阅

关于JS

这个标题下,我们只需要牢记一句话: JavaScript 是单线程语言

既然 JavaScript 是单线程语言,那么就会存在一个问题,所有的代码都得一句一句的来执行。就像我们在食堂排队打饭,必须一个一个排队点菜结账。那些没有排到的,就得等着~

事件循环(Event Loop)

JavaScript 有一个主线程 main thread,和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行

JavaScript 的 Event Loop 是伴随着整个源码文件生命周期的,只要当前 JavaScript 在运行中,内部的这个循环就会不断地循环下去,去寻找 队列queue 里面能执行的 任务task

任务队列(task queue)

task,就是任务的意思,我们这里理解为每一个语句就是一个任务

console.log(1);
console.log(2);

如上语句,其实就是就可以理解为两个任务task

而 队列queue 呢,就是先入先出FIFO的队列!

所以任务队列 Task Queue 就是承载任务的队列。而 JavaScript 的 Event Loop 就是会不断地过来找这个队列queue,问有没有任务task 可以运行运行。

同步任务(SyncTask)、异步任务(AsyncTask)

同步任务说白了就是主线程来执行的时候立即就能执行的代码,比如:

console.log('this is THE LAST TIME');
console.log('Nealyang');

代码在执行到上述 console 的时候,就会立即在控制台上打印相应结果。

而所谓的异步任务就是主线程执行到这个 task 的时候,“唉!你等会,我现在先不执行,等我 xxx 完了以后我再来等你执行” 注意上述我说的是等你来执行。

说白了,异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行

setTimeout(()=>{
  console.log(2)
});
console.log(1);

如上述代码,setTimeout 就是一个异步任务,主线程去执行的时候遇到 setTimeout 发现是一个异步任务,就先注册了一个异步的回调,

然后接着执行下面的语句console.log(1),等上面的异步任务等待的时间到了以后,在执行console.log(2)。具体的执行机制会在后面剖析。

图片

  • 主线程自上而下执行所有代码

  • 同步任务直接进入到主线程被执行,而异步任务则进入到 Event Table 并注册相对应的回调函数

  • 异步任务完成后,Event Table 会将这个函数移入 Event Queue

  • 主线程任务执行完了以后,会从Event Queue中读取任务,进入到主线程去执行。

  • 循环如上

上述动作不断循环,就是我们所说的事件循环(Event Loop)。

宏任务(MacroTask)、微任务(MicroTask)

JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)

 宏任务(MacroTask),所有的同步任务代码都是宏任务(MacroTask)(这么说其实不是很严谨,下面解释),setTimeoutsetIntervalI/OUI Rendering 等都是宏任务。

微任务(MicroTask),为什么说上述不严谨我却还是强调所有的同步任务都是 宏任务(MacroTask)呢,因为我们仅仅需要记住几个 微任务(MicroTask)即可,排除法!别的都是 宏任务(MacroTask)

微任务(MicroTask)包括:Process.nextTickPromise.then catch finally(注意我不是说 Promise)、MutationObserver

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

Promise和async中的立即执行

我们知道Promise中的异步体现在thencatch,所以写在Promise中的代码是被当做同步任务立即执行的。

而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

async await

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

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

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

关于async await

async函数其实是Geneator函数的语法糖。

1.async函数的返回值是Promise对象,可以用then方法指定下一步的操作(添加回调函数)。async函数可以看做多个异步操作,包装成一个Promise对象,await命令就是内部then命令的语法糖

2.当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句。

3.async函数返回一个Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。

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

面试题一:

//请写出输出内容
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
*/

1、首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:

2、然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:

3、script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start

遇到了await时,会将await的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log('async1 end')加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。

4、script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。

5、script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。

根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。

因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 endpromise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。

6、第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。

面试题二:

在第一个变式中我将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
*/

在第一次macrotask执行完之后,也就是输出script end之后,会去清理所有microtask。所以会相继输出promise2async1 end ,promise4

面试题三:

在第二个变式中,我将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
*/

在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按3,2,1的顺序来输出。

面试题四:

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
*/

1、执行console.log('script start')
2、将setTimeout放入宏任务中(宏任务1)
3、将console.log('promise1')放入微任务(微任务1)
4、执行a1()依次输出a1 start、a2,将await返回的promise放入微任务中(微任务2)
5、输出promise2',将resolve('promise2.then')放入微任务中(微任务3)
6、输出'script end'
主线程结束
开始任务队列
1、执行微任务1 ,输出promise1
2、执行微任务2,输出a1 end
3、执行微任务3,输出'promise2.then',将console.log('promise3')放入微任务(微任务4)
4、执行微任务4,输出promise3
5、执行宏任务1,输出setTimeout

面试题五:

const promise = new Promise((resolve, reject) => {
    resolve('success1');
    reject('error');
    resolve('success2');
});
 
promise.then((res) => {
    console.log('then:', res);
}).catch((err) => {
    console.log('catch:', err);
})
 

resolve 函数将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

reject 函数将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

而一旦状态改变,就不会再变
所以 代码中的reject('error'); 不会有作用。

Promise 只能 resolve 一次,剩下的调用都会被忽略
所以 第二次的 resolve('success2'); 也不会有作用。

面试题六:

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
})
 
promise.then(() => {
    console.log(3);
})
 
console.log(4);
 

首先 Promise 新建后立即执行,所以会先输出 1,2,而 Promise.then() 内部的代码在 当次 事件循环的 结尾 立刻执行 ,所以会继续输出4,最后输出3。

面试题七:

const first = () => (new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
        console.log(7);
        setTimeout(() => {
            console.log(5);
            resolve(6);
        }, 0)
        resolve(1);
    });
    resolve(2);
    p.then((arg) => {
        console.log(arg);
    });
 
}));
 
first().then((arg) => {
    console.log(arg);
});
console.log(4);
 

第一轮事件循环

  1. 先执行宏任务,主script ,new Promise立即执行,输出【3】,
  2. 执行 p 这个new Promise 操作,输出【7】,
  3. 发现 setTimeout,将回调放入下一轮任务队列(Event Queue),p 的 then,姑且叫做 then1,放入微任务队列,发现 first 的 then,叫 then2,放入微任务队列。执行console.log(4),输出【4】,宏任务执行结束。
  4. 再执行微任务,执行 then1,输出【1】,
  5. 执行 then2,输出【2】。
  6. 到此为止,第一轮事件循环结束。开始执行第二轮。

第二轮事件循环

  1. 先执行宏任务里面的,也就是 setTimeout 的回调,输出【5】。
  2. resolve(6) 不会生效,因为 p 这个 Promise 的状态一旦改变就不会在改变了

面试题八:

async function a() {
    console.log(1);
    await console.log(2);
    await console.log(3);

    console.log(4);
}
a();
new Promise((resolve, reject) => {
    console.log(5);
    resolve(6);
}).then(res => {
    console.log(res);
});

输出125364 

如果两个await在一个async里面,那么第二个await需要等待第一个await执行完才执行,整个async函数会停止执行,而主线程去执行async函数之外的任务

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值