編者薦語:
本文幫助你們瞭解迭代器Iterator
函數和生成器Generator
函數中的用法特色和原理,以及如何用生成器Generator
函數的特色實現async/await
的內部原理。javascript
在文章的最後,我會帶你們手寫一個async/await
執行原理。前端
在說生成器函數和迭代器函數以前,咱們先來介紹一下幾個概念java
迭代器 Iterator
迭代器Iterator
是 ES6 引入的一種新的遍歷機制,同時也是一種特殊對象,它具備一些專門爲迭代過程設計的專有接口。web
每一個迭代器對象都有一個next()
方法,每次調用都返回一個當前結果對象。當前結果對象中有兩個屬性:編程
value:當前屬性的值數組
done:用於判斷是否遍歷結束,當沒有更多可返回的數據時,返回truepromise
迭代器還會保存一個內部指針,用來指向當前數據對象中值的位置,每調用一次next()
方法,都會返回下一個可用的值,直到遍歷結束。微信
迭代器屬性
迭代器屬性:
Symbol.iterator
數據結構
瞭解過Symbol.iterator
的同窗都知道,它存在於數組、類數組、字符串、arguments的原型對象上。異步
因此咱們看下面一個例子:
let arr = [10, 20, 30];
let it = arr.next();
console.log(it); // arr.next is not a function
數組是能夠被迭代的數據,由於Array.prototype
上有Symbol.iterator
迭代器屬性,
但並非一個迭代器,由於要調用迭代器屬性
,才能生成迭代器,好比看下面的代碼:
let arr = [10, 20, 30];
let it = arr[Symbol.iterator](arr);
console.log(it); // Array Iterator {}
返回的迭代器對象上,能夠經過原型鏈找到迭代方法next()
。
迭代器的分類
-
數組迭代器 -
字符串迭代器 -
...
let str = "";
str[Symbol.iterator]("abcd"); // Array Iterator {}
let arr = [10, 20, 30];
arr[Symbol.iterator](arr); // StringIterator {}
它們的迭代器對象上,也都有Symbol.iterator()
方法和next()
方法
迭代器的執行流程
先經過迭代器屬性
Symbol.iterator
建立一個迭代器,指向當前數據結構的起始位置隨後經過
next()
方法進行向下迭代指向下一個位置,next()
方法會返回當前位置
的對象,對象包含了 value 和 done 兩個屬性, value 是當前屬性的值, done 用於判斷是否遍歷結束當 done 爲 true 時則遍歷結束
對照着代碼來執行一下:
let arr = [10, 20, 30];
let it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: false }
console.log(it.next()); // { value: undefined, done: true }
let str = "abcd";
let it = str[Symbol.iterator]();
console.log(it.next()); // {value: "a", done: false}
console.log(it.next()); // {value: "b", done: false}
console.log(it.next()); // {value: "c", done: false}
console.log(it.next()); // {value: "d", done: false}
console.log(it.next()); // {value: undefined, done: false}
手動實現迭代器原理
function createIterator(items) {
let i = 0; // 計數器
return {
next() {
let done = (i >= items.length) // 數組內元素所有迭代完畢
let value = !done ? items[i++] : undefined; // 先返回當前數組中的元素,再i++到下一索引
return {
value,
done
}
}
}
}
let arr = [10, 20, 30];
let it = createIterator(arr);
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: false }
console.log(it.next()); // { value: undefined, done: true }
可是在實際項目開發中,迭代器都是由生成器函數
建立的,那咱們下面來說解一下生成器函數吧
生成器 Generator
生成器是一種返回迭代器的函數,經過function關鍵字後的星號(*)來表示,函數中會用到新的關鍵字yield
。星號能夠緊挨着function關鍵字,也能夠在中間添加一個空格
生成器函數的執行流程
function *generator() {
yield 1;
yield 2;
yield 3;
}
// 基於生成器函數執行的返回結果就是一個迭代器
let g = generator();
console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
console.log(g.next()); // {value: undefined, done: true}
從上面代碼中能夠看出來,其實生成器函數執行
的返回結果就是一個迭代器
,是由於執行生成器函數
返回的對象中有next()
方法。
生成器函數的特色
每當執行完一條
yield
語句後函數就會自動中止執行。
舉個例子,在上面這段代碼中,執行完語句yield 1
以後,函數便再也不執行其餘任何語句,直到再次調用迭代器的next()
方法纔會繼續執行yield 2
語句。
在後面,我會給你們講解利用這種停止函數執行的特色有不少應用
yield使用限制
yield
關鍵字只可在生成器內部使用,在其餘地方使用會致使程序拋出錯誤
看下面代碼:
function *generator(items) {
items.forEach(function(item) {
// 語法錯誤
yield item + 1;
});
}
從字面上看,yield
關鍵字確實在generator()
函數內部,可是它與return
關鍵字同樣,兩者都不能穿透函數邊界。嵌套函數中的return
語句不能用做外部函數的返回語句,而此處嵌套函數中的yield
語句會致使程序拋出語法錯誤
。
生成器函數表達式
也能夠經過函數表達式
來建立生成器,只需在function關鍵字和小括號中間添加一個星號(*)便可
let generator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let it = generator([1, 2, 3]);
console.log(it.next()); // "{ value: 1, done: false }"
console.log(it.next()); // "{ value: 2, done: false }"
console.log(it.next()); // "{ value: 3, done: false }"
console.log(it.next()); // "{ value: undefined, done: true }"
// 以後的全部調用
console.log(iterator.next()); // "{ value: undefined, done: true }"
【禁忌】:不能用箭頭函數來建立生成器
管理異步編程,處理異步任務
假設咱們如今模擬讀取文件的異步任務,只有當上一步數據返回後,才能執行下一步任務
先來寫一個Promise串行鏈式調用的解決方案:
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
}
readFile('a.js').then(data => {
return readFile(data + 'b.js');
}).then(data => {
console.log(data);
})
瞭解過promise
的小夥伴,都能知道它解決了回到地獄的嵌套問題,咱們會在後面的文章中詳細介紹promise
的原理和用法。
async函數的基本用法
那咱們看看利用上面的生成器函數的特色如何管理異步編程的呢?
先來看這樣一段代碼:
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
}
let data = readFile('a.js');
data = readFile(data + 'b.js');
console.log(data);
上面代碼的執行風格是用同步的方式模擬異步任務
,可是上面的代碼並不能幫咱們完成它們,由於讀取文件的操做是異步的,因此上面的代碼確定會報錯。
說了這麼多,咱們看看如何用async/await
來解決呢?
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
}
async function func() {
let data = await readFile('a.js');
data = await readFile(data + 'b.js');
return data;
}
async
函數返回一個 Promise 對象,可使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。
上面代碼是一個讀取文件的函數,函數前面的async
關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個Promise
對象。
因爲async
函數返回的是 Promise 對象,能夠做爲await
命令的參數。因此,上面的例子也能夠寫成下面的形式。
async function readFile(file) {
await new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
}
async function func() {
let data = await readFile('a.js');
data = await readFile(data + 'b.js');
return data;
}
手動實現async/await原理(重要)
async/await底層實現的機制是基於Generator生成器函數實現的
**核心:**傳遞給我一個
Generator
函數,把函數中的內容基於Iterator
迭代器的特色一步步的執行
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
};
function asyncFunc(generator) {
const iterator = generator(); // 接下來要執行next
// data爲第一次執行以後的返回結果,用於傳給第二次執行
const next = (data) => {
let { value, done } = iterator.next(data); // 第二次執行,並接收第一次的請求結果 data
if (done) return; // 執行完畢(到第三次)直接返回
// 第一次執行next時,yield返回的 promise實例 賦值給了 value
value.then(data => {
next(data); // 當第一次value 執行完畢且成功時,執行下一步(並把第一次的結果傳遞下一步)
});
}
next();
};
asyncFunc(function* () {
// 生成器函數:控制代碼一步步執行
let data = yield readFile('a.js'); // 等這一步驟執行執行成功以後,再往下走,沒執行完的時候,直接返回
data = yield readFile(data + 'b.js');
return data;
})
執行流程:
-
第一次執行生成器函數時:value爲 a.js文件的內容,done爲false -
第二次執行生成器函數時,value爲一個 promise
實例對象,須要把第一次的返回結果data
傳給第二次,done爲false -
第三次時執行生成器函數時,value爲第二步讀取的 a.js + b.js
文件的內容,done爲true
以上,就是本文的最終內容,謝謝你們。
看完三件事❤
若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
-
點贊,轉發,有大家的 『在看』
,纔是我創造的動力。 -
關注公衆號 『前端時光屋』
,不按期分享原創知識。 -
同時能夠期待後續文章ing🚀
本文分享自微信公衆號 - 前端時光屋(javascriptlab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。