宏任务与微任务AND面试题详解

首先我们看一下19年风靡一时的面试题,大家都应该看到过

console.log('script start')

async function async1() {
    
    
await async2()
console.log('async1 end')
}
async function async2() {
    
    
console.log('async2 end')
}
async1()

setTimeout(function() {
    
    
console.log('setTimeout')
}, 0)

new Promise(resolve => {
    
    
console.log('Promise')
resolve()
})
.then(function() {
    
    
console.log('promise1')
})
.then(function() {
    
    
console.log('promise2')
})

console.log('script end')

这个面试题大家都应该很熟悉,今天打工人(me)比较闲看了一下这个面试题,然后看了一下简单的原理,如果哪里写的不到位,还望大佬指点。
主要的流程
1.async await
2.宏任务与微任务
3.本题简单讲解

首先我们说一下async await

async await为ES7为了解决promise回调地狱应运而生的产物,并且使代码更加清晰易懂。
async 是Generator函数的语法糖,并对Generator函数进行了改进。

Generator函数简介

generator跟函数很像,定义如下:

//主要关注点:
//1.*星号
//2.yield
function* foo(x) {
    
    
    yield x + 1;
    yield x + 2;
    return x + 3;
}

当我们执行上边这个函数时

hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }

由结果可以看出,Generator函数被调用时并不会执行,只有当调用next方法、内部指针指向该语句时才会执行,即函数可以暂停,也可以恢复执行。每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点(跟我们接下来说的宏任务和微任务相关):

1.回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
2.Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

一个基于 Promise 对象的简单自动执行器:
当我们用generator函数解析上边的函数解析如下

function* foo(x) {
    let response1 = yield fetch(x+1) //返回promise对象
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch(x+2) //返回promise对象
    console.log('response2')
    console.log(response2)
}
run(foo);

async/await

ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。

前文中的代码,用async实现是这样:

const foo = async () => {
    
    
    let response1 = await fetch(x+1) 
    console.log('response1')
    console.log(response1)
    let response2 = await fetch(x+2) 
    console.log('response2')
    console.log(response2)
}

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await(这就是上边我说的关注的两个点的原因)。

这时候可能有人会说:这样的执行方法跟Generator有啥区别呢,这样只不过是写法上的一种改变

async函数对 Generator 函数的改进,体现在以下四点:

1.内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。

2.更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

3.更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

4.返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

宏任务和微任务以及他们的执行顺序

说到宏任务和微任务,我们就不得不提 Event Loop 了
JS的本质是单线:

  1. 一般来说,非阻塞性的任务采取同步的方式,直接在主线程的执行栈完成。
  2. 一般来说,阻塞性的任务都会采用异步来执行,异步的工作一般会交给其他线程完成,然后回调函数会放到事件队列中。

当主线程的任务执行完了(执行栈空了),JS会去询问事件队列

执行一个宏任务(先执行同步代码)–>执行所有微任务–>UI render–>执行下一个宏任务–>执行所有微任务–>UI render–>…

根据HTML Standard,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行UI render。即:macro-task任务执行完毕,接着执行完所有的micro-task任务后,此时本轮循环结束,开始执行UI render。UI render完毕之后接着下一轮循环。但是UI render不一定会执行,因为需要考虑ui渲染消耗的性能已经有没有ui变动

宏任务

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

宏任务包含:
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

微任务

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

微任务包含:
Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)

当我们看完这些再回头来看上边这个题

console.log('script start')

async function async1() {
    
    
await async2()
console.log('async1 end')
}
async function async2() {
    
    
console.log('async2 end')
}
async1()

setTimeout(function() {
    
    
console.log('setTimeout')
}, 0)

new Promise(resolve => {
    
    
console.log('Promise')
resolve()
})
.then(function() {
    
    
console.log('promise1')
})
.then(function() {
    
    
console.log('promise2')
})

console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析这段代码:

1.执行代码,输出script start。

2.执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。

3.遇到setTimeout,产生一个宏任务

4.执行Promise,输出Promise。遇到then,产生第一个微任务

5.继续执行代码,输出script end

6.代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务

7.执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1

8.执行await,实际上会产生一个promise返回,即

9.let promise_ = new Promise((resolve,reject){ resolve(undefined)})
执行完成,执行await后面的语句,输出async1 end

10.最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

猜你喜欢

转载自blog.csdn.net/lbchenxy/article/details/109382124
今日推荐