或许你真的需要手写一个Promise才能理解它

2023/8/2

我觉得PromiseJS编程中离不开的东西,因为它是来实现异步操作的最优解,而JS的核心就是异步

# 重要的点

很多人都知道的是,Promise是用来解决回调地狱的,但我们要知道Promise的更重要的作用,其实是用来存储异步数据的,我们是利用它自己独特的一种存储异步数据的方式,来帮助我们进行代码的编写~

存储异步数据的方式就是它构造函数的两个参数 resolvereject

  • resolve 存储正常返回的数据
  • reject 存储出现异常的数据

无论是resolve还是rejectpromise的数据都只能存一次,存过一次之后这个值就是该promise的值了,不能再resolvereject

处理异步任务时,难的不是取数据,也不是输出数据,而是读取数据的时间

所以Promise里有一个静态属性 —— PromiseStatus, 就存储了Promise的三种状态,状态就帮我们指示了读取数据的时间

.then方法就是读取Promise中存储的数据,相当于为Promise设置了回调函数(可以想象成给DOM节点绑定事件,如果Promiseresolvereject了,就会调用这个回调函数)

  • 如果PromiseState变为fulfilled,则调用then的第一个回调函数来返回数据
  • 如果PromiseState变为rejected,则调用then的第二个回调函数来返回数据

# 💪手写一个Promise

直接手写一个Promise,是理解Promise构造的最好的方式

::需求分析(简单想一下Promise要实现哪些功能)

  • 构造函数
  • 存数据,解决thisundefined,只能存储一次数据
  • 读数据,读取异步数据(读还没存进去的)
  • then的回调函数应该放到微任务执行
  • 解决多个then调用(反复读数据),回调函数有多个,只有最后一个能读出来,其他的读不出来,导致回调函数丢失
  • 实现链式调用(把onFulfilled的返回值赋给resolve,然后把resolve作为then返回的promise对象的值)

# 构造函数

首先我们要声明一个MyPromise的类,这个类一定是有一个构造函数,同时,因为在我们声明一个promise实例时,传入的构造函数是立即执行的,所以我们设置的构造函数要执行传入的函数

同时传入的两个函数resolvereject是用来存数据的,但这不是我们类的方法,所以我们要在内部声明这两个方法,用来存数据👇

class MyPromise {
  #result;
  constructor(executor) {
    executor(this.#resolve, this.#reject);
  }
  #resolve(value) {
    console.log("value: ", value);
  }
  #reject(value) {}
}

const mp = new MyPromise((resolve, reject) => {
  resolve("test");
});

上面代码的输出结果👇,现在能够拿到用户resolve的值了,但是现在我们还没有存储到我们的promise

1

# 存数据

无论是resolve还是reject,我们最重要的就是要把数据存储起来,但其实这实现起来很简单,定义一个私有变量,用来存储数据即可😄

这里要注意的是,resolvethisundefined,因为此时是以函数形式调用的,严格模式下this就是undefined,所以我们要通过this来访问变量,就要给resolve绑定this是我们的myPromise

class MyPromise {
  // 存储resolve或reject的数据
  #result;
  constructor(executor) {
    executor(this.#resolve.bind(this), this.#reject.bind(this));
  }
  // resolve存数据逻辑
  #resolve(value) {
    this.#result = value;
    console.log("result: ", this.#result);
  }
  // reject存数据逻辑
  #reject(value) {
    this.#result = value;
  }
}

const mp = new MyPromise((resolve, reject) => {
  resolve("test");
});

现在可以看到result存储到数据了

2

但之前说过了,promise的数据只能存一次,所以我们要设置一个状态对象,通过它来判断能不能继续存

const PROMISE_STATUS = {
  PENDING: 0,
  FULFILLED: 1,
  REJECTED: 2,
};

class MyPromise {
  // 存储myPromise的状态
  #status = PROMISE_STATUS.PENDING;
  // ...
  // ...
  // resolve存数据逻辑
  #resolve(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.FULFILLED;
  }
  // reject存数据逻辑
  #reject(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.REJECTED;
  }
}

const mp = new MyPromise((resolve, reject) => {
  resolve("test");
});

# 读数据

读数据要靠then方法,then方法有两个参数,第一个函数用来返回resolve的数据,第二个函数用来返回reject的数据,所以我们要写一个then方法用来读数据 —— 两个参数就是两个回调

class MyPromise{
// ...
  then(onFulfilled, onRejected) {
    if (this.#status === PROMISE_STATUS.FULFILLED) {
      onFulfilled(this.#result);
    }
    if (this.#status === PROMISE_STATUS.REJECTED) {
      onRejected(this.#result);
    }
  }
}

  
const mp = new MyPromise((resolve, reject) => {
  resolve("test");
});

mp.then(
  (result) => {
    console.log("读到了数据: ", result);
  },
  (reason) => {
    console.log("发生了错误: ", reason);
  }
);

3

调用then方法,可以看到我们可以读到result的数据了☝️

但现在还存在一个问题,无法读取异步的数据,也就是我们如果存数据的时候是异步存的,那我们读数据的时候是读不到的,因为数据还没有存进去

所以我们应该在别的地方去执行then传进来的回调 —— resolvereject里执行,因为 resolve了就证明数据存进来了,就可以执行then的回调了

class MyPromise {
  // ...
  // 存储then传进来的回调
  // resolve回调
  #resolvedCallbacks = [];

  // reject回调
  #rejectedCallbacks = [];
  constructor(executor) {
    executor(this.#resolve.bind(this), this.#reject.bind(this));
  }
  // resolve存数据逻辑
  #resolve(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.FULFILLED;
    this.#resolvedCallbacks &&
      this.#resolvedCallbacks.forEach((cb) => {
        cb(this.#result);
      });
  }
  // reject存数据逻辑
  #reject(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.REJECTED;
    this.#rejectedCallbacks &&
      this.#rejectedCallbacks.forEach((cb) => {
        cb(this.#result);
      });
  }

  then(onFulfilled, onRejected) {
    if (this.#status === PROMISE_STATUS.PENDING) {
      onFulfilled && this.#resolvedCallbacks.push(onFulfilled);
      onRejected && this.#rejectedCallbacks.push(onRejected);
    }
    if (this.#status === PROMISE_STATUS.FULFILLED) {
      onFulfilled(this.#result);
    }
    if (this.#status === PROMISE_STATUS.REJECTED) {
      onRejected(this.#result);
    }
  }
}

const mp = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("test");
  }, 1000);
});

mp.then(
  (result) => {
    console.log("读到了数据: ", result);
  },
  (reason) => {
    console.log("发生了错误: ", reason);
  }
);

这样测试一下,发现一秒后控制台输出了 读到了数据: test 的结果~

# 微任务

对js执行机制有了解的,应该知道,promise的回调是在微任务队列中执行的,所以我们在实现时也要放到微任务队列中,通过queueMicrotask方法来实现

  // ...
  // resolve存数据逻辑
  #resolve(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.FULFILLED;
    queueMicrotask(() => {
      this.#resolvedCallbacks &&
        this.#resolvedCallbacks.forEach((cb) => {
          cb(this.#result);
        });
    });
  }
  // ...
  if (this.#status === PROMISE_STATUS.FULFILLED) {
      queueMicrotask(() => {
        onFulfilled(this.#result);
      });
  }

# 链式调用

promise最大的特点之一就是**.then的返回值也是一个promise对象**,该对象中的数据就是上一个promise回调函数的返回值 —— onFulfilled的返回值设置成下一个promise的数据,且是resolve的数据,不是reject的数据 —— 下一个promiseresolve才能访问,reject访问不到

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.#status === PROMISE_STATUS.PENDING) {
        onFulfilled &&
          this.#resolvedCallbacks.push(() => {
            resolve(onFulfilled(this.#result));
          });
        onRejected &&
          this.#rejectedCallbacks.push(() => {
            reject(onFulfilled(this.#result));
          });
      }
      if (this.#status === PROMISE_STATUS.FULFILLED) {
        queueMicrotask(() => {
          resolve(onFulfilled(this.#result));
        });
      }
      if (this.#status === PROMISE_STATUS.REJECTED) {
        queueMicrotask(() => {
          reject(onRejected(this.#result));
        });
      }
    });
  }
const mp = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("test");
  }, 1000);
});

mp.then((result) => {
  console.log("1读到了数据: ", result);
  return "2";
})
  .then((result) => {
    console.log("2读到了数据: ", result);
    return "3";
  })
  .then((result) => {
    console.log("3读到了数据: ", result);
  });
console.log("test");

5

输出的结果☝️,可以看到实现了链式调用和微任务的配置~

# ⭐️最终版

这样我们就得到了一个手撕版本的Promise啦~

const PROMISE_STATUS = {
  PENDING: 0,
  FULFILLED: 1,
  REJECTED: 2,
};

class MyPromise {
  // 存储resolve或reject的数据
  #result;

  // 存储myPromise的状态
  #status = PROMISE_STATUS.PENDING;

  // 存储then传进来的回调
  // resolve回调
  #resolvedCallbacks = [];

  // reject回调
  #rejectedCallbacks = [];
  constructor(executor) {
    executor(this.#resolve.bind(this), this.#reject.bind(this));
  }
  // resolve存数据逻辑
  #resolve(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.FULFILLED;
    queueMicrotask(() => {
      this.#resolvedCallbacks &&
        this.#resolvedCallbacks.forEach((cb) => {
          cb();
        });
    });
  }
  // reject存数据逻辑
  #reject(value) {
    if (this.#status !== PROMISE_STATUS.PENDING) {
      return;
    }
    this.#result = value;
    this.#status = PROMISE_STATUS.REJECTED;
    queueMicrotask(() => {
      this.#rejectedCallbacks &&
        this.#rejectedCallbacks.forEach((cb) => {
          cb();
        });
    });
  }

  // 读数据
  // 返回一个新的MyPromise
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.#status === PROMISE_STATUS.PENDING) {
        onFulfilled &&
          this.#resolvedCallbacks.push(() => {
            resolve(onFulfilled(this.#result));
          });
        onRejected &&
          this.#rejectedCallbacks.push(() => {
            resolve(onRejected(this.#result));
          });
      }
      if (this.#status === PROMISE_STATUS.FULFILLED) {
        queueMicrotask(() => {
          resolve(onFulfilled(this.#result));
        });
      }
      if (this.#status === PROMISE_STATUS.REJECTED) {
        queueMicrotask(() => {
          resolve(onRejected(this.#result));
        });
      }
    });
  }
}

const mp = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("test");
  }, 1000);
});

mp.then((result) => {
  console.log("1读到了数据: ", result);
  return "2";
})
  .then((result) => {
    console.log("2读到了数据: ", result);
    return "3";
  })
  .then((result) => {
    console.log("3读到了数据: ", result);
  });

console.log("test");
How to love
Lil Wayne