js 异步执行顺序

js的执行顺序,先同步后异步
异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列
调用Promise 中的resolvereject属于微任务队列,setTimeout属于宏任务队列
注意以上都是 队列,先进先出。
微任务包括 process.nextTickpromiseMutationObserver
宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

事件循环是什么

首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

在JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

同步任务与异步任务的运行流程图如下:

image.png

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

调用栈(Call Stack)

是一种后进先出的数据结构。当一个脚本执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入调用栈中,然后从头开始执行。

事件队列 (Task Queue)

js引擎遇到一个异步任务后并不会一直等待其返回结果,而是会将这个任务交给浏览器的其他模块进行处理(以webkit为例,是webcore模块),继续执行调用栈中的其他任务。当一个异步任务返回结果后,js引擎会将这个任务加入与当前调用栈不同的另一个队列,我们称之为事件队列。

当一个脚本执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入调用栈中,然后从头开始执行。

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起(其他模块进行处理),继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入到事件队列。

被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这个过程被称为“事件循环(Event Loop)”。

同步任务

首先,我们用一个栈来表示主线程

image.png

当有多个同步任务时,这些同步任务会依次入栈出栈,如下图

image.png

同步任务1先入栈,执行完之后,出栈,接着同步任务2入栈,依此类推

这只是同步任务的执行方式,那么异步任务呢?

异步任务

异步任务会在同步任务执行之后再去执行,那么如果异步任务代码在同步任务代码之前呢?在js机制里,存在一个队列,叫做任务队列,专门用来存放异步任务。也就是说,当异步任务出现的时候,会先将异步任务存放在任务队列中,当执行完所有的同步任务之后,再去调用任务队列中的异步任务

例如下图,现在存在两个同步任务,两个异步任务

image.png

js会先将同步任务1提至主线程,然后发现异步任务1和2,则将异步任务1和2依次放入任务队列

image.png

异步任务分类

js中,又将异步任务分为宏任务和微任务,所以,上述任务队列也分为宏任务队列和微任务队列,那么,什么是宏任务,什么是微任务呢?

I/O、定时器、事件绑定、ajax等都是宏任务

Promise的then、catch、finally和process的nextTick都是微任务

注意:Promise的then等方法是微任务,而Promise中的代码是同步任务,并且,nextTick的执行顺序优先于Promise的then等方法,因为nextTick是直接告诉浏览器说要尽快执行,而不是放入队列

js中,微任务总是先于宏任务执行,也就是说,这三种任务的执行顺序是:同步任务>微任务>宏任务

宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

console.log(1)
setTimeout(()=>{    
  console.log(2)}, 0)
new Promise((resolve, reject)=>{    
    console.log('new Promise')    
    resolve()
  }).then(()=>{    
    console.log('then')
})
console.log(3)

如果按照上面流程图来分析代码,我们会得到下面的执行步骤:

  • console.log(1),同步任务,主线程中执行
  • setTimeout() ,异步任务,放到 Event Table,0 毫秒后console.log(2)回调推入 Event Queue 中
  • new Promise ,同步任务,主线程直接执行
  • .then ,异步任务,放到 Event Table
  • console.log(3),同步任务,主线程执行

所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'

但是实际结果是:1=>'new Promise'=> 3 => 'then' => 2

出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取

例子中 setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反

原因在于异步任务还可以细分为微任务与宏任务

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示

image.png

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

回到上面的题目

console.log(1)
setTimeout(()=>{    
  console.log(2)}, 0)
new Promise((resolve, reject)=>{    
    console.log('new Promise')    
    resolve()
  }).then(()=>{    
    console.log('then')
})
console.log(3)

流程如下

遇到 console.log(1) ,直接打印 1
遇到定时器,属于新的宏任务,留着后面执行
遇到 new Promise,这个是直接执行的,打印 'new Promise'
.then 属于微任务,放入微任务队列,后面再执行
遇到 console.log(3) 直接打印 3
好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

async与await

async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行

async和await是es7提供的语法,相比于es6的promise ,具有更高的代码可读性

从字面意思理解async是异步的意思,await是等待的意思,那么他们的作用就很容易看出了:

async : 声明一个函数是异步的

await : 等待一个异步函数执行完成

语法注意:await必须声明在async内部,因为async会阻断后边代码的执行,说到阻断大家不要慌,因为这里的阻断都是在一个async声明的promise函数里的阻断,不会影响整体代码的执行,async外边的代码还是照常执行

async语法的优势

async语法在处理then链的时候有优势,我们如果用promise的then嵌套可能会写出多个then链,虽然解决了回调地狱的问题,但是感觉好乱啊

用async和await实现多接口调用的代码:

function callServe1(){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve({result:"callserve1"})
        }, 1000);
    })}
function callServe2(res){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(res)
        }, 1000);
    })}
function callServe3(res){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(res)
        }, 1000);
    })}
async function getAll(){
    const result1 = await callServe1()
    const result2 = await callServe2(result1)
    const result3 = await callServe3(result2)
    console.log(result3) //{ result: 'callserve1' }}
getAll()

async

1.函数前面加上 async 关键字,则该函数会返回一个结果为 promise 的对象。

2. async 函数返回 promise 对象的状态。

2.1:如果返回的是一个非 Promise 类型的数据, 则async 函数返回 promise 的状态 为 fulfilled 成功。

2.2:如果返回的是一个 Promise对象,则 async 函数返回 promise 的状态由返回的Promise对象的状态决定。

2.3:如果 throw Errow 抛出异常,则 async 函数返回 promise 的状态为 rejected 失败。

async函数返回一个promise对象,下面两种方法是等效的

function f() {
    return Promise.resolve('TEST');
}
async function asyncF() {
    return 'TEST';
}

await

1.await 右侧的表达式一般为 promise 对象。

2.await 是在等一个表达式的结果,等一个返回值(回调成功的内容)

3.如果 await 右侧是一个 promise 对象,则 await 返回的是 promise 成功的值。

注意:

1.await 必须写在 async 函数中,但 async 函数中可以没有 await。

2.如果 await 的 promise 失败了,就会抛出异常,该异常需要通过 try catch 捕获处理。

3.如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值

async function f(){    // 等同于    // return 123    return await 123}f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}
async function fn2 (){
    console.log('fn2')
}
fn1()
console.log(3)

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1,fn2,3,2

流程分析

通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解

这里直接上代码:

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')
})
async1()new Promise(function (resolve) {
    console.log('promise1')
    resolve()}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析过程:

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

题目如下:

async function async1(){
    console.log('1')
    await async2()
    console.log('2')
}
async function async2(){
    console.log('3')
}
console.log('4')
setTimeout(function(){
    console.log('5') 
},0)  
async1();
new Promise(function(resolve){
    console.log('6')
    resolve();
}).then(function(){
    console.log('7')
})
console.log('8')

答案如下:
。。。。。。。。

原文链接:js原理之事件循环Event Loop 宏任务与微任务 async与await

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容