Promise原理解析

778 阅读11分钟

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时,改变状态的两个函数resolvereject都还没有被调用, 那么此时的状态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)
    }
}

这里大家可能有些疑问

  1. 代码中onResolvedCallbacksonRejectedCallbacks为啥要用数组接收? 本来就一个函数, 直接用个变量保存不就行了么?

    /**
    * 同一个promise对象的then方法可以被多次调用, 
    * 当多次调用then方法时,就需要把每个then方法内的任务都接收
    */
    var promise = new Promise(/**/)
    promise.then()
    promise.then()
    
  2. 为啥在调用执行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方法内使用,这里是如何做到的呢?

image-20210522161247744

链式调用简单, 我们直接在调用then方法时再返回一个promise对象就能实现链式调用了; 对于then方法的值的穿透特性,如果能够在调用then方法返回新promise对象的同时,把then方法内参数函数的返回值也传递到这个新返回的promise对象中,当这个新返回的promise对象调用自己的resolve/reject方法后不就可以在新返回的promise对象的then中使用了么, 有点绕哦......还是直接上代码吧; 由于构造函数Promise的代码还是上面那块,没有变化这里就不复制过来了, 这里变化的主要是then函数,和处理值的resolvePromise函数.

根据Promise/A+规范一个Promise必须提供一个then函数,用于处理它的valuereason, 并且这个then函数接收两个可选的函数作为参数promise.then(onFulfilled, onRejected)

  1. 如果onFulfilled或者onRejected不是一个函数, 那么就直接忽略掉

  2. 如果onFulfilled是一个函数

    它必须在promise的状态是resolved之后被调用,并且用promisevalue值作为它的第一参数

    它不会在promise的状态是resolved之前被调用

    它只会调用一次

  3. 如果onRejected是一个函数

    它必须在promise的状态是rejected之后被调用,并且用promisereason值作为它的第一参数

    它不会在promise的状态是rejected之前被调用

    它只会调用一次

  4. onFulfilled或者onRejected要在当前执行上下文的代码执行完毕之后再被调用(可以理解为异步调用)

  5. onFulfilled或者onRejected必须作为一个函数被调用

  6. then函数可能会被同一个promise对象多次调用

    如果当promise的状态是resolved, 所有各自的onFulfilled方法都会被按照调用then的顺序依次调用执行

    如果当promise的状态是rejected, 所有各自的onFejected方法都会被按照调用then的顺序依次调用执行

  7. 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状态,并且使用promise1value,作为promise2value

    如果onFulfilled不是一个函数并且promise1是一个失败态rejected,promise2必须变也成rejected状态,并且使用promise1reason,作为promise2reason

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+规范:

  1. 如果promise2x指向同一个对象,则调用promise2reject并把这个异常作为promise2reason

  2. 如果返回的x值是一个promise, 那就采用它的状态

    如果x的状态是pending, promise2也必须保持pending状态,直到x的状态变成resolved或者rejected

    如果x的状态是resolved,调用promise2resolve方法,接收当前value作为第一个参数

    如果x的状态是rejected,调用promise2reject方法, 接收当前的reason作为第一个参数

  3. 如果返回的x是一个函数或对象

    判断是否有then属性,如果这个then属性是一个函数,那么就认定这个x是一个promise对象, 如果没有then属性,或者then属性不是一个函数, 那么也把x作为普通值进行处理,直接调用promise2resolve(x)

  4. 如果返回的x不是一个函数或对象,那么就作为普通值处理,直接调用promise2resolve(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)
    }
}

代码不是很复杂就不一句句解释了,这里就直接说我在刚开始学习时遇到的疑问

  1. then方法中为啥then函数的第一个参数缺席用空函数替代, 而第二个参数缺席却需要抛出一个异常?

    首先then函数接收两个函数作为参数, 最终then接收的两个函数只会执行其中的一个,具体执行哪一个函数取决于当前Promise的内部状态, 当Promise内部状态为resolved时执行第一个函数, 当Promise内部状态为rejected时执行第二个函数, 此外需要注意当executor函数在执行过程中抛异常的话,内部状态也会变成rejected;

    根据Promise/A+规范,如果在执行promise1onFulfilled或者onRejected时抛出了一个异常e, 会使promise2的状态变成rejected,并调用执行promise2对象的onRejected

    所以在第二个参数省略时, 如果不抛异常,不管什么情况都会使下一个promise的状态变成resolved,并调用执行下一个promise对象的onResolve方法

  2. 为啥then函数中的有些代码需要放在定时器函数里?

    为了拿到promise2, 就必须使promise2的构造函数执行完,所以只能让同步代码先执行完, 生成promise2对象, 再回过来执行构造函数里的异步代码.

  3. 为啥在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:image-20210616203901633

参考

  1. Promise/A+
  2. 阮一峰的Promise 对象