11 期约与异步

120 阅读8分钟

异步

js代码执行引擎是单线程的,但是渲染进程是有多个线程,异步代码就是将异步操作交给这些线程完成,而js引擎继续向后执行js代码:
  • JS引擎线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程
js任务分为宏任务和微任务:
  • 宏任务包括:整体代码块、setTimeout、setInterval、I/O操作、UI渲染等,执行后立马执行微任务
  • 微任务包括:Promise的回调函数、MutationObserver等
事件循环的执行过程
  1. 执行栈中的宏任务(如同步代码块)被执行。
  2. 宏任务执行完毕后,事件循环会检查微任务队列,并清空队列中的所有微任务
  3. 如果有必要,浏览器会进行渲染更新。
  4. 事件循环会检查宏任务队列,如果有宏任务,就取出并执行。
  5. 事件循环不断重复上述步骤。
const promise1 = new Promise((resolve, reject) => {
  console.log('这里是同步代码,直接执行');
  resolve();
});
promise1.then(() => {
  console.log('这里会在promise变为落定状态后加入微任务队列,等待同步代码执行完后执行')
});
console.log("promise1", promise1);
// 这里是同步代码,直接执行
// promise1 Promise {<fulfilled>: undefined}
// 这里会在promise变为落定状态后加入微任务队列,等待同步代码执行完后执行

setTimeOut

定时器:一旦到期,就将传入的函数或代码放入宏任务队列,等待当前执行队列结束并清空微任务队列后依次调入执行

  • setTimeOut(code,[delay]);
    • code为函数定义
    • delay默认为0,单位为毫秒
  • setTimeOut(functionRef,[delay,arg1,arg2...]);
    • 传入的是函数引用
  • 到期后并不是立马执行,可能会滞后很多
  • 在调用 setTimeout() 的线程结束之前,函数或代码片段不能被执行。
  • 函数返回的是timeoutID,用于clearTimeout()来取消定时器
  • setInterval是用于到点后重复执行代码,用clearInterval()来清理
setTimeout(()=>{console.log('计时器')});
console.log('正常执行');
//正常执行 计时器

期约Promise

尚不存在结果的替身
本质:有特殊行为的对象:对象存在状态,当状态为resolved(fulfilled)和rejected时,会将相应的处理程序调入微任务队列,等待执行

构建

const p1 = new Promise(执行函数);其中执行函数接受两个参数, resolvereject
const p2 = Promise.resolve(value);
const p3 = Promise.reject(reason);

状态

  1. 待定pending,可以由这个状态在执行函数中调用方法,变为另外两个状态,但是注意并不是所有的Promise都起始为这个状态,当然也并不是所有Promise都会脱离此状态。
  2. 兑现fulfilled(resolved),落定状态,不可变为其他状态,拥有一个私有的内部值(默认为undefined),此状态后会调用then中的处理程序并传递私有内部值。
  3. 拒绝rejected,落定状态,不可变为其他状态,拥有一个私有的内部理由(默认为undefined),会调用then中的对应处理程序并传递理由
const p1 = new Promise(()=>{});
const p2 = Promise.resolve('1');
const p3 = Promise.reject('e');
console.log(p1,p2,p3);
//Promise { <pending> } Promise { '1' } Promise { <rejected> 'e' }

执行函数

传入构造函数Promise中的函数,且是同步调用

  1. 传入resolve,reject,这是两个函数,调用后会将Promise对象状态分别变为兑现和拒绝,且reject(reason)调用后,会抛出异步错误,无法使用try/catch捕获
  2. resolve(value)和reject(reason)调用一次后,状态就不会再改变了,即使执行函数后面还有不同的函数调用,只会静默失败
const p1 = new Promise((resolve,reject)=>{
  console.log('这是立即执行');//这是立即执行
  setTimeout(resolve,1000,'value');//虽然后面执行了,但是由于状态已经变为了rejected,所以静默失败
  reject('error');//直接就将p1状态变为了rejected
})
console.log(p1);//Promise { <rejected> 'error' }

静态方法

1.Promise.resolve(值)

实例化一个已解决的Promise,该Promise的值就是传入的值(值是拒绝的期约时并不是已解决的期约)

  • 值可以是任何类型,包括错误对象Error实例
  • 如果传入的值是期约,则该方法就是一个空包装,返回传入的值
    • 特殊:传入的是拒绝的期约,则返回的就是拒绝的期约
const p1 = Promise.resolve(new Error('错误'));
console.log(p1);//Promise {Error: 错误},这就是已解决的期约
const p2 = Promise.resolve(Promise.reject(2));
console.log(p2);//Promise { <rejected> 2 },返回的依然是未解决的期约

2.Promise.reject(理由)

实例化未解决的期约,同时还会抛出异步错误 理由也可以是任何类型,同时如果是期约,则其理由就是传入的期约

const p1 = Promise.reject(new Error('错误'));
console.log(p1);//Promise { <rejected> Error: 错误 }
const p2 = Promise.reject(Promise.reject(2));
console.log(p2);//Promise { <rejected> Promise { <rejected> 2 } }

3.Promise.all/Promise.race(期约数组)

见期约合成

实例方法

thenable接口

对象实现了then()方法

then(onResolved处理程序,onRejected处理程序)

会返回一个新期约,如果还没有执行处理程序,处于待定;执行后,根据处理程序返回值(抛错)切换状态 当期约状态落定后,会将对应的处理程序加入微任务队列中,处理程序调用时默认传入值(理由)

  • then函数可以不止一个
  • 当参数传入不是函数时,会静默处理
  • 可以只传入一个参数,当要传入后者时,需要在前者留下占位符
  • 处理程序执行后会修改新期约的值:
    • 调用任意处理程序,则使用Promise.resolve(返回值)来包装,没有返回值则是undefined,但是注意如果处理程序中抛出错误,则返回拒绝的期约
    • 如果没有传入处理程序,则将原始期约返回
const p1 = new Promise((resolve)=>{
  resolve();
})
let p2 = p1.then(()=>{
  return '值';
})
let p3 = p1.then(()=>{
  throw '抛出错误';
})
p3.catch(()=>{});
setTimeout(console.log,0,p2);//Promise { '值' }
setTimeout(console.log,0,p3);//Promise { '值' }
const p4 = new Promise((resolve,reject)=>{
  reject('error');
})
const p5 = p4.then();//没有处理程序,p4就传给p5
p5.catch(()=>{});
const p6 = p4.then(null,()=>{
  return '值';
})
setTimeout(console.log,0,p5);//Promise { <rejected> 'error' }
setTimeout(console.log,0,p6);//Promise { '值' }

catch(onRejected处理程序)

其实就是then(null,onRejected处理程序)语法糖

finally(处理程序)

无论原期约落定状态是什么,都会执行处理程序

期约连锁与合成

前者多个期约串联,后者多个期约并连

期约连锁

  • 原理:由于then返回的是新期约,且会使用Promise.resolve()包装,所以可以通过then().then()把多个期约串联起来,这样是有顺序的执行
Promise.resolve('1')
  .then(value => {
    console.log(value); // 输出 '1'
    return '2';
  })
  .then(value => {
    console.log(value); // 输出 '2'
    return '3';
  })
  .then(value => {
    console.log(value); // 输出 '3'
  });

期约合成

Promise.all(可迭代对象)

也是创建期约,只是创建的期约落定状态与传入的所有期约有关;对传入的期约起一个监视的作用,注意期约在创建的时候就已经启动,而不是在传入all里面才启动

  • 会对传入的元素使用Promise.resolve()包装
  • 只有传入的期约都解决了,才会解决,如果有一个为拒绝/待定,则创建的期约也为拒绝/待定
  • 如果新期约为解决,则其值为所有期约的值,按照迭代顺序,内部期约执行时是并发执行的
  • 如果为拒绝,则新期约理由是第一个拒绝的期约的理由,同时内部期约都会继续执行下去,不会中断
Promise.race(可迭代对象)
  • 和all基本相同,只是返回的新期约只与第一个落定的期约相关

async/await

底层依然是promise

async

用于声明函数为异步函数,其实如果异步函数没有await和返回值,则其与同步函数无异

  • 函数依然是同步执行
  • 返回的是使用Promise.resolve(返回值)包装的期约
  • 如果抛出错误,则会返回拒绝的期约并抛出异步错误

await

拥有暂停和恢复执行该异步函数的能力

  • await后,会暂停异步函数的执行,将后续代码放入循环机制中,等到后面的Promise为兑现时,再将后续代码放入微任务队列中
  • 如果 Promise 被拒绝(reject),抛出同步异常
  • 如果落定为解决的期约,则会获得其值
  • 只能在异步函数中使用
async function foo(){
  console.log(2);
  console.log(await 5);
  console.log(6);
}
async function bar(){
  console.log(3)
console.log(await Promise.resolve(7));
  console.log(8);
}
console.log(1);
foo();
bar();
console.log(4);
await 的工作流程:
  1. await 遇到一个 Promise:
    • 暂停当前异步函数,将后续代码放入微任务队列。
    • 主线程继续执行其他同步代码。
  2. Promise 的状态变化:
    • 如果该 Promise 的状态变为 fulfilled
      • await 捕获到 resolve 中的值。
      • 将该值作为 await 表达式的结果返回。
    • 如果该 Promise 的状态变为 rejected
      • 抛出 reject 的原因作为同步异常。
  3. 恢复异步函数:
    • 等待 Promise 解决后,重新进入暂停的函数,继续执行后续代码。
async function example() {
  try {
    const promise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error("Oops")), 1000);
    });

    const result = await promise; // 暂停,等待promise拒绝,并抛出同步错误
  } catch (err) {
    console.log(err);
  }
}

example(); // 输出 Error: Oops