Generator 學習筆記

目錄

1. async await

2. Generator


本文僅是學習:異步編程二三事 | Promise/async/Generator實現原理解析 中一部分知識的學習筆記,(強烈推薦閱讀原文)


(一)async await

在多個回調依賴的場景中,儘管Promise通過鏈式調用取代了回調嵌套,但過多的鏈式調用可讀性仍然不佳,流程控制也不方便,ES7 提出的async 函數,終於讓 JS 對於異步操作有了終極解決方案,簡潔優美地解決了以上兩個問題。

async/await 兩個關鍵字均用來實現異步處理,使用這兩個關鍵字,可以書寫比Promise更爲簡潔的異步處理代碼。async函數可以返回Promise,當函數返回值時,Promise返回肯定結果,當async函數拋出異常時,Promise返回否定結果。

async/await實際上是對Generator(生成器)的封裝,是一個語法糖。

async/await 和 Generator 有三點不同:

  • async/await自帶執行器,不需要手動調用next()就能自動執行下一步
  • async函數返回值是Promise對象,而Generator返回的是生成器對象
  • await能夠返回Promise的resolve/reject的值

我們對async/await的實現,其實也就是對應以上三點封裝Generator。

1. 自動執行

初步實現了一個async/await

function* myGenerator() {
  console.log(yield Promise.resolve(1))   //1
  console.log(yield Promise.resolve(2))   //2
  console.log(yield Promise.resolve(3))   //3
}

function run(myGenerator) {
	var g = gen(); //由於每次gen()獲取到的都是最新的迭代器,因此獲取迭代器操作要放在step()之前,否則會進入死循環

	function step(val) {  //封裝一個方法, 遞歸執行next()
		var res = g.next(val)  //獲取迭代器對象,並返回resolve的值
		if (res.done) return res.value; //遞歸終止條件
		res.value.then(val => {  //Promise的then方法是實現自動迭代的前提
			step(val)   //等待Promise完成就自動執行下一個next,並傳入resolve的值
		})
	}

	step(); //第一次執行
}

2.返回Promise & 異常處理

上面實現了Generator的自動執行以及讓yield返回resolve的值,但上邊的代碼還存在着幾點問題:

  • 需要兼容基本類型:這段代碼能自動執行的前提是yield後面跟Promise,爲了兼容後面跟着基本類型值的情況,我們需要把yield跟的內容(gen().next.value)都用Promise.resolve()轉化一遍

  • 缺少錯誤處理:上邊代碼裏的Promise如果執行失敗,就會導致後續執行直接中斷,我們需要通過調用Generator.prototype.throw(),把錯誤拋出來,才能被外層的try-catch捕獲到

  • 返回值是Promiseasync/await的返回值是一個Promise,我們這裏也需要保持一致,給返回值包一個Promise

我們改造一下run方法:

function run(gen) {
  //把返回值包裝成promise
  return new Promise((resolve, reject) => {
    var g = gen()

    function step(val) {
      //錯誤處理
      try {
        var res = g.next(val) 
      } catch(err) {
        return reject(err); 
      }
      if(res.done) {
        return resolve(res.value);
      }
      //res.value包裝爲promise,以兼容yield後面跟基本類型的情況
      Promise.resolve(res.value).then(
        val => {
          step(val);
        }, 
        err => {
          //拋出錯誤
          g.throw(err)
        });
    }
    step();
  });
}

可以測試一下:

function* myGenerator() {
  try {
    console.log(yield Promise.resolve(1)) 
    console.log(yield 2)   //2
    console.log(yield Promise.reject('error'))
  } catch (error) {
    console.log(error)
  }
}

const result = run(myGenerator)     //result是一個Promise
//輸出 1 2 error

(二)Generator

// 生成器函數根據yield語句將代碼分割爲switch-case塊,後續通過切換_context.prev和_context.next來分別執行各個case
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return _context.stop();
    }
  }
}

// 低配版context  
var context = {
  next:0,
  prev: 0,
  done: false,
  stop: function stop () {
    this.done = true
  }
}

// 低配版invoke
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
} 

// 測試使用
var g = gen() 
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true}

這段代碼並不難理解,我們分析一下調用流程:

我們定義的function*生成器函數被轉化爲以上代碼

  1. 轉化後的代碼分爲三大塊:
    • gen$(_context)由yield分割生成器函數代碼而來
    • context對象用於儲存函數執行上下文
    • invoke()方法定義next(),用於執行gen$(_context)來跳到下一步
  2. 當我們調用g.next(),就相當於調用invoke()方法,執行gen$(_context),進入switch語句,switch根據context的標識,執行對應的case塊,return對應結果
  3. 當生成器函數運行到末尾(沒有下一個yield或已經return),switch匹配不到對應代碼塊,就會return空值,這時g.next()返回{value: undefined, done: true}

從中我們可以看出,Generator實現的核心在於上下文的保存,函數並沒有真的被掛起,每一次yield,其實都執行了一遍傳入的生成器函數,只是在這個過程中間用了一個context對象儲存上下文,使得每次執行生成器函數的時候,都可以從上一個執行結果開始執行,看起來就像函數被掛起了一樣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章