【源码共读】| 实现一个Promise A+

Promise 是我们常用的一个对象,它是用来处理异步操作逻辑的。其中,它有三种状态(pending,fulfilled,rejected),用来表示当前的处理状态。

简单实现

我们自己实现一个Promise应该怎么实习呢?
首先,我们先写出实现核心逻辑

  • 异步
  • 链式调用

这也是面试题中常见的手写Promise

function MyPromise(executor) {
  const self = this
  // 存储resolve后的回调函数
  self.cbs = []

  function resolve(value) {
    // 异步处理
    setTimeout(() => {
      self.data = value
      self.cbs.forEach((cb) => cb(value))
    });
  }
  executor(resolve)
}

MyPromise.prototype.then = function (onResolved) {
  // 返回一个新的MyPromise对象,链式调用
  return new MyPromise((resolve) => {
    this.cbs.push(() => {
      // 执行onResolved回调,并将原MyPromise对象的data值作为参数传递
      // this.data是上一个promise中保存的结果
      const res = onResolved(this.data)
      if (res instanceof MyPromise) {
        // 如果是MyPromise对象,当它解决时,调用新MyPromise对象的resolve方法
        res.then(resolve)
      } else {
        // 如果onResolved返回的不是MyPromise对象,直接用返回值解决新MyPromise对象
        resolve(res)
      }
    })
  })
}

// test case
new MyPromise((resolve) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
})
  .then((res) => {
    console.log(res);
    return new MyPromise((resolve) => {
      setTimeout(() => {
        resolve(2);
      }, 500);
    });
  })
  .then(console.log);

上述的代码基本实现了核心的功能,现在有以下问题:

  • 没有reject
  • 没有catch
  • 边界情况考虑

实现Promise A+

那么,如果我们来手写一个符合Promise A+规范的Promise,应该怎么实现呢?

定义构造函数

function Promise(executor) {
    var self = this
    
    // 初始状态 pending
    self.status = 'pending'
    self.data = undefined
    
    // 用来存resolve reject的回调函数
    self.onResolvedCallback = []
    self.onRejectedCallback = []

    // 增加 resolve reject
    function resolve() { }
    function reject() { }

    // 增加 try catch
    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

Q1:我们能不能在外面定义resolve和reject呢?

  • 可以,因为需要访问promise内部的属性,this的指向需要做绑定
function resolve(value){
}

function reject(reason){
}

function Promise(executor) {
    try {
        executor(resolve.bind(this), reject.bind(this))
    } catch(e) {
        reject.bind(this)(e)
    }
}

resolve和reject函数

接下来实现resolve和reject函数

function MyPromise(executor) {
    var self = this
    self.status = 'pending'
    self.data = undefined
    self.onResolvedCallback = []
    self.onRejectedCallback = []

    // 增加 resolve reject
    function resolve(value) {
        if (self.status === 'pending') {
            self.status = 'fulfilled'
            self.data = value
            for (let i = 0; i < self.onResolvedCallback.length; i++) {
                self.onResolvedCallback[i](value)
            }
        }
    }

    function reject(reason) {
        if (self.status === 'pending') {
            self.status = 'rejected'
            self.data = reason
            for (let i = 0; i < self.onRejectedCallback.length; i++) {
                self.onRejectedCallback[i](reason)
            }
        }
    }

    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

then方法

then方法是实现链式调用的核心机制,因为它被绑定在对象的原型上,使得可以顺畅地进行链式调用。

MyPromise.prototype.then = function (onResolved, onRejected) {
    var self = this
    var promise2

    // 增加 onResolved onRejected
    onResolved =
        typeof onResolved === 'function'
        ? onResolved
        : function (v) {
            return v
        }
    onRejected =
        typeof onRejected === 'function'
        ? onRejected
        : function (r) {
            throw r
        }

    if (self.status === 'fulfilled') {
        return (promise2 = new MyPromise(function (resolve, reject) {
            try {
                var x = onResolved(self.data)
                if (x instanceof MyPromise) {
                    x.then(resolve, reject)
                }
                resolve(x)
            } catch (e) {
                reject(e)
            }
        }))
    }
    if (self.status === 'rejected') {
        return (promise2 = new MyPromise(function (resolve, reject) {
            try {
                var x = onRejected(self.data)
                if (x instanceof MyPromise) {
                    x.then(resolve, reject)
                }
            } catch (e) {
                reject(e)
            }
        }))
    }
    if (self.status === 'pending') {
        return (promise2 = new MyPromise(function (resolve, reject) {
            self.onResolvedCallback.push(function () {
                try {
                    var x = onResolved(self.data)
                    if (x instanceof MyPromise) {
                        x.then(resolve, reject)
                    }
                } catch (e) {
                    reject(e)
                }
            })
            self.onRejectedCallback.push(function () {
                try {
                    var x = onRejected(self.data)
                    if (x instanceof MyPromise) {
                        x.then(resolve, reject)
                    }
                } catch (e) {
                    reject(e)
                }
            })
        }))
            }
}

MyPromise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected)
}

then透传

虽然我们已经完成了核心功能的开发,但仍有几种特定场景未能覆盖。

new MyPromise(resolve => resolve(8))
    .then()
    .then()
    .then(function foo(value) {
        console.log(value);
    })
// 
new MyPromise(resolve=>resolve(8))
    .then()
    .catch()
    .then(function(value) {
        console.log(value);
    })

因此,我们对then方法做了调整,现在它只需将接收到的值继续传递下去。

MyPromise.prototype.then = function (onResolved, onRejected) {
    // ...省略
    onResolved = typeof onResolved === 'function' ? onResolved : function (value) { return value }
    onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { return reason  }
    // ...省略
}}

不同Promise的交互

为了确保我们的Promise实现能够与其他Promise对象互操作,符合Promise/A+规范,我们的实现必须能够处理形如p.then的调用。

这就引出一个问题:

  • 如何正确处理传递给then方法的参数?

根据规范,如果then方法接收到的参数不遵循预期标准,可能会引发几个问题:

  • 回调函数可能会被同步执行,而不是按照规范要求的异步执行。
  • 如果then返回了一个Promise,那么这个返回的Promise可能不会被正确处理。
  • 可能导致回调函数被调用多次,违背了Promise规范中的一次性调用准则。

鉴于这些潜在的问题,我们必须设计一种机制来确保即使遇到非规范的then参数,我们的Promise实现也能够可靠地工作。

  • 我们需要一种方法来检测传递给then的参数是否有效,并在必要时采取补救措施,以避免上述问题的发生。我们的目标是创建一个健壮的Promise实现,它能够在各种情况下表现一致,符合Promise/A+的要求。
// 同步执行❌
let badPromise = new MyPromise((resolve, reject) => {
  resolve("This should not be synchronous");
});

badPromise.then(data => {
  console.log(data); // 不符合规范的实现可能会同步打印这条消息
});

// 未正确处理返回的Promise❌
let promiseA = new MyPromise((resolve, reject) => {
  resolve(20);
});

let promiseB = promiseA.then(value => {
  return new MyPromise((resolve, reject) => {
    resolve(value * 2);
  });
});

promiseB.then(value => {
  console.log(value); // 应该打印40,但是如果then的实现不符合规范,可能得不到预期结果
});

// 多次调用❌
let flawedPromise = new MyPromise((resolve, reject) => {
    resolve("Should only be called once");
    resolve("Called again?"); // 根据规范,这个调用应该被忽略
});

flawedPromise.then(data => {
    console.log(data);
}, reason => {
    console.log(reason);
});

这时候我们需要一个函数来处理这些特殊case

function resolvePromise(promise2, x, resolve, reject) {
    let isCalled = false
    let then
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise!'));
    }
    if (x instanceof MyPromise) {
        if (x.status === 'pending') {
            x.then(function (v) {
                resolvePromise(promise2, v, resolve, reject)
            }, reject)
        } else if (x.status === 'fulfilled') {
            resolve(x.data)
        } else if (x.status === 'rejected') {
            reject(x.data)
        }
        return
    }
    if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
        try {
            // 2.3.3.1 因为x.then有可能是一个getter,这种情况下多次读取就有可能产生副作用
            // 即要判断它的类型,又要调用它,这就是两次读取
            // eg. Object.defineProperty(x, 'then', { get: function () { throw new Error() } })
            then = x.then
            if (typeof then === 'function') {
                then.call(x, function rs(y) {
                    if (isCalled) return
                    isCalled = true
                    return resolvePromise(promise2, y, resolve, reject)
                }, function rj(r) {
                    if (isCalled) return
                    isCalled = true
                    return reject(r)
                })
            } else {
                resolve(x)
            }
        } catch (e) {
            if (isCalled) return
            isCalled = true
            return reject(e)
        }
    } else {
        resolve(x)
    }
}

这时候,我们修改then的逻辑处理

MyPromise.prototype.then = function (onResolved, onRejected) {
    var self = this;
    let promise2

    // 确保 onResolved 和 onRejected 是函数
    onResolved = typeof onResolved === 'function' ? onResolved : function (v) { return v; };
    onRejected = typeof onRejected === 'function' ? onRejected : function (r) { throw r; };

    return promise2 = new MyPromise(function (resolve, reject) {

        if (self.status === 'fulfilled') {
            setTimeout(() => {
                try {
                    const x = onResolved(self.data)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (error) {
                    reject(error)
                }
            });
        } else if (self.status === 'rejected') {
            setTimeout(function () { // 异步执行onRejected
                try {
                    const x = onRejected(self.data)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (reason) {
                    reject(reason)
                }
            })
        } else if (self.status === 'pending') {
            self.onResolvedCallback.push(function (value) {
                try {
                    var x = onResolved(value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (r) {
                    reject(r)
                }
            })

            self.onRejectedCallback.push(function (reason) {
                try {
                    var x = onRejected(reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (r) {
                    reject(r)
                }
            })
        }

    })
}

测试

我们怎么知道我们的代码是否符合Promise A+的规范呢,这时候需要一个库来验证
首先保证运行的是comment js
在文件中定义一个deferred方法,并导出Promise对象

// 为了使用PromiseA+规范的测试代码而加入
Promise.deferred = Promise.defer = function () {
  var dfd = {}
  dfd.promise = new Promise(function (resolve, reject) {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}
try {
  module.exports = Promise
} catch (e) { }
npm i -g promises-aplus-tests
promises-aplus-tests Promise.js

【源码共读】| 实现一个Promise A+

停止一个Promise链

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

如上述代码,当调用的过程中如果在中间的步骤出现Big Error并且catch到,但是后面有then,还是会继续执行。

  • 那么,我们应该解决停止一个promise链呢?

根据上面实现的代码,我们透传是通过返回值,如果我们传入一个空函数,相当于截断了整个流程

Promise.cancel = Promise.stop = function() {
  return new Promise(function(){})
}
  • 修改上面的代码,当出错时截停
new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
    return Promise.stop()
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

如果Promise链上有错误,执行下面的代码,并没有将错误信息输出

new MyPromise(function (resolve) {
    resolve(42)
}).then(function (value) {
    alter(value)
})

【源码共读】| 实现一个Promise A+
为了使用者能够清楚的捕获到错误,我们应该捕获异常并抛出,此时修改一下reject函数

function reject(reason) {
        setTimeout(() => {
            if (self.status === 'pending') {
                self.status = 'rejected'
                self.data = reason
                // 如果没有对应的reject函数处理异常,抛出错误信息
                if (self.onRejectedCallback.length === 0) {
                    console.error(reason)
                }
                for (let i = 0; i < self.onRejectedCallback.length; i++) {
                    self.onRejectedCallback[i](reason)
                }
            }
        });
    }

最后,加上done方法,至此,我们已经通过了promise A+的所有测试

MyPromise.prototype.done = function () {
    return this.catch(function (e) { // 此处一定要确保这个函数不能再出错
        console.error(e)
    })
}

【源码共读】| 实现一个Promise A+

参考链接

原文链接:https://juejin.cn/post/7317149843037388819 作者:xianjianlf2

(0)
上一篇 2023年12月28日 上午10:37
下一篇 2023年12月28日 上午10:48

相关推荐

发表回复

登录后才能评论