0%

每天一點點,學習ES6-九-Generators

前言

學會了異步 promise 來來研究Generators

雀躍


傳統方法

ES6 誕生以前,不同步驟的方法,大致有下面四種。

  • 回調函數
  • 事件監聽
  • 發布/訂閱
  • 承諾對象
    Generator 函數將 JavaScript 異步編程帶入了一個全新的階段。

回調函數

就是大家所稱的 CallBack

JavaScript 對異常編寫程序的實際發現,就是回調函數。

所稱回調函數,就是把任務的第二段單獨寫在一個函數裡,等到重新執行進行這個任務的時候,就直接調用這個函數。

1
2
3
4
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});

Promise

promise

如果想深入了解可以去之前 的地方看看:)

以上都是比較常用的異步解決方法,之後還有async/await
接下來要研究今天的主角 Generator

Generator

執行 Generator 函數會返回一個遍歷器Obj對象,可以依次遍歷 Generator 函數內部的每一個狀態

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

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

使用方式

Generator會返回一個遍歷器對象,即具有Symbol.iterator屬性,並且返回給自己

1
2
3
4
5
6
7
8
function* gen(){
// some code
}

var g = gen();

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

通過yield關鍵字可以暫停generator函數返回的遍歷器對象的狀態

1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
hw.next()
// { value: 'hello', done: false }

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

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

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

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

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

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

通過調用next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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進行遍歷

1
2
3
4
5
6
7
8
9
10
11
12
13
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

原生Obj沒有遍歷接口,通過Generator函數為它加上這個接口,就能使用for...of進行遍歷了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

異步的解決方案

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

  • 回調函數 Callback
  • Promise 對象
  • generator 函數
  • async/await
    這裡通過文件讀取案例,將幾種解決異步的方案進行一個比較:

回調函數 CallBack

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

1
2
3
4
5
6
7
8
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這個文件以後,回調函數 CallBack才會執行

Promise 對象

Promise就是為了解決回調地獄而產生的,將回調函數的嵌套,改成鍊式調用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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函數非常適合將異步任務同步化

1
2
3
4
5
6
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形式,更為簡潔,語義化更強了

1
2
3
4
5
6
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編寫代碼相比Generator、async更為複雜化,且可讀性也稍差
  • Generatorasync需要與promise對象搭配處理異步情況
  • async實質是Generator的語法糖,相當於會自動執行Generator函數
  • async使用上更為簡潔,將異步代碼以同步的形式進行編寫,是處理異步編程的最終方案

使用場景

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

1
2
3
4
5
6
7
8
9
10
11
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()

// 卸载UI
loader.next()

包括redux-saga中間件也充分利用了Generator特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

dance


References

你的支持是我活力的來源 :)