Promise原理探索与实现


Promise 原理探索与实现

Promise 简单介绍

其实大家都知道,在传统的异步编程中,如果异步之间存在依赖关系,比如需要一个异步操作执行成功之后再进行下一个一部操作,那就需要在第一个异步操作成功的回调函数中进行第二步异步操作,如果这样的依赖关系比较多的话,那就会造成很多的嵌套层数,从而使得代码的可读性和可维护性都变得很差,产生了所谓“回调地狱”,而 Promise 则是将回调嵌套改为链式调用,增加代码的可读性和可维护性。

Promise 原理解析

首先我们来看一下一个最简单但的 Promise 调用:

console.log("before promise");
const p1 = new Promise((resolve, reject) => {
  console.log("promise start");
  setTimeout(() => {
    resolve("resolve test");
  }, 0);
  console.log("after setTimeout");
});

p1.then(
  (res) => {
    console.log("success");
    console.log(res);
  },
  (err) => {
    console.log("failed");
    console.log(err);
  }
);
console.log("after promise");
// before promise
// promise start
// after setTimeout
// after promise
// success
// resolve test

由运行结果可以看出,Promise 中的操作是立即执行的,一运行到 Promise 里面的内容就开始执行,所以打印了”promise start”,而其内部的异步操作被放入微/宏任务任务队列,等待执行,接着继续执行打印了”after setTimeout”,之后主线程继续执行,打印 after promise 。到此主线程执行完毕,开始检查微任务队列,then()函数被执行,收集 Promise 成功/失败的结果,并触发 resolve/reject,然后执行相应的回调函数,所以先打印”success”再打印 Promise 运行结果”resolve test”。

我们归纳一下,上述 Promise 的调用流程:

  • Promise 的构造方法接受一个 executor(),在 new Promise()的时候就立刻执行这个 executor 回调;
  • executor()内部的同步任务就立即执行了,而异步任务则是被放入微/宏任务事件队列,等待执行;
  • 执行 then(),收集成功/失败的回调,放入成功/失败队列;
  • 执行 executor()内部的异步任务,触发 resolve/reject,从成功/失败队列中取出回调依次执行;

仔细品一品上面的调用流程,我们会发现上述流程其实是一个观察者模式,在 Promise 中,代码执行顺序是: then 收集依赖-> 异步触发 resolve/reject-> 执行相应的回调。因此,我们来试着还原一下 Promise:

class MyPromise {
  // 构造函数接受一个executor回调函数
  constructor(executor) {
    this._resolveQueue = []; // then收集的执行成功的回调队列
    this._rejectQueue = []; // then收集的执行失败的回调队列

    // 为什么使用箭头函数?因为resoleve/reject是在executor内部执行的,需要保证this指向不变,如果不使用
    // 箭头函数,那就需要用一个额外的变量来记录this
    let _resolve = (val) => {
      // 从成功的回调队列中取出回调依次执行
      while (this._resolveQueue.length) {
        // 队列,队尾进入,队首出队
        const callback = this._resolveQueue.shift();
        callback(val);
      }
    };
    // 不使用箭头函数的版本
    // const _this = this;
    // let _resolve = function (val) {
    //   // 从成功的回调队列中取出回调依次执行
    //   //   console.log(this);
    //   while (_this._resolveQueue.length) {
    //     // 队列,队尾进入,队首出队
    //     const callback = _this._resolveQueue.shift();
    //     callback(val);
    //   }
    // };

    // reject逻辑与resolve逻辑相同
    let _reject = (err) => {
      while (this._rejectQueue.length) {
        const callback = this._rejectQueue.shift();
        callback(err);
      }
    };
    // new Promise()时立刻执行executor,并传入resolve或者是reject
    executor(_resolve, _reject);
  }
  // then方法,接受一个成功的回调和一个失败的回调,并push进相应的队列
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn);
    this._rejectQueue.push(rejectFn);
  }
}

测试一下:

//test MyPromise
const mypromise = new MyPromise((resolve, reject) => {
  console.log("before setTimeout");
  setTimeout(() => {
    resolve("test mypromise");
    // reject("promise failed");
  });
});
mypromise.then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);
// before setTimeout
// test mypromise

由结果可以看出,我的实现了 then 和 resolve/reject,我们能在回调函数中取得 promise 异步操作的返回值。这算是成功了第一步,接下来我们要一步步的完善 Promise。

Promise A+规范

如果只实现上述的 Promise 当然会是不行的,在 ES6 中对于 Pormise 定制了详细的规范,即Promises/A+规范。里面对实现 Promise 的一些术语、状态、解决流程都进行了详细的介绍,总结起来也就是下面几点:

  • Promise 本质是一个状态机,且状态机的状态只有三种:Pending(等待态)、Fulfillied(执行态)、Rejected(拒绝态),状态的变更是单向的,只能从 Pending(等待态)->Fulfillied(执行态)或者是 Pending(等待态)->Rejected(拒绝态),且状态一旦改变就再也不能更改了;
  • then 方法接收两个可选参数,分别对应状态改变时触发的回调。then 方法返回一个 promise。而且 then 方法可以被同一个 peomise 多次调用(链式调用);

状态转化.png

Promise 的状态转换

接下来就是根据 Promises/A+规范规范来对我们的 Promise 的状态进行进一步的补充:

then 的链式调用

then 的链式调用应该算是 promise 的重难点了,也是 promise 设计比较巧妙的地方,我们先来看一下,promise 是怎么进行链式调用的:

const testP1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("testP1");
  });
});

testP1
  .then(
    (res) => {
      console.log(res);
      // 可以返回一个promise实例
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("new promise");
        });
      });
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
      // 可以直接返回一个值
      return "new value";
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
    },
    (err) => {
      console.log(err);
    }
  );
// testP1
// new promise
// new value

值得注意的是,当 promise 进行 then 函数链式调用时,第一次 then 函数中的回调会在当前事件循环内执行,而第一个 then 后面的 then 函数中得回调是否执行要看第一个 then 中是否有异步操作,如果有,则第二个 then 中的回调函数不一定在当前事件循环中执行,代码如下:

const testP1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("testP1");
  });
});
const testP2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("testP2");
  });
});

console.log("process start");
setTimeout(() => {
  console.log("setTimeout");
});
testP1
  .then(
    (res) => {
      console.log(res);
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("new promise");
        });
      });
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
      return "new value";
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
    },
    (err) => {
      console.log(err);
    }
  );
testP2.then(
  (res) => {
    console.log(res);
  },
  () => {}
);
// process start
// testP1
// testP2
// setTimeout
// new promise
// new value

由上述例子可以看出,.then()回调是按顺序执行的,哪怕返回是 promise 或者是返回的是一个值,但他的执行顺序仍然是.then()调用的顺序。所以,实现链式调用有两个需要注意的地方:

  1. then()需要返回一个 Promise 这样才能找到下一个.then()方法,因此需要把 then()方法的返回值包装成 Promise;
  2. 考虑到 then()链式调用实行顺序的问题,我们需要对 then 的返回值进行讨论,确保 then 收集回调的顺序;
// 定义三种状态
const PENDING = "pending"; // 等待态
const FULIFILLED = "fulfilled"; // 执行态
const REJECTED = "rejected"; // 拒绝态

class MyPromise {
  constructor(executor) {
    // 增加一个属性记录promise状态
    this.status = PENDING;
    // 成功回调队列
    this._resolveQueue = [];
    // 失败执行回调
    this._rejectQueue = [];

    let _resolve = (val) => {
      // 确保promised的状态只会从pending->fulfilled
      if (this.status !== PENDING) return;
      this.status = FULIFILLED;
      // 使用队列数据结构可以存多个回调函数,实现then的多次调用,如果只是用一个变量存的话
      //  那就算多次调用then也只会执行一次回调函数
      while (this._resolveQueue.length) {
        const callback = this._resolveQueue.shift();
        callback(val);
      }
    };

    let _reject = (err) => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      while (this._rejectQueue.length) {
        const callback = this._rejectQueue.shift();
        callback(err);
      }
    };
    executor(_resolve, _reject);
  }
  then(resolveFn, rejectFn) {
    // return 一个新的promise
    return new Promise((resolve, reject) => {
      // 对resolveFn进行包装,
      const fulfilledFn = (value) => {
        // 注意:这里需要catch错误
        try {
          // 对返回值进行讨论,如果返回的是promise,那么需要等当前promise转态变更,否则直接resolve
          let tempResult = resolveFn(value);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(tempResult);
        } catch (error) {
          reject(error);
        }
      };
      // 将后续then收集到的依赖都放入队列中保证了回调函数的顺序调用
      this._resolveQueue.push(fulfilledFn);
      const rejectedFn = (error) => {
        try {
          // 注意这里是rejectFn而不是rejectedFn
          let tempResult = rejectFn(error);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(error);
        } catch (error) {
          reject(error);
        }
      };
      this._rejectQueue.push(rejectedFn);
    });
  }
}

测试链式调用:

const testP1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("testP1");
  });
});

testP1
  .then(
    (res) => {
      console.log(res);
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("new promise");
        });
      });
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
      return "new value";
    },
    (err) => {
      console.log(err);
    }
  )
  .then(
    (res) => {
      console.log(res);
    },
    (err) => {
      console.log(err);
    }
  );
// testP1
// new promise
// new value

由上述结果可以看出,我们实现的 MyPromise 的链式调用结果和 Promise 的调用结果一致。但是对于 then 方法我们还有两个细节需要处理:

  1. 值穿透:根据规范当 then()方法接收的参数不是 function 的时候,那这个参数应该被忽略。如果没有忽略,那将会抛出异常,导致链式调用中断;
  2. 状态为 resolve/reject 的情况:在实际情况中存在 resolve/reject 在 then()之前就被执行的情况,这种情况就不用再将回调函数放进回调队列,因为对于 fulfilled/rejected 状态的 promise 不会再执行回调,所以这个时候直接执行 then 回调函数即可;

如果对上述两种情况不太清楚的同学可以看下面的例子来加深理解:

const promiseP1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("promise P1");
  });
});
const promiseP2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("promise P2");
  });
});
// then传入参数情况
// 传入字符串或者是promise都不能打印出结果
promiseP1.then("test string", (err) => {
  console.log(err);
});
promiseP1.then(promiseP2, (err) => {
  console.log(err);
});
// 只有传function才可以
promiseP1.then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);

// resolve/reject在then之前调用的情况
Promise.resolve(1).then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);
// 1
// promise P1

继续完善我们的 MyPromise 代码,在 then 中收集回调函数之前对传入的参数进行判断处理,然后对回调函数进行包装,返回一个 promise 实例,并对当前 promise 的状态进行判断,如果是 reject/resolve,则直接执行 then 回调函数。另外,添加了静态的 resolve 和 reject 方法,使得用户可以直接调用 Promise.resolve(),如果用户传入的是一个 promise 则直接返回即可,如果不是则将其包装秤 promise 然后返回:

// 定义三种状态
const PENDING = "pending"; // 等待态
const FULIFILLED = "fulfilled"; // 执行态
const REJECTED = "rejected"; // 拒绝态

class MyPromise {
  constructor(executor) {
    // 增加一个属性记录promise状态
    this.status = PENDING;
    // 成功回调队列
    this._resolveQueue = [];
    // 失败执行回调
    this._rejectQueue = [];
    // 记录then()回调return的值
    this._value = undefined;

    let _resolve = (val) => {
      // 确保promised的状态只会从pending->fulfilled
      if (this.status !== PENDING) return;
      this.status = FULIFILLED;
      this._value = val;
      // 使用队列数据结构可以存多个回调函数,实现then的多次调用,如果只是用一个变量存的话
      //  那就算多次调用then也只会执行一次回调函数
      while (this._resolveQueue.length) {
        const callback = this._resolveQueue.shift();
        callback(val);
      }
    };

    let _reject = (err) => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this._value = err;
      while (this._rejectQueue.length) {
        const callback = this._rejectQueue.shift();
        callback(err);
      }
    };
    executor(_resolve, _reject);
  }
  then(resolveFn, rejectFn) {
    // 判断then接收的回调函数是否是function,如果不是那就忽略,让链式调用继续执行
    typeof resolveFn !== "function"
      ? (resolveFn = (value) => {
          value;
        })
      : null;
    // 如果rejectFn不是函数,则抛出错误
    typeof rejectFn !== "function"
      ? (reson) => {
          throw new Error(reson instanceof Error ? reson.message : reson);
        }
      : null;

    // return 一个新的promise
    return new Promise((resolve, reject) => {
      // 对resolveFn进行包装,
      const fulfilledFn = (value) => {
        // 注意:这里需要catch错误
        try {
          // 对返回值进行讨论,如果返回的是promise,那么需要等当前promise转态变更,否则直接resolve
          let tempResult = resolveFn(value);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(tempResult);
        } catch (error) {
          reject(error);
        }
      };
      // 将后续then收集到的依赖都放入队列中保证了回调函数的顺序调用

      const rejectedFn = (error) => {
        try {
          // 注意这里是rejectFn而不是rejectedFn
          let tempResult = rejectFn(error);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(error);
        } catch (error) {
          reject(error);
        }
      };

      // 检查当前状态,如果是resolve/reject则直接执行回调
      switch (this.status) {
        // 如果是PENDING状态则将回调放入回调队列中
        case PENDING:
          this._resolveQueue.push(fulfilledFn);
          this._rejectQueue.push(rejectedFn);
          break;
        // 如果已经是resolve则执行回调
        // this._value是上一个then回调return的值
        case FULIFILLED:
          fulfilledFn(this._value);
          break;
        case REJECTED:
          rejectedFn(this._value);
          break;
      }
    });
  }

  // 添加静态resolve方法
  static resolve(val) {
    // 如果是Promise则直接return
    if (val instanceof MyPromise) return val;
    return new MyPromise((resolve) => resolve(val));
  }

  // 静态reject方法
  static reject(reson) {
    return new MyPromise((resolve, reject) => reject(reson));
  }
}
//

测试一下我们的代码:

MyPromise.resolve(1).then((res) => {
  console.log(res);
});
// 等价于
new MyPromise((resolve) => {
  resolve(1);
}).then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);

考虑同步任务的情况

我们之前实现的 Promise 只考虑了异步任务的情况,但是实际上 Promise 中的 executor 也可以是一个同步任务,且当 executor 是一个同步任务的时候,我们应该给 resolve/reject 的执行包一个 setTimeout,让其异步执行(实际上 Promise 实现时将 resolve/reject 放进了微任务队列,这里我们使用 setTimeout 是放进了宏任务队列),部分代码如下:

constructor(executor) {
    // 增加一个属性记录promise状态
    this.status = PENDING;
    // 成功回调队列
    this._resolveQueue = [];
    // 失败执行回调
    this._rejectQueue = [];
    // 记录then()回调return的值
    this._value = undefined;

    let _resolve = (val) => {
      const run = () => {
        // 确保promised的状态只会从pending->fulfilled
        if (this.status !== PENDING) return;
        this.status = FULIFILLED;
        this._value = val;
        // 使用队列数据结构可以存多个回调函数,实现then的多次调用,如果只是用一个变量存的话
        //  那就算多次调用then也只会执行一次回调函数
        while (this._resolveQueue.length) {
          const callback = this._resolveQueue.shift();
          callback(val);
        }
      };
      setTimeout(run);
    };

    let _reject = (err) => {
      const run = () => {
        if (this.status !== PENDING) return;
        this.status = REJECTED;
        this._value = err;
        while (this._rejectQueue.length) {
          const callback = this._rejectQueue.shift();
          callback(err);
        }
      };
      setTimeout(run);
    };
    executor(_resolve, _reject);
  }

现在我们来测试一下之前所有的功能:

const p1 = new MyPromise((resolve, reject) => {
  resolve(1); //同步测试
});

p1.then((res) => {
  console.log(res);
  return 2; //链式调用测试
})
  .then() //值穿透测试
  .then((res) => {
    console.log(res);
    return new MyPromise((resolve, reject) => {
      resolve(3); //返回Promise测试
    });
  })
  .then((res) => {
    console.log(res);
    throw new Error("reject测试"); //reject测试
  })
  .then(
    () => {},
    (err) => {
      console.log(err);
    }
  );
// 1
// 2
// 3
// Error: reject测试

到这里我们的 Promise 就实现的差不多了,接下来我们一鼓作气将剩下的几个方法都加上:

Promise.protype.catch()

catch 方法返回的也是以一个 Promise,他的行为等价于 Promise.prototype.then(undefined, onRejected)。

catch(rejectFn) {
  return this.then(undefined, rejectFn);
}
Promise.protype.all()

Promise.all 传入的是一个数组,需要在数组中所有的 promise 都完成后才执行回调,小编之前也实现过一次:实现 promiseAll

// 静态all方法
static all(promisesArr) {
  let index = 0,
    result = [];
  return new MyPromise((resolve, reject) => {
    promisesArr.map((promiseItem, i) => {
      // MyPromise.resolve用于防止promiseItem不为promise的情况
      MyPromise.resolve(promiseItem).then(
        (res) => {
          index++;
          result[i](res);
          if (index === promisesArr.length) {
            resolve(result);
          }
        },
        (err) => {
          reject(err);
        }
      );
    });
  });
}
Promise.protype.race()

Promise.race 传的也是一个 promise 数组,它和 Promise.all 的区别在于他不用等所有的 promise 都执行完,它只要有一个 promise 执行完之后就可以执行回调函数了

// 静态race方法
static race(promisesArr) {
  return new MyPromise((resolve, reject) => {
    for (let promiseItem of promisesArr) {
      MyPromise.resolve(promiseItem).then(
        (value) => {
          resolve(value);
        },
        (err) => {
          reject(err);
        }
      );
    }
  });
}
MyPromise 完整代码
// 定义三种状态
const PENDING = "pending"; // 等待态
const FULIFILLED = "fulfilled"; // 执行态
const REJECTED = "rejected"; // 拒绝态

class MyPromise {
  constructor(executor) {
    // 增加一个属性记录promise状态
    this.status = PENDING;
    // 成功回调队列
    this._resolveQueue = [];
    // 失败执行回调
    this._rejectQueue = [];
    // 记录then()回调return的值
    this._value = undefined;

    let _resolve = (val) => {
      const run = () => {
        // 确保promised的状态只会从pending->fulfilled
        if (this.status !== PENDING) return;
        this.status = FULIFILLED;
        this._value = val;
        // 使用队列数据结构可以存多个回调函数,实现then的多次调用,如果只是用一个变量存的话
        //  那就算多次调用then也只会执行一次回调函数
        while (this._resolveQueue.length) {
          const callback = this._resolveQueue.shift();
          callback(val);
        }
      };
      setTimeout(run);
    };

    let _reject = (err) => {
      const run = () => {
        if (this.status !== PENDING) return;
        this.status = REJECTED;
        this._value = err;
        while (this._rejectQueue.length) {
          const callback = this._rejectQueue.shift();
          callback(err);
        }
      };
      setTimeout(run);
    };
    executor(_resolve, _reject);
  }
  then(resolveFn, rejectFn) {
    // 判断then接收的回调函数是否是function,如果不是那就忽略,让链式调用继续执行
    typeof resolveFn !== "function"
      ? (resolveFn = (value) => {
          value;
        })
      : null;
    // 如果rejectFn不是函数,则抛出错误
    typeof rejectFn !== "function"
      ? (reson) => {
          throw new Error(reson instanceof Error ? reson.message : reson);
        }
      : null;

    // return 一个新的promise
    return new Promise((resolve, reject) => {
      // 对resolveFn进行包装,
      const fulfilledFn = (value) => {
        // 注意:这里需要catch错误
        try {
          // 对返回值进行讨论,如果返回的是promise,那么需要等当前promise转态变更,否则直接resolve
          let tempResult = resolveFn(value);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(tempResult);
        } catch (error) {
          reject(error);
        }
      };
      // 将后续then收集到的依赖都放入队列中保证了回调函数的顺序调用

      const rejectedFn = (error) => {
        try {
          // 注意这里是rejectFn而不是rejectedFn
          let tempResult = rejectFn(error);
          tempResult instanceof MyPromise
            ? tempResult.then(resolve, reject)
            : resolve(error);
        } catch (error) {
          reject(error);
        }
      };

      // 检查当前状态,如果是resolve/reject则直接执行回调
      switch (this.status) {
        // 如果是PENDING状态则将回调放入回调队列中
        case PENDING:
          this._resolveQueue.push(fulfilledFn);
          this._rejectQueue.push(rejectedFn);
          break;
        // 如果已经是resolve则执行回调
        // this._value是上一个then回调return的值
        case FULIFILLED:
          fulfilledFn(this._value);
          break;
        case REJECTED:
          rejectedFn(this._value);
          break;
      }
    });
  }

  // 添加静态resolve方法
  static resolve(val) {
    // 如果是Promise则直接return
    if (val instanceof MyPromise) return val;
    return new MyPromise((resolve) => resolve(val));
  }

  // 静态reject方法
  static reject(reson) {
    return new MyPromise((resolve, reject) => reject(reson));
  }

  catch(rejectFn) {
    return this.then(undefined, rejectFn);
  }
  finally(callback) {
    return this.then(
      //MyPromise.resolve执行回调函数并将返回的值传递给then,也就是传递给下一个Promise
      (value) => MyPromise.resolve(callback()).then(() => value),
      (reason) =>
        MyPromise.resolve(callback()).then(() => {
          throw reason;
        })
    );
  }
  // 静态all方法
  static all(promisesArr) {
    let index = 0,
      result = [];
    return new MyPromise((resolve, reject) => {
      promisesArr.map((promiseItem, i) => {
        // MyPromise.resolve用于防止promiseItem不为promise的情况
        MyPromise.resolve(promiseItem).then(
          (res) => {
            index++;
            result[i](res);
            if (index === promisesArr.length) {
              resolve(result);
            }
          },
          (err) => {
            reject(err);
          }
        );
      });
    });
  }

  // 静态race方法
  static race(promisesArr) {
    return new MyPromise((resolve, reject) => {
      for (let promiseItem of promisesArr) {
        MyPromise.resolve(promiseItem).then(
          (value) => {
            resolve(value);
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
}

总结

这是笔者第一次实现一个 Promise,主要还是跟着别人的帖子进行仿写,其中学到了不少的知识点,也对 Promise 有了一个更深的理解,不过对于 finally 方法的实现,暂时还有一点疑虑,还需要再琢磨琢磨。路漫漫其修远兮,吾将上下而求索!

参考


文章作者: CassielLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CassielLee !
评论
  目录