手撕 promise 扩展方法

38 阅读7分钟

本文代码基于上篇博客 手撕 Promise A+ 中的源码

原型对象上的 catch 方法

let promise = new Promise((resolve, reject) => {
  reject('fail');
}).then().catch(err => {
  console.log('err', err);
}).then(data => {
  console.log('success', data);
});

// err fail
// success undefined

其实我们可以看到,catch 本质上也就是 then 方法,只是省略了成功回调罢了

// 原型上的 catch 方法
class _Promise {
  // ...
  catch(errFn) {
    return this.then(null, errFn);
  }
}

静态方法 resolve、reject

Promise.resolve('hello').then(data => {
  console.log(data);
});

Promise.reject('hello').catch(reason => {
  console.log(reason);
});

静态方法中的 resolve 和 reject 的区别在哪里?除了目标状态不同,还有就是 Promise.resolve 具备等待效果。

Promise.resolve(new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1000);
  }, 1000)
})).then(data => {
  console.log('success', data);
});

Promise.reject(new Promise((resolve, reject) => {
  setTimeout(() => {
    // 外层的 reject 静态方法不会等待此结果成功返回
    resolve('reason msg');
  }, 1000)
})).catch(reason => {
  console.log('err', reason);
});

// 立即输出  "err Promise { <pending> }"
// 1s 后输出 "success 1000"

二者的实现:

class _Promise {
  constructor() {
    const resolve = (value) => {
      // 这个方法并不属于规范中的,只是为了和原生表现一样,加了 value 为 promise 的判断逻辑
      // 这个地方不能判断是否为 thenable 的对象「因为这样写 promise A+ 测试就不通过了」
      if (value instanceof _Promise || value instanceof Promise) {
        // 如果是一个 promise,我们增加等待效果
        // return 必须加,阻断后面代码执行
        // 根据 value 的执行状态,决定当前 promise 的状态
        // 所以 Promise.resolve(x) 并不一定返回成功态,还要取决于内部参数的状态
        return value.then(resolve, reject); // 递归
      }

      if (this.status !== PENDING) return;
      this.value = value;
      this.status = FULFILLED;
      this.onResolvedCallbacks.forEach(cb => cb());
    }
  }

  // ...

  // 静态方法 具有等待效果的 resolve
  static resolve(value) {
    return new _Promise((resolve, reject) => {
      // 当 value 是 promise 时,要做等待处理
      // 所以此处要改写上面的 _Promise 中的 resolve 方法
      resolve(value);
    });
  }

  // 静态方法 reject
  static reject(error) {
    return new _Promise((resolve, reject) => {
      reject(error);
    });
  }
}
测试代码
_Promise.resolve(new _Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1000);
  }, 1000)
})).then(data => {
  console.log('success', data);
});

_Promise.reject(new _Promise((resolve, reject) => {
  setTimeout(() => {
    // 外层的 reject 静态方法不会等待此结果成功返回
    resolve('reason msg');
  }, 1000)
})).catch(reason => {
  console.log('err', reason);
});

// 直接输出 "err  _Promise { status: 'PENDING',  value: undefined, .. }"
// 1s 后输出 "success 1000"

知识点:手动实现 node 新增 promises 和 promisify

promises 是 node 里面新增的一个功能,它可以让 fs 的所有方法都变成 promise

const fs = require('fs').promises;
const util = require('util');

// 可以直接返回一个 promise 对象,通过 then 去调用
fs.readFile('./name.txt', 'utf8').then(data => {
  console.log(data);
});

// 针对单一方法的 promise 风格转化 
const promiseReadFile = util.promisify(fs.readFile);

promiseReadFile('./name.txt', 'utf8').then(data => {
    console.log(data);
})

实现 promisify

/**
 * @param {*} fn 要 promise 化的函数
 * @returns function 返回的新函数
 */
function _promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => {
        err && reject(err);

        resolve(data);
      })
    })
  }
}

const promiseReadFile = _promisify(fs.readFile);

promiseReadFile(path.join(__dirname, './1.txt'), 'utf8').then(data => {
  console.log(data);
}, e => {
  console.log('error', e);
})

实现 promises

// 实际上就是把传入的模块对象的属性「值为函数的」遍历执行 promisify
const _promisify = require('./_promisify');

/**
 * @param { Object } 要转 promise 化的模块对象
 * @return void
 */
function _promises(module) {
  for (let key in module) {
    let curVal = module[key];

    if (typeof curVal === 'function') {
      module[key] = _promisify(curVal);
    }
  };
}

_promises(fs);

fs.readFile(path.join(__dirname, './1.txt'), 'utf8').then(data => {
  console.log(data); 
})

静态方法 all

const fs = require('fs').promises;

Promise.all([fs.readFile('name.txt', 'utf8'), fs.readFile('age.txt', 'utf8')]).then(data => {
  console.log(data);
});

// [ '杨帅', '18' ]

all 的实现

// 细节
//   @1 all 方法接收一个迭代器对象,对象中的元素可以不是 promise 实例
//   @2 all 方法全部成功才是成功态,任意一个失败则返回失败态度『对比下 allSettled』
//   @3 all 方法需要保持有序输出

class _Promise {
  // ... 
  
  // 静态方法 all
  static all(promises) {
    if (!promises || typeof promises[Symbol.iterator] !== 'function') {
      throw new TypeError('object is not iterable');
    }

    let resolveCount = 0;
    let result = [];

    return new _Promise((resolve, reject) => {
      [...promises].forEach((itm, idx) => {
        _Promise.resolve(itm).then(data => {
          result[idx] = data;
          
          if (++resolveCount == promises.length) return resolve(result);
        }, reason => {
          return reject(reason);
        });
      });
    });
  }
}
测试代码
const fs = require('fs').promises;

let p1 = fs.readFile('./_Promise7/name.txt', 'utf8');
let p2 = fs.readFile('./_Promise7/age.txt', 'utf8');

_Promise.all([p1, p2]).then(data => {
  console.log('success', data);
}, reason => {
  console.log('err', reason);
});

// success [ '杨帅', '18' ]

原型对象上的 finally 方法

// 前置知识
//   @1 finally 不接收参数,会把上一个 then 的参数原封不动往下传递
//   @2 finally 无论成功失败都会执行
//   @3 finally 如果返回的是一个 promise,那么会有等待效果

Promise.resolve('ok').finally((...args) => {
  console.log(args); 
}).then(data => {
  // 值能穿透
  console.log('成功', data); // 成功 'ok'
}).catch(e => {
  console.log('失败', e);
});

// [] 
// 成功 'ok' 

其实看到这里,我们就应该能想到,它就是用 then 来实现的,于是这个代码就很好写了。

class _Promise {
  //... 

  finally(cb) {
    return this.then(data => {
      cb();
      return data; // 传递值
    }, reason => {
      cb();
      throw reason;
    });
  }
}

不过还有一些特殊的情况

// 只要链式经过 finally,且 finally 返回一个 promise ,不论成功还是失败都具备等待效果
//   + 如果 finally 内部返回的 promise 状态为成功,则走成功回调,但不会采用成功的值
//   + 如果 finally 内部返回的 promise 状态为失败,则走失败回调,会采用成功的值💦

Promise.resolve('ok').finally(() => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('xxxxxxx');
    }, 1000);
  })
}).then(data => {
  console.log('成功', data); // 1s 后输出 "成功  ok"
}).catch(e => {
  console.log('失败', e);
});


Promise.resolve('ok').finally(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('xxxxxxx');
    }, 1000);
  })
}).then(data => {
  console.log('成功', data); 
}).catch(e => {
  console.log('失败', e); // 1s 后输出 "失败 xxxxxxx"
});

所以需要继续补全 finally 的源码

// 思路
//   @1 不知道 finally 内部函数返回值是不是 promsie,我们手动给它转成 promise
//   @2 这里都用的 Promise.resolve 方法,因为它具备等待效果
//   @3 内部函数返回成功态的 promise,则往下传递上一次的值 val
//   @4 内部函数返回失败态的 promise,则透传 promise 的失败原因 reason
//   @5 理解 Promise.resolve(x).then() 的精髓在于 Promise.resolve(x) 并不一定走成功回
//      调,而是根据 x 的值来决定,如果 x 是一个失败状态的 promise,则整体返回一个失败态
//      的 promise,具体参见源码 resolve 内部第一行递归调用。
//   @6 Promise.resolve(x).then() 没有传递失败回调,所以当 Promise.resolve(x) 返回的
//      是失败态的 promise 时,直接把该 promise 的状态和值透传下去,比较取巧

class _Promise {
  //... 

  finally(cb) {
    let x = cb(); // 函数返回值

    // val -> 上面传递的值,用于往下传递
    return this.then(val => { 
      // 成功回调,Promise.resolve('ok').finally(cb) 会走这里
      //   + Promise.resolve(x) 返回成功态,则往下传递上一次的值 val
      //   + Promise.resolve(x) 返回失败态,也就是 x 为 rejected 的 promise,则 x 
      //     状态和值往下传递(因为没写失败回调)
      return Promise.resolve(x).then(data => val); 
    }, reason => {
      // 失败回调,Promise.reject('ok').finally(cb) 会走这里
      //   + Promise.resolve(x) 返回成功态,则往下传递上一次的值 reason
      //   + Promise.resolve(x) 返回失败态,也就是 x 为 rejected 的 promise,则 x 
      //     状态和值往下传递(因为没写失败回调)
      return Promise.resolve(x).then(x => { throw reason }); 
    });
  }
}
测试代码
// _Promise.resolve('ok').finally((...args) => {
//   console.log(args); 
// }).then(data => {
//   // 值能穿透
//   console.log('成功', data); // 成功 'ok'
// }).catch(e => {
//   console.log('失败', e);
// });

// [] 
// 成功 'ok' 

_Promise.resolve('ok').finally(() => {
  return new _Promise((resolve, reject) => {
    setTimeout(() => {
      reject('xxxxxxx');
    }, 1000);
  })
}).then(data => {
  console.log('成功', data);
}).catch(e => {
  console.log('失败', e);
});

// 1s 后输出 "失败  xxxxxxx"

静态方法 race

Promise.race 方法,只要有一个成功或者失败,则返回结果成功或失败。

const fs = require('fs').promises;
const path = require('path');

let p1 = fs.readFile(path.join(__dirname, './1.txt'), 'utf8');
let p2 = fs.readFile(path.join(__dirname, './2.txt'), 'utf8');

Promise.race([p1, p2]).then(data => {
  console.log(data);
}, e => {
  console.log('error', e);
});

这里实现很巧妙,同时也有很多类似的场景,如果外层 promise 依赖内层 promise 对象的状态,就把外层的 resolve 和 reject 当初内层 promise 的成功和失败回调传入,其实和源码中 resolve 实现类似

/**
 * @param { Iterable } 可迭代对象
 * @returns promise 对象
 */

function _race(Iterable) {
  return new Promise((resolve, reject) => {
    for (let itm of Iterable) {
      if (itm && typeof itm.then === 'function') {
        // promise 对象 这里很巧妙的把外层 resolve, reject 传入了
        itm.then(resolve, reject);
      } else {
        resolve(itm);
      }
    }
  });
}

module.exports = _race;

race 的使用场景

有一个需求,做图片懒加载时,我希望接口如果超过 2s 未返回结果,就超时

// 我可能会这么写
let abort;

let p = new Promise((resolve, reject) => {
  abort = reject; // 保存 reject 方法 
  setTimeout(() => {
    resolve('图片加载完成');
  }, 3000);
});

setTimeout(() => {
  abort('图片加载失败');
}, 2000);

p.then(val => {
  console.log('success', val);
}, e => {
  console.log('error', e);
});

很明显,写的比较繁琐,而且无法复用,而且我们改了原有的 p 的逻辑「保存了它的 reject」,我们借用 race 来改写下。

function wrap(old) {
  let abort;
  // 内置了一个 promise  我们可以控制这个 promise 来影响 promise.race 的结果
  let p2 = new Promise((resolve, reject) => {
    abort = reject; // 保存 reject 方法
  });

  let returnPromise =  Promise.race([old, p2]);

  returnPromise.abort = abort; // 把 abort 挂到 race 返回的 promise 对象上

  return returnPromise;
}

使用起来就方便很多,并且不会动原 promise,随意定制了

let newPromise = wrap(p);

// 2s 超时
setTimeout(() => {
  newPromise.abort('图片加载失败');
}, 2000);

newPromise.then(val => {
  console.log('success', val);
}, e => {
  console.log('error', e);
});

思考:resole 返回值的状态

// promise.resolve 一定走成功回调么
Promise.resolve(new Promise((resolve, reject) => {
  reject('fail');
})).then(data => {
   console.log(data)
}, {
  reason => console.log('reason', reason)
})

// resolve 一定走成功回调么
var promise = new Promise((resolve, reject) => {
  resolve(new Promise((resolve, reject) => {
      reject(1);  
  }));
});

promise.then(val => {
  console.log(val, 'success');
}, reason => {
  console.log(reason, 'fail');
});

思考:如何中断 promise

  1. 如果仅仅是不采用原有 promise 的返回值,我们可以使用上面提到的 promise.race 方法,使用 abort('reject reason') 来提前返回值。
  2. 如果想破坏链式调用,我们返回一个 pending 状态的 promise 对象即可。
Promise.resolve('1').then(data => {
  console.log(data);
  return new Promise(() => {});
}).then(data => {
  console.log(data);
}).then(data => {
  console.log(data);
})

// 1