單線程的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實現異步的關鍵了!

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

不僅如此,更關鍵的是:

 

 

任務隊列

當定時器線程計時執行完之后,會將回調函數放入任務隊列中!

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

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

 

宏任務 & 微任務

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

這又是兩個啥玩意呢?

 

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

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!

 

 左邊這張大圖來自這里

 

又要上代碼了:

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的eventloop坑了,這次是node的鍋》 這里有解釋!

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

Node.js 中的Event Loop是另外一回事!

關於這些知識,有大佬已經有非常好的文章了,我就不多說了!自己看吧!

 

Web Worker

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

通過使用Web Workers,Web應用程序可以在獨立於主線程的后台線程中,運行一個腳本操作。這樣做的好處是可以在獨立線程中執行費時的處理任務,從而允許主線程(通常是UI線程)不會因此被阻塞/放慢。

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

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

 

Web Worker注意事項:

 

 寫了一個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的用途之一 -- 執行費時的處理任務吧!

關於Web Worker更詳情的說明請看阮一峰老師的這篇文章吧

 

本文轉自本人掘金的同名文章:https://juejin.im/post/5eb58478f265da7bcb65f202 (厚顏無恥導個流...哈哈哈

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM