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: