精學ES6系列——利用Generator特色手動實現async/await

編者薦語:

本文幫助你們瞭解迭代器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 = [102030];
let it = arr.next();

console.log(it); // arr.next is not a function

數組是能夠被迭代的數據,由於Array.prototype上有Symbol.iterator迭代器屬性,

但並非一個迭代器,由於要調用迭代器屬性,才能生成迭代器,好比看下面的代碼:

let arr = [102030];
let it = arr[Symbol.iterator](arr);

console.log(it); // Array Iterator {}

返回的迭代器對象上,能夠經過原型鏈找到迭代方法next()

迭代器的分類

  • 數組迭代器
  • 字符串迭代器
  • ...
let str = "";
str[Symbol.iterator]("abcd"); // Array Iterator {}

let arr = [102030];
arr[Symbol.iterator](arr); // StringIterator {}

它們的迭代器對象上,也都有Symbol.iterator()方法和next()方法

迭代器的執行流程

  • 先經過迭代器屬性 Symbol.iterator 建立一個迭代器,指向當前數據結構的起始位置

  • 隨後經過 next() 方法進行向下迭代指向下一個位置, next() 方法會返回當前位置的對象,對象包含了 value 和 done 兩個屬性, value 是當前屬性的值, done 用於判斷是否遍歷結束

  • 當 done 爲 true 時則遍歷結束

對照着代碼來執行一下:

let arr = [102030];
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 = [102030];
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([123]);
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

以上,就是本文的最終內容,謝謝你們。

看完三件事❤

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『在看』 ,纔是我創造的動力。
  2. 關注公衆號 『前端時光屋』 ,不按期分享原創知識。
  3. 同時能夠期待後續文章ing🚀


本文分享自微信公衆號 - 前端時光屋(javascriptlab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索