使用 ES6 实现一个简单的 Promise

Promise 基本结构

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("FULFILLED");
  }, 1000);
});
p1.then(() => {}, () => {});

构造函数 Promise 必须接受一个函数作为参数,同时这个函数又包含 resolvereject 两个参数,这两个参数同样是函数。

Promise 对象存在三种状态,Pending(进行中)Fulfilled(已完成)Rejected(已完成)

resolve 的作用就是将 Promise 对象的状态从 Pending 转换为 Fulfilled。同时传递一个值出去。

reject 则是将 Promise 对象的状态从 Pending 转换为 Rejected。同时传递一个值(可以是错误)出去。

Promise 实例生成后,可以使用 then 指定 Fulfilled 状态和 Rejected 状态的回调函数。

我们定义一个函数,用来判断一个变量是否是函数——

const isFunction = variable => typeof variable === "function";

同时定义一个 MyPromise 的类——

const isFunction = variable => typeof variable === "function";

class MyPromise {
  constructor(fn) {
    if (!isFunction(fn)) {
      throw new Error("MyPromise must accept a function as a parameter");
    }
  }
  then(onFulfilled, onRejected) {}
}

定义 Promise 的三种状态,我们可以借助 ES6 的 Symbol 对象,来生成一个唯一标识的值——

const PENDING = Symbol("pending");
const FULFILLED = Symbol("fulfilled");
const REJECTED = Symbol("rejected");

Symbol 生成的值必定是独一无二的,Symbol 函数的的参数仅仅作为控制台输出时的解释说明,并不会影响到 Symbol 的返回值的内容。

MyPromise 添加状态和值——

const isFunction = variable => typeof variable === "function";

const PENDING = Symbol("pending");
const FULFILLED = Symbol("fulfilled");
const REJECTED = Symbol("rejected");

class MyPromise {
  constructor(fn) {
    if (!isFunction(fn)) {
      throw new Error("MyPromise must accept a function as a parameter");
    }
    this.status = PENDING;
    this.value = undefined;
  }
  then(onFulfilled, onRejected) {}
}

但是这样会出现一个问题,MyPromise 实例化后能够从类的外部获得 statusvalue,并且能够从外界进行篡改。一般来说,应该将这个值设置为私有属性。

ES6 中 Class 并不支持私有属性,虽然有一个提案是使用 # 前缀来表示这个值为私有属性,但是有些还不能支持(虽然 babel 已经支持了)

设置私有属性有几种方法——

  1. 使用 # ,但是在没有 Babel 的情况下无法使用
class MyClass {
  #x;
  constructor(v) {
    #x = v;
  }
}
  1. 在类外声明一个对象,用对象存储属性,一般使用 ES6 的 WeakMap ,因为 WeakMap 的键名所引用的对象都是弱引用,即垃圾回收机制不会将该引用考虑在内(引用计数垃圾收集,不会将存储在 WeakMap 中的内容认为是被 WeakMap 引用)(
const __ = new WeakMap();
function get(that, key) {
  try {
    return __.get(that)[key];
  } catch (e) {
    console.error(e);
    return undefined;
  }
}
function set(that, key, value) {
  const lastValue = __.get(that);
  const newValue = {
    ...lastValue,
    [key]: value
  };
  __.set(that, newValue);
}
class MyClass {
  constructor(v) {
    set(this, x, v); // this.x = v; 但是这个 x 是私有属性,外界无法得到
  }
  anyWay() {
    return get(this, x);
  }
}
  1. 借助 Symbol,类外因为无法拿到这个 Symbol 返回值所以无法使用该属性
const x = Symbol("x");
class MyClass {
  constructor(v) {
    this[x] = v;
  }
  anyWay() {
    return this[x];
  }
}

这里我们采取第二种方法

const isFunction = variable => typeof variable === "function";

const __ = new WeakMap();
function get(that, key) {
  try {
    return __.get(that)[key];
  } catch (e) {
    console.error(e);
    return undefined;
  }
}
function set(that, key, value) {
  const lastValue = __.get(that);
  const newValue = {
    ...lastValue,
    [key]: value
  };
  __.set(that, newValue);
}

const PENDING = Symbol("pending");
const FULFILLED = Symbol("fulfilled");
const REJECTED = Symbol("rejected");

const promiseStatus = Symbol("status");
const promiseValue = Symbol("value");

class MyPromise {
  constructor(fn) {
    if (!isFunction(fn)) {
      throw new Error("MyPromise must accept a function as a parameter");
    }
    set(this, promiseStatus, PENDING);
    set(this, promiseValue, undefined);
  }
  then(onFulfilled, onRejected) {}
}

因为 then 有两个参数,onFulfilledonRejected,都是可选参数。

如果 onFulfilledonRejected 都不是函数,其必须被忽略。

如果 onFulfilled 是函数,当 Promise 状态变为成功时必定调用,且第一个参数应该是 Promise 成功状态 resolve(val) 是传入的值(即 value);在 Promise 状态改变前其不可被调用;被调用的次数不超过一次。

onRejected 同理。

then 方法可以被同一个 Promise 对象调用多次,而且所有的 onFulfilledonRejected 会按照注册顺序依次回调。

const p1 = new Promise(() => {});
p1.then(() => {});
p1.then(() => {});

then 必须返回一个新的 Promise,支持链式调用。

const p1 = new Promise(() => {});
const p2 = p1.then(() => {}).then(() => {});

Promise 的执行规则——

值的传递

如果 onFulfilled 或者 onRejected 返回一个 x——

  • xPromise,则后一个回调函数,就会等待该 Promise 对象的状态改变,才会被调用,并且新的 Promise 状态和 x 的状态相同。
  • x 不为 Promise,则将 x 直接作为新返回的 Promise 对象的传递出去的值,即新的 onFulfilled 或者 onRejected 的参数。
const p1 = new Promise(resolve => {
  setTimeout(() => {
    resolve();
  }, 1000);
});
p1.then(() => {
  return "普通值";
}).then(res => {
  console.log(res); // 普通值
});

p1.then(() => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Promise");
    }, 1000);
  });
}).then(res => {
  console.log(res); // Promise
});

但如果在 onFulfilled 或者 onRejected 中抛出一个异常 e,则 then 返回的 Promise 的状态将变为 Rejected,并传递出 e

const p1 = new Promise(resolve => {
  setTimeout(() => {
    resolve("success");
  }, 1000);
})
  .then(() => {
    throw new Error("这里抛出一个异常");
  })
  .then(
    res => {
      console.log(res);
    },
    err => {
      console.log(err); // 这里抛出一个异常
    }
  );

因为可能出现【使用 then 绑定的 onFulfilledonRejected 的时候,Promise 的状态依旧是 Pending】的情况,所以我们需要使用两个数组去维护他们,同样借助 WeakMap 去维护他们

const isFunction = variable => typeof variable === "function";

const __ = new WeakMap();
function get(that, key) {
  try {
    return __.get(that)[key];
  } catch (e) {
    console.error(e);
    return undefined;
  }
}
function set(that, key, value) {
  const lastValue = __.get(that);
  const newValue = {
    ...lastValue,
    [key]: value
  };
  __.set(that, newValue);
}

const PENDING = Symbol("pending");
const FULFILLED = Symbol("fulfilled");
const REJECTED = Symbol("rejected");

const promiseStatus = Symbol("status");
const promiseValue = Symbol("value");
const fulfilledCallbackQueue = Symbol("fulfilledCallbackQueue");
const rejectedCallbackQueue = Symbol("rejectedCallbackQueue");

const resolve = Symbol("resolve");
const reject = Symbol("reject");

class MyPromise {
  constructor(fn) {
    if (!isFunction(fn)) {
      throw new Error("MyPromise must accept a function as a parameter");
    }
    set(this, promiseStatus, PENDING);
    set(this, promiseValue, undefined);
    set(this, fulfilledCallbackQueue, []);
    set(this, rejectedCallbackQueue, []);
    try {
      fn(this[resolve].bind(this), this[reject].bind(this));
    } catch (e) {
      this[reject](e);
    }
  }

  [resolve](val) {}
  [reject](err) {}

  then(onFulfilled, onRejected) {}
}

因为实例化 Promise 时传入的回调函数包含 resolvereject ,这两个应该在 Promise 中自己实现,并且在 Promise 的构造函数中调用这个回调函数并且传入 resolvereject

这里就涉及到 ES6 中 Class 构建私有方法的问题,一般来说有以下几种方法——

  1. 命名上区分,即在命名前面加一个 _ 表示这应该是一个私有方法,而且不希望外界能够调用这个方法
class MyClass {
  _anyWay() {}
}

但其实类的外部依旧可以调用这个方法。

  1. 将私有方法移出类
function anyWay() {}

class MyClass {
  foo() {
    anyWay.call(this);
  }
}
  1. 借助 Symbol
const anyWay = Symbol("private func");

class MyClass {
  [anyWay]() {}
}

我们这里采取的是第 3 种方案

const isFunction = variable => typeof variable === "function";

const __ = new WeakMap();
function get(that, key) {
  try {
    return __.get(that)[key];
  } catch (e) {
    console.error(e);
    return undefined;
  }
}
function set(that, key, value) {
  const lastValue = __.get(that);
  const newValue = {
    ...lastValue,
    [key]: value
  };
  __.set(that, newValue);
}

const promiseStatus = Symbol("status");
const promiseValue = Symbol("value");
const fulfilledCallbackQueue = Symbol("fulfilledCallbackQueue");
const rejectedCallbackQueue = Symbol("rejectedCallbackQueue");

const PENDING = Symbol("pending");
const FULFILLED = Symbol("fulfilled");
const REJECTED = Symbol("rejected");

const resolve = Symbol("resolve");
const reject = Symbol("reject");

class MyPromise {
  constructor(fn) {
    if (!isFunction(fn)) {
      throw new Error("MyPromise must accept a function as a parameter");
    }
    set(this, promiseStatus, PENDING);
    set(this, promiseValue, undefined);
    set(this, fulfilledCallbackQueue, []);
    set(this, rejectedCallbackQueue, []);
    try {
      fn(this[resolve].bind(this), this[reject].bind(this));
    } catch (e) {
      this[reject](e);
    }
  }

  [resolve](val) {
    const run = () => {
      if (get(this, promiseStatus) !== PENDING) return;
      const runFulfilled = value => {
        let cb;
        while ((cb = get(this, fulfilledCallbackQueue).shift())) {
          cb(value);
        }
      };
      const runRejected = error => {
        let cb;
        while ((cb = get(this, rejectedCallbackQueue).shift())) {
          cb(error);
        }
      };
      if (val instanceof MyPromise) {
        val.then(
          value => {
            set(this, promiseValue, value);
            set(this, promiseStatus, FULFILLED);
            runFulfilled(value);
          },
          err => {
            set(this, promiseValue, err);
            set(this, promiseStatus, REJECTED);
            runRejected(err);
          }
        );
      } else {
        set(this, promiseValue, val);
        set(this, promiseStatus, FULFILLED);
        runFulfilled(val);
      }
    };
    setTimeout(run, 0);
  }
  [reject](err) {
    if (get(this, promiseStatus) !== PENDING) return;
    const run = () => {
      set(this, promiseStatus, REJECTED);
      set(this, promiseValue, err);
      let cb;
      while ((cb = get(this, rejectedCallbackQueue).shift())) {
        cb(err);
      }
    };
    setTimeout(run, 0);
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      const fulfilled = value => {
        try {
          if (!isFunction(onFulfilled)) {
            onFulfilledNext(value);
          } else {
            const res = onFulfilled(value);
            if (res instanceof MyPromise) {
              res.then(onFulfilledNext, onRejectedNext);
            } else {
              onFulfilledNext(res);
            }
          }
        } catch (err) {
          onRejectedNext(err);
        }
      };
      const rejected = error => {
        try {
          if (!isFunction(onRejected)) {
            onRejectedNext(error);
          } else {
            const res = onRejected(error);
            if (res instanceof MyPromise) {
              res.then(onFulfilledNext, onRejectedNext);
            } else {
              onFulfilledNext(res);
            }
          }
        } catch (err) {
          onRejectedNext(err);
        }
      };
      switch (get(this, promiseStatus)) {
        case PENDING:
          const newFulfilledCallbackQueue = [
            ...get(this, fulfilledCallbackQueue),
            fulfilled
          ];
          set(this, fulfilledCallbackQueue, newFulfilledCallbackQueue);
          const newRejectedCallbackQueue = [
            ...get(this, rejectedCallbackQueue),
            rejected
          ];
          set(this, rejectedCallbackQueue, newRejectedCallbackQueue);
          break;
        case FULFILLED:
          fulfilled(get(this, promiseValue));
          break;
        case REJECTED:
          rejected(get(this, promiseValue));
          break;
      }
    });
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容