會(huì)用不如會(huì)造 - Promise

Promise ?

為什么要自己造 Promise

  • Promise已經(jīng)是Ecmascript 6標(biāo)準(zhǔn)中的內(nèi)容
  • Promise是async & await的基礎(chǔ)
  • 不造一下亭病,怎么才能把Promise/A+的標(biāo)準(zhǔn)了解的更透徹?

本文結(jié)構(gòu)

  • Promise認(rèn)知 - 40%
  • Promise輪子 - 60%

對(duì)Promise有足夠了解的讀者雇卷,可酌情閱讀跳過(guò)認(rèn)知部分。

Promise的認(rèn)知階梯

最早開(kāi)始使用Promise是為了不寫(xiě)回調(diào)形式的函數(shù)崎淳;后來(lái)發(fā)現(xiàn)Promise可以讓nodejs里function (err, data)的寫(xiě)法變得更友好柜与;后來(lái)開(kāi)始用 q;再后來(lái)發(fā)現(xiàn) bluebird 這樣順應(yīng) Promise/A+ 的API用起來(lái)更順悄蕾。然后發(fā)現(xiàn)bluebird這個(gè)詞原本就是一個(gè)combinator票顾,compose其實(shí)是它的本意;然后發(fā)現(xiàn)Promise也是一種Monad帆调。

以上是筆者自己對(duì)Promise的認(rèn)知之路奠骄,理解上不斷產(chǎn)生新的變化,其中真正可以提煉出來(lái)的步驟如下:

Phase 1 - Callback Hell 救星

首先番刊,callback是異步操作的產(chǎn)物含鳞,同步操作不需要回調(diào)函數(shù)。早先在純?yōu)g覽器環(huán)境中芹务,會(huì)用到callback的有:

  • ajax請(qǐng)求
  • DOM事件綁定
  • setTimeout

從nodejs開(kāi)始蝉绷,異步IO是根本特性鸭廷,因而callback變得無(wú)處不在,然后出現(xiàn)了nodejs的callback參數(shù)標(biāo)準(zhǔn):function (err, data)

/*
 * callback version
 */
var sillyCopy = function (src, target, callback) {
fs.readFile(src, function (err, data) {
  if (err)  return callback(err);
  fs.write(target, data, function (err) {
    if (err) return callback(err);
    fs.write(src, 'this is silly', function (err) {
      callback(err);
    });
  });
});

sillyCopy('my/dir/src.txt', 'my/dir/target.txt', function (err) {
  if (err)  console.log(err);
  else     console.log('done');
});

/*
 * promise version
 */
var sillyCopy = function (src, target) {
  return promiseFS.readFile(src)
  .then(function (data) {
    return promiseFS.writeFile(target));
  })
  .then(function() {
    return promiseFS.writeFile(src, 'this is silly');
  });
};

sillyCopy('my/dir/src.txt', 'my/dir/target.txt')
.then(function (data) {
  console.log(data);
}, function (err) {
  console.log(err);
});

注: 以上 promise 版本的 sillyCopy 中用到了promise化的 fs熔吗。

可以看到辆床,使用了promise之后的異步操作定義,變得更加更加清晰

Phase 2 - 串聯(lián)異步操作

var flow = function (list) {
  return function (arg) {
    return list.reduce(function (p, fn) {
      return p.then(fn);
    }, Promise.resolve(arg));
};

var sillyProcess = flow([
  pseudo_ReadFile,
  pseudo_AjaxPostSearchFileText,
  pseudo_ReadDB,
  pseudo_WriteToLocalFile
])

這里的flow就是同步模式下用到的 compose桅狠,用于將各種異步操作進(jìn)行串聯(lián)讼载,在充滿異步操作充滿Promise的環(huán)境中,一個(gè)簡(jiǎn)單的flow實(shí)現(xiàn)可以讓代碼變得清晰易懂

Phase 3 - 異常捕獲

回看之前的樣例代碼垂攘,callback形式會(huì)讓對(duì)異常的捕獲散布在各層級(jí)的callback里维雇,看著鬧心有沒(méi)有?

var sillyCopy = function (src, target, callback) {
fs.readFile(src, function (err, data) {
  if (err)  return callback(err);
  fs.write(target, data, function (err) {
    if (err) return callback(err);
    fs.write(src, 'this is silly', function (err) {
      callback(err);
    });
  });
});

// This is what we want
sillyCopy(src, target)
.catch(function (err) {
  console.log(err);
});

在實(shí)際Coding過(guò)程中晒他,異常處理是非常重要的步驟吱型,除了上面看到的串行的異步代碼,還有并行的異步操作陨仅,各種復(fù)雜情況導(dǎo)致對(duì)異常的統(tǒng)一處理變得十分關(guān)鍵津滞,僅異常處理的代碼會(huì)變得更容易維護(hù)。

Phase 4 - Thenable 接口

Thenable在一般場(chǎng)景中我們碰到的不算特別多灼伤,但它的價(jià)值卻不能被低估触徐。Thenable的價(jià)值在于,它是一個(gè)統(tǒng)一的接口定義狐赡,它讓我們可以把不同Library里的撞鹉、使用不同Promise實(shí)現(xiàn)的異步操作,放在一起使用颖侄。

例如鸟雏,jQuery有它自己內(nèi)部對(duì)Promise的實(shí)現(xiàn) (早起使用 defer 創(chuàng)建 Promise 對(duì)象 ,后來(lái)慢慢統(tǒng)一到了 Promise/A+ 的標(biāo)準(zhǔn)模式)览祖。還有 npm 上成千上萬(wàn)的包孝鹊,都可能使用了不同的 Promise 實(shí)現(xiàn)。


開(kāi)始造 Promise

既然是造輪子展蒂,先看下按照什么標(biāo)準(zhǔn)來(lái)造又活,然后確定一下造的步驟,剩下的就是開(kāi)始coding了锰悼。

標(biāo)準(zhǔn)

步驟

我們把實(shí)現(xiàn)過(guò)程分成3部分柳骄,類(lèi)似于對(duì)Promise的認(rèn)知過(guò)程

  1. 串聯(lián)異步操作,忽略異常處理
  • 構(gòu)造函數(shù)
  • 保留 resolve 結(jié)果
  • then箕般,要滿足 resolve 之后的 then 也能得到執(zhí)行
  1. 異常處理
  • then增加reject參數(shù)
  • 捕獲 resolve 和 reject 過(guò)程中的異常
  1. Thenable
  • 允許返回一個(gè) Thenable 的對(duì)象
  • 對(duì) Thenable.then(resolve, reject) 的整體過(guò)程進(jìn)行異常捕獲

串聯(lián)異步操作

  • 一個(gè)Promise對(duì)象可以多次調(diào)用 then
  • 保存 resolve 結(jié)果
var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = 0;

  var resolve = function (value) {
    self._state = 1;
    self._value = value;
    self.next.forEach(function (fn) {
      fn(value);
    });
  };
  executor(resolve);
};

MyPromise.prototype.then = function (onResolve) {
  var self = this;

  if (!self._state) {
    return MyPromise.resolve(onResolve(self._value));
  }

  return new Promise(function (resolve) {
    self.next.push(function (value) {
      resolve(onResolve(value));
    });
  });
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve) {
    resolve(value);
  });
};

/*
 * Demo
 */
var p = Promise.resolve(2);

p
.then(function (a) { return a * 2 })
.then(function (a) { console.log(a) });
// output: 4

setTimeout(function () {
  p
  .then(function (a) { return a * 3 })
  .then(function (a) { console.log(a) });
  // output: 6
}, 100);

至此夹界,串聯(lián)異步操作部分就算完成了,一個(gè)不帶 reject 和 異常捕獲的 Thenable 實(shí)例隘世。從上面的 Demo 可以看出可柿,我們的 Promise API 已初具雛形。但不要得意的太早丙者,Promise 的 API 本身并不復(fù)雜:

  • Promise API
    • Promise Constructor
    • Promise.prototype.then
    • Promise.prototype.catch
    • Promise.all
    • Promise.resolve
    • Promise.reject

不過(guò)同等功能下复斥,越是簡(jiǎn)單的 API 設(shè)計(jì),其內(nèi)部邏輯就會(huì)越復(fù)雜械媒。jQuery.$ 就是一個(gè)很好的例子目锭。

異常捕獲

  • then 接受兩個(gè)參數(shù), onResolve & onReject
  • 對(duì) onResolve 和 onReject 執(zhí)行過(guò)程中產(chǎn)生的異常進(jìn)行捕獲
var PENDING = 0;
var RESOLVED = 1;
var REJECTED = 2;

// Note: onResolve 和 onReject 都會(huì)在這里包一層
// 為了讓兩者的返回值都能繼續(xù)向下傳遞到下一個(gè) onResolve
var wrapHandler = function (state, handler, p2) {
  return function (val) {
    var next = state == RESOLVED ? p2._resolve : p2._reject;
    var ret, then, type, p3;

    if (!handler || typeof handler !== 'function') {
      return next(val);
    }

    try {
      ret = handler(val);
    } catch (e) {
      return p2._reject(e);
    }

    p2._resolve(ret);
  };
};

var genHandler = function (state, p) {
  return function (value) {
    if (p._state !== PENDING) return;
    p._state = state;
    p._value = value;
    p.next.forEach(function (obj) {
      setTimeout(function () {
        obj[state === RESOLVED ? 'onResolve' : 'onReject'](value);
      }, 0);
    });
  };
};

var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = PENDING;
  this._resolve = genHandler(RESOLVED, self);
  this._reject  = genHandler(REJECTED, self);

  executor(this._resolve, this._reject);
};

MyPromise.prototype.then = function (onResolve, onReject) {
  var self = this;
  var p2   = new MyPromise(function () {});
  var ret;

  if (self._state === PENDING) {
    self.next.push({
      onResolve: wrapHandler(RESOLVED, onResolve, p2),
      onReject:  wrapHandler(REJECTED, onReject,  p2)
    });
  } else {
    setTimeout(function () {
      wrapHandler(self._state, self._state === RESOLVED ? onResolve : onReject, p2)(self._value);
    }, 0);
  }

  return p2;
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  });
};

MyPromise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

/*
 * Demo
 */
MyPromise.reject(1)
.then(null, function (e) {
  console.log(e);
  return e + 2;
})
.then(function (n) {
  console.log(n);
  throw n * 2;
})
.then(null, function (e) {
  console.log(e);
});
// output: 1
// output: 3
// output: 6

這里有幾個(gè)非常重要的 Promise 使用方法:

  • 沒(méi)有設(shè)置 onResolve 或 onReject纷捞,對(duì)應(yīng)的返回值或異常將原封不動(dòng)向下傳遞
    Promise.resolve(1)
    .then()
    .then(function (n) { console.log(n); });
    // output: 1
    
    Promise.reject(2)
    .then()
    .then(null, function (e) { console.log(e); });
    // output: 2
    
  • onReject 的返回值會(huì)繼續(xù)向下傳遞給下一個(gè) onResolve
    Promise.reject(1)
    .then(null, function (e) { return e + 2; })
    .then(function (n) { console.log(n); };
    // output: 3
    

Thenable 接口

  • Thenable 類(lèi)型的處理
  • 出現(xiàn)返回Promise實(shí)例自身的情況痢虹,要拋出 TypeError
var PENDING = 0;
var RESOLVED = 1;
var REJECTED = 2;

var wrapHandler = function (state, handler, p2) {
  return function (val) {
    var next = state == RESOLVED ? p2._resolve : p2._reject;
    var ret, then, type, p3;

    if (!handler || typeof handler !== 'function') {
      return next(val);
    }

    try {
      ret = handler(val);
    } catch (e) {
      return p2._reject(e);
    }

    solve(ret, p2);
  };
};

// 處理所有可能情況的 val,包括 Thenable
var solve = function (val, p2) {
  if (val === p2) {
    return p2._reject(new TypeError('no promise circle allowed'));
  }

  type = typeof val;

  if (type === 'function' || type === 'object' && val !== null) {
    try {
      then = val && val.then;
    } catch (e) {
      return p2._reject(e);
    }

    if (typeof then === 'function') {
      try {
        return then.call(val, function (val2) {
          solve(val2, p2);
        }, p2._reject);
      } catch (e) {
        return p2._reject(e);
      }
    }
  }

  return p2._resolve(val);
}

var genHandler = function (state, p) {
  return function (value) {
    if (p._state !== PENDING) return;
    p._state = state;
    p._value = value;
    p.next.forEach(function (obj) {
      setTimeout(function () {
        obj[state === RESOLVED ? 'onResolve' : 'onReject'](value);
      }, 0);
    });
  };
};

var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = PENDING;
  this._resolve = genHandler(RESOLVED, self);
  this._reject  = genHandler(REJECTED, self);

  executor(this._resolve, this._reject);
};

MyPromise.prototype.then = function (onResolve, onReject) {
  var self = this;
  var p2   = new MyPromise(function () {});
  var ret;

  if (self._state === PENDING) {
    self.next.push({
      onResolve: wrapHandler(RESOLVED, onResolve, p2),
      onReject:  wrapHandler(REJECTED, onReject,  p2)
    });
  } else {
    setTimeout(function () {
      wrapHandler(self._state, self._state === RESOLVED ? onResolve : onReject, p2)(self._value);
    }, 0);
  }

  return p2;
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  });
};

MyPromise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

到此為止主儡,一個(gè)滿足 Promise/A+ 規(guī)范的 MyPromise 就算造完了奖唯。

完整代碼在我的 github 上可以找到

https://github.com/kdepp/mash/blob/master/mash.js

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市糜值,隨后出現(xiàn)的幾起案子丰捷,更是在濱河造成了極大的恐慌,老刑警劉巖寂汇,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件病往,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡骄瓣,警方通過(guò)查閱死者的電腦和手機(jī)停巷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)榕栏,“玉大人畔勤,你說(shuō)我怎么就攤上這事【矢啵” “怎么了硼被?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渗磅。 經(jīng)常有香客問(wèn)我嚷硫,道長(zhǎng),這世上最難降的妖魔是什么始鱼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任仔掸,我火速辦了婚禮,結(jié)果婚禮上医清,老公的妹妹穿的比我還像新娘起暮。我一直安慰自己,他們只是感情好会烙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布负懦。 她就那樣靜靜地躺著筒捺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纸厉。 梳的紋絲不亂的頭發(fā)上系吭,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音颗品,去河邊找鬼肯尺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躯枢,可吹牛的內(nèi)容都是我干的则吟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼锄蹂,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼氓仲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起败匹,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤寨昙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后掀亩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體舔哪,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年槽棍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捉蚤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炼七,死狀恐怖缆巧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情豌拙,我是刑警寧澤陕悬,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站按傅,受9級(jí)特大地震影響捉超,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唯绍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一拼岳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧况芒,春花似錦惜纸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)祠够。三九已至,卻和暖如春椭更,著一層夾襖步出監(jiān)牢的瞬間哪审,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工虑瀑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滴须。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓舌狗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親扔水。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痛侍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 你不知道JS:異步 第三章:Promises 在第二章,我們指出了采用回調(diào)來(lái)表達(dá)異步和管理并發(fā)時(shí)的兩種主要不足:缺...
    purple_force閱讀 2,070評(píng)論 0 4
  • 本文適用的讀者 本文寫(xiě)給有一定Promise使用經(jīng)驗(yàn)的人魔市,如果你還沒(méi)有使用過(guò)Promise主届,這篇文章可能不適合你,...
    HZ充電大喵閱讀 7,313評(píng)論 6 19
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持待德,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券君丁,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 11,028評(píng)論 26 95
  • 你離開(kāi)的第二十五天,總覺(jué)得你還在身邊较坛,又覺(jué)得你已經(jīng)走了好久印蔗,平生第一次真正覺(jué)得,時(shí)間過(guò)的太慢丑勤,原來(lái)你不在华嘹,日子真的...
    阿卿卿閱讀 189評(píng)論 0 1
  • (野蠻生長(zhǎng)社2.字?jǐn)?shù)1140) 17歲的時(shí)候,我的夢(mèng)想是當(dāng)個(gè)作家法竞。當(dāng)時(shí)每個(gè)星期至少...
    不息_閱讀 282評(píng)論 2 1