你是怎麼理解ES6中 Generator的?使用場景?

林恒發表於2024-03-14

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

你是怎麼理解ES6中 Generator的?使用場景?

一、介紹

Generator 函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同

回顧下上文提到的解決非同步的手段:

  • 回撥函式
  • promise

那麼,上文我們提到promsie已經是一種比較流行的解決非同步方案,那麼為什麼還出現Generator?甚至async/await呢?

該問題我們留在後面再進行分析,下面先認識下Generator

Generator函式

執行 Generator 函式會返回一個遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態

形式上,Generator函式是一個普通函式,但是有兩個特徵:

  • function關鍵字與函式名之間有一個星號
  • 函式體內部使用yield表示式,定義不同的內部狀態
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

二、使用

Generator 函式會返回一個遍歷器物件,即具有Symbol.iterator屬性,並且返回給自己

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

透過yield關鍵字可以暫停generator函式返回的遍歷器物件的狀態

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

上述存在三個狀態:helloworldreturn

透過next方法才會遍歷到下一個內部狀態,其執行邏輯如下:

  • 遇到yield表示式,就暫停執行後面的操作,並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。
  • 下一次呼叫next方法時,再繼續往下執行,直到遇到下一個yield表示式
  • 如果沒有再遇到新的yield表示式,就一直執行到函式結束,直到return語句為止,並將return語句後面的表示式的值,作為返回的物件的value屬性值。
  • 如果該函式沒有return語句,則返回的物件的value屬性值為undefined
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

done用來判斷是否存在下個狀態,value對應狀態值

yield表示式本身沒有返回值,或者說總是返回undefined

透過呼叫next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

正因為Generator函式返回Iterator物件,因此我們還可以透過for...of進行遍歷

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

原生物件沒有遍歷介面,透過Generator函式為它加上這個介面,就能使用for...of進行遍歷了

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

三、非同步解決方案

回顧之前展開非同步解決的方案:

  • 回撥函式
  • Promise 物件
  • generator 函式
  • async/await

這裡透過檔案讀取案例,將幾種解決非同步的方案進行一個比較:

回撥函式

所謂回撥函式,就是把任務的第二段單獨寫在一個函式里面,等到重新執行這個任務的時候,再呼叫這個函式

fs.readFile('/etc/fstab', function (err, data) {
  if (err) throw err;
  console.log(data);
  fs.readFile('/etc/shells', function (err, data) {
    if (err) throw err;
    console.log(data);
  });
});

readFile函式的第三個引數,就是回撥函式,等到作業系統返回了/etc/passwd這個檔案以後,回撥函式才會執行

Promise

Promise就是為了解決回撥地獄而產生的,將回撥函式的巢狀,改成鏈式呼叫

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};


readFile('/etc/fstab').then(data =>{
    console.log(data)
    return readFile('/etc/shells')
}).then(data => {
    console.log(data)
})

這種鏈式操作形式,使非同步任務的兩段執行更清楚了,但是也存在了很明顯的問題,程式碼變得冗雜了,語義化並不強

generator

yield表示式可以暫停函式執行,next方法用於恢復函式執行,這使得Generator函式非常適合將非同步任務同步化

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async/await

將上面Generator函式改成async/await形式,更為簡潔,語義化更強了

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

區別:

透過上述程式碼進行分析,將promiseGeneratorasync/await進行比較:

  • promiseasync/await是專門用於處理非同步操作的

  • Generator並不是為非同步而設計出來的,它還有其他功能(物件迭代、控制輸出、部署Interator介面...)

  • promise編寫程式碼相比Generatorasync更為複雜化,且可讀性也稍差

  • Generatorasync需要與promise物件搭配處理非同步情況

  • async實質是Generator的語法糖,相當於會自動執行Generator函式

  • async使用上更為簡潔,將非同步程式碼以同步的形式進行編寫,是處理非同步程式設計的最終方案

四、使用場景

Generator是非同步解決的一種方案,最大特點則是將非同步操作同步化表達出來

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 載入UI
loader.next()

// 解除安裝UI
loader.next()

包括redux-saga中介軟體也充分利用了Generator特性

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

還能利用Generator函式,在物件上實現Iterator介面

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

參考文獻

  • https://es6.ruanyifeng.com/#docs/generator-async

相關文章