JS核心知識點梳理——異步,單線程,運行機制

clipboard.png

引言

學習javascipt的時候,經常聽人說,javascipt即是異步的,又是單線程的。究竟什麼是異步,什麼是單線程?javascript在瀏覽器中的運行機制是怎麼樣的?什麼是eventloop,task queue?怎麼寫異步函數?相信讀完這篇文章,相信你會對上面問題有一個全面的認識。

全面瞭解瀏覽器

瀏覽器有許多進程:

  1. Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。
  2. 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
  3. GPU進程:最多一個,用於3D繪製等
  4. 瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的)

在瀏覽器渲染進程中有許多線程:

  • 渲染引擎線程:顧名思義,該線程負責頁面的渲染
  • JS引擎線程:負責JS的解析和執行(主線程)
  • 定時觸發器線程:處理定時事件,比如setTimeout, setInterval
  • 事件觸發線程:處理DOM事件
  • 異步http請求線程:處理http請求

雖然JavaScript是單線程的(說的是JS引擎線程),可是瀏覽器內部不是單線程的。一些I/O操作、定時器的計時和事件監聽(click, keydown...)等都是由瀏覽器提供的其他線程來完成的。
主線程和渲染引擎線程互斥,因爲渲染的時候主線程可能通過dom操作渲染結果,所以主線程必須被阻塞

單線程,異步

判斷標準

之前傻傻的分不清楚單線程多線程,同步異步。其實很簡單

異步的判斷標準:是否阻塞,同步阻塞,異步不阻塞。

單線程的判斷標準:一次是否只做一件事。

JS引擎一次只做一件事。遇到異步任務並不會阻塞後面的同步任務(不等待)。所以我們說JS是異步 單線程的。需要注意的是JS引擎其實並不提供異步的支持,異步支持主要依賴於運行環境(瀏覽器或Node.js)。

while阻塞實驗

var start = new Date();
    while(new Date() - start < 100000) { // delay 10 sec
        ;
    }

上面代碼在chrome控制檯輸入可以手動阻塞當前頁面的js主線程10s。然後我們在當前頁面輸入console.log(1),當前頁面無反應,在另外的頁面輸入console.log(1)直接打印
說明瀏覽器每個頁面都會單獨起一個進程,頁面1的主線程被阻塞並不會影響影響頁面2的主線程

執行機制

clipboard.png

JS Engine和runtime Environment

之前在Stackoverflow看了一個答案,感覺還比較靠譜

JavaScript Engine:parse your code and convert it to runnable commands
JavaScript Runtime Environment :provide some objects to javascript so that it can interact with the outside world.
For example, the Chrome Browser and node.js use the same Engine - V8, but their Runtimes are different: in Chrome you have the window, DOM objects etc, while node gives you require, Buffers and processes.

通俗的講,上面這張圖,左邊你可以看成JS引擎,右邊你可以看成JS運行環境

Eventloop

之前已經說了,JS在設計之初選擇單線程,是以爲單線程簡單,可控。

但是單線程存在一個問題,部分任務比如Ajax請求數據,如果設計成同步的,後面的任務將都去等待Ajax請求完,這個性能是不能接受的。

所以瀏覽器內核(?個人推測,暫時沒有找到相關資料)將任務分爲同步任務和異步任務,所有同步任務放到主線程上執行,形成一個執行棧(execution context stack)。所以異步任務放到其他異步線程上去執行。

當異步任務執行完以後,相關回調函數會放入到消息隊列(也有叫callback queue、task queue)中。

主線程同步任務執行完,每個一段事件會檢查消息隊列一次,有回調函數就會執行,如此往復就成爲Eventloop

個人的理解 :JS引擎是同步的,瀏覽器通過eventloop這種機制實現了異步

看一下How JavaScript works 怎麼描述這個過程的

So, for example, when your JavaScript program makes an Ajax request to fetch some data from the server, you set up the “response” code in a function (the “callback”), and the JS Engine tells the hosting environment:
“Hey, I’m going to suspend execution for now, but whenever you finish with that network request, and you have some data, please call this function back.”

The browser is then set up to listen for the response from the network, and when it has something to return to you, it will schedule the callback function to be executed by inserting it into the event loop.

宏任務,微任務、練習

面試喜歡考宏任務(macrotask),微任務(microtask)。那麼我們就來講一講macrotask和microtask是個啥子

宏任務又成爲task。可以理解是每次執行棧執行的代碼就是一個task,task1->渲染->task1

microtask,可以理解是在當前 task 執行結束後立即執行的任務,所以microtask有歸屬性,只在對應的task執行完後立即執行.task1->microtask1->渲染->task2->microtask2->渲染...

macrotask:主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)

microtask:Promise,process.nextTick等

求下面代碼的結果

console.log('1');
setTimeout(function() { //回調2
    new Promise(function(resolve) {
        console.log('2');
        resolve();
    }).then(function() {
        console.log('3')
    })
    console.log('4');
},2000)
new Promise(function(resolve) {
    console.log('5');
    resolve();
}).then(function() {
    console.log('6')
})

setTimeout(function() {  //回調1
    new Promise(function(resolve) {
        console.log('7');
        resolve();
    }).then(function() {
        console.log('8')
    })
      setTimeout(function(){ //回調3
        console.log('9')
    },2000)
},1000)

//(156) (78) (243) (9)

解析:
task1: 輸出1 5 ----> microtask1 輸出6 --(執行棧空)-->render---->eventloop
1秒以後 callback queue裏面加入回調1 被eventloop捕獲,同步任務入棧,異步任務給settiomeout線程(也就是回調3的那個異步任務)
task2: 輸出7 ----> microtask2 輸出8 --(執行棧空)-->render---->eventloop
2秒以後 callback queue裏面加入回調2 被eventloop捕獲,同步任務入棧
task3: 輸出2 4 ----> microtask3 輸出3 --(執行棧空)-->render---->eventloop
3秒以後 callback queue裏面加入回調3 被eventloop捕獲,同步任務入棧
task4: 輸出9 --(執行棧空)-->render---->eventloop...

異步編程

回調函數實現

let fs = require('fs');
fs.readFile('./1.js','utf-8',(err,data)=>{
    //
    fs.readFile('./2.js','utf-8',(err,data)=>{
         //
         fs.readFile('./3.js','utf-8',(err,data)=>{
                //
         })
    })
})

缺點是容易形成回調地獄,不能return

promise

const fs = require('fs');
const readFile(i) = new Promise(function(){
     fs.readFile(`./${i}.js`,'utf-8',(err,data)=>{
            resolve(data)
         })
})
    readFile(1)
   .then(readFile(2))
   .then(readFile(3))
   .....

async await

async function read(){
 //await後面必須跟一個promise,
 let a = await readFile('./1.txt');
 console.log(a);
 let b = await readFile('./2.txt');
 console.log(b);
 let c = await readFile('./3.txt');
 console.log(c);
 return 'end';
 }

尾聲

以上是我看了多篇文章以後,結合自己的理解,對javascript異步單線程,以及運行機制做的一個總結。如果你感覺哪一部分有點問題,歡迎在評論區留言。

參考

從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
JavaScript 運行機制詳解:再談Event Loop
JavaScript異步機制詳解
JavaScript 運行原理解析
What is the difference between JavaScript Engine and JavaScript Runtime Environment
併發模型與事件循環

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章