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

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

简单实现

我们自己实现一个Promise应该怎么实习呢?

首先,我们先写出实现核心逻辑

  • 异步
  • 链式调用

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

tsx 复制代码
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,应该怎么实现呢?

定义构造函数

tsx 复制代码
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的指向需要做绑定
tsx 复制代码
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函数

tsx 复制代码
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 = 'resolved'
            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方法是实现链式调用的核心机制,因为它被绑定在对象的原型上,使得可以顺畅地进行链式调用。

javascript 复制代码
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 === 'resolved') {
        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透传

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

tsx 复制代码
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方法做了调整,现在它只需将接收到的值继续传递下去。

javascript 复制代码
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+的要求。
tsx 复制代码
// 同步执行❌
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

javascript 复制代码
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的逻辑处理

javascript 复制代码
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对象

javascript 复制代码
// 为了使用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) { }
bash 复制代码
npm i -g promises-aplus-tests
promises-aplus-tests Promise.js

停止一个Promise链

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

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

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

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

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

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

javascript 复制代码
new MyPromise(function (resolve) {
    resolve(42)
}).then(function (value) {
    alter(value)
})

为了使用者能够清楚的捕获到错误,我们应该捕获异常并抛出,此时修改一下reject函数

javascript 复制代码
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+的所有测试

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

参考链接

相关推荐
Larcher27 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐39 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭1 小时前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
熊猫钓鱼>_>2 小时前
Java面向对象核心面试技术考点深度解析
java·开发语言·面试·面向对象··class·oop
十二春秋2 小时前
场景模拟:基础路由配置
前端