天天看點

從異步講起,『函數』和『時間』該作何關系?

前文回顧

不知不覺,專欄已經來到第 5 篇~ 😍😍😍

前 4 篇傳送門、時間線及概要:

​​# ✨從曆史講起,JavaScript 基因裡寫着函數式程式設計​​ - 2022年09月19日

=> JavaScript 閉包起源于 1930 年的 lambda 運算;

​​# ✨從柯裡化講起,一網打盡 JavaScript 重要的高階函數​​ - 2022年09月26日

=> 将函數作為參數輸入或輸出,是封裝進階函數的核心思想;

​​# ✨從純函數講起,一窺最深刻的函子 Monad​​ - 2022年10月09日

=> 寫無副作用的純函數不隻是為了嘴上說說優雅,而是為了函數的組合、演算簡化、及自文檔等好處;

​​# ✨從延遲處理講起,JavaScript 也能惰性程式設計?​​ - 2022年10月12日

=> 延遲處理是連接配接 JavaScript 閉包和異步兩大核心的橋梁,JavaScript 真萬能,惰性程式設計一樣拿捏;

OK,至于本篇,将從異步講起,看看「JS 異步」和 「函數式」能擦出什麼樣的火花?看看異步中的時間與函數該作何關系?

探秘 JS 異步

JavaScript 除了“閉包”這個最經典的設計之外,還有它是“單線程”的設計,一樣可奉為最經典!

這裡先抛出 3 個經典的問題:

  1. “JavaScript 為什麼要是單線程?”
  2. “JavaScript 的單線程,意味着什麼?”
  3. “JavaScipt 異步原理是怎麼實作的?”

如果你能清晰準确地回答出這3個關于異步老生常談的經典問題,可以跳過下一小節的釋義。

經典 3 問

先淺答一下 JS 異步經典 3 問 ~

  1. “JavaScript 為什麼要是單線程?”

答:四字概括,為了:“簡單友善”。JavaScript 最初設計隻是運作在浏覽器的腳本語言,若同一時間要做多件事情便會産生沖突;不像其它後端語言用“鎖”這樣一個機制,也為了極緻簡單,是以 JavaScript 設計是單線程的。

  1. “JavaScript 的單線程,意味着什麼?”

答:單線程意味着任務需要排隊,任務是一個接一個地執行,前一個執行完畢,才會執行下一個。這就意味着前一個任務的執行會阻塞後續任務的執行。

好比去銀行辦理業務,目前隻有一個人工視窗,前面有個人要辦理大額貸款業務,需要填寫很多表格,隻有等這人把全部表格都填完,整個流程都走完,才能讓後面的人接着辦業務。

現實中如果發生這樣的事,肯定要被投訴,哪有這樣設計的?讓後面這麼多人幹等他填表格,并且這個時候視窗服務也是停止的,那效率得多低呀。

是以,正确的做法是,先将這個人挪到一邊,讓他去填表格,把視窗服務騰出來給後面的人繼續辦業務,等表格填完了,再回過頭來給你辦理大額貸款。

将這個比喻映射到 JavaScript 也是同樣的邏輯,JavaScript 通過異步來解決單線程阻塞的問題。這也是 與生俱來 就已經設定好了的(和閉包一樣,都寫在 DNA 裡)。

從異步講起,『函數』和『時間』該作何關系?
  1. “JavaScipt 異步原理是怎麼實作的?”

答:JS 引擎通過混用 2 種記憶體資料結構:棧和隊列 來實作異步。棧與隊列的互動也就是大家所熟知的 JS 事件循環(Event Loop)。

簡單來講:所有同步任務都是在主線程上執行的,形成 執行棧,異步任務的回調消息形成 回調隊列。在執行棧中的任務處理完成後,主線程就開始讀取任務隊列中的任務并執行。按這個規則,不斷往複循環。

上一張經典的圖:

從異步講起,『函數』和『時間』該作何關系?

這裡的 Stack 就相當于是前面所提銀行場景中的唯一人工視窗,Stack 裡面的任務就是等待辦業務的人,遇到辦大額貸款、填很多表格的人,則先挪到一邊去,然後繼續處理後面人的業務。若這人表格全填完了,就把這個消息放到 CallBack queue 裡,等 Stack 裡為空後,再去拿 callBack queue 的消息,繼續為你解決大額貸款。

以上三問,老生常談,溫故知新。

新 3 問

好了,老 3 問隻是開始的小結,這裡本瓜要問異步新 3 問:

  1. “JavaScript 實作異步有哪幾種表現形式?”
  2. “JavaScript 異步和函數式有什麼關系?”
  3. “JavaScript 異步真的簡單嗎?”

在腦袋裡面簡單過一過你的答案?

。。。。。。

下面來逐一詳細解答~~

異步演進

  1. “JavaScript 實作異步有哪幾種表現形式?”

答:

① 回調函數

最簡單實作異步就是使用回調函數。

打個比方,以打電話給客服為例,你有兩種選擇:排隊等待客服接聽 或 選擇客服有空時回電給你。

後面一種就是回調 —— CallBack

🌰代碼示例:

function success(res){
    console.log("API call successful");
}

function fail(err){
    console.log("API call failed");
}

function callApiFoo(success, fail){
    fetch(url)
      .then(res => success(res))
      .catch(err => fail(err));
};

callApiFoo(success, fail);      

回調缺點就是:嵌套調用會形成 回調地獄,加大代碼的閱讀難度,比如:

callApiFooA((resA)=>{
    callApiFooB((resB)=>{
        callApiFooC((resC)=>{
            console.log(resC);
        }), fail);
    }), fail);
}), fail);      

② Promise

為了彌補回調函數的不足,ES6 将異步方案改進為 Promise。

🌰用代碼說話,上述“回調地獄”優化為:

function callApiFooA(){
    return fetch(url); // JS fetch method returns a Promise
}

function callApiFooB(resA){
    return fetch(url+'/'+resA.id);  
}

function callApiFooC(resB){
    return fetch(url+'/'+resB.id);  
}

callApiFooA()
    .then(callApiFooB)
    .then(callApiFooC)
    .catch(fail)      

Promise 也有缺點,當狀态處于 pending 時,不知道程式執行到哪一步了,無法中途取消,這一點前面的文章也提到過。

③ Generator

于是 Generator 生成器函數異步解決方案誕生。

🌰代碼變化:

function *makeIterator() {
   let resA = fetch(url)
   yield resA
   let resB = fetch(url+'/'+resA.id)
   yield resB
   let resC = fetch(url+'/'+resB.id)
   yield resC
}
var it = makeIterator()

it.next() // callApiFooA
it.next() // callApiFooB
it.next() // callApiFooC      

再後來,ES2017 提出 async await 是 Generator 文法糖,不做贅述。

一般來說,寫道 async await ,JS 異步演進就結束了,但,不止于此,還有一種,是本節的亮點,即“響應式”。

④ 響應式

處理多個異步操作資料流是很複雜的,尤其是當它們之間互相依賴時,我們可以用更巧妙地方式将它們組合:響應式處理異步,Observer 登場!

🌰 show me the code:

function callApiFooA(){
    return fetch(urlA); 
 } 
 
 function callApiFooB(){
    return fetch( urlB );  
 }
 
 function callApiFooC( [resAId, resBId] ){
    return fetch(url +'/'+ resAId +'/'+ resBId);  
 } 
 
 function callApiFooD( resC ){
    return fetch(url +'/'+ resC.id);  
 } 
 
 Observable.from(Promise.all([callApiFooA() , callApiFooB() ])).pipe(
    map(([resA, resB]) => ([resA.id, resB.id])), // <- extract ids
    switchMap((resIds) => Observable.from(callApiFooC( resIds ) )),
    switchMap((resC) => Observable.from(callApiFooD( resC ) )),
    tap((resD) => console.log(resD))
).subscribe();      

同步請求 A、B 兩個接口,然後把結果作為請求 C 的參數,然後把請求 C 的傳回作為請求 D,最後列印請求 D 的結果。

這裡用到一些大家可能陌生的新的 api,需稍作解釋:

  • Observable.from 将一個 Promises 數組轉換為 Observable,它是基于 callApiFooA 和 callApiFooB 的結果數組;
  • map — 從 API 函數 A 和 B 的 Respond 中提取 ID;
  • switchMap — 使用前一個結果的 id 調用 callApiFooC,并傳回一個新的 Observable,新 Observable 是 callApiFooC( resIds ) 的傳回結果;
  • switchMap — 使用函數 callApiFooC 的結果調用 callApiFooD;
  • tap — 擷取先前執行的結果,并将其列印在控制台中;
  • subscribe — 開始監聽 observable;

Observable 是多資料值的生産者,它在處理異步資料流方面更加強大和靈活。它在 Angular 等前端架構中被使用。

這樣做有何好處?核心好處是分離 建立(釋出)  和 調用(訂閱消費) 。

異步與回調的核心意義不正在于此嗎?我訂閱你的部落格,你釋出了新内容,于是就通知我這邊,好了,這樣一來,我也不用幹等,隻要你釋出了新的文章,我就可以按照自己的方式來消費它們。各幹各的。并且我消費的方式可以是花裡胡哨的,可以坐着看、躺着看、上班看、睡覺前看、拉屎看,與你釋出無關。

異步和函數式

  1. “JavaScript 異步和函數式有什麼關系?”

有關系嗎?

異步是解決單線程設計的堵塞的,函數式是 JavaScript 的基因其中一種。二者似乎沒關系?

錯,二者有關系,并且關系莫大,粗略分為 3 點:

① 組合特性

在函數式程式設計中,我們把函數組合當作是重點之一,将函數的聲明和函數的組合調用分開。每個函數的功能職責單一,最大範圍内保持資料的不變性、資料計算的易追蹤。

在異步解決方案中,我們也盡量将對異步操作的先後關系确定清楚,誰和誰一起執行、誰先執行誰後執行、誰等待誰的結果,這些也是在調用過程中有很多操作的地方,與聲明隔開。在調用時組合好,資料流沿着時間次元演變。

② 代碼可讀性

異步從回調地獄到 Promise,到 Generator,到 async await,是為了啥?不就是為了代碼讀起來更易讀嗎?

那函數式也是,從無副作用的純函數,清晰可見地控制輸入輸出,再到函數組合,演算,也是為了更可讀。

可謂:二者志同而道和

從異步講起,『函數』和『時間』該作何關系?

③ 函數響應式程式設計

有一種程式設計方式就叫:函數響應式程式設計,你說二者什麼關系?

函數式響應式程式設計(FRP) 是一種程式設計範式,它采用函數式程式設計的基礎部件(如map、reduce、filter等),進行響應式程式設計(異步資料流程程式設計)。FRP被用于GUI、機器人和音樂方面的程式設計,旨在通過顯式的模組化時間來簡化這些問題。—— wikipedia

通俗來講,函數響應式程式設計是面向離散事件流的,在一個時間軸上會産生一些離散事件,這些事件會依次向下傳遞。

從異步講起,『函數』和『時間』該作何關系?

如圖所示,點選一個按鈕事件,随着時間推移,這個點選事件會産生三個不同的結果:

  • 發生錯誤
  • 事件完成

我們可以定義方法用來:捕獲值,捕獲錯誤,捕獲點選事件結束。

對應代碼上的,就涉及幾個基礎概念:

  • Observable(可觀察對象) :就是點選事件流。
  • Observers(觀察者) :就是捕獲值/錯誤/事件結束的方法(其實就是回調函數集合)。
  • Subscription(訂閱) :Observable 産生的值都需要通過一個‘監聽’把值傳給 Observers,這個‘監聽’就是 Subscription。
  • Producer(生産者):就是點選事件,是事件的生産者。
--a---b-c---d---X---|->

a b c d 是産生的值
X 是錯誤
| 是事件結束标志
---> 是時間線      

在前端互動非常複雜的系統中,用戶端都是基于事件程式設計的,對事件處理非常多,在這樣的場景下, 函數響應式程式設計可以更加有效率地處理事件流,而無需管理狀态。能量強大。

異步與時間

  1. “JavaScript 異步真的簡單嗎?”

想一想,JavaScript 異步的設計真的就是簡單嗎?

“給你一段同步代碼,有 10 個函數方法調用” 和 “給你一段同步加異步的代碼,其中 5 個函數方法是同步、5 個函數方法是異步”,你覺得其中哪個會更易了解?

毫無疑問,控制其它變量,盡量選擇有更多同步代碼的會更易了解。

為什麼?因為異步就代表着先後時間關系,代表着複雜!

在你所有的應用裡,最複雜的狀态就是時間。當你操作的資料狀态改變過程比較直覺的時候,是很容易管理的。但是,如果狀态随着時間因為響應事件而隐晦的變化,管理這些狀态的難度将會成幾何級增長。
從異步講起,『函數』和『時間』該作何關系?

很多情況下我們調試錯誤發現最終原因是因為異步處理的回調先後關系出錯。

是以,異步并不簡單。

怎樣才簡單?這裡提供 3 個方法,簡單釋義:

① 減少時間狀态

不喜歡時間是吧,那就異步轉同步,減少時間狀态,promise 或者 async await 就是一個很好的例子。

② 監聽(惰性)

設定監聽,就不用管時間啦,這也是另外一種消除時間狀态的方法。

我們在 Vue 這種架構中用生命周期、鈎子函數、各類監聽,正是如此,不用再管具體時間先後,架構已經幫我們限定好了,按照它的規則處理即可。

③ 函數響應式程式設計

函數響應式程式設計是更規範、更進階的讓異步更簡單的方案。

用純函數、用表達式、用組合、分離 生産者 和 消費者 、用更強大的封裝 API,代碼各司其職,可以很大程度上提高代碼的可讀性和維護性。

結語

為什麼是異步?因為我們不想浪費因同步等待阻塞的時間。

但是你時間又總給函數帶來困惑,異步中,我要沿着時間線不斷去追溯你,協調因響應先後不同帶來的差異。

狀态随着時間發生隐晦的變化,管理這些狀态,難度成幾何級增長。

代碼的可靠性?可預見性?又該從何而得?

時間,時間,請給函數以答案?

。。。。。。

相信你認真看完本篇會有一點想法和答案~~

OK,以上便是本篇分享,專欄第 5 篇,希望各位工友喜歡~ 歡迎點贊、收藏、評論 🤟

繼續閱讀