Promise從入門到精通

廣州蘆葦科技web前端發表於2019-01-14

標籤: Promise


在ES6之前的JavaScript中處理非同步的方法就是使用回撥函式,當我們不知道一件事情會在什麼時候結束,但是又希望在這件事情結束之後再去做一下其他的操作時,我們在這件事情執行之前就先規定好呼叫事情執行完之後再執行的操作,這也就是所謂的回撥,在執行函式之前先告訴這個函式執行完之後下一步需要做什麼。

回撥函式在非同步操作相對較少的情況下使用起來確實不會有太大的問題,但是如果非同步操作增多時,由於非同步的不可預知性,那麼整個程式碼的可讀性就會比較差,因為無法清楚的知道程式碼實際的執行情況,同時多個非同步之間的順序性也是無法保證的。這也就是使用回撥錶達程式和管理併發的兩個主要缺陷:缺乏順序性和可信任性。

什麼是Promise

關於promise的api在理解上並沒有特別的困難,但是我們在學習一個新的工具的時候還是應該先理解其中更深層次的原理與概念,先從這一步來解釋什麼是promise,才能夠更好的是使用promise。

未來值

以一個我們日常在飯店吃飯的例子來類比就是:當我們去一個快餐店吃飯,點了一個青椒炒肉,給收銀員付了15元,很明顯飯店的菜都是現炒的,我們不可能馬上就拿到我們的午飯,這個時候收銀員給了我一個小票,當我的青椒炒肉做好的時候,我可以憑藉這個小票拿到它,這個小票就是我能夠拿到午飯的依據,店員承諾(promise)拿著這個小票可以取到一份青椒炒肉。

只要我好好的儲存這個小票,我就不用擔心吃不上午飯了,因為它代表了我未來的午飯。

當然在這個等待的過程中,我還可以去做一些其他的事情,比如刷一下知乎。

儘管我現在還沒有拿到我的午飯,但是我有了這個小票,它相當於我的午飯的佔位符,從本質上來講,這個佔位符使得這個值不再依賴時間。這是一個未來值。

當服務員叫到我的訂單號時,我拿著我的小票就能夠換回我的午餐——青椒炒肉,也就是說只要我的午餐已經炒好了,那麼我就能夠用店員當初給我的承諾(小票)來換取這個值本身。

當然也還有這麼一種情況,店裡面現在已經沒有肉了,當我拿著小票去找店員拿我的午餐的時候,店員告訴我,做不了青椒炒肉了。從這裡也就能夠看得出來,我所需要的未來值可能失敗也可能成功。

也就是我每次去吃飯的時候,最終要麼得到一份青椒炒肉,要麼到的一個肉已經賣完了的訊息。

當然在程式碼中事情並不會這麼簡單,有可能我的訂單被店員忘記了,那麼我的訂單將永遠都不會被叫到,在這種情況下,我就永遠處於一個未決議的狀態。

  1. 現在值與未來值

下面讓我們使用程式碼來描述一下上面的情況,但是在具體解釋promise之前,先使用比較好理解一點的方式——回撥來解釋一下未來值。

當我們在寫程式碼使用某一個變數的值的時候,實際上我們已經對這個值做了一個非常基本的假設,那就是他應該是我們已經確定他是一個具體的現在值了:

let x,y = 2console.log(x + y)// NaN <
-- 因為x還沒有設定值
複製程式碼

在我們執行x+y的時候,我們假定了x和y都是一個具體的值了,用術語來說就是x和y的值都是已決議的。

從上面的程式碼能夠發現,我們在執行+運算的時候x的值其實並沒有確定,而這個運算是無法檢測x和y的值是否已經決議好,它也無法等待x和y都決議好之後再執行運算。如果在程式碼中有個語句現在完成,而有的語句是將來才完成,那麼就會引起程式的混亂。

如果兩條語句中任意一個可能還沒有完成,我們改如何追蹤者兩條語句的關係能?如果語句2依賴語句1的完成,那麼就只有兩種情況:語句1馬上完成,一切順利進行,要麼語句1沒有完成,而語句2也會因此失敗。

那麼我們應該如何保證在執行x+y這個運算時是安全的呢?其實也就是在執行語句的時候要保證x和y的值都已經準備好了。換一種表述方式就是:“把x和y加起來,但是如果他們其中的任何一個還沒有準備好,就等待兩者都準備好。一旦可以立馬執行加運算。”

首先使用回撥的形式:

function add(getX, getY, cb) { 
var x,y getX(function (xVal) {
x = xVal if(y !== undefined) {
cb(x + y)
}
}) getY(function (yVal) {
y = yVal if(x !== undefined) {
cb(x + y)
}
})
}// fetchX() 和fetchY()是同步或者非同步函式add(fetchX, fetchY, function (sum) {
console.log(sum)
})複製程式碼

雖然我們為了執行一個x+y寫了比較多的程式碼,但是在這段程式碼中我們把x和y當做未來值,並且表達了一個add()運算,這個運算並不會在意x和y現在是否可用。它把現在和將來歸一化了,因此我們可以保證add()運算的輸出是可預測的。

說得更加直白一點就是,未來統一處理現在和將來,我們把它們都變成了將來,即所有的操作都變成了非同步。

  1. Promise值

下面先來看一下如何使用promise函式來表達這個x+y的例子:

function add(xPromise, yPromise) { 
return Promise.all([xPromise, yPromise]).then(function (values) {
return values[0] + values[1]
})
}add(fetchX(), fetchY()).then(function (sum) {
console.log(sum)
})複製程式碼

直接掉呼叫fetchX(), fetchY(),它們的返回值被傳遞給add()。這些promise代表的底層值的可用的時間可能是現在,也可能是未來,但是不管怎麼樣,promise歸一保證了行為的一致性。我們可以按照不依賴時間的方式追蹤x和y。它們是未來值。

當然就像去吃午飯一樣,promise的決議結果可能是拒絕也可能是完成。拒絕值和完成的promise不一樣:完成值是通過我們程式碼最終得到一個我們期望的值,而拒絕值通常是一個拒絕原因。可能是程式的邏輯直接設定的,也可能是從執行異常隱式得到的值。

通過promise,呼叫then()實際上可以接受兩個函式,第一個用於完成情況,第二用於拒絕情況。

add(fetchX(), fetchY()).then(function (sum) { 
console.log(sum)
}, function (err) {
console.log(err)
})複製程式碼

從外部來看,由於Promise封裝了依賴於時間的狀態——等待底層的值完成或拒絕,所以Promise本身是與時間無關的。因此Promise可以按照可預測的方式組成,而不用關心時序或底層結果。

Promise決議之後就是外部不可變的值,我們可以安全的把這個值傳遞給第三方,並確信它不會被修改。如果有多方同時檢視同一個Promise決議時,任何一方都無法影響到另一方對Promise決議的觀察結果。Promise決議之後的不可變性是Promise設計中最基礎和最重要的因素!

完成事件

Promise的決議:一種在非同步任務中作為兩個或更多步驟的流程控制機制,時序上的this-then-that。

假如現在有一個非同步任務foo,我們不知道也不關心它的任何細節,這個函式可能立即完成,也可能需要一段時間才能完成。

我們只需要知道foo在什麼時候結束,這樣就可以繼續繼續下一個任務。換句話來說我們想要通過某種方式在foo完成的時候通知我們一下,以便可以繼續下一步任務。

在JavaScript中,如果需要偵聽某一個通知,可能第一反應就是事件。所以可以把對通知的需求重新組織為對foo發出一個完成事件的監聽。

使用回撥的話,通知就是任務呼叫的回撥。而使用Promise的話,就把這個關係反轉了過來,偵聽來自foo的事件,然後得到通知的時候,根據情況進行下一步的操作。

首先,根據上面的需求我們可以得到下面的虛擬碼:

foo(x) { 
// 做一些非同步操作
}foo(88)on(foo "completion") {
// foo完成開始下一步
}on(foo “error”) {
// 出錯了
}複製程式碼

根據事件的特點,我們呼叫foo()然後建立兩個事件偵聽器,一個用於foo完成,一個用於foo出錯,從本質上來說foo()並不需要了解呼叫程式碼訂閱了那些事件,這樣可以很好的實現關注點分離。

當然原生的JavaScript並沒有提供這樣的東西。下面是在JavaScript中更加自然的表達方式。

function foo(x) { 
// 做一些非同步操作 // 構造一個listener事件通知處理物件來返回 return listener
}let evt = foo(42)evt.on('completion', function () {
// foo完成開始下一步
})evt.on('error', function () {
// 出錯了
})複製程式碼

foo()顯式建立並返回了一個事件訂閱物件,呼叫程式碼得到這個物件,並且在其上註冊了兩個事件處理函式。

相對於我們比較熟悉的面向回撥來說,這裡沒有把回撥傳遞給foo(),而是返回一個名為evt的事件註冊物件,由它來接受回撥,將控制返還給呼叫程式碼。

還有一個重要的好處就是可以把這個事件偵聽物件提供給程式碼中多個獨立的部分,在foo()完成的時候,它們都可以獨立的得到通知,然後執行下一步。

var evt = foo(52)bar(evt)baz(evt)複製程式碼

很明顯使用上面的操作實現了更好的關注點分離,其中bar()和baz()不需要牽扯到foo()的呼叫細節。foo()也不需要知道或者關注bar()和baz()是否存在。

從本質上來說,evt物件就是分離的關注點之間一箇中立的第三方協商機制。

Promise“事件”

很明顯,我們前面說到的事件偵聽物件evt就是Promise的一個模擬。

在基於Promise的方法中,前面的程式碼片段會讓foo()建立並返回一個Promise例項,而且這個Promise會被傳遞到bar()和baz()。

你可能會猜測bar()和baz()的內部實現或許如下:

function bar(fooPromise) { 
fooPromise.then( function () {
// foo執行完畢,執行bar
}, function () {
// 出錯了
} )
}複製程式碼

Promise決議也不一定要像前面將Promise作為未來值檢視時一樣會涉及傳送訊息。它也可以只作為一種流程控制訊號。

另一種實現方式是:

function bar() { 
// foo執行完畢,執行bar
} function baz() {
// foo執行完畢,執行baz
} function oopsBar() {
// 出錯了
}var p = foo(42)p.then(bar, oopsBar)p.then(baz, oopsBar)複製程式碼

作者簡介:李成文,蘆葦科技web前端開發工程師,擅長網站建設、微信公眾號開發、微信小程式開發、小遊戲製作、企業微信製作、H5建設,專注於前端框架、互動設計、影像繪製、資料分析等研究。

個人部落格:LCW blog

歡迎和我們一起並肩作戰: web@talkmoney.cn訪問 www.talkmoney.cn 瞭解更多

提供深圳微信公眾號製作,廣東釘釘開發,專業的企業微信外包,高價效比的微信小程式建設,靠譜的小遊戲製作,高質量的H5開發

來源:https://juejin.im/post/5c3c3729f265da611d66de99#comment

相關文章