ES6生成器,看似同步的異步流程控制表達風格

本文分享自華爲雲社區《3月閱讀周·你不知道的JavaScript | ES6生成器,看似同步的異步流程控制表達風格》,作者: 葉一一。

生成器

打破完整運行

JavaScript開發者在代碼中幾乎普遍依賴的一個假定:一個函數一旦開始執行,就會運行到結束,期間不會有其他代碼能夠打斷它並插入其間。

ES6引入了一個新的函數類型,它並不符合這種運行到結束的特性。這類新的函數被稱爲生成器。

var x = 1;

function foo() {
  x++;
  bar(); // <-- 這一行在x++和console.log(x)語句之間運行
  console.log('x:', x);
}

function bar() {
  x++;
}

foo(); // x: 3

如果bar()並不在那裏會怎樣呢?顯然結果就會是2,而不是3。最終的結果是3,所以bar()會在x++和console.log(x)之間運行。

但JavaScript並不是搶佔式的,(目前)也不是多線程的。然而,如果foo()自身可以通過某種形式在代碼的這個位置指示暫停的話,那就仍然可以以一種合作式的方式實現這樣的中斷(併發)。

下面是實現合作式併發的ES6代碼:

var x = 1;

function* foo() {
  x++;
  yield; // 暫停!
  console.log('x:', x);
}

function bar() {
  x++;
}
// 構造一個迭代器it來控制這個生成器
var it = foo();

// 這裏啓動foo()!
it.next();
console.log('x:', x); // 2
bar();
console.log('x:', x); // 3
it.next(); // x: 3
  • it = foo()運算並沒有執行生成器*foo(),而只是構造了一個迭代器(iterator),這個迭代器會控制它的執行。
  • *foo()在yield語句處暫停,在這一點上第一個it.next()調用結束。此時*foo()仍在運行並且是活躍的,但處於暫停狀態。
  • 最後的it.next()調用從暫停處恢復了生成器*foo()的執行,並運行console.log(..)語句,這條語句使用當前x的值3。

生成器就是一類特殊的函數,可以一次或多次啓動和停止,並不一定非得要完成。

輸入和輸出

生成器函數是一個特殊的函數,它仍然有一些函數的基本特性。比如,它仍然可以接受參數(即輸入),也能夠返回值(即輸出)。

function* foo(x, y) {
  return x * y;
}

var it = foo(6, 7);
var res = it.next();

res.value; // 42

向*foo(..)傳入實參6和7分別作爲參數x和y。*foo(..)向調用代碼返回42。

多個迭代器

每次構建一個迭代器,實際上就隱式構建了生成器的一個實例,通過這個迭代器來控制的是這個生成器實例。

同一個生成器的多個實例可以同時運行,它們甚至可以彼此交互:

function* foo() {
  var x = yield 2;
  z++;
  var y = yield x * z;
  console.log(x, y, z);
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2

val1 = it1.next(val2 * 10).value; // 40   <-- x:20,  z:2
val2 = it2.next(val1 * 5).value; // 600  <-- x:200, z:3

it1.next(val2 / 2); // y:300
// 20300 3
it2.next(val1 / 4); // y:10
// 200 10 3

簡單梳理一下執行流程:

(1) *foo()的兩個實例同時啓動,兩個next()分別從yield 2語句得到值2。

(2) val2 * 10也就是2 * 10,發送到第一個生成器實例it1,因此x得到值20。z從1增加到2,然後20 * 2通過yield發出,將val1設置爲40。

(3) val1 * 5也就是40 * 5,發送到第二個生成器實例it2,因此x得到值200。z再次從2遞增到3,然後200 * 3通過yield發出,將val2設置爲600。

(4) val2 / 2也就是600 / 2,發送到第一個生成器實例it1,因此y得到值300,然後打印出x y z的值分別是20300 3。

(5) val1 / 4也就是40 / 4,發送到第二個生成器實例it2,因此y得到值10,然後打印出x y z的值分別爲200 10 3。

生成器產生值

生產者與迭代器

假定你要產生一系列值,其中每個值都與前面一個有特定的關係。要實現這一點,需要一個有狀態的生產者能夠記住其生成的最後一個值。

迭代器是一個定義良好的接口,用於從一個生產者一步步得到一系列值。JavaScript迭代器的接口,就是每次想要從生產者得到下一個值的時候調用next()。

可以爲數字序列生成器實現標準的迭代器接口:

var something = (function () {
  var nextVal;

  return {
    // for..of循環需要
    [Symbol.iterator]: function () {
      return this;
    },

    // 標準迭代器接口方法
    next: function () {
      if (nextVal === undefined) {
        nextVal = 1;
      } else {
        nextVal = 3 * nextVal + 6;
      }

      return { done: false, value: nextVal };
    },
  };
})();

something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105

next()調用返回一個對象。這個對象有兩個屬性:done是一個boolean值,標識迭代器的完成狀態;value中放置迭代值。

iterable

iterable(可迭代),即指一個包含可以在其值上迭代的迭代器的對象。

從ES6開始,從一個iterable中提取迭代器的方法是:iterable必須支持一個函數,其名稱是專門的ES6符號值Symbol.iterator。調用這個函數時,它會返回一個迭代器。通常每次調用會返回一個全新的迭代器,雖然這一點並不是必須的。

var a = [1, 3, 5, 7, 9];

for (var v of a) {
  console.log(v);
}
// 1 3 5 7 9

上面的代碼片段中的a就是一個iterable。for..of循環自動調用它的Symbol.iterator函數來構建一個迭代器。

for (var v of something) {
  ..
}

for..of循環期望something是iterable,於是它尋找並調用它的Symbol.iterator函數。

生成器迭代器

可以把生成器看作一個值的生產者,我們通過迭代器接口的next()調用一次提取出一個值。

生成器本身並不是iterable,當你執行一個生成器,就得到了一個迭代器:

function *foo(){ .. }

var it = foo();

可以通過生成器實現前面的這個something無限數字序列生產者,類似這樣:

function* something() {
  var nextVal;

  while (true) {
    if (nextVal === undefined) {
      nextVal = 1;
    } else {
      nextVal = 3 * nextVal + 6;
    }

    yield nextVal;
  }
}

因爲生成器會在每個yield處暫停,函數*something()的狀態(作用域)會被保持,即意味着不需要閉包在調用之間保持變量狀態。

異步迭代生成器

function foo(x, y) {
  ajax('http://some.url.1/? x=' + x + '&y=' + y, function (err, data) {
    if (err) {
      // 向*main()拋出一個錯誤
      it.throw(err);
    } else {
      // 用收到的data恢復*main()
      it.next(data);
    }
  });
}

function* main() {
  try {
    var text = yield foo(11, 31);
    console.log(text);
  } catch (err) {
    console.error(err);
  }
}

var it = main();

// 這裏啓動!
it.next();

在yield foo(11,31)中,首先調用foo(11,31),它沒有返回值(即返回undefined),所以發出了一個調用來請求數據,但實際上之後做的是yield undefined。

這裏並不是在消息傳遞的意義上使用yield,而只是將其用於流程控制實現暫停/阻塞。實際上,它還是會有消息傳遞,但只是生成器恢復運行之後的單向消息傳遞。

看一下foo(..)。如果這個Ajax請求成功,我們調用:

it.next(data);

這會用響應數據恢復生成器,意味着暫停的yield表達式直接接收到了這個值。然後隨着生成器代碼繼續運行,這個值被賦給局部變量text。

總結

我們來總結一下本篇的主要內容:

  • 生成器是ES6的一個新的函數類型,它並不像普通函數那樣總是運行到結束。取而代之的是,生成器可以在運行當中(完全保持其狀態)暫停,並且將來再從暫停的地方恢復運行。
  • yield/next(..)這一對不只是一種控制機制,實際上也是一種雙向消息傳遞機制。yield ..表達式本質上是暫停下來等待某個值,接下來的next(..)調用會向被暫停的yield表達式傳回一個值(或者是隱式的undefined)。
  • 在異步控制流程方面,生成器的關鍵優點是:生成器內部的代碼是以自然的同步/順序方式表達任務的一系列步驟。其技巧在於,把可能的異步隱藏在了關鍵字yield的後面,把異步移動到控制生成器的迭代器的代碼部分。
  • 生成器爲異步代碼保持了順序、同步、阻塞的代碼模式,這使得大腦可以更自然地追蹤代碼,解決了基於回調的異步的兩個關鍵缺陷之一。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

 

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