深入JS(一):手写 Promise

0. 前言

Promise是我们经常用来管理和简化异步操作的对象,面试中我们经常会被问到相关的问题,比如结合事件循环机制来回答某一段代码的输出顺序,或者要求实现一个异步的任务管理函数,这些都是需要理解 Promise 的原理才能够有底气的回答。还有另一种常见的问题,就是手写 Promise 或者手写 Promise 的各个静态方法。

碰到手撕类的问题,如果我们没有充分准备或者阅读过 Promise 实现的源码,很容易就GG了,有些观点会提到说这种面试题很没有含金量,但是我认为了解如何实现 Promise 对我们的编码还是有很大帮助的,它可以帮助我们更好的理解 Promise 是如何使用统一的状态管理和链式调用机制来帮我们处理复杂任务。

这篇文章会结合 Promise/A+规范 来渐进式地实现我们自己的 Promise 类。

1. 实现 MyPromise 类

我们来分析一下如何使用 Promise :

  1. 每个 Promise 实例具有三种状态,分别是 PendingFulfilledRejected
  2. 在使用 Promise 的时候我们会传入一个接收 resolvereject 的回调函数,这个回调函数会被同步执行,我们可以在回调函数内部调用入参来修改当前 Promise 实例的状态,同时为了保证 Promise 的可预测性和确定性,我们只能修改一次状态。
  3. 我们可以使用实例的 then 方法,这个方法接收一个onFulfilledonRejected 回调函数用来处理结果或者错误

通过以上分析,我们可以轻松实现一个 Promise 类。

js 复制代码
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
​
class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
      }
    };
​
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
      }
    };
    
    // 立即执行我们传入的回调函数
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  
  then(onFulfilled, onRejected){
    // 根据当前状态选择执行相应的处理函数
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
}

我们初步实现了 MyPromise 类,现在用一些代码来测试一下

js 复制代码
// test1
new MyPromise((resolve, reject) => {
   resolve("1");
}).then(
  (res) => {
    console.log("success", res);
  },
  (err) => {
    console.log("failed", err);
  }
);
​
// test2
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("1");
  }, 1000);
}).then(
  (res) => {
    console.log("success", res);
  },
  (err) => {
    console.log("failed", err);
  }
);

我们发现 test1 中控制台会成功输出"success 1",而 test2 则毫无反应,这是因为我们的 then 实现中只处理了状态已经被敲定的情况,而对于 test2 这种状态异步敲定的情况则未做处理,我们可以通过一个数组来暂存传入的处理函数,在状态敲定时去清空暂存数组来实现。

分析完问题我们来修改一下代码。

js 复制代码
class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    // 定义回调暂存队列
    this.onFulfilledCallbackQueue = []
    this.onRejectedCallbackQueue = []
    
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        
        // 状态敲定时清空对应的队列
        this.onFulfilledCallbackQueue.forEach(fn => fn())
      }
    };
​
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        
        // 状态敲定时清空对应的队列
        this.onRejectedCallbackQueue.forEach(fn => fn())
      }
    };
    
    // 立即执行我们传入的回调函数
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  
  then(onFulfilled, onRejected){
    // 根据当前状态选择执行相应的处理函数
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
    
    // 状态为 pending 时暂存回调函数
    if (this.status === PENDING) {
      this.onFulfilledCallbackQueue.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbackQueue.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

重新测试我们的 test2,我们发现控制台在 1s 后成功打印了内容,到此我们已经实现了一个 Promise 类的基本功能。

2. 链式调用

a. 实现

我们知道,then 方法是支持链式调用的,同时值要在链式调用时往下传递,我们很容易想到一个解决办法:将 then 方法的返回值设置为一个我们的 MyPromise 实例,我们来尝试一下。

js 复制代码
class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    // 定义回调暂存队列
    this.onFulfilledCallbackQueue = []
    this.onRejectedCallbackQueue = []
    
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        
        // 状态敲定时清空对应的队列
        this.onFulfilledCallbackQueue.forEach(fn => fn())
      }
    };
​
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        
        // 状态敲定时清空对应的队列
        this.onRejectedCallbackQueue.forEach(fn => fn())
      }
    };
    
    // 立即执行我们传入的回调函数
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  
  then(onFulfilled, onRejected){
    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          const x = onFulfilled(this.value)
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      
      const handleRejected = () => {
        try {
          const x = onRejected(this.reason)
          // onRejected 是用来处理错误的,想象一下我们有这样一个流程:在 promise 敲定后,如果出现错误,在错误处理函数中修正错误使链式调用能够继续
          // 所以这里调用的是 resolve 而不是 reject
          // fetch('http://bad-url.com')
          //  .then(
          //    response => response.json(),
          //    error => {
          //      console.error('网络请求失败,使用默认数据:', error);
          //      return { status: 'offline', data: 'N/A' }; // 恢复 Promise 链
          //    }
          //  )
          //  .then(data => {
          //    console.log('处理数据:', data);
          //  });
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      // 根据当前状态选择执行相应的处理函数
      if (this.status === FULFILLED) {
        handleFulfilled()
      }
      if (this.status === REJECTED) {
        handleRejected()
      }
​
      // 状态为 pending 时暂存回调函数
      if (this.status === PENDING) {
        this.onFulfilledCallbackQueue.push(() => {
          handleFulfilled()
        })
        this.onRejectedCallbackQueue.push(() => {
          handleRejected()
        })
      }
    })
    
    return promise2
  }
}

我们来测试一下上面的代码

js 复制代码
// test3
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("结果1");
  }, 1000);
})
  .then(
    (res) => {
      console.log("success 1", res);
      return "结果2";
    },
    (err) => {
      console.log("failed", err);
      return "修复的结果2"
    }
  )
  .then(
    (res) => {
      console.log("success 2", res);
    },
    (err) => {
      console.log("failed", err);
    }
  );
​
// test4
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject("结果1");
  }, 1000);
})
  .then(
    (res) => {
      console.log("success 1", res);
      return "结果2";
    },
    (err) => {
      console.log("failed", err);
      return "修复的结果2"
    }
  )
  .then(
    (res) => {
      console.log("success 2", res);
    },
    (err) => {
      console.log("failed", err);
    }
  );

我们运行测试代码,发现输出和我们预期的一样,这样就解决了.then 的链式调用......了吗?

b. 问题

想象一个场景,第一个 Promise 中我们处理的是 用户登录请求,然后第一个 then 中我们根据前面的请求响应的 用户ID 来向服务端请求 用户详细信息 ,第二个 then 中我们根据请求到的详细信息来修改 UI 状态。

js 复制代码
// test5
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("登录信息");
  }, 1000);
})
  .then(
    (userId) => {
      // 根据登录得到的id来请求用户信息
      return new MyPromise((resolve, reject) => {
        setTimeout(() => {
            resolve("用户信息");
        }, 1000);
      })
    },
    (err) => {
      console.log("failed", err);
    }
  )
  .then(
    (userInfo) => {
      console.log("根据获得的结果修改 UI,结果:", userInfo);
    },
    (err) => {
      console.log("failed", err);
    }
  );

我们运行 test5 这段测试代码,发现最后控制台打印出来的结果如下

json 复制代码
根据获得的结果修改 UI,结果: MyPromise {
  status: 'PENDING',
  value: undefined,
  reason: undefined,
  onFulfilledCallbackQueue: [],
  onRejectedCallbackQueue: []
}

这显然和我们得到用户信息的预期相去甚远,分析一下测试代码,我们可以发现原因是第一个 then 中返回的是一个新的实例。查阅一下 MDN 对 Promise.then 方法的返回值的的描述,其中第4、5、6点提到了返回新的 Promise 实例的情况。

3. 根据规范实现链式调用

根据以上描述我们可以得知,我们在 then 中返回的实例(代码里的 promise2)是要根据不同的返回值做出不同的处理,那么这中间又会涉及到很多的情况,如果我们刚开始接触相关的知识学习,很难去理清所有的情况。但是! Promise/A+规范 为我们提供了充足的指导,它是一个由实现者制定,为实现者服务的开放的标准,用于实现互操作的JavaScript Promise。

a. then 方法

我们直接找到描述实现then的这一节,参照着规范的描述来修改我们的 then 方法

js 复制代码
...
then(onFulfilled, onRejected) {
    // 2.2.1 Both onFulfilled and onRejected are optional arguments
    // 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1
    // 2.2.7.4 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
    // 这几条规则明确了传入的回调函数是可选的,如果未传入相关参数,我们需要给这回调函数设置默认值来穿透行为
    // .then().then().then((res) => console.log(res))
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (x) => x;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (x) => {
            throw x;
          };
​
    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        // 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
        // 这条规定了传入 then 的回调函数应该被异步执行,我们这里使用 setTimeout 模拟实现
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            // 2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
            // 这条规定了我们要实现一个处理函数,将回调函数的返回值作为参数处理我们的回调函数
            // run [[Resolve]](promise2, x)
          } catch (e) {
            // 2.2.7.2 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
            reject(e);
          }
        }, 0);
      };
​
      const handleRejected = () => {
        // 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            // run [[Resolve]](promise2, x)
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
​
      if (this.status === FULFILLED) {
        handleFulfilled();
      }
​
      if (this.status === REJECTED) {
        handleRejected();
      }
​
      if (this.status === PENDING) {
        // 2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then.
        // 对应着我们的回调暂存队列,队列的性质是先进先出,在实现中我们直接通过 forEach 从前往后遍历
        this.onFulfilledStack.push(() => {
          handleFulfilled();
        });
        
        // 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
        this.onRejectedStack.push(() => {
          handleRejected();
        });
      }
    });
​
    // 2.2.7 then must return a promise
    return promise2;
  }

通过阅读规则,我们知道了需要实现一个 Promise 解决程序来处理回调函数的返回值,接下来我们就根据规范来实现这个函数。

b. Promise 解决程序

查阅规范我们得知,"Promise 解决程序"是一项抽象操作,它接受一个 Promise 和一个值 x 作为输入,表示为 [[Resolve]](promise, x)。如果 x 是一个 thenable 对象,该程序会尝试让 promise 采用 x 的状态,前提是 x 的行为至少在某种程度上类似于一个 Promise。否则,它将以值 x 来完成(fulfilled)promise。这种对 thenable 的处理方式,使得不同的 Promise 实现能够互相操作,只要它们都暴露一个符合 Promises/A+ 规范的 then 方法。这也让符合 Promises/A+ 规范的实现,能够'同化'那些行为合理但并不完全遵循规范的实现。

这里实际上就是采用了一个适配器模式,只要 x 实现了规范的 then 方法,则可以被 Promise 链吸收,比如说我们在使用多个第三方库的时候,每个库封装了不同的操作,但是都实现了 then 方法,那么我们就可以在同一个链中无痛使用他们。

我们通过函数来实现这个解决程序。

js 复制代码
const resolvePromise = (promise, x, resolve, reject) => {
  // 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
  if (promise === x) {
    return reject(new TypeError("循环引用"));
  }
​
  // 2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
  // const racyThenable = {
  //   then(resolve, reject) {
  //     // 同步调用 resolve
  //     resolve('成功')
  //     throw new Error('resolve后的异常')
  //   }
  // }
  // 不判断 called 的话先被 resolve 然后又会被 catch 捕获调用 reject
  let called;
  // 2.3.3 Otherwise, if x is an object or function,
  if (typeof x === "function" || (typeof x === "object" && x !== null)) {
    try {
      // 2.3.3.1 Let then be x.then
      // 避免 getter 产生副作用
      let then = x.then;
​
      // 2.3.3.3
      // If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where
      if (typeof then === "function") {
        then.call(
          x,
          // 2.3.3.3.1
          // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
          // 递归调用解决函数
          (y) => {
            if (called) return;
            called = true;
            // 递归调用
            resolvePromise(promise, y, resolve, reject);
          },
          // 2.3.3.3.2
          // If/when rejectPromise is called with a reason r, reject promise with r
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // 2.3.3.4 If then is not a function, fulfill promise with x.
        resolve(x);
      }
    } catch (e) {
      // 2.3.3.3.4
      // If calling then throws an exception e
      // 2.3.3.3.4.1
      // If resolvePromise or rejectPromise have been called, ignore it.
      // 2.3.3.3.4.2
      // Otherwise, reject promise with e as the reason.
​
      // 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
​
      // 这两条规范都收敛到同一个 catch 中实现了
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 2.3.4 If x is not an object or function, fulfill promise with x.
    resolve(x);
  }
};

我们测试一下上面的 test5,执行后发现控制台的打印如下:

json 复制代码
根据获得的结果修改 UI,结果: 用户信息

得到的结果符合我们的预期,如果要简化的理解 resolvePromise 的作用,我认为它起到的作用就是从thenable 对象中解包我们真正需要的返回值。

到此我们实现了 Promise 的核心功能,我们可以通过promises-aplus-tests 库来验证一下我们的 Promise 是否符合规范。

完整代码如下

js 复制代码
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
​
const resolvePromise = (promise, x, resolve, reject) => {
  // 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
  if (promise === x) {
    return reject(new TypeError("循环引用"));
  }
​
  // 2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
  // const racyThenable = {
  //   then(resolve, reject) {
  //     // 同步调用 resolve
  //     resolve('成功')
  //     throw new Error('resolve后的异常')
  //   }
  // }
  // 不判断 called 的话先被 resolve 然后又会被 catch 捕获调用 reject
  let called;
  // 2.3.3 Otherwise, if x is an object or function,
  if (typeof x === "function" || (typeof x === "object" && x !== null)) {
    try {
      // 2.3.3.1 Let then be x.then
      // 避免 getter 产生副作用
      let then = x.then;
​
      // 2.3.3.3
      // If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where
      if (typeof then === "function") {
        then.call(
          x,
          // 2.3.3.3.1
          // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
          (y) => {
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 2.3.3.3.2
          // If/when rejectPromise is called with a reason r, reject promise with r
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // 2.3.3.4 If then is not a function, fulfill promise with x.
        resolve(x);
      }
    } catch (e) {
      // 2.3.3.3.4
      // If calling then throws an exception e
      // 2.3.3.3.4.1
      // If resolvePromise or rejectPromise have been called, ignore it.
      // 2.3.3.3.4.2
      // Otherwise, reject promise with e as the reason.
​
      // 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
​
      // 这两个规范都收敛到同一个 catch 中实现了
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 2.3.4 If x is not an object or function, fulfill promise with x.
    resolve(x);
  }
};
​
class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledStack = [];
    this.onRejectedStack = [];
​
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
​
        this.onFulfilledStack.forEach((fn) => fn());
      }
    };
​
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
​
        this.onRejectedStack.forEach((fn) => fn());
      }
    };
​
    // 立即执行 executor
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
​
  then(onFulfilled, onRejected) {
    // 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (x) => x;
​
    // 2.2.7.4 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (x) => {
            throw x;
          };
​
    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        // 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
​
      const handleRejected = () => {
        // 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
​
      if (this.status === FULFILLED) {
        handleFulfilled();
      }
​
      if (this.status === REJECTED) {
        handleRejected();
      }
​
      if (this.status === PENDING) {
        this.onFulfilledStack.push(() => {
          handleFulfilled();
        });
​
        this.onRejectedStack.push(() => {
          handleRejected();
        });
      }
    });
​
    // 2.2.7 then must return a promise
    return promise2;
  }
}
​
// 测试的代码
MyPromise.deferred = function () {
  var result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });
​
  return result;
}
module.exports = MyPromise;

我们在命令行运行 npx promises-aplus-tests 文件路径,可以看到控制台的输出如下

我们成功通过了所有的用例,证明我们的实现是符合规范的。

4. 小结

通过一些测试用例和查阅规范,我们由浅入深地实现了一个 Promise 类。理解了中间的原理之后,其他的静态方法实现起来也很简单,我们可以参考 MDN 上各个静态方法的定义来实现功能,这里不做赘述。

参考文章:

面试官:"你能手写一个 Promise 吗"

从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

相关推荐
Hierifer4 小时前
跨端技术:浅聊双线程原理和实现
前端
FreeBuf_4 小时前
加密货币武器化:恶意npm包利用以太坊智能合约实现隐蔽通信
前端·npm·智能合约
java水泥工5 小时前
基于Echarts+HTML5可视化数据大屏展示-图书馆大屏看板
前端·echarts·html5
EndingCoder5 小时前
Electron 性能优化:内存管理和渲染效率
javascript·性能优化·electron·前端框架
半夏陌离5 小时前
SQL 实战指南:电商订单数据分析(订单 / 用户 / 商品表关联 + 统计需求)
java·大数据·前端
子兮曰5 小时前
🚀Vue3异步组件:90%开发者不知道的性能陷阱与2025最佳实践
前端·vue.js·vite
牛十二5 小时前
mac-intel操作系统go-stock项目(股票分析工具)安装与配置指南
开发语言·前端·javascript
whysqwhw5 小时前
Kuikly 扩展原生 API 的完整流程
前端
whysqwhw5 小时前
Hippy 跨平台框架扩展原生自定义组件
前端