JavaScript异步编程系列(二)——Promise

建立Promise

Promise是一个JavaScript标准内置对象。用来存储一个异步任务的执行结果,以备未来使用。javascript

建立一个Promise对象:java

let promise = new Promise(function(resolve, reject) {
  // executor
});
复制代码

构造函数Promise接收一个函数(称为执行器executor)做为参数,并向函数传递两个函数做为参数:resolve和reject。git

建立的Promise实例对象(如下用promise代替)具备如下内部属性:github

  • state:表示promise的状态,初始值是"pending",调用resolve后变为"fulfilled",调用reject后变为"rejected"。
  • result:表示promise的结果,初始值是undefined,调用resolve后变为value,调用reject后变为error。

当执行new Promise时,executor会当即执行。能够在里面书写须要处理的异步任务,同步任务也支持。 任务处理完获得的结果,须要调用如下回调之一:json

  • resolve(value):能够将任务执行成功的结果做为参数传递给resolve并调用,此时promise的state变为"fulfilled"。
  • reject(error):能够将任务执行出现的Error对象做为参数传递给reject并调用,此时promise的state变为"rejected"。

resolve/reject只须要一个参数(或不包含任何参数),多余会被忽略。api

一个已经“settled”的promise(状态已经变为"fulfilled"或"rejected")将不能再次调用resolve或reject,即resolve或reject只能调用一次。剩下的resolve和reject的调用都会被忽略。数组

示例:promise

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done"), 1000);
});

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});
复制代码

消费promise

经过.then、.catch和.finally来消费promise。浏览器

promise的state和result属性都是内部的,没法直接访问它们。 但咱们可使用 .then/.catch/.finally 来访问。markdown

1. then

.then(f, f) 接收两个函数参数: - 第一个函数在promise resolved后执行并接收结果做为参数; - 第二个函数(非必填)在promise rejected后执行并接收error做为参数;

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(
  result => alert(result), // 1 秒后显示 "done!"
  error => alert(error) // 不运行
);
复制代码
2. catch

.catch(f) 接收一个函数做为参数,该函数接收reject的error做为参数,是.then(null, f)的简写形式。

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

promise.catch(alert); // 1 秒后显示 "Error: Whoops!"
复制代码
3. finally

.finally(f) 接收一个无参数的函数,不论promise被resolve或reject都会执行。

new Promise((resolve, reject) => {
  // ...
}).finally(() => stop loading indicator)
  .then(result => show result, err => show error);
复制代码

finally会继续将promise的结果传递下去。

4. 执行顺序

.then/.catch/.finally异步执行:会等到promise的状态由pending变为settled时,当即执行。

更确切地说,当 .then/catch 处理程序应该执行时,它会首先进入内部队列。JavaScript 引擎从队列中提取处理程序,并在当前代码完成时执行 setTimeout(..., 0)。

换句话说,.then(handler) 会被触发,会执行相似于 setTimeout(handler, 0) 的动做。

在下述示例中,promise 被当即 resolved,所以 .then(alert) 被当即触发:alert 会进入队列,在代码完成以后当即执行。

// 一个被当即 resolved 的 promise
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done!(在当前代码完成以后)

alert("code finished"); // 这个 alert 会最早显示
复制代码

所以在 .then 以后的代码老是在处理程序以前被执行(即便是在预先 resolved 的 promise 的状况下)。

Promise链

promise.then(f)的处理程序f(handler)调用后返回一个promise,handle自己返回的值会做为这个promise的result;result能够传递给下一个.then处理程序链进行传递。 对同一个promise分开.then时,每一次的结果都同样,由于.then只是单纯使用了promise提供的result,并不改变原来的promise自己。

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
  alert(result); // 1
  return result * 2;
}).then(function(result) { // (***)
  alert(result); // 2
  return result * 2;
}).then(function(result) {
  alert(result); // 4
  return result * 2;
});
复制代码

.then(handler) 中所使用的处理程序(handler)能够建立并返回一个 promise。 此时其余的处理程序(handler)将等待它 settled 后再得到其result

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
}).then(function(result) {
  alert(result); // 1
  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });
}).then(function(result) { // (**)
  alert(result); // 2
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
}).then(function(result) {
  alert(result); // 4
});
复制代码
thenable对象

handler也能够返回一个“thenable” 对象 —— 一个具备方法 .then 的任意对象。它会被当作一个 promise 来对待。 第三方库能够实现本身的promise兼容对象。它们能够具备扩展的方法集,但也与原生的 promise 兼容,由于它们实现了 .then 方法。

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 后显示 2
复制代码

handler返回的对象若是具备then方法,会被当即调用并接收resolve和reject做为参数。直到resolve或reject执行后,再将result传递给下一个.then,以此类推,沿着链向下传递。

这个特性能够用来将自定义的对象与 promise 链集成在一块儿,而没必要继承自 Promise。 推荐异步行为始终返回一个promise以便后续对链进行扩展。

若是 .then(或 catch)处理程序(handler)返回一个 promise,那么链的其他部分将会等待,直到它状态变为 settled。当它被 settled 后,其 result(或 error)将被进一步传递下去。

比较promise.then(f1, f2);promise.then(f1).catch(f2);

  • 前者:没有链,因此f1出现错误后不会被处理
  • 后者:error做为result会沿着链传递,因此f1中出现error后会被.catch处理

Promise的方法

Promise类有5种静态方法:

  • Promise.resolve(value) – 根据给定值返回 resolved promise。
  • Promise.reject(error) – 根据给定错误返回 rejected promise。
  • Promise.all(promises) – 等待全部的 promise 为 resolve 时返回存放它们结果的数组。若是任意给定的 promise 为 reject,那么它就会变成 Promise.all 的错误结果,全部的其余结果都会被忽略。
  • Promise.allSettled(promises) (新方法) – 等待全部 promise resolve 或者 reject,并以对象形式返回它们结果数组:
    • state:‘fulfilled’ 或 ‘rejected’
    • value(若是fulfilled)或 reason(若是 rejected)]
  • Promise.race(promises) – 等待第一个 promise 被解决,其结果/错误即为结果。

这五个方法中,Promise.all 在实战中使用的最多。

1. Promise.resolve

let promise = Promise.resolve(value) —— 根据给定的 value 值返回 resolved promise。 等价于:let promise = new Promise(resolve => resolve(value));

2. Promise.reject(实际工做中少用)

let promise = Promise.reject(error) —— 建立一个带有 error 的 rejected promise。 等价于:let promise = new Promise((resolve, reject) => reject(error));

3. Promise.all

let promise = Promise.all([...promises...]); —— 并行执行多个promise,返回一个新的promise,其结果为包含全部promise的结果的有序数组。参数为一个promise数组(严格能够是任何可迭代对象,一般是数组)。

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 当 promise 就绪:每个 promise 即成为数组中的一员

// 经常使用来发送并行请求
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
  .then(responses => {
    // 全部响应都就绪时,咱们能够显示 HTTP 状态码
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 每一个 url 都显示 200
    }
    return responses;
  })
  // 映射 response 数组到 response.json() 中以读取它们的内容
  .then(responses => Promise.all(responses.map(r => r.json())))
  // 全部 JSON 结果都被解析:“users” 是它们的数组
  .then(users => users.forEach(user => alert(user.name)));
复制代码

若是任意一个 promise为reject,Promise.all返回的 promise 就会当即 reject 这个错误。并忽略全部列表中其余的 promise。它们的结果也被忽略。

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
复制代码

Promise.all(...) 接受可迭代的 promise 集合(大部分状况下是数组)。可是若是这些对象中的任意一个不是 promise,它将会被直接包装进 Promise.resolve。

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2, // 视为 Promise.resolve(2)
  3  // 视为 Promise.resolve(3)
]).then(alert); // 1, 2, 3
复制代码
4. Promise.allSettled(最近添加的新特性,老浏览器须要polyfills)

用法同Promise.all,只不过Promise.allSettled会等待全部的 promise 都被处理:即便其中一个 reject,它仍然会等待其余的 promise。处理完成后的数组由如下对象组成: {status:"fulfilled", value:result} 对于成功的响应, {status:"rejected", reason:error} 对于错误的响应。

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => {
    /* results: [ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ]*/
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });
复制代码

若是浏览器不支持 Promise.allSettled,使用 polyfill 很容易让其支持:

if(!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    // p => Promise.resolve(p) 将该值转换为 promise(以防传递了非 promise)
    return Promise.all(promises.map(p => Promise.resolve(p).then(
      v => ({ state: 'fulfilled', value: v }), 
      r => ({ state: 'rejected', reason: r })
    )));
  };
}
复制代码
5. Promise.race

let promise = Promise.race(iterable); —— 与 Promise.all 相似,它接受一个可迭代的 promise 集合,可是只要有一个promise被settled了就会中止等待,将这个promise的结果/错误做为它的结果,其余的promise的结果/错误都会被忽略。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
复制代码

错误处理

  1. 在Promise的执行器executor和处理器handler程序(.then/.catch/.finally中的函数参数)中,若是发生错误,或调用reject(),都会将promise变为rejected,将错误传递给最近的错误处理程序.catch。 就好像代码周围有一个不可见的try..catch
  2. 在链式调用中,最末端加上.catch来处理上面的全部错误,只要有一个错误,控制权就会直接传递到最近的.catch
  3. catch处理完错误后,catch后面能够.then继续处理
// 1. 代码执行错误
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

// 1. 主动调用reject
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

// 2. 处理器中的错误
new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // rejects 这个 promise
}).then((result) => {
  // 这个then不执行
}).catch(alert); // Error: Whoops!

// 3. 执行:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(function(error) {
  alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
复制代码
未捕获的错误

若是promise变为rejected,可是却没有catch处理这个错误,错误就被卡住(stuck),脚本就会死掉。

JavaScript引擎会跟踪此类rejections,生成一个全局错误,能够监听unhandledrejection事件捕获。

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - 产生错误的 promise
  alert(event.reason); // Error: Whoops! - 未处理的错误对象
});

new Promise(function() {
  throw new Error("Whoops!");
}); // 没有 catch 处理错误
复制代码

建议将.catch放在想要处理错误的位置,自定义错误类来帮助分析错误,还能够从新抛出错误。 若是发生错误后没法恢复脚本,那不用catch处理错误也行,可是应该使用unhandledrejection事件来跟踪错误。 使用finally处理必需要发生的任务,好比关闭loading。

有一个浏览器技巧是从 finally 返回零延时(zero-timeout)的 promise。这是由于一些浏览器(好比 Chrome)须要“一点时间”外的 promise 处理程序来绘制文档的更改。所以它确保在进入链下一步以前,指示在视觉上是中止的。

没法捕获的错误 todo

在setTimeout中抛出的错误没法被catch捕获:

const promise = new Promise(function(resolve, reject) {
  setTimeout(function () { throw new Error('test') }, 0)
});
promise.catch(function(error) { console.log(error) });
复制代码

除非显式调用reject:

const promise = new Promise(function(resolve, reject) {
  setTimeout(function () { 
    reject(new Error('test'));
 }, 0)
});
复制代码

缘由:JS事件循环列表有宏任务与微任务之分:setTimeOut是宏任务, promise是微任务,他们有各自的执行顺序;所以这段代码的执行顺序是:

  1. 代码执行栈进入promise触发setTimeOut,此时setTimeOut回调函数加入宏任务队列
  2. 代码执行promise的catch方法(微任务队列)此时setTimeOut回调尚未执行
  3. 执行栈检查发现当前微任务队列执行完毕,开始执行宏任务队列
  4. 执行throw new Error('test')此时这个异常实际上是在promise外部抛出的 但若是在setTimeOut中主动触发了promise的reject方法,所以promise的catch将会在setTimeOut回调执行后的属于他的微任务队列中找到它而后执行,因此能够捕获错误

Promise与微任务

Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。 异步任务须要适当的管理。为此,JavaScript 标准规定了一个内部队列 PromiseJobs —— “微任务队列”(Microtasks queue)(v8 术语)。 这个队列先进先出,只有引擎中没有其余任务运行时才会启动任务队列的执行。 当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序就被放入队列中。等到当前代码执行完而且以前排好队的处理程序都完成时,JavaScript引擎会从队列中获取这些任务并执行。 即使一个 promise 当即被 resolve,.then、.catch 和 .finally 以后的代码也会先执行。 若是要确保一段代码在 .then/catch/finally 以后被执行,最好将它添加到 .then 的链式调用中。

let promise = Promise.resolve();

promise.then(() => alert("promise done"));

alert("code finished"); // 该警告框会首先弹出
复制代码
未处理的 rejection

指在 microtask 队列结束时未处理的 promise 错误。 microtask队列完成时,引擎会检查promise,若是其中任何一个出现rejected状态,就会触发unhandledrejection事件。 但若是在setTimeout里进行catch,unhandledrejection会先触发,而后catch才执行,因此catch没有发挥做用。

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')));
window.addEventListener('unhandledrejection', event => alert(event.reason));
// Promise Failed! -> caught
复制代码

将回调函数Promise化

简单的示例

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(script);
}

// promise改写:
let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err)
      else resolve(script);
    });
  })
}
// 用法:
// loadScriptPromise('path/script.js').then(...)
复制代码

通用的promisify函数:

function promisify(f) {
  return function (...args) { // 返回一个包装函数
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 给 f 用的自定义回调
        if (err) {
          return reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // 在参数的最后附上咱们自定义的回调函数

      f.call(this, ...args); // 调用原来的函数
    });
  };
};

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
复制代码

以上回调函数只能接收两个参数,接收多个参数的示例:

// 设定为 promisify(f, true) 来获取结果数组
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 给 f 用的自定义回调
        if (err) {
          return reject(err);
        } else {
          // 若是 manyArgs 被指定值,则 resolve 全部回调结果
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
};

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
复制代码
相关文章
相关标签/搜索