javascript基礎修煉(7)——Promise,非同步,可靠性

来源:https://www.cnblogs.com/dashnowords/archive/2018/09/26/9709477.html
-Advertisement-
Play Games

開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。 一. 別人是開發者,你也是 技術是【 非同步編程】這個話題中非常重要的,它一度讓我感到熟悉又陌生,我熟悉其所有的 並能夠在編程中相對熟練地運用,卻對其中原理和軟體設計思想感到陌生,即便我讀了很多源碼分析和教程也一度很難理解 ...


開發者的javascript造詣取決於對【動態】和【非同步】這兩個詞的理解水平。

一. 別人是開發者,你也是

Promise技術是【javascript非同步編程】這個話題中非常重要的,它一度讓我感到熟悉又陌生,我熟悉其所有的API並能夠在編程中相對熟練地運用,卻對其中原理和軟體設計思想感到陌生,即便我讀了很多源碼分析和教程也一度很難理解為什麼Promise這樣一個普通的類能夠實現非同步,也曾嘗試著去按照Promise/A+規範來編寫Promise,但很快便陷入了一種更大的混亂之中。直到我接觸到一些軟體設計思想以及軟體工程方面的知識後,從代碼之外的角度來理解一些細節的必要性時,那些陌生才開始一點點消失。

如果你覺得有些新東西很那理解,有很大的原因是因為你和設計者所擁有的基礎知識儲備不是一個水平的,導致你無法理解設計者寫出某段代碼時所基於的指導思想,當你無法理解某些看起來很複雜的東西時,筆者的建議是先瞭解它希望解決的問題,這個問題或許是具體的業務邏輯需求,或許是抽象的軟體設計層面的,然後嘗試自己想辦法去解決它,請永遠記得別人是開發者,你也是,你要做的是面向需求,而不僅僅是跟著別人走。即時最終你沒能開發出這個模塊而去學習源碼時,你也會發現面對需求而進行的主動思考對此帶來的幫助

二. 關於Promise的一些困惑

Promise的本質,是一個分散式的狀態機。而PromiseAPI的本質,就是一個發佈訂閱模型。

  1. Promise解決了什麼問題?

    這是一個最基本的問題,Promise是一個有關可靠性和狀態管理的編程範式,它通常被認為從代碼層面將javascript中著名的回調地獄改變成扁平化的寫法,併為指定的業務邏輯打上狀態標記,讓開發者可以更容易地控制代碼執行的流程。但事實上Promise的設計初衷並不是為了實現非同步,而且很多開發者並沒有意識到,回調並不意味著非同步!!!(你傳入另一個函數的回調函數有可能被非同步執行,也有可能被同步執行)。想更好地理解Promise,就必須把【非同步】這個標簽從中剝離,而圍繞【狀態管理】【可靠性】這些關鍵詞進行展開。

  2. Promise只是一個類,為什麼就能夠實現非同步?

    Promise本身的確只是一個普通的類,而且在不依賴ES6的環境中,開發者甚至可以手動實現這樣一個類,在沒有研究Promise的代碼之前,筆者一直主觀地認為其內部是通過類似於事件監聽的機制來實現非同步的,否則程式本身怎麼會知道發出的http請求什麼時候返回結果。

    這個問題是在筆者學習完EventLoopGenerator函數的相關知識後才理解的,其實Promise本身並沒有實現非同步javascript語言中的非同步都是通過事件迴圈的機制(《javascript基礎修煉(5)——Event Loop(node.js)》)來實現的,簡單地說就是說非同步事件的響應是會被事件迴圈不斷去主動檢測的,當非同步動作滿足了再次被執行的條件時(比如http請求返回了結果,或者在另一個線程開啟的大運算量的邏輯執行完畢後返回了消息),就會被加入調用棧來執行,PromiseGenerator只是配合事件迴圈來進行狀態管理和流程式控制制,它們本身和事件迴圈的機制是解耦的。

  3. Promise作為構造函數調用而生成實例時到底發生了什麼事情?

    這裡所指的是下麵這樣的代碼:

    promise = new Promise(function(resolve, reject){
      //....
    });

    面試中經常會問到有關Promise執行次序的問題,很多非常熟悉Promise用法的讀者也並沒有意識到,實際上傳入的匿名函數是會同步執行的。Promise所做的事情,是為當前這個不知道何時能完成的動作打上一些狀態的標記,並傳入兩個用於回收控制權的方法作為參數來啟動執行這個匿名函數,通過then方法指定的後續執行邏輯會先緩存起來(這裡的描述並不嚴謹),當這個非同步動作完成後調用resolve或者reject方法後,再繼續執行事先被緩存起來的流程。

  4. Promise/A+標準看起來很複雜,該如何去實現?

    Promise/A+規範的確很複雜,我也不建議你直接就通過這樣的方式來瞭解Promise的實現細節,【規範】意味著嚴謹性,也表示其中有很多容錯的機制,這會極大地妨礙你對Promise核心邏輯的理解,Promise代碼最大的複雜性,在於它對於鏈式調用的支持(如果不需要支持鏈式調用,你會發現自己幾乎不需要思考就可以分分鐘擼一個Promise庫出來)。筆者的建議是先想辦法去解決主要問題,再對照Promise/A+規範去檢視自己的代碼。

  5. Promise為什麼要實現鏈式調用?

    鏈式調用的實現,實現了Promise的多步驟流程式控制制功能,對一個多於兩個步驟的流程中,即使沒有實現鏈式調用,Promise實際上依然可以工作,但當你真的那樣做時,你會發現它又變成了一個新的回調地獄

  6. Promise的可靠性是指什麼?

    Promise的可靠性指它的狀態只能被改變一次,之後就不能再修改,且唯一修改它的方法是調用promise實例中的內部resolve( )reject( )方法,它們是定義在Promise內部的,從外部無法訪問到,只能通過Promise內部提供的機制來觸發判定方法(new Promise(executor)生成實例時,當還行到executor時,Promise會將內部的resolvereject方法作為實參傳入executor,從而暴露修改自身狀態的能力),相比之下,普通對象的屬性或者thenable對象(指擁有then方法的非Promise實例對象)的屬性都是可以被直接修改的,所以promise的狀態和結果被認為是更可靠的。

三. 寫給小白的Promise短篇故事

假設有一個非同步的動作A,還有一個希望在A完成以後執行的動作B,和一個在B完成以後去執行的動作C,我們來看一下Promise是如何實現流程式控制制。

第一回 狀態標記

A動作開始之前,我們把它丟進Promise構造函數,Promise給了A一個控制器(上面有resolvereject兩個按鈕)和一個帶有兩個抽屜的儲物櫃(onFulfilledCallbacksonRejectedCallbacks),接著給A交代:我已經登記好信息了,你去執行吧,等你執行完以後,如果你認為執行成功了,就按一下控制器的resolve按鈕,如果認為執行失敗了就按一下reject按鈕,但是你要小心,這個控制器只能用一次,按完它會自動發送消息,儲物柜上有接收器,如果收到resolve信號,onFulfilledCallbacks這個抽屜就會打開,如果收到reject信號,onRejectedCallbacks這個抽屜就會打開,之後另一個柜子就會鎖死,我每隔一段時間會來查看一下你的狀態(註意這裡是在事件迴圈中主動輪詢來查看promise實例是否執行結束的),如果我看到你的儲物櫃有一個抽屜打開了的話的話,就會把裡面的東西拿出來依次執行接下來的事情。在這之前,如果有人想關註你的執行情況的話,我會讓它留下兩張字條,分別寫下不同的抽屜打開的時需要做的事情,因為最終只有一個抽屜可以打開,他必須得寫兩張字條,除非他只關註某個抽屜的動向,然後使用你這個儲物櫃的then方法就可以把字條塞到對應的柜子里,之後等抽屜打開時,我只需要根據字條上的信息打電話給他就行了。A覺得這樣是比較穩妥的,於是拿著promise給它的控制器去執行了。

第二回 回調註冊

代碼繼續執行,這時候出現了一個B,B說我得先看看A的執行結果,再決定做什麼,執行器說你也別在這乾等著了,A在我們這裡存放了一個智能儲物櫃,它回頭會把結果遠程發送回來,你把你的聯繫方式寫在這兩張字條上,然後通過A的儲物櫃的then方法放進去吧,聯繫方式也可以寫成不一樣的,到時候A返回結果的話,對應的抽屜就會打開,我按照你寫的聯繫方式發消息給你就行了。B想了想也是,於是就寫下了兩個不同的號碼放進了A儲物櫃對應的抽屜里,接著就回家睡覺去了。

第三回 機制缺陷

代碼繼續執行,這時候又出現了一個C,C說我想等B返回結果以後再執行,這時候執行器犯難了,B還沒出發呢,我也沒有給它分配回調儲物櫃,所以沒辦法用同樣的方式對待C,執行器只能對C說,我們這規定如果沒有對應標記的儲物櫃的話,暫時不提供服務,這樣吧,你先把你的聯繫方式寫好交給我,等回頭如果B出發的話,我會給它分派儲物櫃,到時候把你的需求放在對應的抽屜里,等B返回對應結果以後我再通知你,C覺得也行,於是就照做了。但是C走後,執行器就想了,要是後面再來DEF都要跟在不同的人後面去執行,那這些事情我都得先保管著,這也太累了,而且容易搞亂,不能這麼搞啊。

第四回 流程管理

上一會講到在現有機制下缺乏多步驟流程管理的機制,當非同步任務A執行且沒有返回結果時,後續所有的動作都被暫存在了執行器手裡,只能隨著時間推移,當標誌性事件發生時再逐步去分發事件。為了能夠實現多步驟的流程管理,執行器想出了一個方法,為每一個來註冊後續業務邏輯的人都提供一個智能儲物櫃,這樣在辦理登記時就可以直接將後續的方法分發到對應的抽屜里,常見的問題就解決了。

四.鏈式調用Promise的影響

如果沒有鏈式調用,第三節中的多步驟的偽代碼可能是如下的樣子:

//為了聚焦核心邏輯,下麵的偽代碼省略了onReject的回調
promiseA = new Promise(function(resolve, reject){
     //A帶著控制器開始執行
    A(resolve,reject); 
});

promiseA.then(function(resA){
    //A執行結束以後,開始判斷B到底是否要執行
    promiseB = new Promise(function(resolveB, rejectB){
        //如果B需要執行,則分配兩個儲物櫃,並派髮狀態控制器,B帶著A返回的數據resA開始執行
        B(resA,resolveB,rejectB);
    });
    
    promiseB.then(function(resB){
        //B執行結束以後,開始判斷C到底是否要執行
        promiseC = new Promise(function(resolveC, rejectC){
            //如果C需要執行,則分配兩個儲物櫃,並派髮狀態控制器,C帶著B返回的數據resB開始執行
            C(resB, resolveC, rejectC);  
        });
        
        //...如果有D的話
    })
});

在邏輯流程中僅僅有3個步驟的時候,回調地獄的苗頭就已經顯露無疑了。Promise被設計用來解決回調嵌套過深的問題,如果只能按上面的方法來使用的話顯然是不能滿足需求的。如果可以支持鏈式調用,那麼上面代碼的編寫方式就變成了:

//為了聚焦核心邏輯,下麵的偽代碼省略了onReject的回調
promiseA = new Promise(function(resolve, reject){
     //A帶著控制器開始執行
    A(resolve,reject); 
});

promiseA.then(function(resA){
    //在使用then方法向A的儲物櫃里存放事件的同時,也生成了自己的儲物櫃
    return new Promise(function(resolveB, rejectB){
         B(resA, resolveB, rejectB);
    });   
}).then(function(resB){
    return new Promise(function(resolveC, rejectC){
         C(resB, resolveC, rejectC);
    });   
}).then(function(resC){
    //如果有D動作,則繼續
})

很明顯,當流程步驟增多時,支持鏈式調用的方法具有更好的擴展性。下一節講一下Promise最關鍵的鏈式調用環節的實現。

五. Promise如何支持鏈式調用

基本原理

如果需要then方法支持鏈式調用,則Promise.prototype.then這個原型方法就需要返回一個新的promise。事實上即使在最初的時間節點上來看,後續註冊的任務也符合在未來某個不確定的時間會返回結果的特點,只是多了一些前置條件的限制。返回新的promise實例是非常容易做到的,但從代碼編寫的邏輯來理解,這裡的promise到底是什麼意思呢?先看一下基本實現的偽代碼:

//為簡化核心邏輯,此處只處理Promise狀態為PENDING的情況
//同時也省略了容錯相關的代碼
Promise.prototype.then = function(onFulfilled, onRejected){
    let that = this;
    return new Promise(function(resolve, reject){
          //對onFulfilled方法的包裝和歸類
          that.onFulfilledCallbacks.push((value) => {
                  let x = onFulfilled(value);
                  someCheckMethod(resolve, x, ...args);
            });
        
         //對onRejected方法的包裝和歸類
          that.onRejectedCallbacks.push((reason) => {
              let x = onRejected(reason);
              someCheckMethod(reject, x, ...args);
          });
    });
};

可以看到在支持鏈式調用的機制下,最終被添加至待執行隊列中的函數並不是通過then方法添加進去的函數,而是通過Promise包裝為其增加了狀態信息,並且將這個狀態改變的控制權交到了onFulfilled函數中,onFulfilled函數的返回結果,會作為參數傳入後續的判定函數,進而影響在執行resolve的執行邏輯,這樣就將新promise控制權暴露在了最外層。

所以,then方法中返回的promise實例,標記的就是添加進去的onFulfilledonRejected方法的執行狀態。這裡的關鍵點在於,onFulfilled函數執行並返回結果後,才會啟動對於這個promise的決議。

支線故事

在新的鏈式調用的支持下,上面的故事流程就發生了變化。當B前來登記事件時,執行器說我們這現在推出了一種委托服務,你想知道那個儲物櫃的最新動態,就把你的電話寫在字條上放在對應的抽屜里,之後當這個抽屜打開後,我們就會把它返回的信息發送到你留在字條上的號碼上,我們會給你提供一個智能儲物櫃(帶有this._onFulfillCallbacks抽屜和this._onRejectedCallbacks抽屜)和一個控制器,這樣別人也可以關註你的動態,但你的控制器暫時不能用,我們將某個消息發送到你留的手機號碼上時,才會同步激活你的控制器功能,但它也只能作用一次。

六. resolve(promise)

再來考慮一種特殊的場景,就是當A動作調用resolve(value )方法來改變狀態機的狀態時,傳入的參數仍然是一個PENDING狀態的promise,這相當於A說自己已經完成了,但是此時卻無法得到執行結果,也就不可能將結果作為參數來啟動對應的apromise._onFulfilledCallbacks隊列或者apromise_onRejectedCallbacks隊列,此時只能先等著這個promise改變狀態,然後才能執行對A動作的決議。也就是說A的決議動作要延遲到這個新的promise被決議以後。用偽代碼來表示這種情況的處理策略就是如下的樣子:

//內部方法
let that = this;//這裡的this指向了promise實例
function resolve(result){
    if(result instanceof Promise){
        return  result.then(resolve, reject);
    }
    //執行相應的緩存隊列里的函數
    setTimeout(() => {
        if (that.status === PENDING) {
            that.status = FULFILLED;
            that.value = result;
            that.onFulfilledCallbacks.forEach(cb => cb(that.result));
        }
    }); 
}

當前promise實例的決議通過result.then(resolve,reject)被推遲到result返回結果之後,而真正執行時所需要操作的對象和屬性,已經通過let that = this與實例進行了綁定 。

很多開發者在這裡會覺得非常混亂,很可能是沒有意識到每一個promise實例都會生成內部方法resolve( )reject( ),即時當Promise類實例化的過程結束後,它們依然會被保持在自己的閉包作用域中,在執行棧中涉及到多個處於PENDING狀態的promise時,它們的內部方法都是存活的。如果還是覺得抽象,可以利用Chrome的調試工具,將下麵的代碼逐步執行,並觀察右側調用棧,就可以看到當傳入決議函數的是另一個promise時,外層的決議函數都會以閉包的形式繼續存在。

let promise1 = new Promise(function(resolve, reject){
     setTimeout(function fn1(){
          let subpromise = new Promise(function (resolvesub,rejectsub) {
              setTimeout(function fn2() {
                  resolvesub('value from fn2');
              },2000);
          });
          resolve(subpromise);
     },2000);
});

promise1.then(function fn3(res) {
    console.log(res);
});

七.Promise/A+規範與造車輪指南

【Promise/A+規範】https://github.com/promises-aplus/promises-spec

理清了上面各種情況的基本策略後,我們已經具備了構建一個相對完備的Promise模塊的能力。我強烈建議你按照Promise/A+規範來親自動手實現一下這個模塊,你會發現在實現的過程中仍然有大量的代碼層面的問題需要解決,但你一定會受益於此。網上有非常多的文章講述如何根據Promise/A+標準來實現這個庫,可是在筆者看來這並不是什麼值得炫耀的事情,就好像對照著攻略在打游戲一樣。

作為工程師,你既要能夠一行一行寫出這樣一個模塊,更要關註規範為什麼要那樣規定。

【Promise/A+測試套件】: https://github.com/promises-aplus/promises-tests

如果你對照規範的要求寫出了這個模塊,可以利用官方提供的測試套件(包含800多個測試用例來測試規範中規定的各個細節)來測試自己編寫的模塊並完善它。javascript語言中都是通過鴨式辯型來檢測介面的,無論你是怎樣實現規範的各個要求,只要最終通過測試套件的要求即可。如果你依舊覺得心裡沒譜,也可以參考別人的博文來學習Promise的細節,例如這篇《Promise詳解與實現》就給了筆者很大幫助。

八.API以外的視角

當越過了語言層面的難點後,推薦你閱讀《深入理解Promise五部曲》這個系列的文章。大多數開發者對於Promise的理解和應用都是用來解決回調地獄問題的,而這個系列的文章會讓你從另一個角度重新認識Promise,不得不說文章中用發佈訂閱模式來類比解釋Promise的實現機制對於筆者理解Promise提供了巨大的幫助,同時它也能夠引發一些通過學習promise/A+規範很難意識到的關於精髓和本質的思考。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今天我遇到了一個需求,是將一個DBF文件導入到Oracle庫中,之前一直使用的是公司提供的遷移工具,但是不知道怎麼回事今天一直報DBF文件無法訪問之類的錯誤,嘗試多次之後,一氣之下棄之不用,另尋他法。 ODBC(Open Database Connectivity)是微軟提供的,專門為解決異構資料庫 ...
  • 最近在使用snapkit過程中遇到一個問題,在github上搜索之後發現另外一個有趣的問題 問題鏈接 看起來很理所當然的,明顯不可以這樣寫,但是具體是什麼原因呢,明明沒有報任何錯誤和警告,但是.multipliedBy()方法卻沒有效果,那我們來看一下snapkit源碼。 1.首先點進equalTo ...
  • 在Android Studio項目中引用第三方jar包的方法: 步驟: 1、在build.gradle文件中添加如下代碼: 備註:要添加在Android作用域下 點擊【Sync Now】,會生成jniLibs文件夾 找到jniLibs文件夾對應的實體目錄,把需要用到的jar包放到該目錄下 在buil ...
  • 在上一章我們提到了一個新的概念,叫做塊級樣式,講到這裡就要科普一下: 標簽又分為兩種: (1)塊級標簽 元素特征:會獨占一行,無論內容多少,可以設置寬高··· (2)內斂標簽(又叫做行內標簽) 元素特征:根據內容的多少占用空間大小,它的上下margin不起作用 (塊級:P h1- h6 div ul ...
  • 1 事件冒泡 子元素觸發的事件,會往上(父元素)傳遞; 例子: 冒泡事件是預設事件,但有些情況,冒泡事件是一種麻煩的事情。如: 這時候需要把預設事件去掉 cancelBubble = false 或者 stopPropagation(); 2 onmouseenter/onmouseleave 和o ...
  • 一.文檔流 1.概念 2.BFC(Block formatting context) 3.BFC規則 內部的Box會在垂直方向,一個接一個地放置。 Box自身垂直方向的位置由margin top決定,屬於同一個BFC的兩個相鄰Box的margin會發生重疊。 Box自身水平方向的位置由margin左 ...
  • 一,vue.js簡介 Vue.js可以作為一個js庫來使用,也可以用它全套的工具來構建系統界面,這些可以根據項目的需要靈活選擇 所以說, vue.js是一套構建用戶界面的漸進式框架 Vue.js的核心庫只關註視圖層,Vue的目標通過儘可能簡單的API實現相應的數據綁定, 在這一點上Vue.js類似於 ...
  • 1.多個標簽寫在一行 效果前: 效果後: 2.將要閉合標簽的地方與開始標簽的地方重合 3.使用註釋頭尾相連 4.在img標簽的父級上寫:font-size:0; 推薦是用這個方法。這個方法我已經實踐簡單實用 效果: 5.使用display:block(img是內聯元素) 效果: 6.使用letter ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...