聊一聊promise的前世今生
promise的概念已經(jīng)出現(xiàn)很久了,瀏覽器、nodejs都已經(jīng)全部實(shí)現(xiàn)promise了。現(xiàn)在來聊,是不是有點(diǎn)過時(shí)了?
確實(shí),如果不扯淡,這篇隨筆根本不會(huì)有太多內(nèi)容。所以,我就盡可能的,多扯一扯,聊一聊promise的另一面。
大家應(yīng)該都知道怎么創(chuàng)建一個(gè)promise
var promise = new Promise(resolve => {setTimeout(() => resolve('tarol'), 3000) });如果從業(yè)時(shí)間長(zhǎng)一點(diǎn),會(huì)知道以前的promise不是這么創(chuàng)建的。比如如果你用過jquery,jquery在1.5引入deferred的概念,里面是這樣創(chuàng)建promise的
var defer = $.Deferred(); var promise = defer.promise();如果你用過angular,里面有個(gè)promise service叫$q,它又是這么創(chuàng)建promise的
var defer = $q.defer(); var promise = defer.promise;好了,這里已經(jīng)有三種創(chuàng)建promise的方式了。其中第一種是現(xiàn)在最常見的,第二種和第三種看上去很像,但卻有細(xì)微的差別。比如jquery里面是通過執(zhí)行函數(shù)promise()返回promise,而angular中defer的屬性就是promise。如果你還有興趣,那么我從頭開始講。
promise的引入是為了規(guī)范化異步操作,隨著前端的邏輯越來越復(fù)雜,異步操作的問題越來越亟待解決。首先大量的異步操作形成了N級(jí)的大括號(hào),俗稱“回調(diào)地獄”;其次callback的寫法沒有標(biāo)準(zhǔn),nodejs里面的callback一般是(err, data) => {...},jquery里面的success callback又是data => {...}。在這種場(chǎng)景下,很多異步流程控制的類庫應(yīng)運(yùn)而生。
作為前端,一般最早接觸promise的概念是在jquery的1.5版本發(fā)布的deferred objects。但是前端最早引入promise的概念的卻不是jquery,而是dojo,而且promise之所以叫promise也是因?yàn)閐ojo。Promises/A標(biāo)準(zhǔn)的撰寫者KrisZyp于09年在google的CommonJS討論組發(fā)了一個(gè)貼子,討論了promise API的設(shè)計(jì)思路。他聲稱想將這類API命名為future,但是dojo已經(jīng)實(shí)現(xiàn)的deferred機(jī)制中用到了promise這個(gè)術(shù)語,所以還是繼續(xù)使用promise為此機(jī)制命名。之后便有了CommonJS社區(qū)的這個(gè)proposal,即Promises/A。如果你對(duì)什么是deferred,什么是promise還存在疑問,不要急,先跳過,后面會(huì)講到。
Promises/A是一個(gè)非常簡(jiǎn)單的proposal,它只闡述了promise的基本運(yùn)行規(guī)則
如果你研究過現(xiàn)在瀏覽器或nodejs的promise,你會(huì)發(fā)現(xiàn)Promises/A好像處處相似,但又處處不同。比如三種狀態(tài)是這個(gè)叫法嗎?progressHandler沒見過啊!get、call又是什么鬼?前面兩個(gè)問題可以先放一放,因?yàn)楹竺鏁?huì)做出解答。第三個(gè)問題這里解釋下,什么是get,什么是call,它們的設(shè)計(jì)初衷是什么,應(yīng)用場(chǎng)景是什么?雖然現(xiàn)在你輕易見不到它們了,但是了解它們有助于理解后面的部分內(nèi)容。
一般來說,promise調(diào)用鏈存在兩條管道,一條是promise鏈,就是下圖一中的多個(gè)promise,一條是回調(diào)函數(shù)中的值鏈,就是下圖二中的多個(gè)value或reason
現(xiàn)在我們都知道,值鏈中前一個(gè)callback(callback1)的返回值是后一個(gè)callback(callback2)的入?yún)?#xff08;這里僅討論簡(jiǎn)單值類型的fulfilled的情況)。但是如果我callback1返回的是a,而callback2的入?yún)⑽蚁M莂.b呢?或許你可以說那我callback1返回a.b就是了,那如果callback1和callback2都是固定的業(yè)務(wù)算法,它們的入?yún)⒑头祷囟际枪潭ǖ?#xff0c;不能隨便修改,那又怎么辦呢?如果promise只支持then,那么我們需要在兩個(gè)then之間插入一個(gè)新的then:promise.then(callback1).then(a => a.b).then(callback2)。而get解決的就是這個(gè)問題,有了get后,可以這么寫:promise.then(callback1).get('b').then(callback2),這樣promise鏈條中就可以減少一些奇怪的東西。同理,當(dāng)a.b是一個(gè)函數(shù),而callback2期望的入?yún)⑹莂.b(c),那么可以這樣寫:promise.then(callback1).call('b', c).then(callback2)。
我們回到之前的話題,現(xiàn)在常見的promise和Promise/A到底是什么關(guān)系,為什么會(huì)有花非花霧非霧的感覺?原因很簡(jiǎn)單,常見的promise是參照Promises/A的進(jìn)階版——Promises/A+定義的。
Promises/A存在一些很明顯的問題,如果你了解TC39 process或者RFC等標(biāo)準(zhǔn)審核流程,你會(huì)發(fā)現(xiàn):
Promises/A+就是基于這樣的問題產(chǎn)生的,要說明的是Promises/A+的維護(hù)者不再是前面提到的KrisZyp,而是由一個(gè)組織維護(hù)的。
組織的成員如下,其中圈出來的另一個(gè)Kris需要留意一下,之后還會(huì)提到他。
Promises/A+在Promises/A的基礎(chǔ)上做了如下幾點(diǎn)修正:
Promises/A+沒有新增任何API,而且刪掉了Promises/A的部分冗余設(shè)計(jì)。這樣一來,Promises/A+其實(shí)只規(guī)定了,promise對(duì)象必須包含指定算法的方法then。接下來我會(huì)歸整下所謂的then算法,以及它存在哪些不常見的調(diào)用方式。
then的基本調(diào)用方式:promise.then(onFulfilled, onRejected),我默認(rèn)你已經(jīng)掌握了基礎(chǔ)的then調(diào)用,所以常見的場(chǎng)景以下不做舉例。Promise.resolve('resolve').then().then(value => console.log(value)) // resolve Promise.reject('reject').then().then(void 0, reason => console.log(reason)) //reason
這個(gè)特性決定了我們現(xiàn)在可以這樣寫異常處理
Promise.reject('reason').then(v => v).then(v => v).then(v => v).catch(reason => console.log(reason)) //reason但是如果你在then鏈條中,插入一個(gè)空的onRejected,reason就流不到catch了。因?yàn)閛nRejected返回了undefined,下一個(gè)promise處于fulfilled態(tài)
Promise.reject('reason').then(v => v).then(v => v).then(v => v, () => {}).catch(reason => console.log(reason))
var name = 'tarol'; var person = {name: 'okal',say: function() {console.log(this.name);} } person.say(); //okal Promise.resolve('value').then(person.say); //tarol
如果你想第二行還是打印出'okal',請(qǐng)使用bind
Promise.resolve('value').then(person.say.bind(person)); //okal
onFulfilled或者onRejected中拋出異常,則promise2狀態(tài)置為rejected
上面的例子中,onFulfilled或者onRejected如果返回了任意值x(如果不存在return語句,則是返回undefined),則進(jìn)入解析過程[[Resolve]](promise2, x)
解析過程[[Resolve]](promise2, x)算法如下var obj = {get then() {throw 'err'}}; Promise.resolve('value').then(v => obj).catch(reason => console.log(reason)); // err
Promise.resolve('value').then(v => {return {name: 'tarol',then: void 0} }).then(v => console.log(v.name)); //tarol
如果then是函數(shù),那么x就是一個(gè)thenable,then會(huì)被立即調(diào)用,傳入?yún)?shù)resolve和reject,并綁定x作為this。
Promise.resolve('value').then(v => {return {then: (resolve, reject) => {throw 'err';resolve('resolve');}} }).then(v => console.log(v), r => console.log(r)); // err
上面的例子中涉及到一個(gè)重要的概念,就是thenable。簡(jiǎn)單的說,thenable是promise的鴨子類型。什么是鴨子類型?搜索引擎可以告訴你更詳盡的解釋,長(zhǎng)話短說就是“行為像鴨子那么它就是鴨子”,即類型的判斷取決于對(duì)象的行為(對(duì)象暴露的方法)。放到promise中就是,一個(gè)對(duì)象如果存在then方法,那么它就是thenable對(duì)象,可以作為特殊類型(promise和thenable)進(jìn)入promise的值鏈。
promise和thenble如此相像,但是為什么在解析過程[[Resolve]](promise2, x)中交由不同的分支處理?那是因?yàn)殡m然promise和thenable開放的接口一樣,但過程角色不一樣。promise中then的實(shí)現(xiàn)是由Promises/A+規(guī)定的(見then算法),入?yún)nFulfilled和onRejected是由開發(fā)者實(shí)現(xiàn)的。而thenable中then是由開發(fā)者實(shí)現(xiàn)的,入?yún)esolve和reject的實(shí)現(xiàn)是由Promises/A+規(guī)定的(見then算法3.3.3)。thenable的提出其實(shí)是為了可擴(kuò)展性,其他的類庫只要實(shí)現(xiàn)了符合Promises/A+規(guī)定的thenable,都可以無縫銜接到Promises/A+的實(shí)現(xiàn)庫中。
Promises/A+先介紹到這里了。如果你細(xì)心,你會(huì)發(fā)現(xiàn)前面漏掉了一個(gè)關(guān)鍵的內(nèi)容,就是之前反復(fù)提到的如何創(chuàng)建promise。Promise/A+中并沒有提及,而在當(dāng)下來說,new Promise(resolver)的創(chuàng)建方式仿佛再正常不過了,普及程度讓人忘了還有deferred.promise這種方式。那么Promise構(gòu)造器又是誰提出來的,它為什么擊敗了deferred成為了promise的主流創(chuàng)建方式?
首先提出Promise構(gòu)造器的標(biāo)準(zhǔn)大名鼎鼎,就是es6。現(xiàn)在你見到的promise,一般都是es6的實(shí)現(xiàn)。es6不僅規(guī)定了Promise構(gòu)造函數(shù),還規(guī)定了Promise.all、Promise.race、Promise.reject、Promise.resolve、Promise.prototype.catch、Promise.prototype.then一系列耳熟能詳?shù)腁PI(Promise.try、Promise.prototype.finally尚未正式成為es標(biāo)準(zhǔn)),其中then的算法就是將Promises/A+的算法使用es的標(biāo)準(zhǔn)寫法規(guī)范了下來,即將Promises/A+的邏輯算法轉(zhuǎn)化為了es中基于解釋器API的具體算法。
那么為什么es6放棄了大行其道的deferred,最終敲定了Promise構(gòu)造器的創(chuàng)建方式呢?我們寫兩個(gè)demo感受下不同
var Q = require("q");var deferred = Q.defer();deferred.promise.then(v => console.log(v));setTimeout(() => deferred.resolve("tarol"), 3000);var p = new Promise(resolve => {setTimeout(() => resolve("tarol"), 3000); });p.then(v => console.log(v));
前者是deferred方式,需要依賴類庫Q;后者是es6方式,可以在nodejs環(huán)境直接運(yùn)行。
如果你習(xí)慣使用deferred,你會(huì)覺得es6的方式非常不合理:
首先,promise的產(chǎn)生的原因之一是為了解決回調(diào)地獄的問題,而Promise構(gòu)造器的方式在構(gòu)造函數(shù)中直接注入了一個(gè)函數(shù),如果這個(gè)函數(shù)在復(fù)雜點(diǎn),同樣存在一堆大括號(hào)。
其次,promise基于訂閱發(fā)布模式實(shí)現(xiàn),deferred.resolve/reject可以理解為發(fā)布器/觸發(fā)器(trigger),deferred.promise.then可以理解為訂閱器(on)。在多模塊編程時(shí),我可以在一個(gè)公共模塊創(chuàng)建deferred,然后在A模塊引用公共模塊的觸發(fā)器觸發(fā)狀態(tài)的切換,在B模塊引用公共模塊使用訂閱器添加監(jiān)聽者,這樣很方便的實(shí)現(xiàn)了兩個(gè)沒有聯(lián)系的模塊間互相通信。而es6的方式,觸發(fā)器在promise構(gòu)造時(shí)就生成了并且立即進(jìn)入觸發(fā)階段(即創(chuàng)建promise到promise被fulfill或者reject之間的過程),自由度減少了很多。
我一度很反感這種創(chuàng)建方式,認(rèn)為這是一種束縛,直到我看到了bluebird(Promise/A+的實(shí)現(xiàn)庫)討論組中某個(gè)帖子的解釋。大概說一下,回帖人的意思是,promise首先應(yīng)該是一個(gè)異步流程控制的解決方案,流程控制包括了正常的數(shù)據(jù)流和異常流程處理。而deferred的方式存在一個(gè)致命的缺陷,就是promise鏈的第一個(gè)promise(deferred.promise)的觸發(fā)階段拋出的異常是不交由promise自動(dòng)處理的。我寫幾個(gè)demo解釋下這句話
var Q = require("q");var deferred = Q.defer();deferred.promise.then(v => {throw 'err' }).catch(reason => console.log(reason)); // errsetTimeout(() => deferred.resolve("tarol"));以上是一個(gè)正常的異常流程處理,在值鏈中拋出了異常,自動(dòng)觸發(fā)下一個(gè)promise的onRejected。但是如果在deferred.promise觸發(fā)階段的業(yè)務(wù)流程中拋出了異常呢?
var Q = require("q");var deferred = Q.defer();deferred.promise.catch(reason => console.log(reason)); // 不觸發(fā)setTimeout(() => {throw "err";deferred.resolve("tarol"); });這個(gè)異常將拋出到最外層,而不是由promise進(jìn)行流程控制,如果想讓promise處理拋出的異常,必須這么寫
var Q = require("q");var deferred = Q.defer();deferred.promise.catch(reason => console.log(reason)); // errsetTimeout(() => {try {throw "err";} catch (e) {deferred.reject(e);} });deferred的問題就在這里了,在deferred.promise觸發(fā)階段拋出的異常,不會(huì)自動(dòng)交由promise鏈進(jìn)行控制。而es6的方式就簡(jiǎn)單了
var p = new Promise(() => {throw "err"; });p.catch(r => console.log(r)); // err可見,TC39在設(shè)計(jì)Promise接口時(shí),首先考慮的是將Promise看作一個(gè)異步流程控制的工具,而非一個(gè)訂閱發(fā)布的事件模塊,所以最終定下了new Promise(resolver)這樣一種創(chuàng)建方式。
但是如果你說:我不聽,我不聽,deferred就是比new Promise好,而且我的promise在觸發(fā)階段是不會(huì)拋出異常的。那好,還有另外一套標(biāo)準(zhǔn)滿足你,那就是Promises/B和Promises/D。其中Promises/D可以看做Promises/B的升級(jí)版,就如同Promises/A+之于Promises/A。這兩個(gè)標(biāo)準(zhǔn)的撰寫者都是同一個(gè)人,就是上面Promises/A+組織中圈起來的大胡子,他不僅維護(hù)了這兩個(gè)標(biāo)準(zhǔn),還寫了一個(gè)實(shí)現(xiàn)庫,就是上面提到的Q,同時(shí)angular中的$q也是參照Q實(shí)現(xiàn)的。
Promises/B和Promises/D(以下統(tǒng)稱為Promises/B)都位于CommonJS社區(qū),但是由于沒有被社區(qū)采用,處于廢棄的狀態(tài)。而Q卻是一個(gè)長(zhǎng)期維護(hù)的類庫,所以Q的實(shí)現(xiàn)和兩個(gè)標(biāo)準(zhǔn)已經(jīng)有所脫離,請(qǐng)知悉。
Promises/B和es6可以說是Promises/A+的兩個(gè)分支,基于不同的設(shè)計(jì)理念在Promises/A+的基礎(chǔ)上設(shè)計(jì)了兩套不同的promise規(guī)則。鑒于Promises/A+在創(chuàng)建promise上的空白,Promises/B同樣提供了創(chuàng)建promise的方法,而且是大量創(chuàng)建promise的方法。以下這些方法都由實(shí)現(xiàn)Promises/B的模塊提供,而不是Promises/B中promise對(duì)象的方法。
不知道以上API的應(yīng)用場(chǎng)景和具體用法不要緊,我們先總結(jié)一下。Promises/B和es6理念上最大的出入在于,es6更多的把promise定義為一個(gè)異步流程控制的模塊,而Promises/B更多的把promise作為一個(gè)流程控制的模塊。所以Promises/B在創(chuàng)建一個(gè)promise的時(shí)候,可以選擇使用makePromise創(chuàng)建一個(gè)純粹的操作數(shù)據(jù)的流程控制的promise,而get、post、put、del、reject、ref等都是通過調(diào)用makePromise實(shí)現(xiàn)的,是makePromise的上層API;也可以使用defer創(chuàng)建一個(gè)deferred,包含promise這個(gè)屬性,對(duì)應(yīng)一個(gè)延時(shí)類的promise。
延時(shí)類的promise經(jīng)過前面的解釋基本都了解用法和場(chǎng)景,那對(duì)數(shù)據(jù)進(jìn)行流程控制的promise呢?在上面Promises/A部分說明了get和call兩個(gè)API的用法和場(chǎng)景,Promises/B的get對(duì)應(yīng)的就是Promises/A的get,call對(duì)應(yīng)的是post。put/set是Promises/B新增的,和前二者一樣,在操作數(shù)據(jù)時(shí)進(jìn)行流程控制。比如在嚴(yán)格模式下,如果對(duì)象a的屬性b的writable是false。這時(shí)對(duì)a.b賦值,是會(huì)拋出異常的,如果異常未被捕獲,那么會(huì)影響后續(xù)代碼的運(yùn)行。
"use strict"; var a = {};Object.defineProperty(a, "name", {value: "tarol",writable: false });a.name = "okay";console.log("end"); // 不運(yùn)行這時(shí)候如果使用Q的put進(jìn)行流程控制,就可以把賦值這部分獨(dú)立開來,不影響后續(xù)代碼的運(yùn)行。
"use strict"; var Q = require("q");var a = {};Object.defineProperty(a, "name", {value: "tarol",writable: false });Q.set(a, "name", "okay").then(() => console.log("success"),() => console.log("fail") // fail );console.log("end"); // end這部分的應(yīng)用場(chǎng)景是否有價(jià)值呢?答案就是見仁見智了,好在Q還提供了makePromise這個(gè)底層API,自定義promise可以實(shí)現(xiàn)比增刪改查這些verbs更強(qiáng)大的功能。比如當(dāng)我做數(shù)據(jù)校驗(yàn)的時(shí)候可以這樣寫
var Q = require("q");var p = Q.makePromise({isNumber: function(v) {if (isNaN(v)) {throw new Error(`${v} is not a number`);} else {return v;}} });p.dispatch("isNumber", ["1a"]).then(v => console.log(`number is ${v}`)).catch(err => console.log("err", err)); // 1a is not a number p.dispatch("isNumber", ["1"]).then(v => console.log(`number is ${v}`)) // number is 1.catch(err => console.log("err", err));以上不涉及任何異步操作,只是用Q對(duì)某個(gè)業(yè)務(wù)功能做流程梳理而已。
而且Q并未和es6分家,而是在后續(xù)的版本中兼容了es6的規(guī)范(Q.Promise對(duì)應(yīng)es6中的全局Promise),成為了es6的父集,加之Q也兼容了Promises/A中被A+拋棄的部分,如progressHandler、get、call(post)。所以對(duì)于Q,你可以理解為promise規(guī)范的集大成者,整體來說是值得一用的。
最后要提到的是最為式微的promise規(guī)范——Promises/KISS,它的實(shí)現(xiàn)庫直接用futures命名,實(shí)現(xiàn)了KrisZyp未竟的心愿。如果比較github上的star,KISS甚至不如我沒有提及的then.js和when。但是鑒于和Q一樣,是有一定實(shí)踐經(jīng)驗(yàn)后CommonJS社區(qū)promise規(guī)范的提案,所以花少量的篇幅介紹一下。
Promises/KISS不將Promises/A作為子集,所以它沒有提供then作為訂閱器,代之的是when和whenever兩個(gè)訂閱器。觸發(fā)器也不是常見的resolve、reject,而是callback、errback和fulfill。其中callback類似于notify,即progressHandler的觸發(fā)器,errback類似于reject,fulfill類似于resolve。
為什么會(huì)有兩個(gè)訂閱器呢?因?yàn)镵ISS不像Promises/A,A中的then中是傳入三個(gè)監(jiān)聽器,其中progressHandler還可以多次觸發(fā)。但是KISS中的when和whenever一次只能傳入一個(gè)監(jiān)聽器,所以它要解決的是,同一種訂閱方式,怎么訂閱三種不同的監(jiān)聽器?
首先,怎么區(qū)分fulfilledHandler和errorHandler呢?KISS借鑒了nodejs的回調(diào)函數(shù)方式,第一個(gè)參數(shù)是err,第二個(gè)參數(shù)是data。所以fulfilledHandler和errorHandler在一個(gè)監(jiān)聽器里這樣進(jìn)行區(qū)分:
function(err, data) {if (err) {...} // errorHandlerelse {...} // fulfilledHandler }那怎么區(qū)分多次調(diào)用的progressHandler呢?使用when注冊(cè)的監(jiān)聽器只能調(diào)用一次,使用whenever注冊(cè)的監(jiān)聽器可以調(diào)用多次。我們寫個(gè)demo區(qū)分Q和KISS的API的不同:
var Q = require("q"); var defer = Q.defer(); defer.promise.then(v => console.log("fulfill", v),err => console.log("reject", err),progress => console.log("progress", progress) ); defer.notify(20); // progress 20 defer.notify(30); // progress 30 defer.notify(50); // progress 50 defer.resolve("ok"); // fulfill okvar future = require("future");var p = new future(); var progressHandler = function(err, progress) {if (err) {console.log("err", err);} else {console.log("progress", progress);} }; p.whenever(progressHandler); p.callback(20); // progress 20 p.callback(30); // progress 30 p.callback(50); // progress 50 p.removeCallback(progressHandler); // 需要移除監(jiān)聽器,不然fulfill時(shí)也會(huì)觸發(fā) p.when(function(err, v) { // 需要在callback調(diào)用后注冊(cè)fulfill的監(jiān)聽器,不然callback會(huì)觸發(fā)if (err) {console.log("reject", err);} else {console.log("fulfill", v);} }); p.fulfill(void 0, "ok"); // fulfill ok
可見,實(shí)現(xiàn)同樣的需求,使用future會(huì)更麻煩,而且還存在先后順序的陷阱(我一向認(rèn)為簡(jiǎn)單類庫的應(yīng)用代碼如果存在嚴(yán)重的先后順序,是設(shè)計(jì)的不合格),習(xí)慣使用es6的promise的童鞋還是不建議使用KISS標(biāo)準(zhǔn)的future。
整篇文章就到這里,前面提到的then.js和when不再花篇幅介紹了。因?yàn)閜romise的實(shí)現(xiàn)大同小異,都是訂閱發(fā)布+特定的流程控制,只是各個(gè)標(biāo)準(zhǔn)的出發(fā)點(diǎn)和側(cè)重點(diǎn)不同,導(dǎo)致一些語法和接口的不同。而隨著es標(biāo)準(zhǔn)的越來越完善,其他promise的標(biāo)準(zhǔn)要么慢慢消亡(如future、then.js),要么給后續(xù)的es標(biāo)準(zhǔn)鋪路(如bluebird、Q)。所以如果你沒有什么執(zhí)念的話,乖乖的跟隨es標(biāo)準(zhǔn)是最省事的做法。而這邊隨筆的目的,一是借機(jī)整理一下自己使用各個(gè)promise庫時(shí)長(zhǎng)期存在的疑惑;二是告訴自己,很多現(xiàn)在看來塵埃落地的技術(shù)并非天生如此,沿著前路走過來會(huì)比站在終點(diǎn)看到更精彩的世界。
轉(zhuǎn)載于:https://www.cnblogs.com/tarol/p/9042407.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的聊一聊promise的前世今生的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt中使用QSqlDatabase::r
- 下一篇: 判断输入的字符串是否为回文_刷题之路(九