javascript有哪些非同步操作方法

2022-02-08 16:00:45

javascript的非同步操作方法有:1、回撥函數;2、事件監聽;3、「釋出/訂閱」模式;4、promise;5、generator;6、「async/await」。

本教學操作環境:windows7系統、javascript1.8.5版、Dell G3電腦。

什麼是非同步操作?

   非同步模式並不難理解,比如任務A、B、C,執行A之後執行B,但是B是一個耗時的工作,所以,把B放在任務佇列中,去執行C,然後B的一些I/O等返回結果之後,再去執行B,這就是非同步操作。

JavaScript為什麼需要非同步操作?

  JavaScript語言的執行環境是「單執行緒」, 所謂單執行緒,就是一次只能完成一件任務, 如果有多個任務就需要排隊,一個完成了,繼續下一個,這種方式在實現來說是非常簡單的,但是如果一個任務耗時很長,那麼後面的任務就需要排隊等著,會拖延整個程式的執行。 常見的瀏覽器無響應假死)就是因為某一段JavaScript程式碼長時間執行(比如死迴圈),導致整個頁面卡死,其他任務無法執行。

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

  同步任務執行的順序和排隊的順序是一致的,而非同步則需要有一個或者多個回撥函數,前一個任務結束後,不是執行後一個任務,而是執行回撥函數,後一個任務則是等著前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的,非同步的。

  非同步模式非常重要,在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是ajax操作,在伺服器端, 非同步操作甚至是唯一方式,因為執行環境是單執行緒的,如果允許同步執行所有的http請求,伺服器效能會急劇下降,很快就會失去響應。

JavaScript中非同步操作的幾種型別。

  JavaScript中非同步程式設計的方法有:

  • 回撥函數
  • 事件監聽
  • 釋出/訂閱
  • promise
  • generator(ES6)
  • async/await (ES7)

  下面我來分別介紹這幾種非同步方法:

一、回撥函數

  回撥函數是非同步程式設計中最基本的方法。假設有三個函數f1、f2、f3f2需要等待f1的執行結果,而f3是獨立的,不需要f1和f2的結果,如果我們寫成同步,就是這樣的:

  f1();
  f2();
  f3();

  如果f1執行的很快,可以; 但是如果f1執行的很慢,那麼f2和f3就會被阻塞,無法執行。這樣的效率是非常低的。但是我們可以改寫,將f2寫成是f1的回撥函數,如下:

  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才能做出真正的回撥函數。

二、事件監聽

  另一種非同步的思路是採用事件驅動模式。任務的執行不取決於程式碼的順序, 而取決於某個事件是否發生。 還是以f1、f2、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。

  這種方法的優點就是比較容易理解,可以繫結多個事件,每個事件可以指定多個回撥函數,而且可以去耦合,有利於實現模組化,缺點就是整個程式都要變成事件驅動型,執行流程會變得很不清晰。

三、釋出/訂閱

  第二種方法的事件,實際上我們完全可以理解為「訊號」,即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);

  這種方法的性質和「事件監聽」非常類似,但是明顯是優於前者的,因為我們可以通過檢視「訊息中心」,瞭解到存在多少訊號、每個訊號有多少個訂閱者,從而監控程式的執行。

四、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);

  而且,他還有一個前面三種方法都沒有的好處:如果一個任務已經完成,再新增回撥函數,該回撥函數會立即執行。 所以,你不用擔心是否錯過了某個事件或者訊號,這種方法的確定就是編寫和理解,都比較困難。 

五、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函數中,否則會報錯

【相關推薦:

以上就是javascript有哪些非同步操作方法的詳細內容,更多請關注TW511.COM其它相關文章!