什么???到现在你都还不会手写Promise!

前言

Promise 对于我们来说并不陌生,无论是在面试还是开发过程中,它都会频繁出现。虽然我们经常使用它,但如果能深入理解其底层原理,无疑能够提升我们的开发效率。因此,深入学习 Promise 是非常必要的。那么,接下来就让我们一起揭开 Promise 的神秘面纱,一起手写一个Pormise吧!

完整代码 前端-JavaScrip-Promise-手写Promise

在开始之前,我们以常见的Promise面试题来理清接下来我们要做什么:

  • Promise 解决了什么问题
  • Promise 常用的 API 有哪些
  • 实现 Promise 某个方法
  • Promise 在事件循环中的执行过程是怎么样的
  • Promise 的缺陷有哪些,可以怎么解决

这几个问题相信大家都不会感到陌生,而且在我们面试的时候,面试官大多都会像这几个问题一样循序循序渐进的来对我们进行提问,那么接下来我们就来一一解答吧!

出现原因

Promise出现以前,我们处理多个异步请求时,代码是这样的:

js 复制代码
function sayHello(){
  setTimeout(function () {
    console.log(name);
  }, 1000);
}

这是比较简单的,但是我们在有些场景中会遇到:第一次请求得到的结果是第二次请求的参数,一两次还好,但要是次数较多时就变成了这样:

js 复制代码
function request1(callback) {
    setTimeout(function() {
        var result1 = 'data from request1';
        callback(null, result1);
    }, 1000);
}

function request2(param, callback) {
    setTimeout(function() {
        var result2 = 'data from request2 with param ' + param;
        callback(null, result2);
    }, 1000);
}

function request3(param, callback) {
    setTimeout(function() {
        var result3 = 'data from request3 with param ' + param;
        callback(null, result3);
    }, 1000);
}

request1(function(error, result1) {
    if (error) {
        console.error('Error in request1:', error);
    } else {
        request2(result1, function(error, result2) {
            if (error) {
                console.error('Error in request2:', error);
            } else {
                request3(result2, function(error, result3) {
                    if (error) {
                        console.error('Error in request3:', error);
                    } else {
                        console.log('Final result:', result3);
                    }
                });
            }
        });
    }
});

是不是看着就很头大,为了解决这样的问题,ES6提出了使用 Promise 来解决类似这样的 "回调地狱"。 来看看Promise实现是什么样的:

js 复制代码
function request1() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            var result1 = 'data from request1';
            resolve(result1);
        }, 1000);
    });
}

function request2(param) {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            var result2 = 'data from request2 with param ' + param;
            resolve(result2);
        }, 1000);
    });
}

function request3(param) {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            var result3 = 'data from request3 with param ' + param;
            resolve(result3);
        }, 1000);
    });
}

request1()
    .then(result1 => request2(result1))
    .then(result2 => request3(result2))
    .then(finalResult => console.log('Final result:', finalResult))
    .catch(error => console.error('Error:', error));

刚才臃肿的嵌套瞬间变得清爽了起来,真的nb!

让我们回到之前的问题,Promise 的出现是为了解决什么样的问题?看完上面的例子想必你已经有了答案:Promise 将嵌套调用改为了链式调用,使得代码的可读性和可维护性都大大的提高了。

开始实现

PS:业界所有的 Promise 都是遵循 Promise/A+规范,有兴趣的小伙伴可以去了解一下~另外,最后我们也会使用工具来测试我们的代码!

不太熟悉Promise可以先去Promise mdn熟悉一下~

基础版

在我们通常的开发中经常会这样使用 Promise:

js 复制代码
let promise = new Promise((resolve, reject) => {
  console.log("promise resolve");
  resolve("ok");
});

console.log('promise start')

promise.then(
  (value) => {
    console.log(value);
  },
  (error) => {
    console.log(error);
  }
);

控制台打印输出:

text 复制代码
promise resolve
promise start
ok

在这个例子中,我们

  • 首先创建一个新的Promise对象并立即执行。
  • 构建Promise时传入一个 executor 函数,Promise的主要业务流程都在这个函数中。
  • 如果运行在executor函数中的业务执行成功则调用 resolve,反之则调用 reject
  • 另外,Promise状态不可逆,一旦从Pending无论是失败还是成功都无法回到pending,也就是说resolvereject同时调用时,默认会采取第一次调用的结果。
  • 最后使用then方法处理Promise的结果。

根据以上内容我们可以总结一下Promise大致有以下特点:

  1. 有三个状态:pendingfulfilledrejected
  2. 默认状态为pending
  3. 状态只能从pendingfulfilled或从pendingrejected(如上图),状态一旦确认就不能再改变;
  4. new Promise时会传入一个executor执行器,执行器立即执行;
  5. executor接受两个参数,分别是resolvereject
  6. 在成功时有一个保存成功状态的值(value),可以是undefinedthenablepromise
  7. 在失败时有一个保存失败状态的值(reason);
  8. 必须有then方法,then接受两个参数:分别是成功的回调onFulfillment、失败的回调onRejection
  9. 如果调用then时,成功则执行onFulfilled,参数为Promisevalue
  10. 如果调用then时,失败则执行onRejcted,参数为Promisereason
  11. 如果then过程中抛出了异常,那么就将异常作为参数传递给下一个then失败的回调onRejcted

接下来,我们将按照上面的特点勾勒出基础版的Promise的形状:

js 复制代码
// 三个状态-特点1
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class Promise {
  // 接受一个执行器参数
  constructor(executor) {
    this.status = PENDING;   // 默认状态-特点2
    this.value = undefined;  // 存放成功的值-特点6
    this.reason = undefined; // 存放失败的值-特点7
    
    let resolve = (value) => {
      if (this.status === PENDING) {
  	  	// 改变状态-特点3
        this.status = FULFILLED;
        this.value = value;
      }
    };
    let reject = (reason) => {
      if (this.status === PENDING) {
  	  	// 改变状态-特点3
        this.status = REJECTED;
        this.reason = reason;
      }
    };

    try {
      // 立即执行且接受两个参数-特点4、5
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  
  // then方法接受成功和失败的回调-特点8
  then(onFulFilled, onRejected) {
  	// 成功-特点9
    if (this.status === FULFILLED) {
      onFulFilled(this.value);
    }
    // 失败-特点10
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}

根据Promise的特点我们可以很轻松的勾勒出其大致的架构,但光勾勒出架构肯定是不够的,所以我们完善我们的代码,将其功能补充完整。

乍一看我们的我们代码写的有模有样的,这不得写点测试代码定位一下我们还有什么地方需要完善的吗?我们就拿上面的面试题测试一下吧

js 复制代码
let promise = new Promise((resolve, reject) => {
  console.log("promise resolve");
  resolve("ok");
});

console.log("promise start");

promise.then(
  (value) => {
    console.log(value);
  },
  (error) => {
    console.log(error);
  }
);
// 输出
// promise resolve
// promise start
// ok

emmm,大致没啥问题,但是目前来说我们实现的版本仅限于同步操作的Promsie,如果在executor中传入一个异步操作的话就会发现没有返回内容,不信我们就将之前的测试代码改为:

js 复制代码
let promise = new Promise((resolve, reject) => {
  console.log("promise resolve");
  setTimeout(() => {
    resolve("ok");
  }, 1000);
});

console.log("promise start");

promise.then(
  (value) => {
    console.log(value);
  },
  (error) => {
    console.log(error);
  }
);

// 输出
// promise resolve
// promise start

执行后我们会发现本该在 1s 后出现的 ok不见了,这是为什么呢?明明我们都写得差不多的了。

这是因为现在Promise在调用 then 方法时,当前的Promise并没有成功,一直处于Pending状态。所以如果当调用 then 方法的时,我们需要将成功或失败的回调分别使用不同的数组存放起来,在executor的异步任务被执行时触发resolvereject并依次调用成功或失败的回调。所以我们需要在executor中分别添加存放成功和失败的回调的数组,接着在then方法中判断当状态为pending的时候将回调都push到对应的数组中,最后在resolve或在reject中将数组中的回调依次执行。接下来我们将尝试先完成当成功(resolve)时有异步操作时的情况并测试一下看看:

js 复制代码
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
const PENDING = "PENDING";

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = []; // 存放成功回调的数组

    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach((fn) => fn()); // 依次执行成功数组中的回调
      }
    };
    let reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  then(onFulFilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulFilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      // 状态为 pending 时将成功的回调存放至数组中
      this.onResolvedCallbacks.push(() => {
        onFulFilled(this.value);
      });
    }
  }
}

我们还是拿上面的代码测试一下,打印结果:

text 复制代码
promise resolve
promise start
ok

完美,看来我们已经完成了成功时的异步问题了,失败回调异步和成功回调异步处理方式一样的,我这里就不多阐述啦。

这里提一嘴:其实这里是一个发布订阅模式---收集依赖--> 触发通知--> 取出依赖执行

链式调用&值的穿透

看完上面的内容后,我们知道Promise使用了链式调用的方式解决了以前回调地狱 的问题。我们在使用Promsie的时候,当 then 方法中返回任何一个值,我们都可以在下一个then方法中获取到,这也就是链式调用 。除此之外,当我们不在then方法中放入参数:Promise.then().then(),那么后面的then仍然可以获取到之前then放回的值,这就是值的穿透。了解完Promise的两大特性,我们一点一点的来捋清楚:每次调用then方法的时候,都需要重新创建一个Promise对象,同时把上一个then返回的结果传递给这个新的Promisethen方法中,这样就使得then方法一直传递下去了。

梳理一下我们将要做的事情:

  1. then方法的参数onFulfilledonRejectd可以缺省, 如果onFulfilledonRejectd不是函数则忽略,且依次在后面的then中获取到之前返回的值;
  2. Promise可以连续then多次,每一次then后都会返回一个新的promsie
  3. 如果then返回的值是一个普通值,那么就将这个值传递给下一个then或成功的回调;
  4. 如果then返回的值是一个promise,且该值同时调用resovlereject,则优先第一次调用,剩余调用则忽略;
  5. 如果then返回的值是一个promise,那么就等待这个promise执行完。如果成功则走下一个then的成功,反之则走下一个then的失败;
  6. 如果then返回的值是和promise是同一个引用对象,为避免造成循环引用 需要抛出异常,将异常传递给下一个then失败的回调中;
  7. 如果then中抛出了异常,那么就需要将其传递给下一个then的失败回调;

按照以上特点,我们尝试将其完成:

js 复制代码
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
const PENDING = "PENDING";

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach((fn) => fn());
      }
    };
    let reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  then(onFulFilled, onRejected) {
  	// 判断`then`传递的值是否缺省-特点1
    onFulFilled = typeof onFulFilled === "function" ? onFulFilled : (v) => v;
    onRejected = typeof onRejected === "function" ? onRejected : (error) => { throw error }
    
    // 每次`then`都会返回一个新的Promise-特点2
    const newPromise = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulFilled(this.value);
            this.#resolvePromise(newPromise, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            this.#resolvePromise(newPromise, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulFilled(this.value);
              this.#resolvePromise(newPromise, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              this.#resolvePromise(newPromise, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });
    return newPromise;
  }

  #resolvePromise(newPromise, x, resolve, reject) {
  	// 如果返回的新Promise和传递的值是同一个引用像会导致循环引用-特点6
    if (newPromise === x) return reject(new TypeError("..."));
    // 防止多次调用-特点4
    let called;
    // x 可能是一个 Promise-特点4
    if ((typeof x === "object" && x !== null) || typeof x === "function") {
      try {
        let then = x.then;
        // 如果`then`是一个函数说明 x 是 Promise-特点5
        if (typeof then === "function") {
          then.call(
            x,
            // 执行成功,将newResolve作为新promise的值-特点5
            (newResolve) => {
              if (called) return;
              called = true;
              // 
              this.#resolvePromise(newPromise, newResolve, resolve, reject);
            },
            // 执行失败,将newReject作为新promise的值-特点5
            (newReject) => {
              if (called) return;
              called = true;
              reject(newReject)
            }
          );
        } else {
          // x 是一个普通值-特点3
          resolve(x);
        }
      } catch (error) {
        // 对`then`中抛出的异常进行处理-特点7
        reject(error);
      }
    } else {
	  // x 是一个普通值-特点3
      resolve(x);
    }
  }
}

这里说明一下,为什么我们要在then方法中添加setTimeout:原生PromiseV8 (感兴趣的小伙伴可以前往查看哈) 引擎提供的微任务,这里使用setTimeout来模拟异步 ,确保onFulfilledonRejected函数在执行环境的栈为空时才被调用,但是这里是宏任务setTimeout会将它的回调函数放入到一个任务队列中,只有当当前的执行栈为空时,才会从队列中取出回调函数执行。PromiseA+规范也提到:这可以通过"宏任务"机制(例如 setTimeoutsetImmediate)或"微任务"机制(例如MutatonObserver)来实现process.nextTick

完成了then方法,接下来我们将其进行测试一下:

js 复制代码
const promise = new Promise((resolve, reject) => {
  // reject("失败");
  resolve("成功");
})
  .then()
  .then()
  .then(
    (data) => {
      console.log("ok", data);
    },
    (err) => {
      console.log("err", err);
    }
  );
// 输出
// ok 成功

到目前为止,我们已经完成了Promise的大部分内容,剩下de Promise API在我们完成这个后已经是小打小闹了。既然开头我们提到了业界的大部分Promise类库都遵循PromiseA+标准,那么我们接下来使用测试工具测试一下。

测试

PromiseA+的测试工具名为promises-aplus-tests,我们先在我们的代码中添加如下代码:

js 复制代码
// just for promise test
Promise.defer = Promise.deferred = function () {
  let dtd = {};
  dtd.promise = new Promise((resolve, reject) => {
    dtd.resolve = resolve;
    dtd.reject = reject;
  });
  return dtd;
};

module.exports = Promise;

然后在终端中执行下面的命令

js 复制代码
npm init -y

npm i promises-aplus-tests

npx promises-aplus-tests 你的文件名

promises-aplus-tests 中共有 872 条测试用例。以上代码,可以完美通过所有用例😏

Promise API

实现promise肯定不能忘记实现其API呀,我们知道原生Promise提供了以下方法:

接下来我们一一进行实现。

Promise.resolve

  • 默认产生一个成功的 Promise
  • 具备等待功能。如果参数是Promise则会等待其解析完成后才会向下执行,所以这里还需要在resolve中处理一下;
js 复制代码
// 处理resolve
let resolve = (value) => {
  if (value instanceof Promise) {
    return value.then(resolve, reject);
  }
  if (this.status === PENDING) {
    this.status = FULFILLED;
    this.value = value;
    this.onResolvedCallbacks.forEach((fn) => fn());
  }
};

resolve 方法:

js 复制代码
static resolve(value) {
  return new Promise((resolve) => {
    resolve(value)
  });
}

测试一下~

js 复制代码
const promise1 = Promise.resolve("Hello, World!");
promise1.then((value) => {
  console.log(value);
});

const promise2 = Promise.resolve(42);
promise2.then((value) => {
  setTimeout(() => {
    console.log(value);
  }, 3000);
});

const promise3 = Promise.resolve(promise1);
promise3.then((value) => {
  console.log(value);
});

// 输出
// Hello, World!
// Hello, World!
// 3s后 - 42

Promise.reject

  • 默认产生一个失败的 Promise
js 复制代码
static reject(reason) {
  return new Promise((resolve, reject) => {
    reject(reason);
  });
}

Promise.prototype.catch

  • 捕获Promise中出现的异常,即没有成功的then
js 复制代码
catch(errorCallback) {
  return this.then(null, errorCallback);
}

Promise.prototype.finally

  • 无论如何都会执行的内容。如果返回Promise则会等待其执行完毕,如果返回成功的Promise会采用上一次的结果,如果返回失败的Promise,会用这个失败的结果传递到catch中。
js 复制代码
finally(callback) {
  return this.then(
    (value) => {
      return Promise.resolve(callback()).then(() => value);
    },
    (reason) => {
      return Promise.resolve(callback()).then(() => {
        throw reason;
      });
    }
  );
}

测试一下:

js 复制代码
Promise.resolve(456)
  .finally(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // resolve(123);
        reject('Error')
      }, 3000);
    });
  })
  .then((data) => {
    console.log(data, "success");
  })
  .catch((err) => {
    console.log(err, "error");
  });

// 输出
// Error error

Promise.all

常用来处理并发请求。

  • 接收一个Promise数组作为参数;
  • 返回成功的Promise
  • 只要有一个失败则失败;
js 复制代码
static all(promises) {
  if (!Array.isArray(promises)) return new TypeError("...");
  return new Promise((resolve, reject) => {
    const res = [];
    let counter = 0;
    const processResultByKey = (value, index) => {
      res[index] = value;
      counter++;
      if (counter === promises.length) return resolve(res);
    };
    for (let i = 0; i < promises.length; i++) {
      if (promises[i] && typeof promises[i].then === "function") {
        promises[i].then((value) => {
          processResultByKey(value, i);
        }, reject);
      } else {
        processResultByKey(promises[i], i);
      }
    }
  });
}

测试一下:

js 复制代码
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
  console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
  setTimeout(() => {
    console.log(value);
  }, 3000);
});
Promise.all([1, 2, 3, promise1, promise2]).then(
  (data) => {
    console.log("resolve", data);
  },
  (err) => {
    console.log("reject", err);
  }
);

// 输出:
// success!
// resolve [ 1, 2, 3, 'success!', 42 ]
// 3s后-42

Promise.race

Promise.race被用来处理多个请求,但只返回最快的那个

  • 接受一个Promise数组;
  • 返回最快的那一个;
  • 有一个成功则成功;
js 复制代码
static race(promises) {
  if (!Array.isArray(promises)) return new TypeError("...");
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      if (promises[i] && typeof promises[i].then === "function") {
        promises[i].then(resolve, reject);
      } else {
        resolve(promises[i]);
      }
    }
  });
}

测试一下:

js 复制代码
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
  console.log(value);
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
  setTimeout(() => {
    console.log(value);
  }, 3000);
});
Promise.race([1, 2, 3, promise1, promise2]).then(
  (data) => {
    console.log("resolve", data);
  },
  (err) => {
    console.log("reject", err);
  }
);
// 输出
// success!
// resolve 1
// 42

Promise.allSettled

  • 接受一个Promise数组;
  • 无论成功或者失败都会返回;
  • 每一个对象都会有一个记录其状态的兑现;
js 复制代码
static allSettled(promises) {
  if (!Array.isArray(promises)) return new TypeError("...");
  return new Promise((resolve, reject) => {
    const res = [];
    let counter = 0;
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i])
        .then((value) => {
          res[i] = { status: FULFILLED, value };
        })
        .catch((reason) => {
          res[i] = { status: REJECTED, reason };
        })
        .finally(() => {
          counter++;
          if (counter === promises.length) resolve(res);
        });
    }
  });
}

测试一下:

js 复制代码
const promise1 = Promise.resolve("success!");
promise1.then((value) => {
  console.log(value);
});

const promise2 = Promise.resolve(42);
promise2.then((value) => {
  setTimeout(() => {
    console.log(value);
  }, 3000);
});
Promise.allSettled([1, 2, 3, promise1, promise2]).then(
  (data) => {
    console.log("resolve", data);
  },
  (err) => {
    console.log("reject", err);
  }
);
// 输出
// success!
// resolve [
//   { status: 'FULFILLED', value: 1 },
//   { status: 'FULFILLED', value: 2 },
//   { status: 'FULFILLED', value: 3 },
//   { status: 'FULFILLED', value: 'success!' },
//   { status: 'FULFILLED', value: 42 }
// ]
// 3s后-42

完美!

总结

在本文中,我们深入探讨了 Promise 及其相关 API 的实现。我们首先了解了 Promise 的基本概念和工作原理,然后我们实现了一个简单的 Promise及其相关的API。通过这个过程,我们可以更深入地理解 Promise 的工作机制,以及 JavaScript 的异步编程模型。总的来说,手写 Promise 及其相关 API 是一个很好的练习,它可以帮助我们更好地理解Promise。但在实际的项目中,我们通常会直接使用 JavaScript 提供的原生 Promise`,因为它已经经过了严格的测试,可以确保在各种情况下都能正常工作。

希望这篇文章能帮助你更好地理解 Promise,以及如何在你的代码中使用它。如果你有任何问题或者想要深入探讨某个话题,欢迎留言讨论。

相关推荐
忆琳4 分钟前
Vue3 全局自动大写转换:一个配置,全站生效
javascript·element
喵个咪8 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
小江的记录本12 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
喵个咪15 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
chenjingming66616 分钟前
jmeter导入浏览器上按F12抓的数据包
前端·chrome·jmeter
张元清16 分钟前
不用 Server Components 也能做 React 流式 SSR —— 实战指南
前端·javascript·面试
前端技术18 分钟前
ArkTS第三章:声明式UI开发实战
java·前端·人工智能·python·华为·鸿蒙
码小瑞22 分钟前
画布文字在不同缩放屏幕上的归一化
前端
神の愛23 分钟前
java日志功能
java·开发语言·前端