1. 程式人生 > 程式設計 >單執行緒JavaScript實現非同步過程詳解

單執行緒JavaScript實現非同步過程詳解

前兩天硬著頭皮在部門內部做了一次技術分享,主題如題。索性整理成文章留個紀念!

要了解非同步實現,首先我們得先了解:

同步 & 非同步

同步:會逐行執行程式碼,會對後續程式碼造成阻塞,直至程式碼接收到預期的結果之後,才會繼續向下執行任務。

非同步:呼叫之後先不管結果,繼續向下執行任務。

網上各種文章對同步和非同步的解釋也不外如是,但是看文字總是有點晦澀難懂!我就生活化的來比擬一下這兩個概念吧!

就好比請人吃飯:

比如你要請兩個人吃飯,一個是巴菲特,由於他是舉世矚目股神想請他吃飯的人從這裡排到了法國,你為表誠意,你會精心打扮自己,然後租一架飛機親自去美國,請他跟你吃頓特色菜...那麼為了請他吃個烤腰子,你全程都在為些事費心費力,投入大量的精力!

所以,也就阻塞了你幹別的事情,是的,這就是同步!

請人吃頓飯就這麼難嗎?當然,也沒有那麼難!不信,你請我吃飯試試:

如果你想請我吃飯,那你只需要打個電話通知我一聲:喂,今天晚上請你吃個海底撈啊!我:好啊!然後你不要來接我,到了點我自己去了!期間,你該幹嘛就去幹嘛!

看,其他也很簡單嘛?瞧,這就是非同步!

那麼回到程式碼層面:

同步程式碼:(程式碼片段1)

function someTime() {
  let s = Date.now();
  while(true) {
    if (Date.now() - s > 2000) {
      console.log(2)
      break;
    }
  }
}
console.log(1);
someTime();
console.log(3);
// 其列印順序:1 ...(2秒以後)... 2 3

非同步程式碼:(程式碼片段2)

function someTime() {
  setTimeout(() => {
    console.log(2);
  },2000)
}

console.log(1);
someTime();
console.log(3);

// 其列印順序:1 3 ...(2秒以後)... 2

看看,同步程式碼,當執行這種耗時操作時,就會停在原地,一定要等待這時間過去之後才會執行後面的程式碼!而非同步程式碼,後面的執行完全不受影響...

JavaScript單執行緒

眾所周知JavaScript是單執行緒的,所謂單執行緒是指程式執行時,所走的程式路徑按照連續順序排下來,前面的必須處理好,後面的才會執行!這個解釋跟【同步】的解釋如出一轍!

如此看起來非同步程式設計對於單執行緒而言似乎並非正統,甚至有點矛盾。然而,通過剛才的例子,我們發現,JavaScript是真的實現了非同步程式設計的!為啥加了個setTimeout()不能不阻塞了呢?按單執行緒的執行的話那如下程式碼會是怎麼樣的呢?

function timeOut() {
  setTimeout(() => {
    console.log('timeOut');
  },0)
}
function someTime() {
  let s = Date.now();
  while(true) {
    if (Date.now() - s > 2000) {
      console.log('some Time')
      break;
    }
  }
}
console.log(1);
timeOut();
someTime();
console.log(3);

如果是以單執行緒那種解釋來執行的話,這個列印順序應該是:1 - time Out - some Time - 3才對!然而,其真正的執行結果卻是:1 - some Time - 3 - time Out

為什麼?瀏覽器的多執行緒

JavaScript是指令碼語言,它需要在一個宿主環境裡才能執行,顯然我們接觸較多的宿主環境就是--瀏覽器!雖說JavaScript是單執行緒的,然而瀏覽器卻不是!

單執行緒JavaScript實現非同步過程詳解

如圖所求,JavaScript引擎執行緒稱為主執行緒,它負責解析JavaScript程式碼;其他可以稱為輔助執行緒,這些輔助執行緒便是JavaScript實現非同步的關鍵了!

如(程式碼片段2):主執行緒負責自上而下順序執行,當遇到setTimeout函式後,便將其交給定時器執行緒去執行,自己繼續執行下面的程式碼!從而達到非同步的目的。

不僅如此,更關鍵的是:

單執行緒JavaScript實現非同步過程詳解

任務佇列

當定時器執行緒計時執行完之後,會將回調函式放入任務佇列中!

當這些任務加入到任務佇列後並不會立即執行,而是處於等候狀態!等主執行緒處理完了自己的事情後,才來執行任務佇列中任務!

這個過程我感覺像是古代嬪妃被翻了牌子後,就需要在自己寢宮裡精心準備,等待皇上批完湊折後的駕臨...(哦,別想歪了!)

巨集任務 & 微任務

然而,非同步任務卻又分為兩種:一種叫“巨集任務”(MacroTask 或者 Task),一種叫“微任務”(MicroTask)!

這又是兩個啥玩意呢?

單執行緒JavaScript實現非同步過程詳解

光看這個依然晦澀難懂,那我們來看一段程式碼吧!

console.log(1);
setTimeout(() => {
  console.log(2);
},0);
Promise.resolve().then(() => {
  console.log(3);
});
console.log(4);

這段程式碼的執行結果:1 - 4 - 3 - 2。LOOK!2是最後列印的,哪怕該計時器的時間設定為0。通過之前的同步和非同步的解釋,1和4先於2列印應該很好理解了,但同樣是非同步,3也優先於2列印,這又是為什麼呢?答案就是因為 setTimeout屬於巨集任務,而Promise屬於微任務!

好吧~ 這就是巨集任務和微任務的差別...什麼?沒懂?

微任務是皇后所生的,是嫡子;而巨集任務是某個小妃子所生, 是庶子!你說選太子的時候誰優先?

瀏覽器的Event Loop

1.執行全域性Script同步程式碼,形成一個執行棧;

2.在執行程式碼時當遇到如上非同步任務時便會按上文所描述的將巨集任務回撥加入巨集任務佇列,微任務回撥加入微任務佇列;

3.然而,回撥函式放入任務佇列後也不是立即執行;會等待執行棧中的同步任務全部執行完清空了棧後引擎才能會去任務佇列檢查是否有任務,如果有那便會將這些任務加入執行棧,然後執行!

4.執行棧清空後,會先去檢查微任務佇列是否有任務,逐一將其任務加入執行棧中執行,期間如果又產生了微任務那繼續將其加入到列隊末尾,並在本週期內執行完,直到微任務佇列的任務全部 清空,執行棧也清空後,再去檢查巨集任務佇列是否有任務,取到佇列隊頭的任務放入到執行棧中執行,其他可能又會產生微任務,那當本次執行棧中的任務結果清空後又會去檢查微任務佇列...

5.引擎會迴圈執行如上步驟,這就是Event Loop!

單執行緒JavaScript實現非同步過程詳解

又要上程式碼了:

console.log('start');
setTimeout(() => {
  console.log('time1');
  Pormise.resolve().then(() => {
    console.log('promise1');
  })
},0);
setTimeout(() => {
  console.log('time2');
  Pormise.resolve().then(() => {
    console.log('promise2');
  })
},0);
Pormise.resolve().then(() => {
  console.log('promise3');
});
console.log('end');

這段程式碼的列印順序:

start - end - promise3 - timer1 - promise1 - timer2 - promise2

據說:node 10.x版本上面的輸入結果會是:

start - end - promise3 - timer1 - timer2 - promise1 - promise2

node 11.x版本以後改了,輸出跟瀏覽器輸出一致了!

Web Worker

HTML5中支援了Web Worker,使得能夠同時執行兩段JS了,那是不是就是說JS實現了“多執行緒”了呢?我們來看看Web Worker的官方解釋:

通過使用Web Workers,Web應用程式可以在獨立於主執行緒的後臺執行緒中,執行一個指令碼操作。這樣做的好處是可以在獨立執行緒中執行費時的處理任務,從而允許主執行緒(通常是UI執行緒)不會因此被阻塞/放慢。

獨立執行緒,看似像是實現了“多執行緒”,然而他是獨立於主執行緒,也就是主執行緒依然是那個主執行緒沒有變!雖然你大媽已經不是你大媽了,但是你大爺還是你大爺!JS單執行緒的本質依然沒有變!

WebWorker是向瀏覽器申請一個子執行緒,該子執行緒服務於主執行緒,完全受主執行緒控制。

Web Worker注意事項:

單執行緒JavaScript實現非同步過程詳解

寫了一個demo:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Web Worker</title>
</head>
<body>
  <button onclick="startWorker()">開始</button>
  <button onclick="stopWorker()">停止</button>
  <button onclick="updateNum()">在執行時點選</button>
  <div id="output"></div>
  <div id="num"></div>

  <script id="worker" type="app/worker">
    function updateSync() {
      for (let i = 0; i < 10000000000; i++) {
        if (i % 100000 === 0) {
          postMessage(i);
        }
      }
    }
    updateSync();
  </script>

  <script>
    let worker;
    function startWorker() {
      let blob = new Blob([document.querySelector('#worker').textContent]);
      let url = window.URL.createObjectURL(blob);
      console.log(url);
      worker = new Worker(url);

      worker.onmessage = function(e) {
        document.getElementById('output').innerHTML = e.data;
      }
    }

    function stopWorker() {
      if (worker) {
        worker.terminate();
      }
    }
    
    let num = 0;
    function updateNum() {
      num++;
      document.getElementById('num').innerHTML = num;
    }
  </script>
</body>
</html>

這段程式碼可以稍微解釋一下Web Worker的用途之一 --執行費時的處理任務吧!

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。