本文原创:duxiaoxue
一些引用
一旦一个 Promise 决议,不论是现在还是将来,下一个步骤总是相同的。
既然 Promise 是通过
new Promise(..)语法创建的,那你可能就认为可以通过p instanceof Promise来检查。但遗憾的是,这并不足以作为检查方法。识别 Promise(或者行为类似于 Promise 的东西)就是定义某种称为 thenable 的东西,将其定义为任何具有then(..)方法的对象和函数。我们认为,任何这样的值就是 Promise 一致的 thenable。Promise 模式构建的可能最重要的特性:信任。
即使是立即完成的 Promise(类似于
new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。也就是说,对一个 Promise 调用then(..)的时候,即使这个 Promise 已经决议,提供给then(..)的回调也总会被异步调用。一个 Promise 决议后,这个 Promise 上所有的通过
then(..)注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。-- YDKJS
先说异步
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 ... 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
--阮一峰《ES6入门》
在 Promise 之前:回调函数
setTimeout(() => {
// statements
}, 1000)
var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
缺点:
- 大量的嵌套导致的回调地狱让人难以理解代码的实际发生顺序
- 控制反转产生信任问题,回调执行情况无法保证
大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。 回调函数的调用控制交与第三方函数内部,无法保证回调函数一定会被正确调用。可能出现很多异常情况:所需参数传递错误、调用回调过早或过晚、调用回调次数太多或太少、吞掉可能出现的错误与异常等等。 回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。 ... 我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。 也需要一个通用方案来解决信任问题。
-- YDKJS
Promise登场
promise的思想
如果我们不把自己程序的 continuation 传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么。这种范式就称为
Promise。
Promise决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。特别是对于多方查看同一个 Promise 决议的情况,尤其如此。一方不可能影响另一方对 Promise 决议的观察结果。 不可变性听起来似乎一个学术话题,但实际上这是Promise设计中最基础和最重要的因素。
Promise是一种封装和组合未来值的易于复用的机制。-- YKDJS
社区推动
最早的Promise是由社区首先提出和实现的,早期比较有名的有jQuery的Deferred对象、bluebird、Q等等。ES6也将Promise纳入语言标准,提供了原生的Promise对象。
最新的规范是在2014年发布的promise/A+。
Promise/A+规范
一个promise指示一个异步操作的最终结果。与promise交互的主要方法是通过它的then方法,在then方法中注册了接收promise的最终值或是无法被完成原因的回调。
Promise/A+只专注于提供可操作的then方法的规范。
规范中指出,ECMAScript语言规范中的Promise对象基于本规范还实现了许多额外的要求,也就是说我们可以自行实现一个完全遵守promise/A+规范但不必完全遵守ECMAScript语言规范的Promise,某种程度上说ES6里面的Promise也只是许多promise/A+规范实现的一种。
术语
promise: 带有按规范实现的then方法的对象或函数thenable: 定义了then方法的对象或函数value: 任何合法的JavaScript值(包括undefined、thenable或是promise),终值exception: 使用throw抛出的值reason: 指示promise被拒原因的值,拒因
规范要求
(一). Promise的状态
一个`promise`必须是这三种状态之一:
- `pending`(进行中、等待中)
- `fulfilled`(被完成、被执行)
- `rejected`(被拒绝)
状态的迁移:
1. 当`pending`时,`promise`的状态可以变到`fulfilled`或是`rejected`
2. 当`fulfilled`时,`promise`的状态不可再变,同时必须持有一个**不可变**的`value`(终值)
3. 当`rejected`时,`promise`的状态不可再变,同时必须持有一个**不可变**的`reason`(拒因)
这里的**不可变**指的是恒等(即可用 `===` 判断相等),但不意味着更深层次的不可变(如非基本类型值时,只要求引用地址相等)。
(二). then方法
一个`promise`必须提供一个`then`方法以访问其当前值、终值和据因。方法接受两个参数:
```js
promise.then(onFulfilled, onRejected)
```
1. `onFulfilled`、`onRejected`均为可选参数,如果不是函数类型,则必须被忽略
2. 如果`onFulfilled`是个函数:在promise被完成前不可被调用;在promise被完成后必须被调用,以promise的终值作为第一个参数;不能被多次调用。
3. 如果`onRejected`是个函数:在promise被拒绝前不可被调用;在promise被拒绝后必须被调用,以promise的据因作为第一个参数;不能被多次调用。
4. 调用时机
保证`onFulfilled`、`onRejected`在`then`被调用的那轮事件循环之后的新执行栈中异步执行。
这一点可以使用宏任务(macro-task)机制如`setTimeout`、`setImmediate`,或微任务(micro-task)机制如`MutationObserver`、`process.nextTick`来实现。
5. 调用要求
`onFulfilled`和`onRejected`必须被作为函数调用(即没有 this 值)(在严格模式中,函数`this`的值为`undefined`;在非严格模式中其为全局对象)
6. `then`方法可以被同一个promise调用多次
- 当promise被完成时,所有`onFulfilled`按注册顺序依次回调
- 当promise被拒绝时,所有`onRejected`按注册顺序依次回调
7. `then`方法必须返回一个promise对象
```js
promise2 = promise1.then(onFulfilled, onRejected);
```
- 如果`onFulfilled`、`onRejected`返回一个值x,则运行下面决议`promise`的过程:`[[Resolve]](promise2, x)`
- 如果`onFulfilled`、`onRejected`抛出一个异常e,则`promise2`必须拒绝执行,并返回拒因e
- 如果`onFulfilled`不是函数且`promise1`成功执行, `promise2`必须成功执行并返回相同的值
- 如果`onRejected`不是函数且`promise1`拒绝执行,`promise2`必须拒绝执行并返回相同的拒因
(三). 决议promise的过程(即[[Resolve]](promise, x)的具体实现)
1. 如果`x`与`promise`相等,以`TypeError`为拒因拒绝执行promise
2. 如果`x`为`Promise`的实例,则使`promise`接受`x`的状态
- 如果`x`进行中,`promise`也需保持进行中的状态直至`x`被完成或拒绝
- 如果`x`被完成,用相同的值执行`promise`
- 如果`x`被拒绝,用相同的据因拒绝`promise`
3. 如果`x`为函数或者对象
1. 把`x.then`赋值给`then`
2. 如果取`x.then`的值时抛出错误`e` ,则以`e`为据因拒绝`promise`
1. 如果`then`是函数,将`x`作为函数的作用域`this`调用之。传递两个回调函数作为参数,第一个参数叫做`resolvePromise`,第二个参数叫做`rejectPromise`:
- 如果`resolvePromise`以值`y`为参数被调用,则运行`[[Resolve]](promise, y)`
- 如果`rejectPromise`以据因`r`为参数被调用,则以据因`r`拒绝`promise`
- 如果`resolvePromise`和`rejectPromise`均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
- 如果调用`then`方法抛出了异常`e`:
- 如果`resolvePromise`或`rejectPromise`已经被调用,则忽略之
- 否则以`e`为据因拒绝`promise`
2. 如果`then`不是函数,以`x`为参数执行`promise`
3. 如果`x`不为对象或者函数,以`x`为参数执行`promise`
测试
如果按照规范自行实现Promise,可以使用下面官方提供的工具监测是否符合规范。
练习题
掘金上可以找到一些不错的练习题,用来巩固知识再好不过了。