Promises/A+规范
为什么需要异步编程方式
一个函数执行之后,在它后面顺序编写的代码中,如果能够直接使用它的返回结果或者它修改之后的引用参数,那么我们通常认为该函数是同步的。
如果一个函数的执行结果或者其修改的引用参数,需要通过设置回调函数或者回调事件的方式来获取,而在其后顺序编写的代码中无法直接获取的话,那么我们通常认为这样的函数是异步的。
阻塞带来的问题是当前线程(或进程)会陷入等待,一直等到阻塞结束,这样就会造成线程(或进程)资源的浪费。所以通常认为阻塞是不够高效的。
如果要编写非阻塞代码,使用同步方式会变得有些复杂,且不够灵活。同步方式的非阻塞代码通常会使用 select 模式,例如 curl_multi_select、stream_select、socket_select等就是PHP中提供的一些典型的 select 模式的函数。我们说它复杂且不够灵活,是因为例如使用上面的 select 模式编写同步的非阻塞代码时我们需要先构造一个并发任务的列表,之后手动构造循环来执行这些并发的任务,在循环开始之后,虽然这几个任务可以并发,但是这个循环相对于其后的代码总体上仍然是阻塞的,我们要想拿到这些并发任务的结果时,仍然需要等待。select 虽然可以同时等待多个任务中某一个或几个就位后,再执行后续操作,但仍然有一部分时间是被等待消耗掉的。而且如果是纯同步非阻塞的情况下,我们也很难在循环开始后,动态添加更多的任务到这个循环中去。所以希望程序能够更加高效、更加灵活,就需要引入异步方式。
传统的异步方式有什么问题
回调是最简单直接的异步模式,只要在调用异步函数时设置一个或多个回调函数,函数就会在完成时自动调用回调函数。如果你的程序逻辑够简单,简单的一两层回调也许并不会让你觉得异步方式的编程有什么麻烦。但如果你的程序逻辑一旦有些复杂,你可能就会被层层回调搞得疲惫不堪了。当然,实际上你的程序需要层层回调的原因,也许并不是你的程序逻辑真的复杂,而是你没有办法将回调函数中的参数结果传出来,所以你就不得不将另一个回调函数传进去。
为什么要引入Promise
Promise要解决的问题是,如何将回调方法的参数从回调方法中传递出来,让它可以像同步函数的返回结果一样,在回调函数以外的控制范围内,可以传递和复用。
Promise是抽象异步处理对象以及对其进行各种操作的组件
Promises/A+规范
- 解决(fulfill):指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但是在后世的 promise 实现多以 resolve 来取代之。
- 拒绝(reject):指一个 promise 失败时进行的一系列操作。
- 终值(eventual value):所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。
- 拒因(reason):也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。
Promise 表示一个异步操作的最终结果,与之进行交互的方式主要是 then 方法,该方法注册了两个回调函数,用于接收 Promise 的终值或本 Promise 不能执行的原因。
术语:
- Promise:它是一个拥有 then 方法的对象或函数,其行为符合本规范;
- thenable:是一个定义了 then 方法的对象或函数,即拥有 then 方法;
- 值(value):指任何 JavaScript 的合法值(包括 undefined、thenable 和 promise);
- 异常(exception):是使用 throw 语句抛出的一个值;
- 拒因(reason):表示一个 promise 的拒绝原因;
要求:
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
等待态(Pending)
处于等待态时,promise 需要满足以下条件:
- 可以迁移至执行态或拒绝态
执行态(Fulfilled)
处于执行态时,promise 需要满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的终值
拒绝态(Rejected)
处于拒绝态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的拒因
这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)
Then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值和拒因。
promise 的 then 方法接受两个参数:
promise.then(onFulfilled, onRejected)
参数可选
onFulfilled 和 onRejected 都是可选参数。
- 如果 onFulfilled 不是函数,其必须被忽略
- 如果 onRejected 不是函数,其必须被忽略
onFulfilled 特性
如果 onFulfilled 是函数:
- 当 promise 执行结束后其必须被调用,其第一个参数为 promise 的终值
- 在 promise 执行结束前其不可被调用
- 其调用次数不可超过一次
onRejected 特性
如果onRejected 是函数:
- 当 promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的拒因
- 在 promise 被拒绝执行前其不可被调用
- 其调用次数不可超过一次
调用时机
onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用(平台代码:指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用"宏任务macrotask"机制或"微任务microtask"机制来实现。由于 promise 的实施代码本身就是平台代码(即都是JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。)
macrotask 和 microtask 两个概念,这表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
两个类别的具体分类如下:
- macro-task:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering
- micro-task:process.nextTick、Promises(浏览器原生的Promises)、Object.observe、MutationObserver
调用要求
onFulfilled 和 onRejected 必须被作为函数调用(即没有 this 值)
多次调用
then 方法可以被同一个 promise 调用多次
- 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
- 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
返回
then 方法必须返回一个 promise 对象
promise2 = promise1.then(onFulfilled, onRejected);
- 如果 onFulfilled 或者 onRejected 返回一个值 x,则运行下面的 Promise 解决过程:[ [ Resolve ] ] (promise2,x)
- 如果 onFulfilled 或者 onRejected 抛出一个异常 e,则 promise2 必须拒绝执行,并返回拒因 e
- 如果 onFulfilled 不是函数且 promise1 成功执行,promise2 必须成功执行并返回相同的值
- 如果 onRejected 不是函数且 promise1 拒绝执行,promise2 必须拒绝执行并返回相同的拒因
不论 promise1 被 reject 还是被 resolve 时 promise2 都会被 resolve,只有出现异常时才会被 rejected。
Promise 解决过程
Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [ [ Resolve ] ] (promise,x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise。
这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。
运行 [ [ Resolve ] ] (promise,x) 需要遵循以下步骤:
x 与 promise 相等
如果 promise 和 x 指向同一个对象,以 TypeError 为拒因拒绝执行 promise
x 为 promise
如果 x 为 Promise,则使 promise 接受 x 的状态:
- 如果 x 处于等待态,promise 需要保持为等待态直至 x 被执行或拒绝
- 如果 x 处于执行态,用相同的值执行 promise
- 如果 x 处于拒绝态,用相同的拒因拒绝 promise
x 为对象或函数
如果 x 为对象或函数:
- 把 x.then 赋值给 then
- 如果取 x.then 的值时抛出错误 e,则以 e 为拒因拒绝 promise
- 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise,第二个参数叫做 rejectPromise:
- 如果 resolvePromise 以值 y 为参数被调用,则运行 [ [ Resolve ] ](promise,y)
- 如果 rejectPromise 以拒因 r 为参数被调用,则以拒因 r 拒绝 promise
- 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
- 如果调用 then 方法抛出了异常 e :
- 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
- 否则以 e 为拒因拒绝 promise
- 如果 then 不是函数,以 x 为参数执行 promise
- 如果 x 不为对象或者函数,以 x 为参数执行 promise
如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [ [Resolve] ](promise,thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为拒因来拒绝 promise。