以前的文章 寫到了 Generator 與異步編程的關係,其實簡化異步編程只是 Generator 的「副業」,Generator 自己卻不是爲異步編程而存在。git
咱們看 Generator 自身的含義——生成器,就是產生序列用的。好比有以下函數:github
function* range(start, stop) { for (let item = start; item < stop; ++item) { yield item; } }
range 就是一個生成器函數,它自身是函數能夠調用(typeof range === 'function' // true
),但又與普通函數不一樣,生成器函數(GeneratorFunction)永遠返回一個生成器(Generator)編程
注:咱們一般所說的 Generator
實際上指生成器函數
(GeneratorFunction),而把生成器函數
返回的對象稱做迭代器
(Iterator)。因爲感受「生成器函數」返回「生成器」這句話有些拗口,下文沿用生成器和迭代器的說法。segmentfault
初次調用生成器實際上不執行生成器函數的函數體,它只是返回一個迭代器,當用戶調用迭代器的 next 函數時,程序纔開始真正執行生成器的函數體。當程序運行到 yield 表達式時,會將 yield 後面表達式的值做爲 next
函數的返回值(的一部分)返回,函數自己暫停執行。數組
const iterator = range(0, 10); // 獲取迭代器 const value1 = iterator.next().value; // 獲取第一個值 => 0 const value2 = iterator.next().value; // 獲取第二個值 => 1
next
返回值是一個對象,包含兩個屬性 value
和 done
。value 即爲 yield 後面表達式的值,done 表示函數是否已經結束(return)。若是函數 return(或執行到函數尾退出,至關於 return undefined
),則 done 爲 true,value 爲 return 的值。瀏覽器
for...of 是遍歷整個迭代器的簡單方式。異步
上面說到,生成器就是生成序列用的。可是與直接返回數組不一樣,生成器返回序列是一項一項計算並返回的,而返回數組老是須要計算出全部值後統一返回。因此至少有三種狀況應該考慮使用生成器。異步編程
range(0, Infinity)
是容許的,由於生成器沒生成一個值就會暫停執行,因此不會形成死循環,能夠由調用者選擇什麼時候中止。函數
注意此時不能使用 for...of
,由於迭代器永遠不會 done
this
若是計算一項的值須要 1ms,那麼計算 1000 項就須要 1s,若是不將這 1s 拆分,就會致使瀏覽器卡頓甚至假死。這時能夠用生成器每生成幾項就將控制權交還給瀏覽器,用於響應用戶事件,提高用戶體驗(固然這裏更有效的方法是將代碼放到 Web Worker 裏執行)
若是序列很長,直接返回數組會佔用較大內存。生成器返回值是一項一項返回,不會一次性佔用大量內存(固然生成器爲了保留執行上下文比一般函數佔用內存更多,可是它是個定值,不隨迭代次數增長)
Array#map、Array#filter 是 ES5 引入的(絕對不算新的)兩個很是經常使用的函數,前者將數組每一項經過回調函數映射到新數組(值變量不變),後者經過回調函數過濾某些不須要的項(量變值不變),他們都會生成新的數組對象,若是數組自己較長或者寫了很長的 map、filter 調用鏈,就可能形成內存浪費。
這時就能夠考慮使用生成器實現這兩個函數,很是簡單:
function* map(iterable, callback) { let i = 0; for (const item of iterable) { // 遍歷數組 yield callback(item, i++, iterable); // 獲取其中一項,調用回調函數,yield 返回值 } } function* filter(iterable, callback) { let i = 0; for (const item of iterable) { // 遍歷數組 if (callback(item, i++, iterable)) { // 獲取其中一項,調用回調函數 yield item; // 僅當回調函數返回真時,才 yield 值 } } }
能夠看到我在代碼中寫的是「可迭代的」(iterable
),而不限於數組(因爲實現了 Symbol.iterator 因此數組也是可迭代對象)。好比能夠這麼用:
const result = map( // (1) filter( // (2) range(1, 10000), // (3) x => x % 2 === 0, ), x => x / 2, ) console.log(...result); // (4)
注意,程序在解構運算符 ...result
這一步才真正開始計算 result 的值(所謂的懶加載),並且它的值也是一個一個計算的:
for...of
遍歷(2)提供的迭代器,因而(2)開始執行for...of
遍歷(3)提供的迭代器,因而(3)開始執行1
。遇到 yield
關鍵字,將值 1
輸出給(2)for...of
得到一個值 1
,執行函數體。callback 返回 false,忽略之。回到 for...of
,繼續問(3)索要下一個值2
for...of
得到一個值 2
,執行函數體。callback 返回 true,將值 2
輸出給 (1)for...of
得到一個值 2
,執行函數體獲得 1
。將值 1
輸出給(4),console.log 得到第一個參數for...of
檢測到迭代器已被關閉(done爲true),循環結束,函數退出,(2)返回的迭代器被關閉console.log
的參數列表輸出總結一下,代碼執行順序大概是這樣:(3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> (2) -> (3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> ……
是否是至關複雜?異步函數中「跳來跳去」的執行順序也就是這麼來的。跟遞歸函數同樣,不要太糾結生成器函數的執行順序,而要着重理解它這一步究竟要作什麼事情。
這樣的代碼 map(filter(range(1, 100), x => x % 2 === 0), x => x / 2)
彷佛有些d疼,好像又看到了被回調函數支配的恐懼。雖然有人提出了管道符的提議,但這種 stage 1
的提議被標準接受直至有瀏覽器實現實在是遙遙無期,有沒有其餘辦法呢?
普通函數能夠簡單的經過在類中返回 this 實現函數的鏈式調用(例如經典的 jQuery),可是這點在生成器中不適用。咱們前面說過生成器函數自己永遠返回一個迭代器,而生成器中的 return
語句其實是關閉迭代器的標誌,return this
實際表明 { value: this, done: true }
。生成器中的 return 和普通函數用法相近但實際含義大大不一樣。
鏈式調用須要函數的返回值是個對象,而且對象中包含可鏈式調用的全部函數。生成器函數返回的迭代器自己就是一個對象,很容易想到改變對象的原型實現。
迭代器有以下原型繼承鏈:
迭代器對象 -> 生成器.prototype -> 生成器.prototype.prototype(Generator) -> Object.prototype -> null
能夠看到,生成器返回的迭代器對象就好像是被生成器 new
出來的同樣(可是生成器不是構造函數不能被 new)。可是總之咱們能夠經過給生成器函數的 prototype
添加方法實現給迭代器添加方法的效果。實現以下
function* range(start, stop) { for (let item = start; item < stop; ++item) { yield item; } } function* map(callback) { let i = 0; for (const item of this) { yield callback(item, i++, this); } } function* filter(callback) { let i = 0; for (const item of this) { if (callback(item, i++, this)) { yield item; } } } [range, map, filter].forEach(x => Object.assign(x.prototype, { range, map, filter })); // 使用 const result = range(1, 100) .filter(x => x % 2 === 0) .map(x => x / 2); console.log(...result);
筆者業(xian)餘(de)時(dan)間(teng)使用迭代器實現了幾乎全部 ES7 中 Array 的成員方法和靜態方法,廣而告之,歡迎來噴:https://github.com/CarterLi/q...