漫話JavaScript與非同步·第三話——Generator:化非同步為同步

来源:https://www.cnblogs.com/leegent/archive/2018/01/10/8207246.html
-Advertisement-
Play Games

ES6新增的特性中,Generator無疑是最為強大者之一,它與Promise結合起來,為令前端頭疼的非同步回調難題提供了終極解決方案! ...


一、Promise並非完美

我在上一話中介紹了Promise,這種模式增強了事件訂閱機制,很好地解決了控制反轉帶來的信任問題、硬編碼回調執行順序造成的“回調金字塔”問題,無疑大大提高了前端開發體驗。但有了Promise就能完美地解決非同步問題了嗎?並沒有。

首先,Promise仍然需要通過then方法註冊回調,雖然只有一層,但沿著Promise鏈一長串寫下來,還是有些讓人頭暈。

更大的問題在於Promise的錯誤處理比較麻煩,因為Promise鏈中拋出的錯誤會一直傳到鏈尾,但在鏈尾捕獲的錯誤卻不一定清楚來源。而且,鏈中拋出的錯誤會fail掉後面的整個Promise鏈,如果要在鏈中及時捕獲並處理錯誤,就需要給每個Promise註冊一個錯誤處理回調。噢,又是一堆回調!

那麼最理想的非同步寫法是怎樣的呢?像同步語句那樣直觀地按順序執行,卻又不會阻塞主線程,最好還能用try-catch直接捕捉拋出的錯誤。也就是說,“化非同步為同步”!

痴心妄想?

我在第一話里提到,非同步和同步之間的鴻溝在於:同步語句的執行時機是“現在”,而非同步語句的執行時機在“未來”。為了填平鴻溝,如果一個非同步操作要寫成同步的形式,那麼同步代碼就必須有“等待”的能力,等到“未來”變成“現在”的那一刻,再繼續執行後面的語句。

在不阻塞主線程的前提下,這可能嗎?

聽起來不太可能。幸好,Generator(生成器)為JS帶來了這種超能力!

 

二、“暫停/繼續”魔法

ES6引入的新特性中,Generator可能是其中最強大也最難理解的之一,即使看了阮一峰老師列舉的大量示例代碼,知道了它的全部API,也仍是不得要領,這是因為Generator的行為方式突破了我們所熟知的JS運行規則。可一旦掌握了它,它就能賦予我們巨大的能量,極大地提升代碼質量、開發效率,以及FEer的幸福指數。

我們先來簡單回顧一下,ES6之前的JS運行規則是怎樣的呢?

1. JS是單線程執行,只有一個主線程

2. 宿主環境提供了一個事件隊列,隨著事件被觸發,相應的回調函數被放入隊列,排隊等待執行 

3. 函數內的代碼從上到下順序執行;如果遇到函數調用,就先進入被調用的函數執行,待其返回後,用返回值替代函數調用語句,然後繼續順序執行

對於一個FEer來說,日常開發中理解到這個程度已經夠用了,直到他嘗試使用Generator……

function* gen() {
    let count = 0;
    while(true) {
        let msg = yield ++count;
        console.log(msg);
    }
}

let iter = gen();
console.log(iter.next().value);
// 1
console.log(iter.next('magic').value);
// 'magic'
// 2

等等,gen明明是個function,執行它時卻不執行裡面的代碼,而是返回一個Iterator對象?代碼執行到yield處竟然可以暫停?暫停以後,竟然可以恢復繼續執行?說好的單線程呢?另外,暫停/恢復執行時,還可以傳出/傳入數據?怎麼肥四?難道ES6對JS做了什麼魔改?

其實Generator並沒有改變JS運行的基本規則,不過套用上面的naive JS觀已經不足以解釋其實現邏輯了,是時候掏出長年在書架上吃灰的電腦基礎,重溫那些考完試就忘掉的知識。

  

三、法力的秘密——棧與堆

(註:這個部分包含了大量的個人理解,未必準確,歡迎指教)

理解Generator的關鍵點在於理解函數執行時,記憶體里發生了什麼

一個JS程式的記憶體分為代碼區、棧區、堆區和隊列區,從MDN借圖一張以說明(圖中沒有畫出代碼區):

隊列(Queue)就是FEer所熟知的事件迴圈隊列。

代碼區保存著全部JS源代碼被引擎編譯成的機器碼(以V8為例)。

棧(stack)保存著每個函數執行所需的上下文,一個棧元素被稱為一個棧幀,一個棧幀對應一個函數。

對於引用類型的數據,在棧幀里只保存引用,而真正的數據存放在堆(Heap)里。堆與棧不同的是,棧記憶體由JS引擎自動管理,入棧時分配空間,出棧時回收,非常清楚明瞭;而堆是程式員通過new操作符手動向操作系統申請的記憶體空間(當然,用字面量語法創建對象也算),何時該回收沒那麼明晰,所以需要一套垃圾收集(GC)演算法來專門做這件事。

扯了一堆預備知識,終於可以回到Generator的正題了:

普通函數在被調用時,JS引擎會創建一個棧幀,在裡面準備好局部變數函數參數臨時值代碼執行的位置(也就是說這個函數的第一行對應到代碼區里的第幾行機器碼),在當前棧幀里設置好返回位置,然後將新幀壓入棧頂。待函數執行結束後,這個棧幀將被彈出棧然後銷毀,返回值會被傳給上一個棧幀。

當執行到yield語句時,Generator的棧幀同樣會被彈出棧外,但Generator在這裡耍了個花招——它在堆里保存了棧幀的引用(或拷貝)!這樣當iter.next方法被調用時,JS引擎便不會重新創建一個棧幀,而是把堆里的棧幀直接入棧。因為棧幀里保存了函數執行所需的全部上下文以及當前執行的位置,所以當這一切都被恢復如初之時,就好像程式從原本暫停的地方繼續向前執行了。

而因為每次yield和iter.next都對應一次出棧和入棧,所以可以直接利用已有的棧機制,實現值的傳出和傳入

這就是Generator魔法背後的秘密!

 

四、終極方案:Promise+Generator

Generator的這種特性對於非同步來說,意味著什麼呢?

意味著,我們終於獲得了一種在不阻塞主線程的前提下實現“同步等待”的方法!

為便於說明,先上一段直接使用回調的代碼:

let it = gen();  // 獲得迭代器

function request() {
    ajax({
        url: 'www.someurl.com',
        onSuccess(res){
            it.next(res);  // 恢復Generator運行,同時向其中塞入非同步返回的結果
        }
    });
}

function* gen() {
    let response = yield request();
    console.log(response.text);
}

it.next();  // 啟動Generator

註意let response = yield request()這行代碼,是不是很有同步的感覺?就是這個Feel!

我們來仔細分析下這段代碼是如何運行的。首先,最後一行it.next()使得Generator內部的代碼從頭開始執行,執行到yield語句時,暫停,此時可以把yield想象成return,Generator的棧幀需要被彈出,會先計算yield右邊的表達式,即執行request函數調用,以獲得用於返回給上一級棧幀的值。當然request函數沒有返回值,但它發送了一個非同步ajax請求,並註冊了一個onSuccess回調,表示在請求返回結果時,恢復Generator的棧幀並繼續運行代碼,並把結果作為參數塞給Generator,準確地說是塞到yield所在的地方,這樣response變數就獲得了ajax的返回值。

可以看出,這裡yield的功能設計得非常巧妙,好像它可以“賦值”給response。

更妙的是,迭代器不但可以.next,還可以.throw,即把錯誤也拋入Generator,讓後者來處理。也就是說,在Generator里使用try-catch語句捕獲非同步錯誤,不再是夢!

先別急著激動,上面的代碼還是too young too simple,要真正發揮Generator處理非同步的威力,還得結合他的好兄弟——Promise一起上陣。代碼如下:

function request() {  // 此處的request返回的是一個Promise
    return new Promise((resolve, reject) => {
        ajax({
            url: 'www.someurl.com',
            onSuccess(res) {
                resolve(res);
            },
            onFail(err) {
                reject(err);
            }
         });
    });
}

let it = gen();
let p = it.next().value;  // p是yield返回的Promise
p.then(res => it.next(res),
    err => it.throw(err)  // 發生錯誤時,將錯誤拋入生成器
);

function* gen() {
    try {
        let response = yield request();
        console.log(response.text);
    } catch (error) {
        console.log('Ooops, ', error.message);  // 可以捕獲Promise拋進來的錯誤!
    }
}

這種寫法完美結合了Promise和Generator的優點,可以說是FEer們夢寐以求的超級武器。

但聰明的你一定看得出來,這種寫法套路非常固定,當Promise對象一多時,就需要寫許多類似於p.then(res => ...., err => ...)這樣的重覆語句,所以人們為了偷懶,就把這種套路給提煉成了一個更加精簡的語法,那就是傳說中的async/await

async funtion fetch() {
    try {
        let response = await request();  // request定義同上一端段示例代碼
        console.log(response.text);
    } catch (error) {
        console.log('Ooops, ', error.message);
    }
}

fetch();

這這這。。。就靠攏同步風格的程度而言,我覺得async/await已經到了登峰造極的地步~

順便說一句,著名Node.js框架Koa2正是要求中間件使用這種寫法,足見其強大和可愛。

前端們,擦亮手中的新銳武器,準備迎接來自非同步的高難度挑戰吧!

 

寫在最後

距離發表第二話(Promise)已經過去大半年了,原本設想的終章——第三話(Generator),卻遲遲未能動筆,因為筆者一直沒能弄懂Generator這個行為怪異的家伙究竟是如何存在於JS世界的,又如何成為“回調地獄”的終極解決方案?直到回頭彌補了一些電腦基礎知識,才最終突破了理解上的障礙,把Generator的來龍去脈想清楚,從而敢應用到實際工作中。所以說,基礎是很重要的,這是永不過時的真理。前端發展非常迅速,框架、工具日新月異,只有基礎扎實,才能從容應對,任他風起雲涌,我自穩坐釣魚台。


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

-Advertisement-
Play Games
更多相關文章
  • 看到很多小程式里,點客服,提示關註公眾號,比如製作器里這個功能,能夠自動引導關註公眾號,圖文體驗非常好,研究了小程式客服介面後,我們就自己把它做成一個工具了,方便小程式的運營人員。芝麻小客服 體驗傳送門 http://xiaokefu.hotapp.cn 特點: (1)不需要開發,只需要在微信的小程 ...
  • 首先,咱得先說下註意點: Android中主要通過RecognizerIntent來實現語音識別,其實代碼比較簡單,但是如果找不到設置,就會拋出異常 ActivityNotFoundException,所以我們需要捕捉這個異常。而且語音識別在模擬器上是無法測試的,因為語音識別是訪問google 雲端 ...
  • 首先看一下安裝apk文件的代碼 測試發現該段代碼在7.0一下的機型上可以成功打開指定路徑下的指定apk文件 , 但是在7.0+的機型上調用該代碼會報錯: 原因在於:Android 7.0 版本開始 禁止向你的應用外公開 file:// URI。 如果一項包含文件 file:// URI類型 的 In ...
  • libyuv是Google開源庫,可用作圖像數據格式的轉換,比如視頻流編解碼時格式的轉換,YUV數據轉化RGB等 libyuv靜態庫 為了方便使用,已經將libyuv源代碼打包成了iOS靜態庫, "libyuv靜態庫" libyuv使用 下麵以nv12(yuv420sp)轉化為I420(yuv420 ...
  • 最近都在折騰 Sagit 架框的記憶體釋放的問題,所以對這一塊有些心得。對於新手,學到的文章都在教你用:typeof(self) __weak weakSelf = self。對於老手,可能早習慣了到處了WeakSelf了。這次,就來學學,如何不用WeakSelf。 ...
  • 1,navigator 跳轉時 wxml頁面(參數多時可用“&”) 或者添加點擊事件,js用navigateTo跳轉傳參,兩種效果一樣 js頁面 在onLoad里直接獲取 2.全局變數 app.js頁面 賦值: 取值: 3.列表index下標取值 wxml頁面 如果需要傳遞多個,可以寫多個data- ...
  • 先看一下消息轉發流程: 在forwardInvocation這一步,你必須要實現一個方法: 該方法用於說明消息的返回值和參數類型。NSMethodSignature是方法簽名,它是用來記錄返回值和參數類型的一個對象。看一下與該類相關的方法: 2和3兩個方法是根據SEL來構造NSMethodSigna ...
  • 空頁面的顯示很常用,所以自己做了一個通用的空頁面顯示,先看效果圖 在有網路的時候正常載入顯示,在沒有網路的時候自動載入空頁面,點擊空頁面重新載入網路請求的一個功能 1:定義一個xml頁面,頁面佈局是一個iamgeview和一個textview的顯示 2:添加輔助類,控制載入空頁面和顯示隱藏等邏輯 3 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...