gogoend / blog

blogs, ideas, etc.
MIT License
9 stars 2 forks source link

如何手写一个 Promise ? #56

Open gogoend opened 4 years ago

gogoend commented 4 years ago

Promise犹如一个信使,无论是好消息或是坏消息,只要有了消息,就会告诉每一个想要知道消息的人。

回顾一下曾经在来看看Promise到底是什么以及怎么用立过的一个 Flag —— 手写一个 Promise。。。怎么写呢?这是个问题。

gogoend commented 4 years ago

分析浏览器中的Promise对象

来看看Promise到底是什么以及怎么用一文中,笔者对Promise的相关用法进行了概述。

image 类方法包括:

image 实例方法包括:

image 此外实例上还包括了几个不能够直接被访问的属性:

当然,以上API要想全实现恐怕并不是一步登天的事情。首先,我们来实现一个简单的、可以通过Promise A+规范测试的Promise吧。

要想实现一个可以通过 promises-aplus-tests 全部用例的 Promise,我们需要实现以下内容:

  1. Promise类的构造函数
  2. Promise实例的then方法
  3. Promise链式调用
gogoend commented 4 years ago

编写一个可以通过Promise A+测试的Promise

通过常量定义Promise三种状态

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

这里定义了Promise的执行过程的三种状态 ——

实现Promise类构造函数

const Promise = function (executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCbs = [];
    this.onRejectedCbs = [];
    const resolve = (value) => {
        if (this.status === PENDING) {
            this.status = FULFILLED;
            this.value = value;
            this.onFulfilledCbs.forEach(func => {
                func()
            })
        }
    }
    const reject = (reason) => {
        if (this.status === PENDING) {
            this.status = REJECTED;
            this.reason = reason;
            this.onRejectedCbs.forEach(func => {
                func()
            })
        }
    }
    try {
        executor(resolve, reject)
    } catch (err) {
        reject(err)
    }
}

这是Promise类的定义。 类中包含这些属性:

值得注意的是, 根据Promise A+ 规范(下列简称“根据规范”)2.1,一旦实例的状态由 pending 转变为其他值,实例的状态便不能够再次发生改变。

同样这里还有 resolve 和 reject 两个函数:

接下来在将会立即执行 Promise 实例在构造时所传入的函数(又被称为“执行器”)。

实现实例方法 then

// 注释1 - gogoend
Promise.prototype.then = function (fulfilledCb, rejectedCb) {
    // 注释2 - gogoend
    fulfilledCb = typeof fulfilledCb === 'function' ? fulfilledCb : val => val;
    rejectedCb = typeof rejectedCb === 'function' ? rejectedCb : reason => { throw reason }
    // 注释3 - gogoend
    let promise2 = new Promise((resolve, reject) => {
        // 注释4 - gogoend
        if (this.status === PENDING) {
            this.onFulfilledCbs.push(() => {
                setTimeout(() => {
                    try {
                        let x = fulfilledCb(this.value)
                        // 注释5 - gogoend
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (err) {
                        reject(err)
                    }
                })
            })
            this.onRejectedCbs.push(() => {
                setTimeout(() => {
                    try {
                        let x = rejectedCb(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (err) {
                        reject(err)
                    }
                })
            })
        }
        // 注释6 - gogoend
        if (this.status === FULFILLED) {
            setTimeout(() => {
                try {
                    let x = fulfilledCb(this.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (err) {
                    reject(err)
                }
            })
        }
        if (this.status === REJECTED) {
            setTimeout(() => {
                try {
                    let x = rejectedCb(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (err) {
                    reject(err)
                }
            })
        }
    })
    return promise2
}

代码中的注释:

  1. then函数可接受两个参数,这两个参数的类型都是函数。分别表示前一Promise实例被 resolve 、被 reject 后将会执行的函数。这两个函数分别接受Promise执行成功的结果、执行失败的原因来作为参数。

  2. 根据规范2.2.1,这两个参数为可选参数,若两者非函数,则忽略;这里直接返回值或抛出错误。

  3. 规范2.2.7指出,then函数需要返回一个Promise实例,所以接下来所有操作都在将要返回的实例中进行。

  4. 每当执行then的时候,都要判断一次当前Promise是何种状态。若Promise当前状态是pending,则表示Promise实例还未被接受或是拒绝,then中的函数则将会被放入到数组中等待执行;

  5. 此处有一个 resolvePromise 函数,该函数用于对Promise各种结果的值进行不同判断,从而进行不同操作。详见下文

  6. 若Promise当前状态不再是pending,则表示Promise实例已被接受或拒绝,then中的函数将会立即执行。根据规范2.2.4以及3.1,fulfilledCb、rejectedCb函数需要被异步执行,因此外面使用了setTimeout进行包裹。

实现Promise链式调用

function resolvePromise(promise2, x, resolve, reject) {
    // 注释1 - gogoend
    if (x === promise2) {
        reject(new TypeError('循环引用'))
    }
    // 注释2 - gogoend
    if ((x !== null && typeof x === 'object') || typeof x === 'function') {
        // 注释3 - gogoend
        let called = false
        // 注释4 - gogoend
        try {
            let then = x.then
            if (typeof then === 'function') {
                then.call(
                    x,
                    y => {
                        if (called) return
                        called = true
                        resolvePromise(promise2, y, resolve, reject)
                    },
                    reason => {
                        if (called) return
                        called = true
                        reject(reason)
                    }
                )
            } else {
                if (called) return
                called = true
                resolve(x)
            }
        } catch (err) {
            if (called) return
            called = true
            reject(err)
        }
    } else {
        resolve(x)
    }
}

本函数主要用于判断上一步的Promise中所得到的结果是什么,从而进行对应操作。 注释:

  1. 如果上一步返回的结果x与上一步的Promise是同一个值,则表示Promise发生了循环调用。形如下方代码:

    let thePromise = new Promise((resolve,reject)=>{
    resolve();
    }).then(
    ()=> thePromise
    )

    这种情况下,Promise实例则将会一直在等待自己完成,状态永远不会转变为 pending 之外的状态。

  2. 此处用于判断上一步结果x是什么类型的值。如果x是一个对象或者函数,且x中包含 then 方法,则意味着 x 具有 Promise 的一些特性,也就是thenable;此时将会调用then方法,调用时thenthis指向x,在then中的fulfilledCb参数中传入新的结果y, 并将y 传入再次调用的resolvePromise进行递归调用(调用时传入promise2、新的执行结果y、resolve函数、reject函数),这就实现链式调用,直到最终结果不再thenable;如果 x 是一个普通的、无 Promise 特征的值,则表示 x 已经是最终结果了,此时就执行 resolve(x)

  3. called - 成功/失败后不允许再让Promise发生状态改变。

  4. 为何此处需要使用 try……catch……? 如下示例:

    Object.defineProperty(window,'then',{get(){throw new Error()}})

    虽然该用例很极端,然而为了代码健壮性也值得考虑考虑。

gogoend commented 2 years ago

链式调用的核心,即解析函数

解析函数大致类型定义:

function resolvePromiseChain (
currentPromise: Promise<any>, // 当前要被解析的Promise
currentResult: any, // 当前Promise对应的结果 - 可能是最终结果,也可能需要继续解析
resolve, // 当前Promise中的resolve函数
reject // 当前Promise的reject函数
) => any
  1. 如果currentPromise === currentResult,则reject(new TypeError)
  2. 如果currentResult是另外一个Promise实例,则根据它的state进行一些别的操作
    1. 如果currentResult为pending状态,则currentPromise需要保持这一状态,直到currentResult被fulfilled或rejected
    2. 如果currentResult被fulfilled,则currentPromise也将被fulfilled
    3. 如果currentResult被rejected,则currentPromise也将被rejected
  3. 否则,如果currentResult是个对象或函数
    1. 声明一个变量then,初始化赋值为currentResult.then
    2. 若在访问currentResult.then时抛出错误,则reject currentPromise,原因即为该错误
    3. 若then是一个函数,则以currentPromise作为this,来对它进行调用;第一个参数为新的onResolve回调(回调参数reason为nextResult),第二个参数为新的onReject回调(回调参数为reason)

      这里相当于在当前Promise加了一个回调来处理一些内部状态,递归调用解析函数,并返回其结果。形如:

      currentPromise.then(
          (nextResult) => {...}, // onResolve
          (reason) => {...} // onReject
      )
      1. 如果resolve被调用,参数为nextResult,则在onResolve回调中执行解析函数(递归地resolve currentPromise): resolvePromiseChain(currentPromise, nextResult, resolve, reject)
      2. 如果reject被调用,参数为reason,则在onReject回调中reject currentPromise
      3. 如果resolve与reject同时被调用,则以第一次调用的为准
      4. 如果调用then时抛出了错误,
      5. 如果onResolve或onReject已被调用,则忽略这一错误
      6. 否则reject currentPromise
    4. 如果then不是函数,则fulfill当前promise,结果为currentResult
  4. 如果x不是对象或函数,则fulfill当前promise,结果为currentResult