JavaScript之Promise的使用

566 阅读9分钟

前言

我还记得我刚开始学习JavaScript的Promise很多概念都不懂很"懵逼", 现在已经工作有小半年时间了, 整理整理笔记和个人在工作中的使用情况, 写个文章记录一下(PS只记录怎么使用, 原理太深奥功力不够), 如果有不对的地方前辈勿喷👀

Promise简介

Promise也称期约, 是ES6推出的一个异步解决方案, 可以有效的解决异步函数嵌套太深("回调地狱")的问题

什么是回调地狱?

假设有个需求需要获取用户的指定数据用户数据3这个数据依赖用户数据2用户数据2又依赖于用户数据1, 所以正确的数据获取数据顺序为: 用户数据1-->用户数据2-->用户数据3, 模拟一下使用回调函数的写法如下:

Node接口:

const router = require('express').Router();

const data1 = { data: "用户数据1" };
router.get('/testData1', (req, res) => {
    res.json(data1);
})

const data2 = { data: "用户数据1,用户数据2" };
router.get('/testData2', (req, res) => {
    if (req.query.data === data1.data) {
        res.json(data2);
    } else {
        res.status(401).json("参数错误");
    }
})

router.get('/testData3', (req, res) => {
    if (req.query.data === data2.data) {
        res.json({ data: "用户数据1,用户数据2,用户数据3" });
    } else {
        res.status(401).json("参数错误");
    }
})

module.exports = router;

前端请求代码:

// 简单封装的 XMLHttpRequest 请求函数
const baseUrl = "http://localhost:8888/test";
const request = (url, cb) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', baseUrl + url);
  xhr.send();
  xhr.onreadystatechange = () => {
    const { readyState, status, response } = xhr;
    if (readyState === 4) {
      if (status >= 200 && status <= 299) {
        cb(JSON.parse(response));
      } else {
        throw new Error(response);
      }
    }
  }
}

// 因为下一个请求需要上一个请求的数据所以需要一个回调函数嵌套一个回调函数
request("/testData1", res1 => {
  console.log(res1); // => {data: '用户数据1'}
  request(`/testData2?data=${res1.data}`, res2 => {
    console.log(res2); // => {data: '用户数据1,用户数据2'}
    request(`/testData3?data=${res2.data}`, res3 => {
      console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
      // ....
    })
  })
})

这个代码看着就头大, 如果需求复杂的话, 我只能用一张图表示(这张图片我也不记得从哪里保存的忘了😂)

微信图片_20220506160036.jpg

这种一个回调嵌套一个回调的代码可读性和可维护性都很差, 被称为"回调地狱", 而Promise的出现就可以很好的解决这个问题

Promise的特点

Promise对象有一个状态, 这个状态不受外界影响, 状态一共分为3种:

  • Pending状态 (进行中(又称待定)) 初始状态
  • Fulfilled状态 (成功(又称兑现))
  • Rejected状态(失败(又称拒绝))

关于Promise的状态叫法下文统一使用成功,失败

一旦Promise对象的状态改变(成功或失败)就不会再变, 是单向的:

Pending(进行中) -> Fulfilled(成功)

Pending(进行中) -> Rejected(失败)

创建Promise实例

创建Promise实例需要newPromise构造函数, 该构造函数接受一个函数(处理器函数)作为参数, 该函数会收到两个参数, 这两个参数分别是resolvereject(叫什么都行一般还是要语义化名称)它们是两个函数, 不用自己实现就可以使用, 如下:

const p = new Promise((resolve, reject) => {

})
console.log(p);

image.png

可以看到这个状态默认是Pending(进行中)

resolvereject这两个参数(函数)可以在处理器函数里面调用(可以传递参数), 这样将会改变 Promise 对象的状态:

const p = new Promise((resolve, reject) => {
  resolve();
})
console.log(p);

image.png

当调用resolve函数时会将Priomise对象的状态修改为Fulfilled(成功), 调用reject函数则会将状态修改为Rejected(失败)

then方法

Promise实例的then方法, 可以接受两个参数(都是函数)分别指定Primise实例里面状态(成功或失败)改变时调用的回调函数(并且Promise的then方法是异步的微任务):

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功 
  resolve();
})
    
console.log("同步代码");
p.then(
  () => {
    console.log("成功的回调");
  },
  () => {
    console.log("失败的回调");
  }
)

结果如下:

image.png

反之调用reject函数就会触发then方法的第二个回调函数, 如果将resolve函数reject函数都调用只会生效最先调用的(因为状态时单向的嘛)

resolve 和 reject 的参数传递

经过上面的测试我们知道resolvereject这两个函数是可以修改状态并且触发Promise的then方法的回调函数的, 那么是函数就可以传递参数, 这个参数会被传递给对应的then方法的回调函数接收到:

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功, 并且传递一个参数
  resolve("ok");
})

console.log("同步代码");
p.then(
  // 这里接收 resolve 函数传递的参数
  res => {
    console.log("成功的回调", res);
  },
  // 这里接收 reject 函数传递的参数
  err => {
    console.log("失败的回调");
  }
)

结果如下:

image.png

then()链式调用

了解完then方法以后我们就可以稍微修改一下一开始最上面的需求:

// 请求函数使用 Promise 封装
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
  // 返回一个Promise实例
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', baseUrl + url);
    xhr.send();
    xhr.onreadystatechange = () => {
      const { readyState, status, response } = xhr;
      if (readyState === 4) {
        if (status >= 200 && status <= 299) {
          const res = JSON.parse(response);
          // 修改状态为成功并且把响应传递过去
          resolve(res);
        } else {
          // 修改状态为失败也把响应传递过去
          reject(response);
        }
      }
    }
  })
}

// 使用then方法
request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    request(`/testData2?data=${res1.data}`).then(
      res2 => {
        console.log(res2); // {data: '用户数据1,用户数据2'}
        request(`/testData3?data=${res2.data}`).then(
          res3 => {
            console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
          }
        )
      }
    )
  }
)

写完以后发现好像还不如使用回调函数的方式写, 看到这里好像Promise还是不能很好的解决回调嵌套的问题; 换个思路如果我们在then方法中再返回一个Promise实例, 那么不就又可以调用then方法了吗? 代码中测试一下:

const p1 = new Promise((resolve, reject) => {
  resolve("p1数据");
})

// 这里的p2就是p1.then方法成功回调里面返回的p2
const p2 = p1.then(
    // 这里的res1参数就是p1的处理器函数中调用 resolve("p1数据") 传递的参数
    res1 => {
      console.log(res1); // p1数据

      // 这里新建一个新的Promise实例, 记作p2
      const p2 = new Promise((resolve, reject) => {
        // 0.5s后修改状态
        setTimeout(() => {
          resolve(res1 + ",p2数据");
        }, 500);

      })
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // p1数据,p2数据
    // 这里和上面的同理
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res2 + ",p3数据");
      }, 500);

    })
    return p3;
  }
)

p3.then(
  res3 => {
    console.log(res3); // p1数据,p2数据,p3数据
  }
)

发现可行后, 把需求代码再修改一下:

// 这里的p1就是 request("/testData1") 返回的Promise实例
const p1 = request("/testData1");

// 这里的p2就是p1.then方法成功回调返回的p2
const p2 = p1.then(
    res1 => {
      console.log(res1); // {data: '用户数据1'}

      // 再调用request方法将其返回的Promise实例记作p2
      const p2 = request(`/testData2?data=${res1.data}`);
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 这里和上面的同理
    const p3 = request(`/testData3?data=${res2.data}`);
    return p3;
  }
)

p3.then(
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

需求实现是实现了, 就是代码有点冗余, 可以精简一下, 如下:

request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    // 这里直接返回 request方法的返回值Promise实例
    return request(`/testData2?data=${res1.data}`);
  }
).then( 
  // 这个成功回调时上一个then方法返回的Promise实例的成功回调
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 同理
    return request(`/testData3?data=${res2.data}`);
  }
).then(
  // 同理
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

上面的代码格式就像链条一样所以又被称为"链式调用"

then()的返回值

经过上面的代码测试, then方法除了返回Promise对象外还可以返回其他任意的值, 返回会被转换为Promise对象, 内部的状态视返回值而定:

const p1 = new Promise(resolve => resolve());
p1.then(() => {
  // 这里相当于是 return undefined
}).then(
  res1 => {
    console.log(res1); // undefined
    return "hello";
  }
).then(
  res2 => {
    console.log(res2); // hello
    return { name: "张三" };
  }
).then(
  res3 => {
    console.log(res3); // { name: "张三" }

    // 返回错误对象该Promise对象的状态也是成功
    return new Error("error object");
  }
).then(
  res4 => {
    console.log(res4 instanceof Error); // true
    console.log(res4.message); // error object

    // 抛出一个错误则该Promise对象的状态是失败
    throw "thorw error";
  }
).then(
  () => { },
  err => {
    console.log(err); // thorw error
  }
)

catch方法

上面使用then方法的链式调用可以解决回调嵌套太深的问题, 但是还没处理请求之间的失败回调处理, then方法的第二个回调就是指定失败的回调, 但是一般都不使用这个回调来处理错误, 而是使用catch方法来失败, 使用格式如下:

p1.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch可以捕获这一条调用链上的错误
    // 处理错误
})

finally方法

除了catch方法那自然就有finally方法, finally方法和try...catch...finally中的finally是一样的作用, 使用格式如下:

p.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch方法可以捕获这一条调用链上的错误
  // 处理错误
    
}).finally(() => { // 无论成功还是失败这个finally中指定的回调都会执行
  // 清除loading, 重置状态...
})

Promise的方法

Promise.resolve()

立即返回一个状态是成功(Fulfilled)Promise 对象, 可以传递参数, 参数会被其返回的Promise实例的then方法的回调(异步微任务)接受到:

const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res)); // Promise.resolve

也可以利用Promise.resolve()来创建一个微任务:

console.log("同步代码");

setTimeout(() => console.log("setTimeout"), 0);

const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res));

结果如下:

image.png

Promise.reject()

Promise.resolve()一样不过返回的状态是失败(Rejected)Promise对象(同样是微任务)

console.log("同步代码");

setTimeout(() => console.log("setTimeout"), 0);

const p = Promise.reject("Promise.reject");
p.then().catch(err => console.log("Promise.reject"));

Promise.all()

Promise.all接收一个Promise的iterable类型(就是可迭代对象里面存放着Promise实例, Array, Map, Set都属于ES6的iterable类型), Promise.all 会等待所有的Promise对象都完成(或第一个失败) , 根据给定的参数返回不同的参数

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

// 依次打印数据
p1.then(res => console.log(res)); // 0.5s 后打印 p1 data
p2.then(res => console.log(res)); // 1.0s 后打印 p2 data
p3.then(res => console.log(res)); // 1.5s 后打印 p3 data


const proArray = [p1, p2, p3];
Promise.all(proArray).then(resList => {
  console.log(resList); // 1.5s后打印数据 ['p1 data', 'p2 data', 'p3 data']
}).catch(err => {
  // 如果在`Promise.all`方法中出现了失败的状态, 那么这个参数会是这个失败状态返回的参数(如果有的话)
  console.error("error: ", err); 
})

利用Promise.all()方法的特定可以用于同时发送多个请求, 如下例子:

Node接口:

const getRandom = () => Math.random() * 9 + 1;
router.get('/testData4', (req, res) => {
    let n = req.query.n;
    const random = getRandom();

    // 定时器模拟接口响应时间差
    setTimeout(() => {
        res.json(`第${++n}个请求${random}`);
    }, random * 50);
});

前端发送多个请求:

const proArray = [];
for (let i = 0; i < 10; i++) {
  // 将10个请求方法返回的Promsie对象添加到proArray数组中
  proArray.push(request(`/testData4?n=${i}`));
}
Promise.all(proArray).then(resList => {
  for (const res of resList) {
    // 每个请求的返回结果
    console.log(res);
  }
})

Promise.allSettled()

Promise.allSettled()方法和Promise.all()很类似只不过是接受的期约对象无论是成功还是失败都会触发then方法的成功回调, 每个期约都会返回一个对象status属性表示状态, value表示成功回调的值, 如果状态是失败的那么失败回调的值存储在reason属性中:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
  // p2的Promise实例的状态修改为失败
  setTimeout(() => reject("p2 err"), 1000);
})

const proArray = [p1, p2];
Promise.allSettled(proArray).then(resList => {
  console.log(resList); // (2) [{…}, {…}]

  for (const res of resList) {
    const { status, value, reason } = res;
    if (reason) {
      console.log(`失败: ${status}, 原因是: ${reason}`); // 失败: rejected, 原因是: p2 err
    } else { 
      console.log(`成功: ${status}, 数据是: ${value}`); // 成功: fulfilled, 数据是: p1 data
    }
  }
}).catch(err => {
  // 这里不会捕获p2的失败的回调 
  console.error("error: ", err);
})

Promise.race()

Promise.race()Promise.all()类似, 都接收一个可以迭代的参数, 但是不同之处是

Promise.race()的状态变化不是受全部参数的状态影响, 一旦迭代器中的某个Promise解决或拒绝,返回的 Promise就会解决或拒绝

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => reject("p2 err"), 1000);
})

const proArray = [p1, p2];
Promise.race(proArray).then(res => {
    console.log(res); // p1 data
}).catch(err => {
    console.error(err);
})

async 和 await

async函数函数

async函数函数就是使用async关键字声明的函数(也叫异步函数), async函数和普通的函数使用没有什么区别:

// 函数声明
async function asyncFn1() {
  console.log("asyncFn1");
}

// 函数表达式
const asyncFn2 = async () => {
  console.log("asyncFn2");
}

asyncFn1();
asyncFn2();

// 立即调用
(async () => {
  console.log("asyncFn3");
})();

await

await操作符用于等待一个Promise对象, 它只能在async function中使用, 使用async+await可以将异步的代码"变"的跟同步的一样:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("data");
  }, 1000);
});

// async 函数
async function asyncFn() {
  console.log("asyncFn函数开始执行");

  // await 会等待右边的Promise对象的状态变成功后返回其值
  const res = await p;
  console.log(res); // data

  console.log("asyncFn函数执行完了");
}

// 调用
asyncFn();

上面的代码会先输出"asyncFn函数开始执行"后, 等待1s左右输出"data", 然后再输出"asyncFn函数执行完了"

注意: 异步函数不会阻塞主线程的执行, 它是异步的:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("data");
  }, 1000);
});

// async 函数
async function asyncFn() {
  console.log("asyncFn函数开始执行");
  const res = await p;
  console.log(res); 
  console.log("asyncFn函数执行完了");
}

console.log("hello");
asyncFn(); 
console.log("javascript");

等待大约1s后上面的代码执行结果如下:

image.png

根据上面的代码执行结果, 我们发现异步函数内await关键字会等待其右边的Promise对象的返回值, 等待结束后, 后面的代码才会被执行, 利用这个特性我们可以在JavaScript中可以实现类似Java的Thread.sleep()方法, 如下:

const sleep = async time => new Promise(resolve => setTimeout(resolve, time));

(async () => {
  console.log("1");
  await sleep(1000);
  console.log("2");
  await sleep(500);
  console.log("2.5");
})();

了解完async和await的使用后我们可以使用异步函数再来完成我们一开始的需求:

// 请求函数使用 Promise 封装
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
  // 返回一个Promise实例
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', baseUrl + url);
    xhr.send();
    xhr.onreadystatechange = () => {
      const { readyState, status, response } = xhr;
      if (readyState === 4) {
        if (status >= 200 && status <= 299) {
          const res = JSON.parse(response);
          // 修改状态为成功并且把响应传递过去
          resolve(res);
        } else {
          // 修改状态为失败也把响应传递过去
          reject(response);
        }
      }
    }
  })
}

async function asyncFn() {
  const res1 = await request("/testData1");
  console.log(res1); // {data: '用户数据1'}

  const res2 = await request(`/testData2?data=${res1.data}`);
  console.log(res2); // {data: '用户数据1,用户数据2'}

  const res3 = await request(`/testData3?data=${res2.data}`);
  console.log(res3); // {data: '用户数据1,用户数据2,用户数据3'}
}

asyncFn();

可以看到使用async和await来发送网络请求写的代码很简洁, 也很直观

异步函数的错误处理

异步函数的异常处理可以使用try...catchcatch处理:

try...catch

async function asyncFn() {
  let res1, res2, res3;
  try {
    res1 = await request("/testData1");

    try {
      if (res1?.data) {
        res2 = await request(`/testData2?data=${res1.data}`);
      }

      try {
        if (res2?.data) {
          res3 = await request(`/testData3?data=${res2.data}`);
        }

      } catch (error) {
        // 处理res3 error
      }

    } catch (error) {
      // 处理res2 error
    }

  } catch (error) {
    // 处理res3 error
  }
}

catch

async function asyncFn() {
  const res1 = await request("/testData1")
    .catch(err => {
      // 处理res1 error
    });

  if (res1?.data) {
    const res2 = await request(`/testData2?data=${res1.data}`)
      .catch(err => {
        // 处理res2 error
      });

    if (res2?.data) {
      const res3 = await request(`/testData3?data=${res2.data}`)
        .catch(err => {
          // 处理res3 error
        });
    }
  }
}

异步函数同样适用于Promise的一些静态方法

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

const proArray = [p1, p2, p3];
async function asyncFn() {
  const list = await Promise.all(proArray);
  console.log(list); // ['p1 data', 'p2 data', 'p3 data']
}
asyncFn();

for await...of

一个数组中存储多个Promise对象我们可以通过Promise.all获取或者通过循环来等待其返回值, 我们使用循环:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

const proArray = [p1, p2, p3];

async function asyncFn() {
  for (const pro of proArray) {
    // 这里不要忘记 await
    const res = await pro;
    console.log(res);
  }
}
asyncFn();

ES9开始有一个新语法就是for await..of可以自动等待每次循环的项

async function asyncFn() {
  for await (const item of proArray) {
    console.log(item);
  }
}
asyncFn();

参考:

  1. MDN Promise
  2. MDN async
  3. MDN for await...of
  4. JavaScript权威指南
  5. 阮一峰ES6入门之promise

文中有一些可能跟其他博客有雷同的地方, 因为是从个人笔记整理出来(个人学习的时候写的笔记就是看博客文章或视频参考写的)👀