学习 ES6 Promise

Promise

A promise represents the eventual result of an asynchronous operation.

本文可能不太适合初次接触 Promise 的人,你可以先大致了解一下 Promise 再来看,可以参考 promisejs.org 的介绍

基础介绍

Promise 把异步处理对象和处理规则规范化,统一接口编写, 模块化异步操作 ,这使得 Promise 异常的方便与强大。

优点:

  • 模式化的操作使得异步的操作以同步的形式表达,避免了回调地狱
  • 更加方便的异步操作
  • 方便的异常处理

特点:

  • 状态不受外界影响
  • 状态改变之后不会在变

缺点:

  • 一旦启动,不可终止
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
  • 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

本文主要介绍 ES6 Promise

经常看到的 Promise/A+ 是 ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有很多共通的内容。


Promise 概览

Promise 有三种状态

  • pending: initial state, not fulfilled or rejected.
  • fulfilled: meaning that the operation completed successfully.
  • rejected: meaning that the operation failed.

备注

  • 后两者状态为 settled 状态
  • resolved 状态指 promise 变成 settled 状态或者 it has been “locked in” to match the state of another promise
  • 很多博客并不区分 resolved 与 fulfilled,这是不太正确的

    误解原因可能是因为在构造函数中的参数为 fn(resolve, reject),而在then中参数为 .then(function onFulfilled(value), function onRejected(error))

    基本流程如下:
    promise

    各个细节 :

new Promise

  • 构造函数 : 接受一个接受两个函数参数(resolve, reject) 的函数
  • resolve: Pending => Resolved,将结果传入下一步
  • reject : Pending => Rejected,将结果传入下一步
var promise = new Promise(function (resolve, reject) {
    // 异步操作
    if (/* 操作成功 */) {
        resolve(result);
    } else {
        reject(error);
    }
})

注意

  • 一旦创建,立即执行
  • 一定是异步操作,即使是在 Promise 进行同步操作
    • 会比 setTimeout中的时延函数更早执行,这是由于一般 Promise 的实现会使用更加迅速的异步执行操作(setTimeout至少有 4ms 时延)
      例如 nodeJs 的 process.nextTick()
    • 为什么同步也要异步执行呢?
      为了保证执行顺序的稳定: 如果在 Promise 中某些操作可能是同步也可能是异步,执行顺序将不确定
  • reject 参数: 无限制,建议是 Error
  • resolve 和 reject 参数都可接受一个 thenable 对象(一个有 then 方法的对象),转化方法可参考之后的 [[Resolve]](promise, x)
  • 返回值没什么用

then

promise2 = promise1.then(onFulfilled, onRejected);

  • 接受两个回调函数
  • 第一个函数为 状态变为 Fulfilled 时调用,参数为 resolve 函数处理得到
  • 第二个函数为 状态变为 Rejected 时调用,参数为 reject 函数传入的值
  • 这两个函数最多执行一次
  • 两个函数都可省略(或者不是函数,会被忽略) : 省略的话,会值穿透,即会将应该处理的 value 传递给下一个 then 函数

注意:

  • then 的返回是一个新 Promise 实例, 所以可以链式调用
  • 回调函数 return 的值(x)会由 [[Resolve]](promise2, x). 包装处理
  • 函数中抛出的错误(无论是被动或者是主动使用 throw ) 均会使 promise2 以 reason 被 rejected
  • 主动进入 rejected 状态的方法可以是 throw 抛出错误或者返回一个 rejected 的Promise

catch

  • .then(null, onRejected) 的别名
  • 因为其实就是 then,所以具体细节同 then
  • resolved 之后抛出错误无效
  • 记得之前的值穿透吧 : 错误会一直传递到捕获为止
    • 说这一点是因为有时候链式使用 then 不写相应的处理错误方法(catch 或 第二个 then 参数)
    • 之所以强调值穿透,是因为有时候会遇到第一个函数省略的情况,这种情况少见,但是一旦出现,容易迷惑

为什么要使用 catch 呢?

  • 因为 catch 可以捕获之前 then 中 fulfill 调用发生的错误
    • 这一点其实很强大,比如异步(网络,文件等)获得一个 json 文件,然后在 fulfill 函数中调用 JSON.parse() 是有可能错误的
    • 如果你忘了写对应的 try-catch 块,错误发生的时候, 下一级的 catch 可以帮助捕获这个错误
    • 注意,then 中的 onRejected 函数总是与 onFulfilled 函数同级,而 catch 是下一级,所以 catch 块更适合去处理错误
  • 这也是一把双刃剑
    • 因为 catch 会捕获错误,比如如果错误是一些比较奇怪的错误,比如因为你拼写错误导致的错误也会被捕捉
  • 不用 catch, 那么错误会去哪呢?
    • 会被 Promise 捕捉,catch 只是用来处理传递过来的错误的,并没有捕捉能力
    • 这就意味着 then, catch 块总可能有错误被 Promise 捕捉,而不报错
    • 但其实许多 Promise 库在有未处理的错误时会有提示,可以在 Chrome 下试试
    • 非 ES6 Promise 标准的 .done 方法总是可以向上抛出错误(现在可以想想怎么做,稍后来讲)

注意 ECMAScript3 保留字无法作为对象属性名使用, 因此不能使用 Promise.catch() 的代码
需要使用中括号表示法或者某些类库使用了 caught 作为函数名

The Promise Resolution Procedure

[[Resolve]](promise, x) 解析过程如下(翻译自 Promises/A+):

  1. promise 与 x 指向同一对象, 以 TypeError 错误进入 rejected
  2. x 是 Promise
    1. x 处于 pending, promise 也保持 pending 直到 x 进入 fulfilled 或 rejected 状态
    2. x fulfilled 或者 rejected, 使 promise 也进入相同的状态并拥有相同的值
  3. x 是对象或者函数
    1. 定义 then 为 x.then
    2. 尝试 retrieve x.then,如果出错,抛出 Error,则使promise 以 Error 进入rejected
    3. 如果 x 是函数,则设 x 为 this 调用 x, 参数为 resolve 和 reject
      • 如果 resolve 调用,参数为 y, 则运行 [[Resolve]](promise, y)
      • 如果 reject 调用,参数为 r, 则 promise 因 r 而 reject
      • 如果 resolve, reject 都调用或者多次调用, 取第一次调用,其他被忽略
      • 调用 then 抛出异常:
        如果 resolve, reject 已被调用,则忽略
        否则 reject Promise 以异常 e
    4. then 不是函数,promise 以 x 进入 fulfilled
  4. x 不是对象或者函数,promise 以 x 进入 fulfilled

这里要特别强调的是,非常推荐看一次 Promises/A+ 规范,正是因为 thenable 有着规范,所以 promise 可以在不同的类库之间相互转换

thenable 指有 then 方法的对象

Promise.resolve()

将现有对象转为一个 Promise 对象,也可用于迅速的创建对象

方法参数(此处参考 阮一峰的 ECMAScript 6 入门 : Promise对象

  1. 参数是一个Promise实例,将直接返回实例
  2. 参数是一个 thenable 对象,Promise.resolve方法会将这个对象转为新的 Promise 然后就立即执行 thenable 对象的then方法
  3. 如果参数是一个原始值,或者是一个不具有then方法的对象,再或不带参数,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved,传递给下一步 then 的值为参数

Promise.reject()

返回一个状态为 rejected 的新 Promise 实例,参数用法与Promise.resolve方法一致(注意如果参数是一个 Promise, reject 依旧会返回一个新 Promise)。

Promise.all()

将多个 Promise 实例,包装成一个新的Promise实例: var p = Promise.all([p1, p2, p3]);

  • 数组参数如果不是 Promise,调用 Promise.resolve() 方法转化
  • 参数可以不是数组,但必须具有Iterator接口
  • 并发执行

返回

  • 只有全部状态均变为 fulfilled, p 才会变为 fulfilled
  • 只要有一个 rejected, p 就以 第一个 reject 的值 进入 rejected
  • fulfill 得到的结果是一个与输入 Promise 顺序相同的数组

Promise.race()

将多个Promise实例,包装成一个新的Promise实例 : var p = Promise.race([p1,p2,p3]);


  • 只要有一个先改变状态,p 即 随之变化
  • 其他同 Promise.all()

还记得吗? Promise 无法取消已执行的任务
所以即使有一个 promise 率先返回, 其他 promise 依旧会执行
这一点对于上述的 Promise.all 如果也成立

不属于 ES6 规范但有用的两个方法

done()

  • 保证可以抛出之前任何可能的未处理错误
  • 实现其实非常简单,如下
Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected)
    .catch(function (reason) {
        setTimeout(() => { throw reason }, 0);
    });
};

finally()

  • 不管 promise 最后状态如何,一定会执行的操作
  • 实现也很简单
Promise.prototype.finally = function (callback) {
    let p = this.constructor;
    return this.then(
        value  => p.resolve(callback()).then(() => value),
        reason => p.resolve(callback()).then(() => { throw reason })
    );
};

实现与测试

Promise 简单的实现并不难,建议自己独立实现一次

一些有关实现 Promise 的参考文章

一个小测试

如下四种写法有什么区别?

doSomething().then(function () {
  return doSomethingElse();
});

doSomething().then(function () {
  doSomethingElse();
});

doSomething().then(doSomethingElse());

doSomething().then(doSomethingElse);

答案:

  1. 要注意的是 then 第一个函数中没有参数代表 doSomething 函数的返回值
  2. 没有返回 doSomethingElse 函数的返回值
  3. then 里放的是 doSomethingElse 的返回值,还记得如果不是函数就会忽略这个参数吧?所以此 promise 下一个 then 中接到的值依旧是 doSomething 的返回值
  4. 最后一个,相当于 doSomethingElse 就是 onFulfilled 函数, doSomethingElse 函数的参数是 doSomething 的返回值,doSomethingElse 的返回值将传递下去

此问题来自We have a problem with promises,此篇文章写的很好,讲了常见的一些 Promise 的使用误区

你也可以看其翻译版: 谈谈使用 promise 时候的一些反模式


其他

  • Promise 并不总是处理异步编程的最佳形式,比如有时需要用到 stream 模式
  • 本文没有提到 Deferred,不建议使用它,是 Promise anti patterns(参考:Promise anti patterns

除了上述提到的文章外,本篇还参考了:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值