JavaScript中的異步操做

什麼是異步操做?

   異步模式並不難理解,好比任務A、B、C,執行A以後執行B,可是B是一個耗時的工做,因此,把B放在任務隊列中,去執行C,而後B的一些I/O等返回結果以後,再去執行B,這就是異步操做。node

 

JavaScript爲何須要異步操做?

  JavaScript語言的執行環境是「單線程」, 所謂單線程,就是一次只能完成一件任務, 若是有多個任務就須要排隊,一個完成了,繼續下一個,這種方式在實現來講是很是簡單的,可是若是一個任務耗時很長,那麼後面的任務就須要排隊等着,會拖延整個程序的執行。 常見的瀏覽器無響應假死)就是由於某一段JavaScript代碼長時間運行(好比死循環),致使整個頁面卡死,其餘任務沒法執行。 jquery

  爲了解決這個問題,JavaScript語言將任務的執行模式分爲兩種:同步(Synchronous)和異步(Asynchronous)。 git

  同步任務執行的順序和排隊的順序是一致的,而異步則須要有一個或者多個回調函數,前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是等着前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的,異步的。github

  異步模式很是重要,在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是ajax操做,在服務器端, 異步操做甚至是惟一方式,由於執行環境是單線程的,若是容許同步執行全部的http請求,服務器性能會急劇降低,很快就會失去響應。ajax

 

JavaScript中異步操做的幾種類型。

  JavaScript中異步編程的方法有:編程

  • 回調函數
  • 事件監聽
  • 發佈/訂閱
  • promise
  • generator(ES6)
  • async/await (ES7)

  下面我來分別介紹這幾種異步方法:json

 

1、回調函數

      回調函數是異步編程中最基本的方法。假設有三個函數f一、f二、f3f2須要等待f1的執行結果,而f3是獨立的,不須要f1和f2的結果,若是咱們寫成同步,就是這樣的:api

  f1();
  f2();
  f3()

  若是f1執行的很快,能夠; 可是若是f1執行的很慢,那麼f2和f3就會被阻塞,沒法執行。這樣的效率是很是低的。可是咱們能夠改寫,將f2寫成是f1的回調函數,以下:promise

  function f1(callback){
    setTimeout(function () {
      // f1的任務代碼
      callback();
    }, 1000);
  }

   那麼這時候執行代碼就是這樣:瀏覽器

f1(f2);
f3()

  這樣,就是一個異步的執行了,即便f1很費時間,可是因爲是異步的,那麼f3()就會很快的獲得執行,而不會受到f1和f2的影響。

  注意: 若是咱們把f1寫成這樣呢?

function f1(callback){
  // f1的任務代碼
  callback();
}

  而後,咱們一樣能夠這麼調用:

f1(f2);
f3()

  這時候仍是異步的嗎? 答案:不是異步。 這裏的回調函數並不是真正的回調函數,若是沒有利用setTimeout含函數,那麼f3()的執行一樣須要等到f1(f2)徹底執行完畢,這裏要注意。而咱們就是利用setTImeout才能作出真正的回調函數。

 

 

 

2、事件監聽

  另外一種異步的思路是採用事件驅動模式。任務的執行不取決於代碼的順序, 而取決於某個事件是否發生。 仍是以f一、f二、f3爲例子。 首先,爲f1綁定一個事件(這裏採用jquery的寫法):

f1.on('done', f2);
f3()

  這裏的意思是: 當f1發生了done事件,就執行f2, 而後,咱們對f1進行改寫:

 

  function f1(){
    setTimeout(function () {
      // f1的任務代碼
      f1.trigger('done');
    }, 1000);
  }

  

  f1.trigger('done')表示, 執行完成後,當即觸發done事件,從而開始執行f2。

  這種方法的優勢就是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠去耦合,有利於實現模塊化,缺點就是整個程序都要變成事件驅動型,運行流程會變得很不清晰。

 

 

3、發佈/訂閱

  第二種方法的事件,實際上咱們徹底能夠理解爲「信號」,即f1完成以後,觸發了一個 'done',信號,而後再開始執行f2。

  咱們假定,存在一個「信號中心」,某個任務執行完成,就向信號中心「發佈」(publish)一個信號,其餘任務能夠向信號中心「訂閱」這個信號, 從而知道何時本身能夠開始執行。 這個就叫作「發佈/訂閱模式」, 又稱爲「觀察者」模式 。 

  這個模式有多種實現, 下面採用Ben Alman的Tiny PUb/Sub,這是jQuery的一個插件。

  首先,f2向"信號中心"jquery訂閱"done"信號,

jQuery.subscribe("done", f2);

  而後,f1進行以下改寫:

  function f1(){
    setTimeout(function () {
      // f1的任務代碼
      jQuery.publish("done");
    }, 1000);
  }

  jquery.pushlish("done")的意思是: f1執行完成後,向「信號中心」jQuery發佈「done」信號,從而引起f2的執行。 

  此外,f2完成執行後,也能夠取消訂閱(unsubscribe)。

  

 jQuery.unsubscribe("done", f2);

  這種方法的性質和「事件監聽」很是相似,可是明顯是優於前者的,由於咱們能夠經過查看「消息中心」,瞭解到存在多少信號、每一個信號有多少個訂閱者,從而監控程序的運行。

  

  

  

4、promise對象

  promise是commonjs工做組提出來的一種規範,目的是爲異步編程提供統一接口。

  簡答的說,它的思想是每個異步任務返回一個promise對象,該對象有一個then方法,容許指定回調函數。 好比,f1的回調函數f2,能夠寫成:

f1().then(f2);

  f1要進行下面的改寫(這裏使用jQuery的實現):

 

 function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任務代碼
      dfd.resolve();
    }, 500);
    return dfd.promise;
  }

    

  這樣的優勢在於,回調函數編程了鏈式寫法,程序的流程能夠看得很清楚,並且有一整套的配套方法,能夠實現不少強大的功能 。

  如:指定多個回調函數:

 f1().then(f2).then(f3);

  再好比,指定發生錯誤時的回調函數:

f1().then(f2).fail(f3);

  並且,他還有一個前面三種方法都沒有的好處:若是一個任務已經完成,再添加回調函數,該回調函數會當即執行。 因此,你不用擔憂是否錯過了某個事件或者信號,這種方法的肯定就是編寫和理解,都比較困難。 

  

  

 

 

5、generator函數的異步應用

      在ES6誕生以前,異步編程的方法,大體有下面四種:

  • 回調函數
  • 事件監聽
  • 發佈/訂閱
  • promise對象

 

    沒錯,這就是上面講得幾種異步方法。 而generator函數將JavaScript異步編程帶入了一個全新的階段!

   好比,有一個任務是讀取文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。

     相應地,連續的執行就叫作同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着

     

協程

  
  傳統的編程語言中,早就有了異步編程的解決方案,其中一種叫作協程,意思是多個線程互相協做,完成異步任務

  協程優勢像函數,又有點像線程,運行流程以下:

  • 第一步,協程A開始執行。
  • 第二步,協程A執行到一半,進入暫停執行權轉移到協程B
  • 第三步,(一段時間後)協程B交還執行權
  • 第四步,協程A恢復執行

  上面的協程A,就是異步任務,由於它分爲兩段(或者多段)執行。 

  舉例來講,讀取文件的協程寫法以下:

function *asyncJob() {
  // ...其餘代碼
  var f = yield readFile(fileA);
  // ...其餘代碼
}

  上面代碼的函數asyncJob是一個協程,奧妙就在於yield命令, 它表示執行到此處,執行權交給其餘協程,也就是說yield命令是異步兩個階段的分界線。 

  協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續向後執行,它的最大優勢就是代碼的寫法很是像同步操做,若是去除yield命令,簡直是如出一轍。

  

協程的Generator函數實現

  Generator函數是協程在ES6中的實現,最大特色就是能夠交出函數的執行權(即暫停執行)。

  整個Generator函數就是一個封裝的異步任務,或者說異步任務的容器。 異步任務須要暫停的地方,都用yield語句註明。 以下:

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

  在調用gen函數時 gen(1), 會返回一個內部指針(即遍歷器)g。 這是Generator函數不一樣於普通函數的另外一個地方,即執行它(調用函數)不會返回結果, 返回的一個指針對象 。調用指針g的next方法,會移動內部指針(即執行異步任務的第一階段),指向第一個遇到的yield語句,這裏咱們是x + 2,可是實際上這裏只是舉例,實際上 x + 2 這句應該是一個異步操做,好比ajax請求。 換言之,next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。 value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,便是否還有下一個階段。

 

 

Generator函數的數據交換和錯誤處理

  

  Generator 函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。除此以外,它還有兩個特性,使它能夠做爲異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。

  next返回值的value屬性,是 Generator 函數向外輸出數據;next方法還能夠接受參數,向 Generator 函數體內輸入數據。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

  

  上面代碼中,第一next方法的value屬性,返回表達式x + 2的值3。第二個next方法帶有參數2,這個參數能夠傳入 Generator 函數,做爲上個階段異步任務的返回結果,被函數體內的變量y接收。所以,這一步的value屬性,返回的就是2(變量y的值)。

Generator 函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。

 

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了

 

上面代碼的最後一行,Generator 函數體外,使用指針對象的throw方法拋出的錯誤,能夠被函數體內的try...catch代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。

 

異步任務的封裝

  下面看看如何使用 Generator 函數,執行一個真實的異步任務。

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

  上面代碼中,Generator 函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從 JSON 格式的數據解析信息。就像前面說過的,這段代碼很是像同步操做,除了加上了yield命令。

  執行這段代碼的方法以下。

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

  

  

  上面代碼中,首先執行 Generator 函數,獲取遍歷器對象,而後使用next方法(第二行),執行異步任務的第一階段。因爲Fetch模塊返回的是一個 Promise 對象,所以要用then方法調用下一個next方法。

  能夠看到,雖然 Generator 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。

 

  以下:

function* gen(x) {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}
var a = gen();
console.log(a.next());
console.log(a.next());
console.log(a.next());
console.log(a.next());

  最終,打印臺輸出

即開始調用gen(),並無真正的調用,而是返回了一個生成器對象,a.next()的時候,執行第一個yield,並馬上暫停執行,交出了控制權; 接着,咱們就能夠去a.next() 開始恢復執行。。。 如此循環往復。  

每當調用生成器對象的next的方法時,就會運行到下一個yield表達式。 之因此稱這裏的gen()爲生成器函數,是由於區別以下:

  • 普通函數使用function來聲明,而生成器函數使用 function * 來聲明
  • 普通函數使用return來返回值,而生成器函數使用yield來返回值。
  • 普通函數式run to completion模式 ,即一直運行到末尾; 而生成器函數式 run-pause-run 模式, 函數能夠在執行過程當中暫停一次或者屢次。而且暫停期間容許其餘代碼執行。

 

 

async/await

  async函數基於Generator又作了幾點改進:

  • 內置執行器,將Generator函數和自動執行器進一步包裝。
  • 語義更清楚,async表示函數中有異步操做,await表示等待着緊跟在後邊的表達式的結果。
  • 適用性更普遍,await後面能夠跟promise對象和原始類型的值(Generator中不支持)

  不少人都認爲這是異步編程的終極解決方案,由此評價就可知道該方法有多優秀了。它基於Promise使用async/await來優化then鏈的調用,其實也是Generator函數的語法糖。 async 會將其後的函數(函數表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,並將其 resolve 的結果返回出來。

  await獲得的就是返回值,其內部已經執行promise中resolve方法,而後將結果返回。使用async/await的方式寫回調任務:

async function dolt(){
    console.time('dolt');
    const time1=300;
    const time2=await step1(time1);
    const time3=await step2(time2);
    const result=await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd('dolt');
}

dolt();

  能夠看到,在使用await關鍵字所在的函數必定要是async關鍵字修飾的。

  功能還很新,屬於ES7的語法,但使用Babel插件能夠很好的轉義。另外await只能用在async函數中,不然會報錯

 

 

 

 

 

 

參考文章:

   https://juejin.im/entry/58ed90268d6d8100580c715b

相關文章
相關標籤/搜索