天天看點

JavaScript與異步程式設計

JavaScript與異步程式設計

【引自老文章的部落格】什麼是異步(asynchrony)

按照維基百科上的解釋:獨立于主要制流之外發生的事件就叫做異步。比如說有一段順序執行的代碼

void function main() { 

  fa(); 

  fb(); 

}(); 

fa => fb 是順序執行的,永遠都是 fa 在 fb 的前面執行,他們就是 同步 的關系。加入這時使用 settimeout 将 fb 延後

  settimeout(fa, 1000); 

這時,fa 相對于 fb 就是異步的。main 函數隻是聲明了要在一秒後執行一次 fa,而并沒有立刻執行它。這時,fa 的控制流就獨立于 main 之外。

javascript——天生異步的語言

因為 settimeout 的存在,至少在被 ecma 标準化的那一刻起,javascript 就支援異步程式設計了。與其他語言的 sleep 不同,settimeout 是異步的——它不會阻擋目前程式繼續往下執行。

然而異步程式設計真正發展壯大,ajax 的流行功不可沒。ajax 中的 a(asynchronous)真正點到了異步的概念——這還是 ie5、ie6 的時代。

回調函數——異步程式設計之痛

異步任務執行完畢之後怎樣通知開發者呢?回調函數是最樸素的,容易想到的實作方式。于是從異步程式設計誕生的那一刻起,它就和回調函數綁在了一起。

例如 settimeout。這個函數會起一個定時器,在超過指定時間後執行指定的函數。比如在一秒後輸出數字 1,代碼如下:

settimeout(() => { 

  console.log(1); 

}, 1000); 

正常用法。如果需求有變,需要每秒輸出一個數字(當然不是用 setinterval),javascript 的初學者可能會寫出這樣的代碼:

for (let i = 1; i < 10; ++i) { 

  settimeout(() => { // 錯誤! 

    console.log(i); 

  }, 1000); 

執行結果是等待 1 秒後,一次性輸出了所有結果。因為這裡的循環是同時啟了 10 個定時器,每個定時器都等待 1 秒,結果當然是所有定時器在 1 秒後同時逾時,觸發回調函數。

解法也簡單,隻需要在前一個定時器逾時後再啟動另一個定時器,代碼如下:

  settimeout(() => { 

    console.log(2); 

    settimeout(() => { 

      console.log(3); 

      settimeout(() => { 

        console.log(4); 

        settimeout(() => { 

          console.log(5); 

          settimeout(() => { 

            // ... 

          }, 1000); 

        }, 1000); 

      }, 1000) 

    }, 1000) 

  }, 1000) 

層層嵌套,結果就是這樣的漏鬥形代碼。可能有人想到了新标準中的 promise,可以改寫如下:

function timeout(delay) { 

  return new promise(resolve => { 

    settimeout(resolve, delay); 

  }); 

timeout(1000).then(() => { 

  return timeout(1000); 

}).then(() => { 

  console.log(2); 

  console.log(3); 

  console.log(4); 

  console.log(5); 

  // .. 

}); 

漏鬥形代碼是沒了,但代碼量本身并沒減少多少。promise 并沒能幹掉回調函數。

因為回調函數的存在,循環就無法使用。不能循環,那麼隻能考慮遞歸了,解法如下:

let i = 1; 

function next() { 

  console.log(i); 

  if (++i < 10) { 

    settimeout(next, 1000); 

  } 

settimeout(next, 1000); 

注意雖然寫法是遞歸,但由于 next 函數都是由浏覽器調用的,是以實際上并沒有遞歸函數的調用棧結構。

generator——javascript 中的半協程

很多語言都引入了協程來簡化異步程式設計,javascript 也有類似的概念,叫做 generator。

mdn 上的解釋:generator 是一種可以中途退出之後重入的函數。他們的函數上下文在每次重入後會被保持。簡而言之,generator 與普通 function 最大的差別就是:generator 自身保留上次調用的狀态。

舉個簡單的例子:

function *gen() { 

  yield 1; 

  yield 2; 

  return 3; 

  var iter = gen(); 

  console.log(iter.next().value); 

代碼的執行順序是這樣:

請求 gen,得到一個疊代器 iter。注意此時并未真正執行 gen 的函數體。

調用 iter.next(),執行 gen 的函數體。

遇到 yield 1,将 1 傳回,iter.next() 的傳回值即為 { done: false, value: 1 },輸出 1

調用 iter.next()。從上次 yield 出去的地方繼續往下執行 gen。

遇到 yield 2,将 2 傳回,iter.next() 的傳回值即為 { done: false, value: 2 },輸出 2

遇到 return 3,将 3 傳回,return 表示整個函數已經執行完畢。iter.next() 的傳回值即為 { done: true, value: 3 },輸出 3

調用 generator 函數隻會傳回一個疊代器,當使用者主動調用了 iter.next() 後,這個 generator 函數才會真正執行。

你可以使用 for ... of 周遊一個 iterator,例如

for (var i of gen()) { 

輸出 1 2,最後 return 3 的結果不算在内。想用 generator 的各項生成一個數組也很簡單,array.from(gen()) 或直接用 [...gen()] 即可,生成 [1, 2] 同樣不包含最後的 return 3。

generator 是異步的嗎

generator 也叫半協程(semicoroutine),自然與異步關系匪淺。那麼 generator 是異步的嗎?

既是也不是。前面提到,異步是相對的,例如上面的例子

我們可以很直覺的看到,gen 的方法體與 main 的方法體在交替執行,是以可以肯定的說,gen 相對于 main 是異步執行的。然而此段過程中,整個控制流都沒有交回給浏覽器,是以說 gen 和 main 相對于浏覽器是同步執行的。

用 generator 簡化異步代碼

回到最初的問題:

for (let i = 0; i < 10; ++i) { 

  // 等待上面 settimeout 執行完畢 

關鍵在于如何等待前面的 settimeout 觸發回調後再執行下一輪循環。如果使用 generator,我們可以考慮在

settimeout 後 yield 出去(控制流返還給浏覽器),然後在 settimeout 觸發的回調函數中

next,将控制流交還回給代碼,執行下一段循環。

let iter; 

function* run() { 

  for (let i = 1; i < 10; ++i) { 

    settimeout(() => iter.next(), 1000); 

    yield; // 等待上面 settimeout 執行完畢 

iter = run(); 

iter.next(); 

請求 run,得到一個疊代器 iter。注意此時并未真正執行 run 的函數體。

調用 iter.next(),執行 run 的函數體。

循環開始,i 初始化為 1。

執行 settimeout,啟動一個定時器,回調函數延後 1 秒執行。

遇到 yield(即 yield undefined),控制流傳回到最後的 iter.next() 之後。因為後面沒有其他代碼了,浏覽器獲得控制權,響應使用者事件,執行其他異步代碼等。

1 秒後,settimeout 逾時,執行回調函數 () => iter.next()。

調用 iter.next()。從上次 yield 出去的地方繼續往下執行,即 console.log(i),輸出 i 的值。

一次循環結束,i 自增為 2,回到第 4 步繼續執行

……

這樣即實作了類似同步 sleep 的要求。

async、await——用同步文法寫異步代碼

上面的代碼畢竟需要手工定義疊代器變量,還要手工 next;更重要的是與 settimeout 緊耦合,無法通用。

我們知道 promise 是異步程式設計的未來。能不能把 promise 和 generator 結合使用呢?這樣考慮的結果就是 async 函數。

用 async 得到代碼如下

async function run() { 

    await timeout(1000); 

run(); 

按照 chrome 的設計文檔,async 函數内部就是被編譯為 generator 執行的。run 函數本身會傳回一個

promise,用于使主調函數得知 run 函數什麼時候執行完畢。是以 run() 後面也可以 .then(xxx),甚至直接 await

run()。

注意有時候我們的确需要幾個異步事件并行執行(比如調用兩個接口,等兩個接口都傳回後執行後續代碼),這時就不要過度使用 await,例如:

const a = await querya(); // 等待 querya 執行完畢後 

const b = await queryb(); // 執行 queryb 

dosomething(a, b); 

這時 querya 和 queryb 就是串行執行的。可以略作修改:

const promisea = querya(); // 執行 querya 

const b = await queryb(); // 執行 queryb 并等待其執行結束。這時同時 querya 也在執行。 

const a = await promisea(); // 這時 queryb 已經執行結束。繼續等待 querya 執行結束 

我個人比較喜歡如下寫法:

const [ a, b ] = await promise.all([ querya(), queryb() ]); 

将 await 和 promise 結合使用,效果更佳!

結束語

如今 async 函數已經被各大主流浏覽器實作(除了 ie)。如果要相容舊版浏覽器,可以使用 babel 将其編譯為

generator。如果還要相容隻支援 es5 的浏覽器,還可以繼續把 generator 編譯為 es5。編譯後的代碼量比較大,小心代碼膨脹。

如果是用 node 寫 server,那就不用糾結了直接用就是了。koa 是用 async 是你的好幫手。

作者:老文章

來源:51cto

繼續閱讀