[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能相关的知识,希望对你有一定的参考价值。

在上一篇 [万字详解]JavaScript 中的异步模式及 Promise 使用 笔记中讲了 Promise 的使用方式,这里就通过手写 Promise 更加深刻的了解一下 Promise 的使用。

毕竟一来日常生活中对 Promise 使用的需求还是很大的,二来手写 Promise 也是一个常见的前端考题。

完整实现的源码在这里:03.handwritePromise.js

源码中包含所有的测试案例,以及 Promise.race() 和 Promise.reject() 的实现。

基础核心功能实现

这里主要分为基础功能与 then 两个部分

Promise 基础功能的分析与实现

基础功能除了 constructor 之外,全都不是绑定在原型链上的函数,因此会使用箭头函数去进行实现。

这里的基础功能指的是 Promise.reject() 和 Promise.resolve() 这两个函数。

Promise 基础功能的分析

已知 Promise 的基础使用是这样的:

const promise = new Promise(function (resolve, reject) {
  if (successful) {
    resolve(value);
  } else {
    reject(error);
  }
});

根据使用得知,Promise 有着以下几个特点:

  • Promise 是一个对象

  • 新建 Promise 的时候传进去一个回调函数

  • 回调函数需要接受两个回调函数 resolve 和 reject 作为参数

    resolve 在调用成功时使用

    reject 在调用失败后使用

  • resolve 和 reject 会被用来去修改 Promise 的状态——Promise 的初始状态为 pending,表示待定

    resolve 会将 pending 状态更改为 fulfilled

    reject 会将 pending 状态更改为 rejected

    并且,一旦状态确定后,就无法继续被更改

Promise 基础功能的实现

这里会以类的方式实现 Promise,其主要实现的目的有以下几个:

  • 完成 PromiseR 的基础结构实现,如构造函数、状态

    这里命名为 PromiseR 主要还是会有命名冲突的关系,我喜欢一边写一边测试,盲写自己都不敢百分百保证是对的……

  • 完成 resolve 和 reject 的实现

    这里主要保证了状态只能从 pending 到 fulfilled 或 rejected,而状态为 fulfilled 与 rejected 后不能改变

// 常量,用来定义 Promise 中的状态
// 另一方面,使用变量也会有提示,实现起来更加的方便
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseR {
  // 定义 Promise 中的状态,默认为pending
  status = PENDING;

  // executor/执行器 是传进 Promise中的回调函数
  constructor(executor) {
    // 这个回调函数会被立即执行,判断是否应该对状态进行更改
    executor(this.resolve, this.reject);
  }

  // 实现 resolve & reject,用来更改 Promise 的状态
  // 使用箭头函数可以减少 this 指向造成的问题,将其绑定在 Promise 的实例对象
  resolve = () => {
    // 只有当状态是 pending 的时候才能修改状态
    if (this.status !== PENDING) return;
    this.status = FULFILLED;
  };
  reject = () => {
    // 只有当状态是 pending 的时候才能修改状态
    if (this.status !== PENDING) return;
    this.status = REJECTED;
  };
}

// 调用resolve和reject去验证状态在确认之后不可变
const promise = new PromiseR((resolve, reject) => {
  resolve('resolved');
  reject('fail');
});

console.log(promise.status); // fulfilled

PromiseR 分析和实现 thenable 功能

Promise 最有意义的功能就是在 Promise 之后,去调用的 then 方法。毕竟没有 then 方法,那么就没有办法使用 Promise 返回的数据。

分析 then 函数

这里看一下 then 方法是怎么使用的:

const existedFile = readFile('./test.txt');
existedFile.then(
  (data) => {
    console.log('content: ', Buffer.from(data).toString());
  },
  (error) => {
    console.log(error);
  }
);

由此可见,then 函数有以下的几个特性:

  • then 函数接收两个参数

    第一个在异步函数操作成功时调用

    第二个在异步函数操作失败时调用

  • then 函数必须要有能够分析 Promise 状态的能力,再根据状态去调用 成功回调函数 或是 失败回调函数

  • then 方法是被定义在原型对象上的:Promise.prototype.then()

  • then 的成功函数会接受一个成功后的值作为参数,失败后也会接受一个失败的原因作为参数

实现 then 函数

这里需要实现的功能有以下几个部分组成:

  • 在原型上实现 then 方法
  • 修改 resolve 函数,保存成功的值
  • 修改 reject 函数,保存失败的原因
class PromiseR {
  // 实例属性们,这些属性都与实例后的对象所绑定
  // 成功和失败的值默认都是未定义
  value = undefined;
  reason = undefined;

  // 实现 resolve & reject,用来更改 Promise 的状态
  // 使用箭头函数可以减少 this 指向造成的问题,将其绑定在 Promise 的实例对象
  // 修改参数,让 resolve 接收成功后传来的值
  resolve = (value) => {
    // 只有当状态是 pending 的时候才能修改状态
    if (this.status !== PENDING) return;
    this.status = FULFILLED;
    // 保存成功的值
    this.value = value;
  };
  // 修改参数,让 reject 接收成功后传来的原因
  reject = (reason) => {
    // 只有当状态是 pending 的时候才能修改状态
    if (this.status !== PENDING) return;
    this.status = REJECTED;
    // 保存失败的值
    this.reason = reason;
  };

  // 接收两个回调函数,前者在状态为 fulfilled 时使用,后者在状态为 rejected 时使用
  then(successCB, failCB) {
    // 判断状态,根据状态去调用合适的回调函数,并且传入对应的值
    // value 和 reason 在 resolve 和 reject 部分已经保存到实例属性上了
    if (this.status === FULFILLED) {
      successCB(this.value);
    } else if (this.status === REJECTED) {
      failCB(this.reason);
    }
  }
}

添加测试功能

// 调用resolve和reject去验证状态在确认之后不可变
const successPromise = new PromiseR((resolve, reject) => {
  resolve('resolved');
  reject('fail');
});

console.log('ln57', successPromise.status); // ln57 fulfilled

successPromise.then(
  (value) => {
    console.log('ln61', value); // ln61 resolved
  },
  (reason) => {
    console.log(reason); // 没有输出
  }
);

const failPromise = new PromiseR((resolve, reject) => {
  reject('fail');
});

console.log('ln72', failPromise.status); // ln72 rejected

failPromise.then(
  (value) => {
    console.log('ln76', value); // 没有输出
  },
  (reason) => {
    console.log('ln79', reason); // ln79 fail
  }
);

至此,一个基础的、同步执行的 Promise 就已经实现完成了。

添加异步逻辑

通常情况下会在 Promise 中被调用的是异步函数,下面就是使用 setTimeout 实现的,一个简单的异步函数的例子:

const asyncPromise = new PromiseR((resolve, reject) => {
  setTimeout(() => {
    resolve('success');
  }, 2000);
});

asyncPromise.then((value) => {
  console.log('ln90', value); // 没有输出结果
});

这种情况下没有任何的输出,原因也很简单,回想一下 then 函数的实现是这样的:

if (this.status === FULFILLED) {
  successCB(this.value);
} else if (this.status === REJECTED) {
  failCB(this.reason);
}

其中并没有对状态 pending 进行任何的处理。

主线程对于函数的调用也是同步的,它会执行对 PromiseR 对象的实例,接着直接调用 then 函数。此时的 setTimeout 会在 Web API 中被调用,一直到约两秒钟后,主线程执行完毕了,setTimeout 的时间到了,它才会通过 消息队列 与 事件循环 机制被压到 执行栈 中。

此时 then 函数的执行早就结束了。

then 中添加异步的分析

if...else-if... 这个逻辑中显然没有对状态 pending 的判断,而当状态为 pending 时,也就代表着传到 PromiseR 中的异步函数还没有执行完毕。

这个情况下,可以将 successCB 和 failCB 存到一个变量中去,放到 resolve 或 reject 去调用。

注意,这里在定时器后,一定是会调用 resolve 或 reject 去表明当前函数执行成功,或是执行失败:

setTimeout(() => {
  // 这里调用了 resolve
  resolve('success');
}, 2000);

then 中添加异步的实现

根据上文的分析,其实异步的实现并不是非常难:

class PromiseR {
  // ...省略其他实例属性
  // 保存成功和失败的回调函数
  successCB = undefined;
  failCB = undefined;

  resolve = (value) => {
    // 省略其他函数
    // 调用成功函数
    this.successCB && this.successCB(this.value);
  };

  reject = (reason) => {
    // 省略其他函数
    // 调用失败函数
    this.failCB && this.failCB(this.reason);
  };

  then(successCB, failCB) {
    // 判断状态,根据状态去调用合适的回调函数,并且传入对应的值
    // value 和 reason 在 resolve 和 reject 部分已经保存到实例属性上了
    if (this.status === FULFILLED) {
      successCB(this.value);
    } else if (this.status === REJECTED) {
      failCB(this.reason);
    }
    // 函数还没有执行完毕,只能等待
    else {
      // 将两个回调函数的值存储起来
      this.successCB = successCB;
      this.failCB = failCB;
    }
  }
}

测试 then 函数中的异步操作

同样的代码再运行一遍:

const asyncPromise = new PromiseR((resolve, reject) => {
  setTimeout(() => {
    resolve('success');
  }, 2000);
});

asyncPromise.then((value) => {
  console.log('ln103', value);
});

这是命令行上就会有输出了:

promise-dir>node 03.handwritePromise.js
ln103 success

实现 then 方法的多次调用

这一步也是 then 函数功能强大的原因一部分,以及链式调用 then 方法的基础。

多次调用 then 的分析

链式调用 then 方法也分为两种情况:

  1. 同步调用 then 方法

    这个逻辑比较简单,直接调用 successCB 或者是 failCB 即可

  2. 异步调用 then 方法

    这时候就需要将所有的回调函数全都存储起来,等到执行完毕之后依次调用

    这样的话就又有一个问题了……之前的回调函数保存的都是一个函数,那就不符合业务需求。所以,还需要对之前保存的对象属性进行修改和优化

多次调用 then 的实现

这里主要对上面 then 函数的实行进行了修改。

class PromiseR {
  // 保存成功和失败的回调函数
  successCB = [];
  failCB = [];

  resolve = (value) => {
    // 这里改为使用 shift 去将值弹出
    // 弹出的值本身就是一个函数,因此可以直接调用
    while (this.successCB.length) {
      this.successCB.shift()(this.value);
    }
  };
  // 修改参数,让 reject 接收成功后传来的原因
  reject = (reason) => {
    // 这里改为使用 shift 去将值弹出
    // 弹出的值本身就是一个函数,因此可以直接调用
    while (this.failCB.length) {
      this.failCB.shift()(this.reason);
    }
  };

  then(successCB, failCB) {
      resolve 和 reject 部分已经保存到实例属性上了
    if (this.status === FULFILLED) {
      successCB(this.value);
    } else if (this.status === REJECTED) {
      failCB(this.reason);
    }
    else {
      // 数组需要通过push将两个回调函数的值存储起来
      this.successCB.push(successCB);
      this.failCB.push(failCB);
    }
  }
}

多次调用 then 的测试

const multiplePromise = new PromiseR((resolve, reject) => {
  setTimeout(() => {
    console.log('success');
    resolve('success');
  }, 2000);
});
multiplePromise.then((value) => {
  console.log('success1'); // success1
});
multiplePromise.then((value) => {
  console.log('success2'); // success2
});

这里命令行中的输出顺序为:

[nodemon] restarting due to changes...
[nodemon] starting `node 03.handwritePromise.js`
success
success1
success2
[nodemon] clean exit - waiting for changes before restart

能够看到 success 还是在 success2 和 success3 之前调用的,这也就能证明 setTimeout 依旧还在工作,多次调用 then 的顺序问题可以保证。

这里就发现,对 then 函数的多次调用就成功了,下一步就可以实行对 then 函数进行链式调用的修改。

实现 then 方法的链式调用

对于这个功能的视线,有以下几个需求:

  • 实现 thenable 功能

    也就是在不考虑其他功能的前提下,先完成链式调用的嵌套

  • 判断返回的应该是一个值,还是应该是 Promise 对象

    这还需要判断 Promise 对象返回的结果再决定调用 resolve 还是 reject

实现 thenable 功能

实现链式调用的必然前提就是,每一个 then 函数都必须要返回一个 Promise 对象,否则就无法衔接使用 then 函数

这步的实现需要首先添加 then 函数的返回值,让 then 函数返回一个 Promise 对象,所以,也就需要先在 then 函数中新建一个 Promise 对象,以供返回

在新建的 Promise 内部使用 resolv 和 reject 去消化返回的值

thenable 的代码实现

class PromiseR {
  then(successCB, failCB) {
    // 新建一个Promise对象,否则没有东西可以被返回
    const thenablePromise = new PromiseR(() => {
      // promise 对象需要接受一个立即执行函数
      // 本身 then 之中的逻辑也是需要被立即执行的
      // 因此可以将原本的逻辑作为 立即执行函数 executor 传入到 Promise 对象中
      if (this.status === FULFILLED) {
        const thenableValue = successCB(this.value);
        resolve(thenableValue);
      } else if (this.status === REJECTED) {
        const thenableReason = failCB(this.reason);
        reject(thenableReason);
      } else {
        this.successCB.push(successCB);
        this.failCB.push(failCB);
      }
    });

    return thenablePromise;
  }
}

thenable 的代码测试

// chaining then
const thenablePromise = new PromiseR((resolve, reject) => {
  resolve('success');
});

thenablePromise
  .then((value) => {
    console.log(value);
    return 'next success';
  })
  .then((value) => {
    console.log(value);
  });

运行结果是正常的:

[nodemon] restarting due to changes...
[nodemon] starting `node 03.handwritePromise.js`
Debugger attached.
success
next success
Waiting for the debugger to disconnect...
[nodemon] clean exit - waiting for changes before restart

可以看到,thenable 的功能已经实现,可以进行下一步的开发

实现 thenable 中返回 Promise

这里就需要做逻辑判断去决定返回值,它存在以下几种情况:

  • 返回值是数值,这点上面已经做到解决了
  • 返回值是 Promise 对象,这个问题是这里需要解决的
    • Promise 对象执行成功,调用 resolve
    • Promise 对象执行失败,调用 reject
    • Promise 对象还未执行,将回调函数推入准备好的数组中

thenable 中返回 Promise 代码实现

class PromiseR {
  then(successCB, failCB) {
    const thenablePromise = new PromiseR((resolve, reject) => {
      if (this.status === FULFILLED) {
        const thenableValue = successCB(this.value);
        resolvePromise(thenableValue, resolve, reject);
      } else if (this.status === REJECTED) {
        const thenableReason = failCB(this.reason);
        resolvePromise(thenableReason, resolve, reject);
      } else {
        this.successCB.push(successCB);
        this.failCB.push(failCB);
      }
    });

    return thenablePromise;
  }
}

// 这段函数会被调用2次,所以单独抽离封装到外部会比较好
// 这里的逻辑主要就负责判断是判断传进来的 thenable 对象是 Promise吗,是的话调用 then 函数去处理,

以上是关于[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能的主要内容,如果未能解决你的问题,请参考以下文章

vue一步一步带你封装一个按钮组件

一步一步带你进入Java世界_Java环境配置

Android一步一步带你实现RecyclerView的拖拽和侧滑删除功能

一步一步带你入门MySQL中的索引和锁 (转)

一步一步带你入门MySQL中的索引和锁

一步一步带你体验算法之魅力