(一)面试题训练——Promise源码理解

150 阅读5分钟

Promise可以说是面试的一个非常重要的点了,可以考的东西非常多,但是如果当你真正掌握了源码逻辑,那么才算真正彻底搞懂所有Promise类型的题目了,而且大厂面试过程中手写Promise源码也是挺常见的,所以本文总结如何理解Promise源码,让我们手写的时候理清头绪

直接上源码,主要的一些思路都写在注释里了。我们需要写的源码实际上就是一个Promise类的封装,我们这里采用ES6class声明类,以下是我们需要声明和实现的

构造函数

  • constructor(executor)
    1. 初始化实例属性
    2. 声明resolvereject方法
    3. 执行executor并传入2中的两个方法,执行过程需要被try...catch包裹,出错跳reject

实例属性

  • state:只有三种值,且只会从1跳到2或3
    1. pending
    2. fulfilled
    3. rejected
  • value:resolve函数的参数
  • reason:reject函数的参数
  • onResolvedCallbacks:resolve后的回调函数数组
  • onRejectedCallbacks:reject后的回调函数数组 原型方法
  • then:需要满足一下场景
    1. 同一个promise可以多次调用
    2. 可以链式调用
  • catch
    1. excuetor执行过程中出错会跳到这
    2. then函数中reject参数未传时跳到这 静态方法
  • all
  • race
  • resolve
  • reject

先来个API目录版的

class Promise{
    constructor(executor){}
    
    then(resolve,reject){}
    catch(fn){}
    
    static resolve(val){}
    static reject(reason){}
    static all(promiseArr){}
    static race(promiseArr){}
}
// 对then的返回值做处理,单独封装
function resolvePromise(promise2,x,resolve,reject){}

简单完整源码

class Promise{
  constructor(executor){
    // 初始化参数
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    // 为什么需要保存回调函数?因为Promise初始化的函数可能是异步的,可以通过事件监听,将两种状态变化的回调函数用两个数组保存
    // 为什么是数组?因为同一个Promise可以调用多次then函数,意味着回调函数可能不止一个
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    
    // 这个两个函数实际上是作为参数预先传入到生成函数中的,当在生成器中执行这两个函数时
    // 说明state发生变化了,且需要调回调函数
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // resolve的回调函数依次执行
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        // reject的回调函数依次执行
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    
    // 整个执行过程中都是被try...catch包裹
    try{
      // 直接执行了executor,这也就说明了为什么
      // new Promise(fn(reslove,reject){})中的函数是同步被执行的
      executor(resolve, reject);
    } catch (err) {
      // 执行过程中有错误就会跳到reject,这里执行捕获到同步代码的错误
      reject(err);
    }
  }
  
  
  then(onFulfilled,onRejected) {
    // 这里需要考虑到我们调用then的时候参数缺省或类型不是function的情况
    // 需要由一个默认的函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    
    // 由于then是可以链式调用的,这就决定了他的返回值也是一个promise
    let promise2 = new Promise((resolve, reject) => {
      // 前两个if是有可能在executor中已经同步执行了resolve或reject
      // 也有可能是then是被异步调用的,在调用时state已经发生了变化,这种情况下直接执行该回调就完事了
      if (this.state === 'fulfilled') {
        // 但是这种执行也不是直接执行的而是异步的,
        // 我们这里用setTimeout来代替这种异步,
        // 但我们知道promise是个微任务,而setTimeout是个宏任务,这里知道就行了,可以用别代替
        setTimeout(() => {
          try {
            // 用我们传的函数来处理这个value,也就是我们一般定义的参数名res
            // 为啥要保存这个执行完的返回值x呢?因为如果这个返回值就是promise的实例的话那就返回他就行了,下面这个函数就是来判断这个的
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      // 同上
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      // 因为executor中的函数一般是异步的,所以我们调用then函数的时候还没有执行到resolve或reject
      // 这也就意味着state还是pending
      if (this.state === 'pending') {
        // 往回调函数数组里push,但是需要和上面一样封装一下
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        // 同上
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    return promise2;
  }
  // 实际上是一个then的语法糖,这样就非常好理解了
  catch(fn){
    return this.then(null,fn);
  }
  
//resolve方法
static resolve(val){
  return new Promise((resolve,reject)=>{
    resolve(val)
  });
}
//reject方法
static reject(val){
  return new Promise((resolve,reject)=>{
    reject(val)
  });
}
//race方法 
static race(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
static all(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    // 每当有一个好了就+1,直到所有的都fulfilled
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
}

}
function resolvePromise(promise2, x, resolve, reject){
  // 防止死循环
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  // 这里主要是分x是否是一个promise(有无then属性)两种情况讨论
  // 如果x不是promise,将其直接传到下一个then的res参数
  // 如果x是promise
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      // 这里就是当x是promise的情况,就递归调用resolvePromise
      // 解释一下就是,如果一直找到返回值不是promise为止才,再把那个值返回出来
      if (x instancof Promise) { 
        x.then(y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}

总结

  1. 构造函数中需要初始化好参数,定义好resolvereject,主要是修改状态,保存valuereason,依次执行回调,调用执行器。
  2. then的实现需要注意链式调用必然需要返回一个promise实例,这个实例resolve有一个条件,如果当前的回调函数的返回值是一个promise实例,那么他会等这个promise的实例的resolve,相当于是一个递归,也就意味着下一个then的值res或err必定不会是一个promise;then中resolve执行的时候出错都会传递给下一个reject。
  3. catch实际上一个语法糖,只有reject而没有resolve
  4. 4个静态方法都比较简单,不多做解析了