手写Promise

118 阅读11分钟

不啰嗦,先来一个最基本的:

// v1.0.0
class PPromise {
  constructor(exector) {
    const self = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    function resolve(value) {
      // 因为不是箭头函数,所以稍微注意一下this的指向问题
      if (self.state === 'pending') {
        self.state = 'fulfilled';
        self.value = value;
      }
    }   
    function reject(reason) {
      // 一样注意一下this的指向问题
      if (self.state === 'pending') {
        self.state = 'rejected';
        self.reason = reason;
      }
    }
    try {
      exector(resolve, reject);
    } catch(e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      // 有可能想不到这里要判断一下是否是函数(毕竟一开始学到的就是then的参数是两个函数)
      typeof onFulfilled === 'function' && onFulfilled(this.value);
    } else if (this.state === 'rejected') {
      typeof onRejected === 'function' && onRejected(this.reason);
    }
  }
}

上面这些就已经实现了一个最基本的Promise,可以用最基本的例子试一下:

const p = new PPromise((resolve, reject) => {
  resolve(12345); // 或reject(67890);
});
p.then(res => {
  console.log(res);
}, err => {
  console.error(err);
});

继续升级。之所以说上面的是最基本的,是因为只要加入一个小小的延时,就不好使了:

const p = new PPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(12345);
  }, 500);        
});
p.then(res => {
  // 执行到此处时,state还是pending, 500ms之后才会变为fulfilled,所以这里不会打印
  console.log(res);
}, err => {
  console.error(err);
})

可以联想到,要解决这个问题,就要引入发布-订阅模式了:

// v1.0.1
class PPromise {
  constructor(exector) {
    const self = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = []; // 增加成功队列
    this.onRejectedCallbacks = []; // 增加失败队列
    function resolve(value) {
      if (self.state === 'pending') {
        self.state = 'fulfilled';
        self.value = value;
        // 发布,轮询成功队列
        self.onResolvedCallbacks.forEach(fn => fn(value));
      }
    }   
    function reject(reason) {
      if (self.state === 'pending') {
        self.state = 'rejected';
        self.reason = reason;
        // 发布,轮询失败队列
        self.onRejectedCallbacks.forEach(fn => fn(reason));
      }
    }
    try {
      exector(resolve, reject);
    } catch(e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      typeof onFulfilled === 'function' && onFulfilled(this.value);
    } else if (this.state === 'rejected') {
      typeof onRejected === 'function' && onRejected(this.reason);
    } else {
      // 成功订阅
      typeof onFulfilled === 'function' && this.onResolvedCallbacks.push(onFulfilled);
      // 失败订阅
      typeof onRejected === 'function' && this.onRejectedCallbacks.push(onRejected);
    }
  }
}

接着升级。我们都知道then函数是异步任务(微任务),即:

console.log('1');
const p = new Promise((resolve, reject) => {
  console.log('2');
  resolve('result');             
});
console.log('3');
p.then(res => {
  console.log('5');
  console.log(res);
});
console.log('4');
// 结果为1 -> 2 -> 3 -> 4 -> 5 -> result

很明显,v1.0.1中,then函数是同步任务,若按照上面的测试用例执行,结果会是1 -> 2 -> 3 -> 5 -> result -> 4。所以,要将then函数的执行放到异步中(一个延时为0的setTimeout):

// v1.0.2
class PPromise {
  constructor(exector) {
    const self = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value) {
      if (self.state === 'pending') {
        self.state = 'fulfilled';
        self.value = value;
        // 发布,轮询成功队列
        self.onResolvedCallbacks.forEach(fn => fn());
      }
    }   
    function reject(reason) {
      if (self.state === 'pending') {
        self.state = 'rejected';
        self.reason = reason;
        // 发布,轮询失败队列
        self.onRejectedCallbacks.forEach(fn => fn());
      }
    }
    try {
      exector(resolve, reject);
    } catch(e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      setTimeout(() => {
      	typeof onFulfilled === 'function' && onFulfilled(this.value);
      }, 0); // 当然,延时为0的setTimeout的真实含义不在本文的讨论范围中
    } else if (this.state === 'rejected') {
      setTimeout(() => {
      	typeof onRejected === 'function' && onRejected(this.reason);
      }, 0);
    } else {
      // 成功订阅
      typeof onFulfilled === 'function' && this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
        	onFulfilled(this.value);
        }, 0);
      });
      // 失败订阅
      typeof onRejected === 'function' && this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
        	onRejected(this.reason)
        }, 0);
      });
    }
  }
}

尽管then函数是异步任务(微任务),但为什么不能用一个setTimeout将then函数中的所有内容包起来,而非得在每个if条件中分别用 setTimeout包裹?其实如果只考虑fulfilled和 rejected状态的 state,的确用一个setTimeout包裹 then函数中的内容就好,但实际上,onFulfilled和onRejected的执行,在state的各个状态下都是异步的,看下面这个例子:

// 真实的Promise
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(); // 尽管先执行resolve
    console.log(1);
  }, 500)           
});
p.then(res => {
  console.log(2);        
});
// 最终结果,500ms后先打印1再打印2

所以,为保证onFulfilled和onRejected一定异步执行,向onResolvedCallbacks(onRejectedCallbacks)注入的函数,也得是异步函数。

至此,一个50行左右的简单Promise就完成了,下面的升级就要考虑支持链式调用了。

首先,既然支持链式调用,那么then函数肯定不能像上面一样什么也不返回,而是一定要返回了一个新的PPromise,像下面这样:

then(onFulfilled, onRejected) {
  // return一个新的PPromise
  const p2 = new PPromise((resolve, reject) => {
    // ......
  });
  return p2;
}

参照promise/A+规范,then函数的规则是这样的:

  1. 若onFulfilled(onRejected)返回一个值x,则执行p2的resolve,x作为参数;
  2. 若onFulfilled(onRejected)抛出异常e,则执行p2的reject,e作为参数;
  3. 若onFulfilled不是函数且初始promise的resolve执行,执行p2的resolve(有参数则传递参数);
  4. 若onRejected不是函数且初始promise的reject执行,执行p2的reject(有参数则传递参数); 后两点或许不太好想象(再一次,毕竟我们一开始学到的就是then的参数是两个函数,顶多是undefined),其实就是下面这样:
// 这样
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1122334)
  }, 1000);
});
p.then(1).then(r => {
  console.log(r); // 这里打印1122334
});
// 或者这样
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(5566778)
  }, 1000);
});
p.then(1, 2).then(null, e => {
  console.error(e); // 这里打印5566778
});;

既然onFulfilled(onRejected)执行的结果有多种可能, 甚至有可能onFulfilled(onRejected)根本不是函数,那么是不是可以这样处理:

  1. 如果onFulfilled(onRejected)不是函数,那么它是什么其实并不重要(例如上面代码第7行的onFulfilled是数字1,难不成第8行要打印数字1?),所以可以将onFulfilled(onRejected)改为一个函数,不必复杂,简单的返回value(reason)即可;
  2. onFulfilled(onRejected)的执行结果有多种可能,那么可以记录一下结果,然后交由另一个函数统一处理。
  3. 既然onFulfilled(onRejected)的执行结果有多种可能,当然也有可能在执行过程中出错,所以最好将其包在try {}catch{}中。 如下:
then(onFulfilled, onRejected) {
  // 不是函数就改为一个函数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
  // return一个新的PPromise
  const p2 = new PPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      setTimeout(() => {
        try {
          const x = onFulfilled(this.value);
          // “另一个函数”命名为handlePromise
          // 既然是统一处理函数,当然resolve和reject也要作为参数传递
          handlePromise(x, resolve, reject);
        } catch (err) {
          // 已经出错,当然也就没必要再进行处理,直接reject就好
          reject(err);
        }
      }, 0);      
    } else if (this.state === 'rejected') {
        setTimeout(() => {        
          try {
            const x = onRejected(this.reason);
            // 它是一个统一处理函数,onFulfilled和onRejected的返回结果都由它处理
            handlePromise(x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);        
    } else {
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            handlePromise(x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      });
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            handlePromise(x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      });
    }
  });
  return p2;
}

function handlePromise(x, resolve, reject) {
	// ......
}

下面分两种可能来讨论:

  1. 若x为原始类型数据或undefined,不必过多处理,直接resolve即可;
  2. 若x为对象或函数,为防止对象上或函数的原型上有then属性(属性值是当然是函数),需要其他处理(其实就是对thenable对象的处理)。 关于第一点可能会有疑惑,为什么一定是resolve(x),若x是onRejected的执行结果,也是resolve(x)而不是reject(x)吗?这就跟promise/A+规范相关了,无论x来自onFulfilled还是onRejected,x都是正确执行的结果(要分清“正确”和“非期望”,执行onRejected并不代表出错,只是当发生非我们期望的情况时,我们一般用onRejected处理),reject只负责处理执行出错的情况。 如下:
function handlePromise(x, resolve, reject) {
    // 谁让typeof null也是'object'呢,当然要排除一下
    if ((typeof x === 'object' || typeof x === 'function') && x !== null) {
    const then = x.then;
    if (typeof then === 'function') {
        // 对then的某种形式的调用
    } else {
        // 若then不是函数(或x上根本没有then属性),那x对于我们而言就是一个普通的数据,直接resolve就好
    	resolve(x);
    }
  } else {
      // 即便第(上)一个then执行的是onRejected,下一个then执行的也是onFulfilled
      // 大家可以用真实的Promise试验一下
      resolve(x);
  };
}

可以联想到,既然是thenable对象,那么若then是函数,对其的调用形式就如对一般对then函数的调用形式一样,同时既然是调用函数,就一定有执行出错的风险,所以也要用try{}catch{}包一下:

function handlePromise(x, resolve, reject) {
  if ((typeof x === 'object' || typeof x === 'function') && x !== null) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
          // 防止有this的指向问题,借助一下call
          then.call(x, r => {
            resolve(r);        	
          }, e => {
            reject(e);
          })
      } else {
        resolve(x);
      }
    } catch (e) {
      // 当然直接reject就好
      reject(e);
    }
  } else {
    resolve(x);
  };
}

-----------------------华丽的分割线----------------------

这里添加一条分割线,目的是什么最后再说。

接着升级。上面代码第8行,r也有可能又是一个Promise,那么就又得取出then,执行then.call(......),然后又有可能得到一个Promise......既然有这样的可能,那么应该很容易联想到,我们在第8行不应该简单的resolve,而应该执行递归,这样就可一劳永逸:

// 不写那么多了,就是上面的第8行改为:
handlePromise(r, resolve, reject);

另外还有一种极端的情况,若x是p2,那么就会造成死循环、无法结束的情况,就像下面这样:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(12345);
  }, 500);        
});
const p1 = p.then(res => {
  return p1; // 自己等待自己完成,出错
});

所以在handlePromise的一开始,判断一下x是否和外层的then返回的PPromise相等,若相等直接reject即可,当然,这样的话就得给handlePromise增加一个参数了,即外层的then返回的PPromise:

// 多一个参数
function handlePromise(x, resolve, reject, newPPromise) {
  if (x === newPPromise) {
    // 抛一个TypeError
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
  }
  if ((typeof x === 'object' || typeof x === 'function') && x !== null) {
    try {
    	const then = x.then;
      if (typeof then === 'function') {
          then.call(x, r => {
            handlePromise(r, resolve, reject, newPPromise); // 这里也多一个     	
          }, e => {
            reject(e);
          })
      } else {
        resolve(x);
      }
    } catch (e) {
      reject(e);
    }
  } else {
    resolve(x);
  };
}
// 当然,then函数中四处对handlePromise的调用也要多传一个参数

不知会不会好奇,为什么上面代码第9行,也要包在try{}里,难道从一个对象上获取一个数据还能出错?就算没有该属性也能得到一个undefined不是么?其实,这就是典型的防范“小人”了,看看下面这个例子,是不是就能理解为什么了:

const x = {};
Object.defineProperty(x, 'then', {
  get() {
    throw new Error('就是坑你,咋地');
  }
});

继续。我们都知道Promise的resolve和reject只能执行一个,不能两个都执行,thenable对象的then函数也不例外,即下面这样是不允许的:

const p = new Promise((resolve, reject) => {
  resolve('result');             
});
p.then((r) => {
  return {
    then(resolve, reject) {
      resolve(r);
      reject(r); // 这里不会执行
    }
  };
})

所以在handlePromise还要增加判断机制,判断若已执行过一个,就不再执行另一个了:

function handlePromise(x, resolve, reject, newPPromise) {
  if (x === newPPromise) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
  }
  if ((typeof x === 'object' || typeof x === 'function') && x !== null) {
    let done = false; // 增加done变量
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          r => {
            if (done) {
              return; // 已执行过,就不执行了
            }
            done = true;
            handlePromise(r, resolve, reject, newPPromise);
          },
          e => {
            if (done) {
              return;
            }
            done = true;
            reject(e);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (done) {
        return;
      }
      done = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

到这里,一个初版的Promise就完成了,附一下完整代码:

// v1.0.3
class PPromise {
  constructor(exector) {
    const self = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value) {
      if (self.state === 'pending') {
        self.state = 'fulfilled';
        self.value = value;
        self.onResolvedCallbacks.forEach(fn => fn());
      }
    }   
    function reject(reason) {
      if (self.state === 'pending') {
        self.state = 'rejected';
        self.reason = reason;
        self.onRejectedCallbacks.forEach(fn => fn());
      }
    }
    try {
      exector(resolve, reject);
    } catch(e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
    const p2 = new PPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            handlePromise(x, resolve, reject, p2);
          } catch (err) {
            reject(err);
          }
        }, 0);      
      } else if (this.state === 'rejected') {
          setTimeout(() => {        
            try {
              const x = onRejected(this.reason);
              handlePromise(x, resolve, reject, p2);
            } catch (err) {
              reject(err);
            }
          }, 0);        
      } else {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              handlePromise(x, resolve, reject, p2);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              handlePromise(x, resolve, reject, p2);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
      }
    });
    return p2;
  }  
}

function handlePromise(x, resolve, reject, newPPromise) {
  if (x === newPPromise) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
  }
  if ((typeof x === 'object' || typeof x === 'function') && x !== null) {
    let done = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          r => {
            if (done) {
              return;
            }
            done = true;
            handlePromise(r, resolve, reject, newPPromise);
          },
          e => {
            if (done) {
              return;
            }
            done = true;
            reject(e);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (done) {
        return;
      }
      done = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

如何测试正确性?node上有个promises-aplus-tests的插件,我们可以用它来测试:

// 1. npm i promises-aplus-tests
// 2. 新建js文件,像下面这样组织代码
const promisesAplusTests = require('promises-aplus-tests');

class PPromise {
  // ......
}
function handlePromise(x, resolve, reject, newPPromise) {
  // ......
}

PPromise.deferred = PPromise.defer = function () {
  var dfd = {};
  dfd.promise = new PPromise(function (fulfill, reject) {
    dfd.resolve = fulfill;
    dfd.reject = reject;
  });
  return dfd;
};

promisesAplusTests(PPromise, function (err) {});
// 3. 保存,命令行执行“node 文件名”

image.png

可以看到,通过了872项测试用例。

总结:

  1. 是否还记得那条“华丽的分割线”?分割线之后的对PPromise的修改(优化),包括两处“特别说明”,个人感觉都不是第一次写Promise就能写出来的,需要经过对Promise的长期使用、足够了解,甚至用promises-aplus-tests测试出错,根据错误信息才发觉的。毕竟根据一开始对Promise的学习,很可能我们没想过、甚至根本想不到“还有这种骚操作”。
  2. 当然,这里只实现了then函数,resolve/reject/race/all等api都还未实现,毕竟Promise也还在不断进化中(例如ES2020加入的allSettled,目前也并非所有浏览器都支持),后续慢慢补充。 补充一些Promise的api:
class PPromise {
  // ......
  // resolve/reject比较容易,就是返回一个fulfilled/rejected状态的Promise即可
  static resolve(value) {
    return new PPromise(resolve => {
      resolve(value);
    });
  }
  
  static reject(reason) {
    return new PPromise((_, reject) => {
  		reject(reason);
  	});
  }
  
  // race也比较容易,可以联想到函数整体返回一个Promise,其中依次轮询Promise数组,有一个执行完成即整体执行完成
  static race(list) {
    // 当然,暂只考虑核心逻辑,其他非标准(异常)情况暂不考虑
  	return new PPromise((resolve, reject) => {
      list.forEach(item => {
        item.then(resolve, reject);
      });
    });
  }
  
  // all的基本套路与race相同,也是依次轮询Promise数组,稍微复杂之处在于需要知道all的特性
  // 即“全部成功才成功,有一个失败就失败”,且结果数组的顺序和Promise数组的顺序一一对应
  static all(list) {
    // 同样,暂只考虑核心逻辑
    return new PPromise((resolve, reject) => {
      const out = [];
      let len = 0;
      list.forEach((item, index) => {
        item.then(res => {
          out[index] = res;
          len += 1;
          len === list.length && resolve(out);
        }, reject);
      });      
    });   
  }
}

共勉。