Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现Promise,遵循Promise/A+规范 #9

Closed
lhz960904 opened this issue Jul 10, 2020 · 0 comments
Closed

实现Promise,遵循Promise/A+规范 #9

lhz960904 opened this issue Jul 10, 2020 · 0 comments

Comments

@lhz960904
Copy link
Owner

lhz960904 commented Jul 10, 2020

Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。现在前端应用中Promise已经得到了广泛使用。本文通过实现符合Promise/A+规范的Promise,对其加深印象。

构造函数

我们在使用Promise时,通常是使用new操作符进行构造,传入resolver函数,该函数会接受成功(resolve)、失败(reject)的回调函数,当我们确定结果时,需要调用resolve或reject,具体代码如下:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000)
})
// 1s后控制台打印success
p1.then(res => console.log(res))

所以我们的Promise也需要是个构造函数,并且执行用户传入的resolver函数,将定义好的回调函数传进去。下面是具体的代码:

注:本文代码的实现,下划线开头代表私有属性、私有方法。

// 定义Promise的三种状态常量
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

function Promise(resolver) {
  // 传入的必须是函数
  if (typeof resolver !== 'function') {
    throw new TypeError('Promise resolver ' + resolver + ' is not a function');
  }
  
  // resolve或reject的结果值
  this._result = undefined;
  // 状态
  this._status = PENDING;

  try {
    // 执行
    resolver(this._resolve.bind(this), this._reject.bind(this));
  } catch (error) {
    // 捕获错误
    this._reject(error);
  }
}
// 私有方法,传给resolver的成功、失败回调
Promise.prototype._resolve = function() {}
Promise.prototype._reject = function() {}

接下来我们来实现_resolve、_reject私有方法,其实逻辑很简单,我们只需要改变Promise状态,以及成功的值或者失败的原因。但要注意Promise一旦状态改变,就不会再变,任何时候都可以得到这个结果,所以我们只有在状态时PENDING的时才会执行。

Promise.prototype._resolve = function(value) {
  // setTimeout 为了异步执行
  setTimeout(() => {
    if (this._status !== PENDING) return;
    this._status = FULFILLED;
    this._result = value;
  });
}

Promise.prototype._reject = function (reason) {
  // setTimeout 为了异步执行
  setTimeout(() => {
    if (this._status !== PENDING) return;
    this._status = REJECTED;
    this._result = reason;
  });
};

Promise.prototype.then

Promise的核心就是then方法,Promise/A+规范大都也是针对then方法进行阐述,实现了then方法后,我们再来实现其他API就方便了很多。

Promise的then方法的规范有如下几点

  1. 接受两个参数,onFulfilled(成功回调), onRejected(失败回调),当回调不是函数时, 其必须被忽略,支持透传

  2. then 方法可以被同一个 Promise 调用多次

    • 当 Promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
    • 当 Promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
  3. then 方法必须返回一个 Promise 对象

    • 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的Promise解决过程
    • 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e

我们针对上述几点分别来实现一下

关于第一点,可以控制台执行下面代码

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000)
})
// 1s后控制台打印success
p1.then(1).then(res => console.log(res))

第二个then方法依然可以接受到resolve成功的值,所以当then方法传入的不是函数时,我们要规范使其变成函数支持透传。

Promise.prototype.then = function (onFulfilled, onRejected) {
  // 保证是函数,不是函数要实现透传
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
  onRejected = typeof onRejected === 'function' ? onRejected : (v) => { throw v };
}

实现第一点很简单,我们只需要吧把结果/错误 -> 返回/抛出传递给出去就可以啦。

我们再来看第二点,为了可以多次调用并且依次执行,我们需要改下之前写过的代码,我们需要增加俩个回调队列,成功、失败各一个。其实也可以用一个队列来存储,我这里采用的分别存储。

function Promise(resolver) {
  // 忽略无关代码...

  // resolve的回调队列
+ this._resolveCbs = [];
  // reject的回调队列
+ this._rejectCbs = [];
}

Promise.prototype._resolve = function(value) {
  setTimeout(() => {
    // 忽略无关代码...
+   this._resolveCbs.forEach((callback) => callback(value));
  });
}

Promise.prototype._reject = function(reason) {
  setTimeout(() => {
    // 忽略无关代码...
+   this._rejectCbs.forEach((callback) => callback(reason));
  });
}

我们在then方法中,如果状态还处于PENDING,就需要将传入的onFulfilled(成功回调), onRejected(失败回调)插入对应的队列中,否则直接执行就好。这也就是我们要实现第三点的核心逻辑。

根据第三点所述,我们总是需要执行onFulfilled 或 onRejected,然后传入Promise解决过程,此外还需要捕获这个过程,直接reject。具体核心代码如下

let promise = undefined;
return (promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    try {
      var x = onFulfilled(this._result);  // 或者   var x = onRejected(this._result);
      // resolvePromise Promise解决过程 下一段讲
      resolvePromise(promise, x, resolve, reject);
    } catch (e) {
      return reject(e);
    }
  });
}));

下面我们就是把这段代码分别用在 PENDING, FULFILLED, REJECTED三种状态,完整代码如下:

Promise.prototype.then = function (onFulfilled, onRejected) {
  // 保证是函数,不是函数要实现透传
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (v) => {
          throw v;
        };

  let promise = undefined;

  // 已经resolve
  if (this._status === FULFILLED) {
    return (promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try {
          var x = onFulfilled(this._result);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
  // 已经reject
  if (this._status === REJECTED) {
    return (promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try {
          var x = onRejected(this._result);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
  // pending时直接放入回调队列中,放入队列汇总不需要加setTimeout,因为执行时候已经是setTimeout中
  if (this._status === PENDING) {
    return (promise = new Promise((resolve, reject) => {
      this._resolveCbs.push((value) => {
        try {
          var x = onFulfilled(value);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
      this._rejectCbs.push((reason) => {
        try {
          var x = onRejected(reason);
          resolvePromise(promise, x, resolve, reject);
        } catch (e) {
          return reject(e);
        }
      });
    }));
  }
};

上面代码看似很多,很复杂,但其实根据规范来看,其实很简单,而且有大量的重复代码。那我们还有一个resolvePromise函数没有完成,接下来我希望读者可以自己去读一下Promise 解决过程的逻辑。点击链接去查看。因为函数的实现完全照规范的逻辑书写,没有技巧可言。

这里简单的总结几点

  1. 为了和其他promise并存,我们不能只判断onFulfilled,onRejected函数返回的是否是promise,我们只需要保证其返回值存在then方法就去尝试按promise处理。

  2. 如果没有then属性,或者then属性不是函数的话,直接按照resolve(x)处理

  3. 如果then存在并且是函数,按照promise处理的同时,需要捕获错误按reject(x)处理

  4. 如果传入的回调均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用(只能调用一次,需要有标志位)

按照上述四点,我们可以写出resolvePromise函数的代码

function resolvePromise(promise, x, resolve, reject) {
  // 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
  if (x === promise) {
    return reject(new TypeError('Chaining cycle detected for promise!'));
  }
  // 用于 “优先采用首次调用并忽略剩下的调用”的标志位
  let invoked = false;
  // 尝试把 x.then 赋值给 then
  let then = undefined;
  // x 为对象或函数
  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    try {
      then = x.then;
      if (typeof then === 'function') {
        // 如果 then 是函数,将 x 作为函数的作用域 this 调用之
        then.call(
          x,
          (y) => {
            if (invoked) return;
            invoked = true;
            return resolvePromise(promise, y, resolve, reject);
          },
          (r) => {
            if (invoked) return;
            invoked = true;
            return reject(r);
          }
        );
      } else {
        // 如果 then 不是函数,以 x 为参数执行 promise
        return resolve(x);
      }
    } catch (e) {
      // 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
      if (invoked) return;
      invoked = true;
      return reject(e);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    return resolve(x);
  }
}

Promise.prototype.catch

.catch()发生错误时的回调函数 相当于使用.then(null, onRejected),我们实现了then方法,所以catch方法就相当简单

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};

Promise.prototype.finally

.finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

那我们就可以通过then方法,传入的成功、失败回调函数中都去执行callback

Promise.prototype.finally = function (callback) {
  return this.then(
    (value) => Promise.resolve(callback()).then(() => value),
    (reason) =>
      Promise.resolve(callback()).then(() => {
        throw reason;
      })
  );
};

注意我们要.then将结果透传,因为finally后面还可以继续调用then方法。

// 最后一个then理应接受到2作为参数
Promise.resolve(2).finally(() => { }).then(res => console.log(res))

Promise.resolve

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

我们只需要把then方法中的成功逻辑拿出来使用就可以。(其中x不是onFulfilled执行的值,直接是传入的参数)

Promise.resolve = function (value) {
  let promise;
  return (promise = new Promise((resolve, reject) => {
    resolvePromise(promise, value, resolve, reject);
  }));
};

Promise.reject

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

Promise.reject = function (reason) {
  return new Promise((_, reject) => reject(reason));
};

Promise.all

/**
 * Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
 * 只有所有实例的状态都变成fulfilled,新的实例状态才会变成fulfilled,新实例参数
 * 只要其中有一个被rejected,新的实例就变成rejected,此时第一个被reject的实例的返回值,会传递给新实例的回调函数。
 */
Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    let resolvedCount = 0;
    let promiseCount = promises.length;
    let resolvedValues = new Array(promiseCount);
    for (let i = 0; i < promiseCount; i++) {
      Promise.resolve(promises[i]).then(
        (value) => {
          resolvedCount++;
          resolvedValues[i] = value;
          // 数量相同说明promise实例都是成功
          if (resolvedCount == promiseCount) {
            return resolve(resolvedValues);
          }
        },
        (reason) => {
          // 率先reject的直接失败,传入原因
          return reject(reason);
        }
      );
    }
  });
};

Promise.race

/**
 * Promise.race()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
 * 只要其中有一个状态变更,新的实例就跟随着变更,参数会传递给新实例的回调函数。
 */
Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (var i = 0; i < promises.length; i++) {
      // 谁快谁说了算!
      Promise.resolve(promises[i]).then(
        (value) => {
          return resolve(value);
        },
        (reason) => {
          return reject(reason);
        }
      );
    }
  });
};

总结

Promise的实现难点主要集中在then方法上,其他方法都是基于then方法实现的。实现一个Promise也是笔试的高频题目,希望本文章可以给你带来帮助。

完整源码传送门

参考

@lhz960904 lhz960904 added JavaScript Draft 草稿-要写的内容 and removed Draft 草稿-要写的内容 labels Jul 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant