结合异步面试题浅谈eventloop

最近室友疯狂在面试,面试题也刷了不少,中标率比较高的就有关于异步事件执行顺序的问题。下面总结一下我对这类题的解题思路:(1)了解js的事件执行机制(也就是我们常说eventloop) (2)了解清楚微任务和宏任务,异步事件哪些属于微任务,哪些属于宏任务。

先上一道面试题,主要涉及的是async、await、setTimeout、promise的执行顺序

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

下面我们要先了解一些概念

javascript是单线程

javascript是单线程,不能使用多线程,比如修改dom这种操作,如果是多个线程在操作,浏览器不知道是以哪个线程为结果。单线程能保证执行队列,但限制了性能及扩展。

任务队列

1、js事件分为同步任务和异步任务

2、同步任务都在主线程上执行,形成一个执行栈

3、除主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,便将事件加入任务队列中。

4、当执行栈中的所有任务执行完毕,便去读取任务队列中的任务,将这些可运行的异步任务添加到执行中,开始运行。

任务队列又分为两种:一种是微任务队列,一种是宏任务队列。来自于同一任务源的任务被放到同一种任务队列。

下面展示一张示意图加强大家理解


宏任务

(macro)task,也叫宏任务,可以将每次主线程上执行的任务看做宏任务。宏任务包括script(整体代码块),setTimeout、setInterval、I/O、UI交互时间、postMessage、MessageChannel、setImmediate(Node.js 环境)。

浏览器为了js内部(macro)task任务和DOM任务能有序执行,会在上一个macro task执行完毕之后,对页面进行重新渲染,然后再执行下一个macro task。

              上一个macro task  ----->  页面渲染   ----->  下一个macro task             

微任务

(micro)task,也叫微任务,可以理解为当前任务执行完毕之后马上执行的任务,也就是在页面渲染之前执行。microtask主要包含:Promise.then、async/await、MutaionObserver、process.nextTick(Node.js 环境)。

综合上述宏任务的执行,整个事件执行顺序我是这样理解

主线程的同步任务(macro task)  -----> 异步队列的微任务队列  ----->  页面渲染   ----->  异步
队列的宏任务队列  (以此循环)复制代码

运行机制

在事件循环中,每一次循环都称为一次tick,每一次tick的任务关键步骤如下

1、执行主线程上的宏任务(执行栈中没有去任务队列中取)

2、执行过程中遇到异步事件,对其进行判断(属于微任务的将它添加到微任务队列,属于宏任务将它添加到宏任务队列)

3、执行栈中任务执行完毕之后,将微任务队列中的所有微任务添加到执行栈,并立即执行(先入先出)

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

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

下面是运行机制流程图


promise和async中的立即执行

promise的异步执行在then和catch中,promise中的代码都是同步在执行的,async/await中await之前的代码都是同步执行。而具体await做了什么?实际上await是一个让出线程的标志,当代码执行遇到await的时候,会立即执行await 后面的表达式,然后将表达式之后的代码,加入microtask队列,然后跳出async函数继续来执行后面的代码。

所以对于上题中的async1()函数

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

相当于

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

所以根据上面的解释说明,开篇的异步输出的题目可以详解为以下的过程

所有的代码块相当于一个宏任务,事件执行顺序始于一个宏任务。首先定义了两个函数,接着执行console,所以先输出script start;然后遇到setTimeout,将其加入宏任务队列。接着执行async1函数,await之前的代码立即会执行,输出async1 start;紧跟await的表达式也会立即执行,接着输出async2,之后的代码将其加入微任务队列;接着遇到promise,promise中的代码立即执行,输出promise1,然后将then中的代码加入微任务队列,再执行script end的输出。到这一步所有同步任务执行完毕

同步任务输出顺序为

script start
async1 start
async2
promise1
script end复制代码

此时异步队列分为微任务队列和宏任务队列,根据进入队列顺序如下

微任务队列

async1 end
promise2复制代码

宏任务队列

setTimeout复制代码

微任务执行在宏任务之前,根据先入先出原则,最终输出结果为

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout复制代码

如果大家想看自己是不是理解了,下面还有几道题可以试着做

变式一

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复制代码

变式二

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复制代码


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值