Generator函數非同步應用

来源:https://www.cnblogs.com/unclekeith/archive/2018/01/23/8335279.html
-Advertisement-
Play Games

轉載請註明出處: "Generator函數非同步應用" 上一篇文章詳細的介紹了Generator函數的語法,這篇文章來說一下如何使用Generator函數來實現非同步編程。 或許用Generator函數來實現非同步會很少見,因為ECMAScript 2016的async函數對Generator函數的流程式控制 ...


轉載請註明出處: Generator函數非同步應用

上一篇文章詳細的介紹了Generator函數的語法,這篇文章來說一下如何使用Generator函數來實現非同步編程。

或許用Generator函數來實現非同步會很少見,因為ECMAScript 2016的async函數對Generator函數的流程式控制製做了一層封裝,使得非同步方案使用更加方便。

但是呢,我個人認為學習async函數之前,有必要瞭解一下Generator如何實現非同步,這樣對於async函數的學習或許能給予一些幫助。

文章目錄

  1. 知識點簡單回顧
  2. 非同步任務的封裝
  3. thunk函數實現流程式控制制
  4. Generator函數的自動流程式控制制
  5. co模塊的自動流程式控制制

知識點簡單回顧

在Generator函數語法解析篇的文章中有說到,Generator函數可以定義多個內部狀態,同時也是遍歷器對象生成函數。yield表達式可以定義多個內部狀態,同時還具有暫停函數執行的功能。調用Generator函數的時候,不會立即執行,而是返回遍歷器對象。

遍歷器對象的原型對象上具有next方法,可以通過next方法恢復函數的執行。每次調用next方法,都會在遇到yield表達式時停下來,再次調用的時候,會在停下的位置繼續執行。調用next方法會返回具有value和done屬性的對象,value屬性表示當前的內部狀態,可能的值有yield表達式後面的值、return語句後面的值和undefined;done屬性表示遍歷是否結束。

yield表達式預設是沒有返回值的,或者說,返回值為undefined。因此,想要獲得yield表達式的返回值,就需要給next方法傳遞參數。next方法的參數表示上一個yield表達式的返回值。因此在調用第一個next方法時可以不傳遞參數(即使傳遞參數也不會起作用),此時表示啟動遍歷器對象。所以next方法會比yield表達式的使用要多一次。

更加詳細的語法可以參考這篇文章。傳送門:Generator函數語法解析

非同步任務的封裝

yield表達式可以暫停函數執行,next方法可以恢復函數執行。這使得Generator函數非常適合將非同步任務同步化。接下來會使用setTimeout來模擬非同步任務。

const person = sex => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        sex,
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)
}
const g = gen()
const next1 = g.next() // {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})

從上面代碼可以看出,第一次調用next方法時,啟動了遍歷器對象,此時返回了包含value和done屬性的對象,由於value屬性值是promise對象,因此可以使用then方法獲取到resolve傳遞過來的值,再使用帶有data參數的next方法給上一個yield表達式傳遞返回值。

此時在const data = yield person()這句語句中,就可以得到非同步任務傳遞的參數值了,實現了非同步任務的同步化。

但是上面的代碼會有問題。每次獲取非同步的值時,都要手動執行以下步驟

const g = gen()
const next1 = g.next() {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})

上面的代碼實質上就是每次都會重覆使用value屬性值和next方法,所以每次使用Generator實現非同步都會涉及到流程式控制制的問題。每次都手動實現流程式控制制會顯得麻煩,有沒有什麼辦法可以實現自動流程式控制制呢?實際上是有的: )

thunk函數實現流程式控制制

thunk函數實際上有些類似於JavaScript函數柯里化,會將某個函數作為參數傳遞到另一個函數中,然後通過閉包的方式為參數(函數)傳遞參數進而實現求值。

函數柯里化實現的過程如下

function curry (fn) {
  const args1 = Array.prototype.slice.call(arguments, 1)
  return function () {
    const args2 = Array.from(arguments)
    const arr = args1.concat(args2)
    return fn.apply(this, arr)
  }
}

使用curry函數來舉一個例子: )

// 需要柯里化的sum函數
const sum = (a, b) => {
  return a + b
}
curry(sum, 1)(2)   // 3

而thunk函數簡單的實現思路如下:

// ES5實現
const thunk = fn => {
  return function () {
    const args = Array.from(arguments)
    return function (callback) {
      args.push(callback)
      return fn.apply(this, args)
    }
  }
}

// ES6實現
const thunk = fn => {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback)
    }
  }
}

從上面thunk函數中,會發現,thunk函數比函數curry化多用了一層閉包來封裝函數作用域。

使用上面的thunk函數,可以生成fs.readFile的thunk函數。

const fs = require('fs')
const readFileThunk = thunk(fs.readFile)
readFileThunk(fileA)(callback)

使用thunk函數將fs.readFile包裝成readFileThunk函數,然後在通過fileA傳入文件路徑,callback參數則為fs.readFile的回調函數。

當然,還有一個thunk函數的升級版本thunkify函數,可以使得回調函數只執行一次。原理和上面的thunk函數非常像,只不過多了一個flag參數用於限制回調函數的執行次數。下麵我對thunkify函數做了一些修改。源碼地址: node-thunkify

const thunkify = fn => {
  return function () {
    const args = Array.from(arguments)
    return function (callback) {
      let called = false
      // called變數限制callback的執行次數
      args.push(function () {
        if (called) return
        called = true
        callback.apply(this, arguments)
      })
      try {
        fn.apply(this, args)
      } catch (err) {
        callback(err)
      }
    }
  }
}

舉個例子看看: )

function sum (a, b, callback) {
  const total = a + b
  console.log(total)
  console.log(total)
}

// 如果使用thunkify函數
const sumThunkify = thunkify(sum)
sumThunkify(1, 2)(console.log)
// 列印出3

// 如果使用thunk函數
const sumThunk = thunk(sum)
sumThunk(1, 2)(console.log)
// 列印出 3, 3

再來看一個使用setTimeout模擬非同步並且使用thunkify模塊來完成非同步任務同步化的例子。

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
const g = gen()
const next = g.next()
next.value(data => {
  g.next(data)
})

從上面代碼可以看出,value屬性實際上就是thunkify函數的回調函數(也是person的第二個參數),而'boy'則是person的第一個參數。

Generator函數的自動流程式控制制

在上面的代碼中,我們可以將調用遍歷器對象生成函數,返回遍歷器和手動執行next方法以恢復函數執行的過程封裝起來。

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value(next)
  }
  next()
}

使用run函數封裝起來之後,run內部的next函數實際上就是thunk(thunkify)函數的回調函數了。因此,調用run即可實現Generator的自動流程式控制制。

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
run(gen)
// {sex: 'boy', name: 'keith', height: 180}

有了這個執行器,執行Generator函數就方便多了。不管內部有多少個非同步操作,直接把Generator函數傳入run函數即可。當然,前提是每一個非同步操作,都要是thunk(thunkify)函數。也就是說,跟在yield表達式後面的必須是thunk(thunkify)函數。

const gen = function *gen () {
  const f1 = yield personThunk('boy') // 跟在yield表達式後面的非同步行為必須使用thunk(thunkify)函數封裝
  const f2 = yield personThunk('boy')
  // ...
  const fn = yield personThunk('boy')
}
run(gen)  // run函數的自動流程式控制制

上面代碼中,函數gen封裝了n個非同步行為,只要執行run函數,這些操作就會自動完成。這樣一來,非同步操作不僅可以寫得像同步操作,而且一行代碼就可以執行。

co模塊的自動流程式控制制

在上面的例子說過,表達式後面的值必須是thunk(thunkify)函數,這樣才能實現Generator函數的自動流程式控制制。thunk函數的實現是基於回調函數的,而co模塊則更進一步,可以相容thunk函數和Promise對象。先來看看co模塊的基本用法

const co = require('co')
const gen = function *gen () {
  const f1 = yield person('boy') // 調用person,返回一個promise對象
  const f2 = yield person('boy')
}
co(gen)   // 將thunk(thunkify)函數和run函數封裝成了co模塊,yield表達式後面可以是thunk(thunkify)函數或者Promise對象

co模塊可以不用編寫Generator函數的執行器,因為它已經封裝好了。將Generator函數co模塊中,函數就會自動執行。

co函數返回一個Promise對象,因此可以用then方法添加回調函數。

co(gen).then(function (){
  console.log('Generator 函數執行完成')
})

co模塊原理;co模塊其實就是將兩種自動執行器(thunk(thunkify)函數和Promise對象),包裝成一個模塊。使用co模塊的前提條件是,Generator函數的yield表達式後面,只能是thunk(thunkify)或者Promise對象,如果是數組或對象的成員全部都是promise對象,也可以使用co模塊。

基於Promise對象的自動執行

還是使用上面例子,不過這次是將回調函數改成Promise對象來實現自動流程式控制制。

const person = (sex, fn) => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)   // {name: 'keith', height: 180}
}
const g = gen()
g.next().value.then(data => {
  g.next(data)
})

手動執行實際上就是層層使用then方法和next方法。根據這個可以寫出自動執行器。

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value.then(data => {
      next(data)
    })
  }
  next()
}
run(gen)  // {name: 'keith', height: 180}

如果對co模塊感興趣的朋友,可以閱讀一下它的源碼。傳送門:co

關於Generator非同步應用的相關知識也就差不多了,現在稍微總結一下。

  1. 由於yield表達式可以暫停執行,next方法可以恢復執行,這使得Generator函數很適合用來將非同步任務同步化。
  2. 但是Generator函數的流程式控制制會稍顯麻煩,因為每次都需要手動執行next方法來恢復函數執行,並且向next方法傳遞參數以輸出上一個yiled表達式的返回值。
  3. 於是就有了thunk(thunkify)函數和co模塊來實現Generator函數的自動流程式控制制。
  4. 通過thunk(thunkify)函數分離參數,以閉包的形式將參數逐一傳入,再通過apply或者call方法調用,然後配合使用run函數可以做到自動流程式控制制。
  5. 通過co模塊,實際上就是將run函數和thunk(thunkify)函數進行了封裝,並且yield表達式同時支持thunk(thunkify)函數和Promise對象兩種形式,使得自動流程式控制制更加的方便。

參考資料

  1. Generator 函數的非同步應用
  2. node-thunkify
  3. co

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

-Advertisement-
Play Games
更多相關文章
  • 我錄屏的方式是分別錄製音頻和視頻,最後合併成mp4格式,比較麻煩,因為網上完整的教程比較少,所以我打算寫一個完整版的,照著我的代碼寫完之後,至少是能夠實現功能的,而不是簡單的介紹下用法。 1既然是錄製視頻,我們應該有一個按鈕控制開始和結束。 2在錄製之前,需要先判斷一下Android系統的版本是否大 ...
  • 先上效果圖 灑豆子的效果,突發奇想,覺得這個動畫挺有意思的,就抽空寫了一個玩玩 繪製流程: 定義6個‘’豆子‘’,每個豆子有各自的屬性,大小,拋出的速度等,然後控制每個的方向和狀態,回彈效果使用差值器 BounceInterpolator package com.fragmentapp.view.b ...
  • 本文中我們將講解一下App的長連接實現。一般而言長連接已經是App的標配了,推送功能的實現基礎就是長連接,當然了我們也可以通過輪訓操作實現推送功能,但是輪訓一般及時性比較差,而且網路消耗與電量銷毀比較多,因此一般推送功能都是通過長連接實現的。 那麼如何實現長連接呢?現在一般有這麼幾種實現方式: 使用 ...
  • Genymotion是一款非常好用的虛擬機,利用它可以在window、Liunx或MAC系統上實現Android的模似器。對於開發人員來說,有了Android模似器,就可以在電腦上實時調試安卓app,而不用外接手機設置,非常方便。 現在我們開始安裝一個試下。 1.保證CPU是否支持虛擬化技術。 我們 ...
  • 最近發現css遺忘了很多,原因在於平時很少用到一些樣式,現記錄一些平時工作中使用頻率比較少的屬性以備查看。 1.文本屬性 首行文本縮進,針對於塊級元素,text-indent 可以使用所有長度單位,包括百分比值。百分比是相對於父級元素設置的。 文本對齊屬性值中有個justify是兩端對齊,之前用的比 ...
  • 簡介 前面寫了一篇文章講解了position常用的幾個屬性:《CSS 屬性之 position講解》一般都知道下麵幾個常用的: 在https://developer.mozilla.org/zh-CN/docs/Web/CSS/position還說了下麵這三個值: 估計大部分都沒有用過positio ...
  • 1:字元串 JS中的任何數據類型都可以當作對象來看。所以string既是基本數據類型,又是對象。 2:聲明字元串 var sStr = ‘字元串’; var oStr = new String(‘字元串’); 3:字元串屬性 1.length 計算字元串的長度(不區分中英文)。 2.construc ...
  • 模塊載入過程: 路徑分析 文件定位 模塊編譯 Node對引入過的模塊都會進行緩存,以減少二次引入時的開銷。緩存的是編譯和執行之後的對象。require時對緩存中的模塊是第一優先順序的 路徑分析 模塊標識符:require的參數,按書寫形式可以分成以下幾類: 核心模塊:如http,fs,path 文件模 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...