讲讲 promise

Promise 是异步编程的一种解决方案。Promise 本质上是一个绑定了回调的对象,而不是将回调传进函数内部,从它可以获取异步操作的信息

人物介绍

Promise 异步操作有三种状态:

  • pending(进行中)
  • fulfilled(已成功)— resolve
  • rejected(已失败)— reject

Promise 对象只有两种状态转换:

  • pending => fulfilled
  • pending => rejected

从形式上看,Promise API 就是 Promise、resolve、reject 三个元件

  • Promise
let promise = new Promise((resolve, reject) => {});
  • resolve — 进入 fulfilled 状态
// Promise.resolve(value)的返回值是一个 promise 对象
// 可以对返回值进行.then调用
Promise.resolve(1).then(function (value) {
  console.log(value); // 打印出1
});
  • reject — 进入 rejected 状态

Promise.then(链式调用)

当连续执行两个或者多个异步操作时,每一个后来的操作都在前面的操作执行成功之后,带着上一步操作所返回的结果开始执行

promise 基本原理

then 方法可以让我们以扁平化的形式处理异步,它将返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。

// 添加多个回调函数,会按照插入顺序并且独立运行。
// 后面的请求依赖于前面请求的结果
const p = new Promise(function (resolve, reject) {
  resolve(1);
})
  .then(function (value) {
    // 第一个then // 1
    console.log(value);
    return value * 2;
  })
  .then(function (value) {
    // 第二个then // 2
    console.log(value);
  })
  .then(function (value) {
    // 第三个then // undefined
    console.log(value);
    return Promise.resolve("resolve");
  })
  .then(function (value) {
    // 第四个then // resolve
    console.log(value);
    return Promise.reject("reject");
  })
  .then(
    function (value) {
      // 第五个then // reject:reject
      console.log("resolve:" + value);
    },
    function (err) {
      console.log("reject:" + err);
    }
  );

错误机制

大多数浏览器中不能终止的 Promise 链里的 rejection,建议后面都跟上

// 一个 Promise 链式程序遇到异常就会停止,查看链式的底端
// 寻找 catch 处理程序来代替当前执行
.catch(error => console.log(error));

从源头发起的错误,会依次流经 .then(resolve, reject).catch(), 只要错误被提供的 reject 方法处理了,下游将不会有这个错误出现;只要存在错误,并且不曾被方法处理,最终都会被.catch()捕获。用传统的异步回调处理这些错误,很大概率会乱成一团~

其他方法

Promise.all(统一回调)

Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。

// 传统异步回调处理三个请求
$.get("url_a", function (data) {
  if (checkAllWith("a")) {
    mainRender(data);
  }
});
$.get("url_b", function (data) {
  if (checkAllWith("b")) {
    mainRender(data);
  }
});
$.get("url_c", function (data) {
  if (checkAllWith("c")) {
    mainRender(data);
  }
});
// 使用promise.all
// $getA 是上文中用过的 $.get() 方法的 promise 封装
let $getA = function () {
  return $get("a").then(callback_a);
};
let $getB = function () {
  return $get("b").then(callback_b);
};
let $getC = function () {
  return $get("c").then(callback_c);
};
let all = Promise.all([$getA, $getB, $getC]);
// 3个promise 全部完成后,才会触发回调
all.then((data) => {
  mainRender(data);
});
// 定时器例子
let wake = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${time / 1000}秒后醒来`);
    }, time);
  });
};

let p1 = wake(3000);
let p2 = wake(2000);

Promise.all([p1, p2])
  .then((result) => {
    console.log(result);
    // [ '3秒后醒来', '2秒后醒来' ]
    // 同接收到的数组顺序保持一致
  })
  .catch((error) => {
    console.log(error);
  });

在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用 Promise.all 毫无疑问可以解决这个问题。

Promise.race(竞跑)

Promise.race([p1, p2, p3])里面哪个结果返回的快,就返回那个结果,不管结果本身是成功状态还是失败状态

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("success");
  }, 1000);
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("failed");
  }, 500);
});

Promise.race([p1, p2])
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error); // 打开的是 'failed'
  });

尝试实现 api

all

并行执行多个 promise,并等待所有 promise 都准备就绪,但是如果中途有发生错误,则会忽略其他所有 promise

其实它的实现很简单,就是遍历执行 promise 的 then 函数,将回调追加到微任务里。加到微任务的时机则由 promise 确定

function promiseAll(promiseArr) {
  let result = new Array(promiseArr.length).fill(null);
  let count = 0;
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
        .then((res) => {
          result[i] = res;
          count++;
          if (count === promiseArr.length) {
            resolve(result);
          }
        })
        .catch((err) => {
          reject(err);
        });
    }
  });
}

allSettled

这是新出的 api,可以解决 all 这个方法的不足。allSettled 可以并行执行多个 promise,并等待所有 promise 就绪,不论是成功还是失败

function promiseAllSettled(promiseArr) {
  let result = new Array(promiseArr.length).fill(null);
  let count = 0;
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
        .then((res) => {
          result[i] = { status: "rejected", value: res };
          count++;
          if (count === promiseArr.length) {
            resolve(result);
          }
        })
        .catch((err) => {
          result[i] = { status: "rejected", reason: err };
          count++;
          if (count === promiseArr.length) {
            resolve(result);
          }
        });
    }
  });
}

race

与 Promise.all 类似,同样是并行执行多个 promise,但只等待第一个 settled 的 promise 并获取其结果

function promiseRace(promiseArr) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
        .then((res) => {
          resolve(res);
        })
        .catch((err) => {
          reject(err);
        });
    }
  });
}

resolve/reject

  • Promise.resolve 方法会将传入的对象转为 Promise 对象,并执行 then 方法
function promiseResolve(promise) {
  return new Promise((resolve, reject) => {
    promise.then((res) => {
      resolve(res);
    });
  });
}
  • Promise.reject 方法会将传入的对象转为 Promise 对象,并执行 catch 方法
function promiseReject(promise) {
  return new Promise((resolve, reject) => {
    promise.then((res) => {
      reject(res);
    });
  });
}

模拟实现串行调用

最简单的应该就是 promise 嵌套或 async/await

// promise
function serialPromise(ajaxArray) {
  let p = Promise.resolve();
  let arr = [];
  ajaxArray.forEach((promise) => {
    p = p.then(promise).then((data) => {
      arr.push(data);
      return arr;
    });
  });
  return p;
}
// async
function serialPromise(promiseArr) {
  let arr = [];
  async function run() {
    for (let p of promiseArr) {
      let val = await p();
      arr.push(val);
    }
    return arr;
  }
  return run();
}

也可以借助 reduce 实现

function serialPromise(tasks) {
  var result = [];
  return tasks.reduce((accumulator, item, index) => {
    return accumulator.then((res) => {
      return getResponse(item).then((res) => {
        result[index] = res;
        return index == tasks.length - 1 ? result : item;
      });
    });
  }, Promise.resolve());
}

参考

使用 Promises - MDN

ES6 Promise 对象 - 菜鸟教程

重读 ES6 - 简书