Promise由来
在Promise
出现以前,我们通常主主要的解决异步问题的方式就是通过回调嵌套,在异步逻辑执行完毕之后通过回调函数的方式获取异步执行结果. 层层回调嵌套会使得代码逻辑不直观也不利于后期开发维护.
fs.readFile("./a.txt", (err, data) => {
fs.readFile(data, (err, data) => {
fs.readFile(data, (err, data) => {
//回调黑洞
})
})
})
Promise
巧妙的将异步回调形式的层层嵌套,转变成同步回调形式的链式调用.本质上Promise
内部用的还是回调函数的方式,解决异步问题, 但是可以通过同步的方式体现.
new Promise((reslove, reject)=> {
//异步逻辑
}).then(
//异步逻辑
).then(
//异步逻辑
).then(
)
同步版的Promise
直接上Promise
的源码吧, Promise
的源码都是根据Promise/A+规范来实现的
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected'
function Promise(executor) {
/**
* Promise对象内部有一个状态state, 默认为初始状态pending,
* 根据Promise的执行结果操作状态的变化, 而且整个过程状态只能变化一次,
* 要么是由等待态pending变成成功态resolved,要么是由等待态变成失败态rejected;
**/
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
let resolve = (value) => {
//只能是在当前状态为pending的时候才能改变状态
if (this.state === PENDING) {
this.state = RESOLVED;
this.value = value;
}
}
let reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
}
}
/**
* Promise接收一个函数作为参数,这个函数会在new Promise时立即执行,
* 并且该函数接收两个函数作为参数,在Promise操作成功的时候调用第一个参数resolve(),
* 在Promise操作失败的时候调用第二个参数reject()
*/
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function (onResolved, onRejected) {
/**
* 生成的每个Promise实例对象都具有then方法, then方法也同样接受两个函数操作参数,
* 在调用then方法时,如果此时的状态为resolved则调用执行第一个函数onResolved,
* 当此时的状态时rejected时执行第二个函数onRejected
*/
if (this.state === RESOLVED) {
onResolved(this.value)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
}
但是上面的Promise
是一个基础版本,只能解决同步问题, 因为生成的Promise
对象在调用then
方法时,此时的状态state已经发生改变,可以执行对应的逻辑, 但是思考一下,如果executor
函数内执行的是一个异步操作, 那么生成promise
实例对象在调用then
时,改变状态的两个函数resolve
、reject
都还没有被调用, 那么此时的状态state
就还是等待态pending
. 而在我们上面的then
方法中,也没有针对状态为pending
时的处理逻辑.这里就涉及到异步问题处理了.
通常我们遇到这种问题的处理方式都是通过回调来解决,假设我们在调用then
方法时,内部状态为pending
, 我们可以先把传递给then
方法的两个函数参数存放起来, 在异步操作执行完毕后,即在executor
接收的两个函数参数中的某一个被调用时,再回过头去执行刚才在then
内暂存的方法:
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected'
function Promise(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
//存放then的第一个参数, 即状态为成功时要调用的回调函数
this.onResolvedCallbacks = [];
//存放then的第二个参数, 即状态为失败时要调用的回调函数
this.onRejectedCallbacks = [];
let resolve = (value) => {
if (this.state === PENDING) {
this.state = RESOLVED;
this.value = value;
//在调用resolve,即在Promise状态为成功时执行then内的onResolved函数
this.onRejectedCallbacks.forEach(fn => fn())
}
}
let reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
//在调用reject,即在Promise状态为失败时执行then内的onRejected函数
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function (onResolved, onRejected) {
if (this.state === RESOLVED) {
onResolved(this.value)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
/**
* 对于异步操作, 那么此时的状态肯定还是等待态, 先将处理函数存放,
* 待异步操作执行完毕后,再回过头来执行
**/
if (this.state === PENDING) {
this.onResolvedCallbacks.push(onResolved)
this.onRejectedCallbacks.push(onRejected)
}
}
这里大家可能有些疑问
-
代码中
onResolvedCallbacks
、onRejectedCallbacks
为啥要用数组接收? 本来就一个函数, 直接用个变量保存不就行了么?/** * 同一个promise对象的then方法可以被多次调用, * 当多次调用then方法时,就需要把每个then方法内的任务都接收 */ var promise = new Promise(/**/) promise.then() promise.then()
-
为啥在调用执行
then
方法时,同步操作的状态改变, 而异步操作状态没改变还是pending
?var p = new Promise((resolve, reject) => { /* 代码块1 */}) p.then(/*代码块2*/)
先理一下上面这块假代码的执行顺序: 在用
new
操作符调用Promise
时, 先执行代码块1 ,执行完毕之后生成一个对象p
,然后对象p
调用执行then
时,开始执行代码块2; 如果上面代码块中的操作都是同步的,那就没啥悬念,代码从上到下依次执行. 但是当代码块1是一个异步操作时,js引擎的执行顺序是先把当前的同步的代码执行完,才会去掉用执行异步代码, 所以如果代码块1是异步操作,那么在调用then方法时,状态还没有改变.
继续, 还没完..., 接下来的才是重点.
Promise的链式调用和then方法的值的穿透特性
我们在使用Promise
时发现它的then
方法可以一直不断地进行链式调用,而且上一个then
方法内函数参数的返回值能够在下一个then
方法内获取到,甚至可以在下下个then
方法内使用,这里是如何做到的呢?
链式调用简单, 我们直接在调用then
方法时再返回一个promise
对象就能实现链式调用了;
对于then
方法的值的穿透特性,如果能够在调用then
方法返回新promise对象的同时,把then方法内参数函数的返回值也传递到这个新返回的promise
对象中,当这个新返回的promise
对象调用自己的resolve
/reject
方法后不就可以在新返回的promise
对象的then
中使用了么, 有点绕哦......还是直接上代码吧; 由于构造函数Promise
的代码还是上面那块,没有变化这里就不复制过来了, 这里变化的主要是then
函数,和处理值的resolvePromise
函数.
根据Promise/A+规范一个Promise
必须提供一个then
函数,用于处理它的value
或reason
, 并且这个then
函数接收两个可选的函数作为参数promise.then(onFulfilled, onRejected)
-
如果
onFulfilled
或者onRejected
不是一个函数, 那么就直接忽略掉 -
如果
onFulfilled
是一个函数它必须在
promise
的状态是resolved
之后被调用,并且用promise
的value
值作为它的第一参数它不会在
promise
的状态是resolved
之前被调用它只会调用一次
-
如果
onRejected
是一个函数它必须在
promise
的状态是rejected
之后被调用,并且用promise
的reason
值作为它的第一参数它不会在
promise
的状态是rejected
之前被调用它只会调用一次
-
onFulfilled
或者onRejected
要在当前执行上下文的代码执行完毕之后再被调用(可以理解为异步调用) -
onFulfilled
或者onRejected
必须作为一个函数被调用 -
then
函数可能会被同一个promise
对象多次调用如果当
promise
的状态是resolved
, 所有各自的onFulfilled
方法都会被按照调用then
的顺序依次调用执行如果当
promise
的状态是rejected
, 所有各自的onFejected
方法都会被按照调用then
的顺序依次调用执行 -
then
函数必须返回一个promise
即:promise2 = promise1.then(onFulfilled, onRejected)
如果
onFulfilled
或者onRejected
返回了一个值x
, 那么执行resolvePromise(promise2, x)
如果
onFulfilled
或者onRejected
抛出了一个异常e
,promise2
必须变成rejected
状态,并把这个e
作为reason
如果
onFulfilled
不是一个函数并且promise1
是一个成功态resolved
,promise2
必须变成resolved
状态,并且使用promise1
的value
,作为promise2
的value
如果
onFulfilled
不是一个函数并且promise1
是一个失败态rejected
,promise2
必须变也成rejected
状态,并且使用promise1
的reason
,作为promise2
的reason
Promise.prototype.then = function (onResolved, onRejected) {
/**
* 根据Promise/A+规范规定then方法接收的两个参数函数是可选的,
* 而且如果传入的不是函数会忽略掉, 或用空函数替代或抛出异常
**/
onResolved = typeof onResolved === 'function' ? onResolved : v => v
onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err }
let promise2 = new Promise((resolve, reject) => {
if (this.state === RESOLVED) {
/**
*为了能够让下面的resolvePromise函数拿到promise2,需要Promise函数执行完,
*才会返回对象,并赋值给变量promise2, 否则promise2拿到的就是undefined,
*所以这里才会采用setTimeout使用异步逻辑,通过延迟执行,使Promise函数执行完
**/
setTimeout(() => {
//try...catch只能捕获同步异常,无法捕获异步异常
try {
const x = onResolved(this.value)
//根据返回的x类型,决定promise2的状态,再调用对应的状态改变函数
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.state === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onResolved(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
resolvePromise
函数用于处理then
参数函数的返回值, 根据返回值类型决定返回的下一个promise
对象状态,从而决定到底执行下一个then
的哪个参数函数,根据Promise/A+规范:
-
如果
promise2
和x
指向同一个对象,则调用promise2
的reject
并把这个异常作为promise2
的reason
-
如果返回的
x
值是一个promise
, 那就采用它的状态如果
x
的状态是pending
,promise2
也必须保持pending
状态,直到x
的状态变成resolved
或者rejected
如果
x
的状态是resolved
,调用promise2
的resolve
方法,接收当前value
作为第一个参数如果
x
的状态是rejected
,调用promise2
的reject
方法, 接收当前的reason
作为第一个参数 -
如果返回的
x
是一个函数或对象判断是否有
then
属性,如果这个then
属性是一个函数,那么就认定这个x是一个promise
对象, 如果没有then
属性,或者then
属性不是一个函数, 那么也把x
作为普通值进行处理,直接调用promise2
的resolve(x)
-
如果返回的
x
不是一个函数或对象,那么就作为普通值处理,直接调用promise2
的resolve(x)
function resolvePromise(promise2, x, resolve, reject) {
//在一个promise中resolve/reject最多只能调用一次
let called;
if (promise2 === x) {
if (called) return;
called = true
reject(new TypeError("TypeError"))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
try {
let then = x.then
if (typeof then === 'function') {
//这里就认定x是一个promise, 因为没法再进一步判断了
then.call(x, v => {
if (called) return;
called = true
resolvePromise(promise2, v, resolve, reject)
}, r => {
if (called) return;
called = true
reject(r)
})
} else {
if (called) return;
called = true
resolve(x)
}
} catch (e) {
if (called) return;
called = true
reject(e)
}
} else {
if (called) return;
called = true
resolve(x)
}
}
代码不是很复杂就不一句句解释了,这里就直接说我在刚开始学习时遇到的疑问
-
then
方法中为啥then
函数的第一个参数缺席用空函数替代, 而第二个参数缺席却需要抛出一个异常?首先
then
函数接收两个函数作为参数, 最终then
接收的两个函数只会执行其中的一个,具体执行哪一个函数取决于当前Promise
的内部状态, 当Promise
内部状态为resolved
时执行第一个函数, 当Promise
内部状态为rejected
时执行第二个函数, 此外需要注意当executor
函数在执行过程中抛异常的话,内部状态也会变成rejected
;根据Promise/A+规范,如果在执行
promise1
的onFulfilled
或者onRejected
时抛出了一个异常e
, 会使promise2
的状态变成rejected
,并调用执行promise2
对象的onRejected
所以在第二个参数省略时, 如果不抛异常,不管什么情况都会使下一个
promise
的状态变成resolved
,并调用执行下一个promise
对象的onResolve
方法 -
为啥
then
函数中的有些代码需要放在定时器函数里?为了拿到
promise2
, 就必须使promise2
的构造函数执行完,所以只能让同步代码先执行完, 生成promise2
对象, 再回过来执行构造函数里的异步代码. -
为啥在
resolvePromise
代码里用then.call(x)
调用, 而不是直接用x.then
?// 如果是通过下面的方式定义的属性then,在第一次获取时不会报错, 但在第二次获取时就会报错let times = 1 Object.defineProperty(x, 'then', getter() { if(times>1) { times++ return new Error() } return () => {} } )
Promise验证
网上有一个根据Promise/A+
规范写的一个验证工具, 我们可以使用它来验证自己手写的promise
代码,能否通过验证.
npm i promises-aplus-tests
先下载安装之后, 需要在自己的Promise
文件中加上一段执行代码
Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }) return dfd;}
执行测试命令 promises-aplus-tests promise.js
: