Q:你瞭解非同步程式設計、程式、單執行緒、多執行緒嗎?

Juicyangxj發表於2017-11-28

相關定義

Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。

  1. 同步:一個程式在執行某個請求的時候,若該請求需要一段時間才能返回資訊,那麼這個程式將會一直等待下去,直到收到返回資訊才繼續執行下去。
  2. 非同步:程式不需要一直等下去,而是繼續執行下面的操作,不管其他程式的狀態。當有訊息返回時系統會通知程式進行處理,這樣可以提高執行的效率。
  3. 程式:狹義上,就是正在執行的程式的例項。廣義上,程式是一個具有一定獨立功能的程式關於某個資料集合的一次執行活動。它是作業系統動態執行的基本單元,在傳統的作業系統中,程式既是基本的分配單元,也是基本的執行單元。
  4. 執行緒:執行緒是程式中一個單一的順序控制流程。程式內一個相對獨立的、可排程的執行單元,是系統獨立排程和分派CPU的基本單位。指執行中的程式的排程單位。
  5. 單執行緒:單執行緒在程式執行時,所走的程式路徑按照連續順序排下來,前面的必須處理好,後面的才會執行。單執行緒就是程式裡只有一個執行緒。
  6. 多執行緒:在單個程式中同時執行多個執行緒完成不同的工作,稱為多執行緒。

知識點

js是單執行緒的

JS執行在瀏覽器中,是單執行緒的,每個window一個JS執行緒,既然是單執行緒的,在某個特定的時刻只有特定的程式碼能夠被執行,並阻塞其它的程式碼。而瀏覽器是事件驅動的,瀏覽器中很多行為是非同步的,會建立事件並放入執行佇列中,JavaScript引擎是單執行緒處理它的任務佇列。當非同步事件發生時,滑鼠點選事件發生、定時器觸發事件發生、XMLHttpRequest完成回撥觸發等,將他們放入執行佇列,等待當前程式碼執行完成。

瀏覽器不是單執行緒的

雖然JS執行在瀏覽器中,是單執行緒的,但瀏覽器不是單執行緒的,例如Web kit引擎,可能有如下執行緒:

  • JavaScript引擎執行緒
  • 介面渲染執行緒
  • 瀏覽器事件觸發執行緒
  • HTTP請求執行緒

當一個非同步事件發生的時候,它就進入事件佇列。瀏覽器有一個內部大訊息迴圈,Event Loop(事件迴圈),會輪詢事件佇列並處理事件。比如,瀏覽器當前正在忙於處理onclick事件,這時window onSize事件發生了,這個非同步事件就被放入事件佇列等待處理,只有前面的處理完畢了,空閒了才會執行這個事件。

為什麼JavaScript是單執行緒的卻能讓AJAX非同步傳送和回撥請求,為什麼setTimeout也看起來像是多執行緒的?

Ajax請求確實是非同步的,這請求是由瀏覽器新開了一個執行緒請求,事件回撥的時候是放入Event loop單執行緒事件佇列等候處理。當瀏覽器空閒的時候出佇列任務被處理,JavaScript引擎始終是單執行緒執行回撥函式、單執行緒處理它的任務佇列。

setTimeout(func, 0)神奇在哪兒?那就是告訴js引擎,在0ms以後把func放到主事件佇列中,等待當前的程式碼執行完畢再執行,注意:重點是改變了程式碼流程,把func的執行放到了主事件佇列中。這就是它的神奇之處了。它的用處有三個:

  1. 讓瀏覽器渲染當前的變化(很多瀏覽器UI render和js執行是放在一個執行緒中,執行緒阻塞會導致介面無法更新渲染)
  2. 重新計算script執行時間,即重新判斷”script is running too long”這個警告
  3. 改變了執行順序

詳細解釋見下一篇文章《巧用setTimeout(func, 0)》。(2017-11-30注:本來想寫的,偶然翻到一篇文章《這一次,徹底弄懂 JavaScript 執行機制》覺得已經寫得很好了,就收藏啦(#^.^#))

非同步程式設計三種方法

一:回撥函式

這是非同步程式設計最基本的方法。
假定有兩個函式f1和f2,後者等待前者的執行結果。  

 f1();
 f2();複製程式碼

如果f1是一個很耗時的任務,可以考慮把f2寫成f1的回撥函式。

 function f1(callback){
    setTimeout(function () {
      // f1的任務程式碼
      callback();
    }, 1000);
  }複製程式碼

執行程式碼就變成下面這樣:  

 f1(f2);複製程式碼

採用這種方式,我們把同步操作變成了非同步操作,f1不會堵塞程式執行。回撥函式的優點是簡單、容易理解和部署,缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂,而且每個任務只能指定一個回撥函式

二、事件監聽

另一種思路是採用事件驅動模式。任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。
還是以f1和f2為例。首先,為f1繫結一個事件(這裡採用的jQuery的寫法)。

  f1.on('done', f2);複製程式碼

上面這行程式碼的意思是,當f1發生done事件,就執行f2。然後,對f1進行改寫:

  function f1(){
    setTimeout(function () {
      // f1的任務程式碼
      f1.trigger('done');
    }, 1000);
  }複製程式碼

f1.trigger('done')表示,執行完成後,立即觸發done事件,從而開始執行f2。
這種方法的優點是比較容易理解,可以繫結多個事件,每個事件可以指定多個回撥函式,而且可以"去耦合",有利於實現模組化。缺點是整個程式都要變成事件驅動型,執行流程會變得很不清晰。

三、Promises物件

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案“回撥函式”和“事件”——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。
基本用法如下:

    const promise = new Promise(function(resolve, reject) {
      // ... some code
      if (/* 非同步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });
    promise.then(function(value) {
    // success
    }, function(error) {
     // failure
    });複製程式碼

下面列出非同步操作失敗、抓捕異常的另一種寫法

const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});複製程式碼

這樣寫的優點在於,回撥函式變成了鏈式寫法,程式的流程可以看得很清楚,可以實現許多強大的功能。
比如,指定多個回撥函式等等。

相關文章