日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

从如何停掉 Promise 链说起

發布時間:2023/12/2 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从如何停掉 Promise 链说起 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在使用Promise處理一些復雜邏輯的過程中,我們有時候會想要在發生某種錯誤后就停止執行Promise鏈后面所有的代碼。

然而Promise本身并沒有提供這樣的功能,一個操作,要么成功,要么失敗,要么跳轉到then里,要么跳轉到catch里。

如果非要處理這種邏輯,一般的想法是拋出一個特殊的Error對象,然后在Promise鏈后面的所有catch回調里,檢查傳來的錯誤是否為該類型的錯誤,如果是,就一直往后拋,類似下面這樣

doSth() .then(value => {if (sthErrorOccured()) {throw new Error('BIG_ERROR')}// normal logic }) .catch(reason => {if (reason.message === 'BIG_ERROR') {throw reason}// normal logic }) .then() .catch(reason => {if (reason.message === 'BIG_ERROR') {throw reason}// normal logic }) .then() .catch(reason => {if (reason.message === 'BIG_ERROR') {throw reason}// normal logic })

這種方案的問題在于,你需要在每一個catch里多寫一個if來判斷這個特殊的Error,繁瑣不說,還增加了耦合度以及重構的困難。

如果有什么辦法能直接在發生這種錯誤后停止后面所有Promise鏈的執行,我們就不需要在每個catch里檢測這種錯誤了,只需要編寫處理該catch塊本應處理的錯誤的代碼就可以了。

有沒有辦法不在每個catch里做這種判斷呢?

辦法確實是有的,那就是在發生無法繼續的錯誤后,直接返回一個始終不resolve也不reject的Promise,即這個Promise永遠處于pending狀態,那么后面的Promise鏈當然也就一直不會執行了,因為會一直等著。類似下面這樣的代碼

Promise.stop = function() {return new Promise(function(){}) }doSth() .then(value => {if (sthBigErrorOccured()) {return Promise.stop()}// normal logic }) .catch(reason => {// will never get called// normal logic }) .then() .catch(reason => {// will never get called// normal logic }) .then() .catch(reason => {// will never get called// normal logic })

這種方案的好處在于你幾乎不需要更改任何現有代碼,而且兼容性也非常好,不管你使用的哪個Promise庫,甚至是不同的Promise之間相互調用,都可以達到目的。

然而這個方案有一個不那么明顯的缺陷,那就是會造成潛在的內存泄露。

試想,當你把回調函數傳給Promise的then方法后,如果這時Promise的狀態還沒有確定下來,那么Promise實例肯定會在內部保留這些回調函數的引用;在一個robust的實現中,回調函數在執行完成后,Promise實例應該會釋放掉這些回調函數的引用。如果使用上述方案,那么返回一個永遠處于pending狀態的Promise之后的Promise鏈上的所有Promise都將處于pending狀態,這意味著后面所有的回調函數的內存將一直得不到釋放。在簡單的頁面里使用這種方案也許還行得通,但在WebApp或者Node里,這種方案明顯是不可接受的。

Promise.stop = function() {return new Promise(function(){}) }doSth() .then(value => {if (sthBigErrorOccured()) {return Promise.stop()}// normal logic }) .catch(reason => {// this function will never got GCed// normal logic }) .then() .catch(reason => {// this function will never got GCed// normal logic }) .then() .catch(reason => {// this function will never got GCed// normal logic })

那有沒有辦法即達到停止后面的鏈,同時又避免內存泄露呢。

讓我們回到一開始的思路,我們在Promise鏈上所有的catch里都加上一句if,來判斷傳來的錯誤是否為一個無法處理的錯誤,如果是則一直往后面拋,這樣就達到了即沒有運行后面的邏輯,又避免了內存泄露的問題。

這是一個高度一致的邏輯,我們當然可以把它抽離出來。我們可以實現一個叫next的函數,掛在Promise.prototype上面,然后在里面判斷是否是我們能處理的錯誤,如果是,則執行回調,如果不是,則一直往下傳:

var BIG_ERROR = new Error('BIG_ERROR')Promise.prototype.next = function(onResolved, onRejected) {return this.then(function(value) {if (value === BIG_ERROR) {return BIG_ERROR} else {return onResolved(value)}}, onRejected) }doSth() .next(function(value) {if (sthBigErrorOccured()) {return BIG_ERROR}// normal logic }) .next(value => {// will never get called })

進一步,如果把上面代碼中“致命錯誤”的語義換成“跳過后面所有的Promise”,我們就可以得到跳過后續Promise的方式了:

var STOP_SUBSEQUENT_PROMISE_CHAIN = new Error()Promise.prototype.next = function(onResolved, onRejected) {return this.then(function(value) {if (value === STOP_SUBSEQUENT_PROMISE_CHAIN) {return STOP_SUBSEQUENT_PROMISE_CHAIN} else {return onResolved(value)}}, onRejected) }doSth() .next(function(value) {if (sthBigErrorOccured()) {return STOP_SUBSEQUENT_PROMISE_CHAIN}// normal logic }) .next(value => {// will never get called })

為了更明顯的語義,我們可以把“跳過后面所有的Promise”單獨封裝成一個Promise:

var STOP = {} Promise.stop = function(){return Promise.resolve(STOP) }Promise.prototype.next = function(onResolved, onRejected) {return this.then(function(value) {if (value === STOP) {return STOP} else {return onResolved(value)}}, onRejected) }doSth() .next(function(value) {if (sthBigErrorOccured()) {return Promise.stop()}// normal logic }) .next(value => {// will never get called })

這樣就實現了在語義明確的情況下,不造成內存泄露,而且還停止了后面的Promise鏈。

為了對現有代碼盡量少做改動,我們甚至可以不用新增next方法而是直接重寫then:

(function() {var STOP_VALUE = Symbol()//構造一個Symbol以表達特殊的語義var STOPPER_PROMISE = Promise.resolve(STOP_VALUE)Promise.prototype._then = Promise.prototype.thenPromise.stop = function() {return STOPPER_PROMISE//不是每次返回一個新的Promise,可以節省內存}Promise.prototype.then = function(onResolved, onRejected) {return this._then(function(value) {return value === STOP_VALUE ? STOP_VALUE : onResolved(value)}, onRejected)} }())Promise.resolve(8).then(v => {console.log(v)return 9 }).then(v => {console.log(v)return Promise.stop()//較為明確的語義 }).catch(function(){// will never called but will be GCedconsole.log('catch') }).then(function(){// will never called but will be GCedconsole.log('then') })

以上對then的重寫并不會造成什么問題,閉包里的對象在外界是訪問不到,外界也永遠也無法構造出一個跟閉包里Symbol一樣的對象,考慮到我們只需要構造一個外界無法“===”的對象,我們完全可以用一個Object來代替:

(function() {var STOP_VALUE = {}//只要外界無法“===”這個對象就可以了var STOPPER_PROMISE = Promise.resolve(STOP_VALUE)Promise.prototype._then = Promise.prototype.thenPromise.stop = function() {return STOPPER_PROMISE//不是每次返回一個新的Promise,可以節省內存}Promise.prototype.then = function(onResolved, onRejected) {return this._then(function(value) {return value === STOP_VALUE ? STOP_VALUE : onResolved(value)}, onRejected)} }())Promise.resolve(8).then(v => {console.log(v)return 9 }).then(v => {console.log(v)return Promise.stop()//較為明確的語義 }).catch(function(){// will never called but will be GCedconsole.log('catch') }).then(function(){// will never called but will be GCedconsole.log('then') })

這個方案的另一個好處(好處之一是不會造成內存泄露)是可以讓你非常平滑地(甚至是一次性的)從“返回一個永遠pending的Promise”過度到這個方案,因為代碼及其語義都基本沒有變化。在之前,你可以定義一個Promise.stop()方法來返回一個永遠pending的Promise;在之后,Promise.stop()返回一個外界無法得到的值,用以表達“跳過后面所有的Promise”,然后在我們重寫的then方法里使用。

這樣就解決了停止Promise鏈這樣一個讓人糾結的問題。

在考察了不同的Promise實現后,我發現Bluebird和瀏覽器原生Promise都可以在Promise.prototype上直接增加實例方法,但Q和$q(Angular)卻不能這么做,具體要在哪個子對象的原型上加或者改方法我就沒有深入研究了,但相信肯定是有辦法的。

可是這篇文章如果到這里就結束的話,就顯得太沒有意思了~~

順著上面的思路,我們甚至可以實現Promise鏈的多分支跳轉。

我們知道,Promise鏈一般來說只支持雙分支跳轉。

按照Promise鏈的最佳寫法實踐,處理成功的回調只用then的第一個參數注冊,錯誤處理的回調只使用catch來注冊。這樣在任意一個回調里,我們可以通過return或者throw(或者所返回Promise的最終狀態的成功與否)跳轉到最近的then或者catch回調里:

doSth() .then(fn1) .catch(fn2) .catch(fn3) .then(fn4) .then(fn5) .catch(fn6)

以上代碼中,任意一個fn都只能選擇往后跳到最近一then或者catch的回調里。

但在實際的使用的過程中,我發現雙分支跳轉有時滿足不了我的需求。如果能在不破壞Promise標準的前提下讓Promise實現多分支跳轉,將會對復雜業務代碼的可讀性以及可維護性有相當程度的提升。

順著上面的思路,我們可以在Promise上定義多個有語義的函數,在Promise.prototype上定義對應語義的實例方法,然后在實例方法中判斷傳來的值,然后根據條件來執行或者不執行該回調,當這么說肯定不太容易明白,我們來看代碼分析:

(function() {var STOP = {}var STOP_PROMISE = Promise.resolve(STOP)var DONE = {}var WARN = {}var ERROR = {}var EXCEPTION = {}var PROMISE_PATCH = {}Promise.prototype._then = Promise.prototype.then//保存原本的then方法Promise.prototype.then = function(onResolved, onRejected) {return this._then(result => {if (result === STOP) {// 停掉后面的Promise鏈回調return result} else {return onResolved(result)}}, onRejected)}Promise.stop = function() {return STOP_PROMISE}Promise.done = function(value) {return Promise.resolve({flag: DONE,value,})}Promise.warn = function(value) {return Promise.resolve({flag: WARN,value,})}Promise.error = function(value) {return Promise.resolve({flag: ERROR,value,})}Promise.exception = function(value) {return Promise.resolve({flag: EXCEPTION,value,})}Promise.prototype.done = function(cb) {return this.then(result => {if (result && result.flag === DONE) {return cb(result.value)} else {return result}})}Promise.prototype.warn = function(cb) {return this.then(result => {if (result && result.flag === WARN) {return cb(result.value)} else {return result}})}Promise.prototype.error = function(cb) {return this.then(result => {if (result && result.flag === ERROR) {return cb(result.value)} else {return result}})}Promise.prototype.exception = function(cb) {return this.then(result => {if (result && result.flag === EXCEPTION) {return cb(result.value)} else {return result}})} })()

然后我們可以像下面這樣使用:

new Promise((resolve, reject) => {// resolve(Promise.stop())// resolve(Promise.done(1))// resolve(Promise.warn(2))// resolve(Promise.error(3))// resolve(Promise.exception(4))}).done(value => {console.log(value)return Promise.done(5)}).warn(value => {console.log('warn', value)return Promise.done(6)}).exception(value => {console.log(value)return Promise.warn(7)}).error(value => {console.log(value)return Promise.error(8)}).exception(value => {console.log(value)return}).done(value => {console.log(value)return Promise.warn(9)}).warn(value => {console.log(value)}).error(value => {console.log(value)})

以上代碼中:


  • 如果運行第一行被注釋的代碼,這段程序將沒有任何輸出,因為所有后面的鏈都被“停”掉了

  • 如果運行第二行被注釋的代碼,將輸出1 5 9

  • 如果運行第三行被注釋的代碼,將輸出2 6 9

  • 如果運行第四行被注釋的代碼,將輸出3 8

  • 如果運行第五行被注釋的代碼,將輸出4 7

即return Promise.done(value)將跳到最近的done回調里

依次類推。

這樣就實現了Promise鏈的多分支跳轉。針對不同的業務,可以封裝出不同語義的靜態方法和實例方法,實現任意多的分支跳轉。

但這個方案目前有一點不足,就是不能用then來捕獲任意分支:

new Promise((resolve) => {resolve(Promise.warn(2)) }) .then(value => {}) .warn(value => {})

這種寫法中,從語義或者經驗上講,then應該捕獲前面的任意值,然而經過前面的改動,這里的then將捕獲到這樣的對象:

{flag: WARN,value: 2 }

而不是2,看看前面的代碼就明白了:

Promise.prototype.then = function(onResolved, onRejected) {return this._then(result => {if (result === STOP) {return result} else {return onResolved(result)// 將會走這條分支,而此時result還是被包裹的對象}}, onRejected) }

目前我還沒有找到比較好的方案,試了幾種都不太理想(也許代碼寫丑一點可以實現,但我并不想這么做)。所以只能在用到多分支跳轉時不用then來捕獲傳來的值。

不過從有語義的回調跳轉到then是可以正常工作的:

doSth() .warn() .done() .exception() .then() .then() .catch()

同樣還是可以根據上面的代碼看出來。

最后,此文使用到的一個anti pattern是對原生對象做了更改,這在一般的開發中是不被推薦的,本文只是提供一個思路。在真正的工程中,可以繼承Promise類以達到幾乎相同的效果,此處不再熬述。

多謝各位同僚的閱讀,如有紕漏之處還請留言指正~

原文鏈接:https://github.com/xieranmaya/blog/issues/5

總結

以上是生活随笔為你收集整理的从如何停掉 Promise 链说起的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。