js异步机制

1、什么是异步机制?为什么js需要异步机制?

异步机制和同步机制是相对应的,异步是指:当代码按照顺序执行到一些比较耗时的操作,不会立刻执行,而是将这些操作推到一个队列中等待合适的时机从队列中取出任务执行(涉及到js的事件循环机制,这里不做展开),这就是js的异步机制。

因为js是单线程的,所有的JavaScript代码都在渲染主线程中执行,如果比较耗时的操作,如定时器设置了10秒后执行某项操作,如果没有异步机制,渲染主线程就会被阻塞在这里,10秒钟时间白白浪费,网页也会卡在那里不动,可想而知,用户的体验会有多差。有了异步机制,这种情况就会避免发生。

2、什么是异步操作?异步函数?回调函数?怎么实现异步机制?

异步操作是指那些比较耗时或者是需要等待的操作,比如说Dom元素事件监听,定时器操作,网络请求......

异步函数是指包含异步操作的函数,下面举个例子:

bash 复制代码
function asyncFn(){
    setTimeout(()=>{
        let result='123'
    },1000)
}

如上,asyncFn就是一个异步函数。里面包含setTimeout异步操作,调用asyncFn(),进入到异步函数内部,发现定时器异步操作,不会立即执行定时器任务。而是将定时器任务推到消息队列中,然后退出函数,继续向下执行。等待1000毫秒后,会将定时器任务从消息队列中取出,加入到渲染主线程中执行。

回调函数是用来接收和处理异步操作产生的结果的。比如说上面的定时器,这个异步操作在一秒钟后产生了结果'123',我想要将这个结果转换成一个Number类型,并且在控制台中打印出来,该怎么办呢?答案是用到一个回调函数,如下:

bash 复制代码
function asyncFn(callback){
    setTimeout(()=>{
        let result='123'
        callback(result)
    },1000)
}

const callback=(res)=>{
    console.log(Number(res))
}

asyncFn(callback)

如上,你定义一个回调函数,把他作为参数传递给异步函数。同时,将异步操作产生的结果作为参数传给回调函数,这样,就能在异步操作产生结果时,拿到这个结果,在回调函数中进行相应的操作。如此,你就实现了一种异步机制。

你可能会说,直接像下面这样不就得了吗?用的着那么麻烦?

bash 复制代码
function asyncFn(){
    setTimeout(()=>{
        let result='123'
        console.log(Number(result))
    },1000)
}

确实,上面这种方式实现了同样的功能。但是你有没有想过,setTimeout其实就是一个异步函数,他的第一个参数就是回调函数,只不过这其中的异步操作没有产生结果而已,因此他的回调函数没有接收参数。

所以,其实是一样的,只是你理解的角度不同。你把async作为异步函数,他其中的异步操作是产生了结果(定时器函数定义的变量result),你传递一个异步函数进去处理结果;你把setTimeout作为异步函数,他其中的异步操作是没有产生结果的,你只需要进行你想要的操作就是了。

3、以上这种使用异步函数+回调函数实现的异步机制有什么缺点?

首先,你需要在异步操作初始化时就定义好回调函数。而且这个异步操作产生的值只在短时间内存在,只有把这个值作为参数传给回调函数才能接收到他。

另外,想象一个需求,以上面的例子为例,如果说一秒后想把异步操作的结果变为Number类型打印出来,再过两秒把这个数字+1,再过五秒把这个数字+2......

bash 复制代码
function asyncFn(callback){
    setTimeout(()=>{
        let result='123'
        callback(result)
    },1000)
}

const callback3=(num)=>{
    console.log(num+2)
}

const callback2=(num)=>{
    console.log(num+1)
    let num2=num+1;
    setTimeout(()=>{
        callback3(num2)
    },5000)
}

const callback=(res)=>{
    console.log(Number(res))
    let num=Number(res)
    setTimeout(()=>{
        callback2(num)
    },2000)
}

asyncFn(callback)

如此,便实现了以上需求,虽然把每一次异步操作产生的结果都用抽离出来的回调函数进行处理,但还是会出现回调函数中嵌套调用异步函数的情况,出现回调地狱的问题。

4、一种新的异步机制,Promise(红宝书里翻译'期约')?

期约是es6中的一个异步编程的新机制。它的诞生解决了异步函数+回调函数实现异步机制而产生的回调地狱问题。期约是对异步操作的包装,并且能够拿到异步操作的结果,通过期约的一些实例方法可对异步操作产生的结果进行处理。

5、如何创建一个期约,期约有哪些状态?

通过new Promise(()=>{})可创建一个期约实例,参数为一个执行器函数,且这个执行器函数为必传。执行器函数中的代码同步执行,执行器函数本身有两个参数(resolve,reject),用于改变期约的状态,期约的状态改变了之后便不可逆。

通过构造函数,传递一个执行器函数创建出来的期约状态默认为待定(pending)状态,在执行器函数内部,你可以初始化异步操作,通过resolve或reject改变期约的状态为兑现(fulfilled)或拒绝(rejected),并将异步操作产生的结果存储在创建出来的Promise实例中,通过实例的方法如then方法来处理异步操作的结果。

通过Promise.resolve()可以实例化一个状态为兑现(fulfilled)的期约,该方法可将传给他的第一个参数,无论是什么类型的数据,全部包装成一个期约实例。但如果该参数本身就是一个期约实例,则相当于一个空包装,返回还是期约本身,如下:

bash 复制代码
let p=Promise.reject('1')
let p2=Promise.resolve(p)
console.log(p2)
console.log(p===p2)

通过Promise.reject()可以实例化一个状态为拒绝(rejected)的期约,该方法可以将传给他的第一个参数包装为一个状态为拒绝(rejected)的期约实例。与Promise.resolve()不同的是,如果这个参数是一个期约实例,他将作为新的期约实例的拒绝理由,如下:

bash 复制代码
let p=Promise.resolve('1')
let p2=Promise.reject(p)
console.log(p2)
console.log(p===p2)

简短对比总结Promise.resolve()和Promise.reject():

Promise.resolve()实例化出来的期约可能是兑现状态,也可能是拒绝状态(参数为一个拒绝状态的期约)。而Promise.reject()实例化出来的期约永远都是拒绝状态,且不存在幂等关系,就算传入的参数是拒绝状态的期约,也只是作为新期约的拒绝理由。

6、期约到底是同步的还是异步的,他的真实异步特性是怎样的?

通过Promise.reject()抛出错误,并不会被try/catch捕获到,且不会阻断后续代码的执行,如下:

bash 复制代码
try {
  Promise.reject()
  console.log('没有阻断执行')
}catch(e){
  console.log('捕获到了错误',e)
}

可以看到,没有阻断代码执行,最后异步抛出错误,没有被捕获到。

对比同步代码抛出错误:

bash 复制代码
try {
  throw new Error('123')
  console.log('没有阻断执行')
}catch(e){
  console.log('捕获到了错误',e)
}

错误被捕获到,阻断了代码执行,进入了catch.

由此可见,期约对象本身执行在同步模式中,但是他也是异步执行模式的媒介,错误抛出是在异步模式中,因此不会阻塞同步代码的执行。代码一旦进入异步执行模式,唯一与之交互的方式就是使用异步结构---------更具体的说,就是期约的方法。如上的错误想要捕获到就是调用期约的catch方法,如

这样,就捕获到了错误。

7、期约的实例方法有哪些?调用了期约的实例方法返回新期约,新期约状态如何?

由于在Promise.prototype上定义了then,catch,finally方法,因此实例化出来的期约对象都拥有这些实例方法。

  • 首先,介绍then方法,他接收onResolved和onRejected两个处理程序作为参数,分别处理期约的状态为兑现和拒绝。传给then方法的处理程序必须为函数形式,否则将被忽略。如果你只想传递onRejected处理程序,则将onResolved处理程序传为null。如下:
javascript 复制代码
let p=new Promise(()=>{})
let callback=(res)=>{
  console.log(res)
}
//有效传递
p.then(callback)
//或者
p.then((res)=>{
  console.log(res)
})

//无效传递
p.then('123')

//只传递onRejected处理程序
p.then(null,callback)

then方法返回的新期约的状态分为以下几种情况:

A)如果没有传递任何的处理程序,新期约即和父期约一致(但不是同一个对象)。

B)如果传递了处理程序,会根据父期约的落定状态,看相应处理程序中是否有返回值,如果有返回值,则用Promise.resolve()包装这个返回值作为新期约。如果没有返回值,则用Promise.resolve包装undefined作为新期约。

C)如果在处理程序中抛出异常,则会将异常对象作为拒绝期约的理由,产生新期约。

D)如果父期约的初始状态为pending,则新期约初始的状态也为pending。待父期约的状态更新后,新期约的状态也会根据以上ABC三条规则进行更新。

  • catch方法,在父期约的状态变为拒绝的时候调用。通常用来捕获异步操作的错误。

catch方法产生新期约的规则和then方法产生新期约的规则一致。

  • finally方法,在父期约的状态改为兑现或拒绝的时候都会执行,通常是用来添加清理代码。

finally方法产生新期约的状态规则如下:

A)通常情况下与父期约一致,无论处理程序的返回值是什么。

B)如果返回了一个待定的期约,新期约为待定期约

C)如果在onFinally处理程序中显式抛出错误或者返回一个拒绝的期约,新期约为拒绝期约。

特别注意:

返回待定期约的情况并不常见,这是因为只要期约一解决,新期约仍然和父期约一致,而不是待定期约解决的状态。如下:

javascript 复制代码
let p1=Promise.resolve('foo')
let p2=p1.finally(()=>{
  console.log('执行了finally')
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve('bar')
    },1000)
  })
})
console.log('首次p2',p2)
setTimeout(()=>{
  console.log('再次p2',p2)
},2000)

如果期约被拒绝,则新期约的状态为拒绝,如下:

javascript 复制代码
let p1=Promise.resolve('foo')
let p2=p1.finally(()=>{
  console.log('执行了finally')
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      reject('bar')
    },1000)
  })
})
console.log('首次p2',p2)
setTimeout(()=>{
  console.log('再次p2',p2)
},2000)

8、期约的非重入特性?如果一个企业有多个处理程序,他们的执行顺序是怎样的?

期约的非重入特性是指:当期约的状态发生改变时,对应期约的处理程序不是立马执行的,而是等跟在期约后面的同步代码执行完,再执行期约的处理程序。如下:

javascript 复制代码
console.log('开始')
let p=Promise.resolve('1');
p.then((res)=>{
  console.log('处理程序执行,拿到结果:',res)
})
console.log('结束')

如果一个期约有多个处理程序,他们的执行顺序是按照添加的顺序执行的。如下:

javascript 复制代码
console.log('开始')
let p=Promise.resolve('1');
p.then((res)=>{
  console.log('处理程序执行,拿到结果:',res)
})
p.then((res)=>{
  console.log('第二次处理程序执行,拿到结果:',res)
})
console.log('结束')

除了then方法的处理程序,catch,finally方法的处理程序,均遵循这个非重入特性。

这里插一道社招的面试题,如下:

javascript 复制代码
setTimeout(()=>{
  console.log(1)
},0)
let p=new Promise((resolve)=>{
  console.log(2)
  resolve(3)
  Promise.resolve(4).then(console.log)
  console.log(5)
}).then(console.log)
console.log(6)

这里主要记住一个点,就是then方法注册的onResolved处理程序什么时候进入微任务队列?

两种情况:

1、resolve时,then方法的onResolved处理程序已经注册

2、执行到then方法时,期约的状态已经为fulfilled

上面的面试题例子便是第二种情况。

9、什么是期约连锁?为什么期约能够进行链式调用?用来解决什么问题?

期约连锁就是期约的链式调用,上一步产生的结果,传递给下一个期约,产生的结果再传递给下一个期约......

因为期约的实例方法then,catch,finally返回的都是期约对象,因此期约是支持链式调用的。

期约的链式调用实际上就是用来解决回调地狱的问题的,比如前面提到的把一个字符串转换为数字,然后定时+的需求,用期约的链式调用就可避免回调地狱的问题。如下:

javascript 复制代码
let p= new Promise((resolve)=>{
  setTimeout(()=>{
    let result='123'
    resolve(Number(result))
  },1000)
}).then((res)=>{
  console.log(res) //一秒后打印数字 123
  return new Promise((resolve)=>{
    setTimeout(()=>{
      let num=res+1
      resolve(num)
    },2000)
  })
}).then((res)=>{
  console.log(res) //又过2秒 打印124
  setTimeout(()=>{
    console.log(res+2) //再过5秒 打印126
  },5000)
})

10、什么是期约合成?期约合成的应用场景是怎样的?

将多个期约实例合成为一个期约叫做期约合成。Promise提供了两个静态方法Promise.all()和Promise.race()来完成期约合成。

Promise.all()

接收一个可迭代对象作为参数

A)可迭代对象的元素会通过Promise.resolve()转换为期约对象

B)如果可迭代对象的元素为空,相当与Promise.resolve

C)如果没有传参,或者传递的参数不是可迭代对象,会抛出错误,合成一个拒绝的期约对象

  • 只有当所有传入的 Promise 都变为"已成功"(fulfilled)状态时Promise.all() 返回的 Promise 才会成功,其结果是一个数组,包含每个 Promise 的成功结果。
  • 如果任意一个 Promise 被拒绝(rejected)Promise.all() 返回的 Promise 会立即变为"已拒绝"(rejected)状态,拒绝理由(reason)是第一个被拒绝的 Promise 的拒绝理由

Promise.race()

接受一个可迭代对象作为参数,关于参数处理规则,和Promise.all()处理一致。

Promise.race对于解决期约和拒绝期约不会区别对待,无论是解决还是拒绝期约,只要第一个状态落定,就会包装其解决值或拒绝值并返回新期约。

Promise.allSettled()

将可迭代对象中所有的期约全部执行完毕,得到所有的解决值或拒绝值,如下:

11、async/await是怎么实现异步机制的?他的诞生解决了什么问题?

async/await是es8的新特性,他诞生的目的是解决异步编程的代码风格问题,增加代码的可读性和可维护性。

async关键字用于声明一个异步函数,有多种方式,如下:

javascript 复制代码
async function foo(){}
const foo= async function(){}
const foo= async ()=>{}
class C{
  async foo(){}
}
  • async 声明的异步函数返回值一定是一个期约对象。期约对象的值为Promise.resolve包装的异步函数return返回值(如果没有显式return,返回值为undefined)。
  • 如果在异步函数内部用throw抛出错误,异步函数会返回拒绝期约,并可用catch方法捕获。
  • 但是,在异步函数内部,拒绝期约的错误不会被捕获到。因为该异步函数返回的并不是拒绝期约,而是被Promise.resolve()包装的undefined。

因为异步函数内部会包含一些耗时的异步操作,因此他需要具备暂停和恢复执行的能力。这就是await关键字的作用(类似于生成器函数中的yield)。

await关键字期待一个实现了thenable接口的对象,然后将他'解包'。但这不是必须的,如果不是的话,这个值就被当做已解决的期约,await也会解包其中的值。

重点注意:前面提到过,在异步函数内部,拒绝期约的错误不会被捕获到。但是对拒绝的期约使用await关键字会释放错误值(将拒绝期约返回,且后面代码不会执行),如下:

javascript 复制代码
async function foo(){
  console.log(1)
  await Promise.reject('2')
//以下两行代码不会执行
  console.log('123')
  return '成功'
}

foo();

12、await关键字的使用有什么限制?他是怎么暂停和恢复函数的执行的?

限制:await关键字必须放在async 异步函数中使用,否则会抛出错误。

async/await实现异步机制,主要起作用的还是await关键字。因为只靠async关键字,函数的执行和普通函数没有什么太大的区别(大概只是返回值的区别)。javaScript运行时会在遇到await关键字的时候记录在哪里暂停执行。等到await右边的值可用了,javaScript运行时会向消息队列中推送一个任务,即恢复异步函数的执行。

看一个示例:

javascript 复制代码
async function foo(){
  console.log(2);
  console.log(await 8);
  console.log(9)
}

async function bar(){
  console.log(4)
  console.log(await 6);
  console.log(7)
}


console.log(1)
foo();
console.log(3)
bar()
console.log(5)

我们来一步一步分析这段 JavaScript 代码的执行过程,特别关注异步函数(async/await)的行为。


📜 代码分析

复制代码
async function foo(){
  console.log(2);
  console.log(await 8);
  console.log(9)
}

async function bar(){
  console.log(4)
  console.log(await 6);
  console.log(7)
}

console.log(1)
foo();
console.log(3)
bar()
console.log(5)

🧠 JavaScript 执行机制简述

  • JavaScript 是单线程的,事件循环机制分为 同步任务异步任务

  • await 会暂停 async 函数的执行,将后续代码放入 微任务队列(microtask queue) 中等待执行。

  • await 8 实际相当于 await Promise.resolve(8),返回一个已解析的 Promise,但依旧是异步行为。


执行顺序分析

同步代码先执行:
复制代码
console.log(1)     // 输出:1
foo();             // 执行 foo()
  console.log(2)   // 输出:2
  await 8          // 中断 foo,后面的 console.log(9) 放入微任务
console.log(3)     // 输出:3
bar();             // 执行 bar()
  console.log(4)   // 输出:4
  await 6          // 中断 bar,后面的 console.log(7) 放入微任务
console.log(5)     // 输出:5

此时的同步输出是:

复制代码
1
2
3
4
5
接下来处理微任务队列:
  1. 执行 foo()await 后的部分:

    复制代码
    console.log(8);  // await 8 的值
    console.log(9);
  2. 然后执行 bar()await 后的部分:

    复制代码
    console.log(6);  // await 6 的值
    console.log(7);

所以微任务部分输出:

复制代码
8
9
6
7

最终输出顺序为

复制代码
1
2
3
4
5
8
9
6
7

🧾 总结原因

  • async/await 是基于 Promise 的异步机制,await 会将其后代码延后至微任务队列。

  • 所有同步代码先执行,await 后的部分等当前同步和微任务执行完后才运行。

相关推荐
zengyuhan5035 小时前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休5 小时前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running5 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔5 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654265 小时前
Android的自定义View
前端
WILLF5 小时前
HTML iframe 标签
前端·javascript
枫,为落叶5 小时前
Axios使用教程(一)
前端
小章鱼学前端5 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah5 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝5 小时前
手搓一个简简单单进度条
前端