首先我們先看看同步與異步的定義,及瀏覽器的執行機制,方便我們更好地理解同步異步編程。
瀏覽器是多線程的,JS是單線程的(瀏覽器只分配一個線程來執行JS)
進程大線程小:一個進程中包含多個線程,例如在瀏覽器中打開一個HTML頁面就占用了一個進程,加載頁面的時候,瀏覽器分配一個線程去計算DOM樹,分配其它的線程去加載對應的資源文件...再分配一個線程去自上而下執行JS
同步:在一個線程上(主棧/主任務隊列)同一個時間只能做一件事情,當前事情完成才能進行下一個事情(先把一個任務進棧執行,執行完成,在把下一個任務進棧,上一個任務出棧...)
異步:在主棧中執行一個任務,但是發現這個任務是一個異步的操作,我們會把它移除主棧,放到等待任務隊列中(此時瀏覽器會分配其它線程監聽異步任務是否到達指定的執行時間),如果主棧執行完成,監聽者會把到達時間的異步任務重新放到主棧中執行...
[宏任務:macro task]
- 定時器
- 事件綁定
- ajax
- 回調函數
- Node中fs可以進行異步的I/O操作
[微任務:micro task]
- Promise(async/await) => Promise並不是完全的同步,當在Excutor中執行resolve或者reject的時候,此時是異步操作,會先執行then/catch等,當主棧完成后,才會再去調用resolve/reject把存放的方法執行
- process.nextTick (node中實現的api,把當前任務放到主棧最后執行,當主棧執行完,先執行nextTick,再到等待隊列中找)
- MutationObserver (創建並返回一個新的 MutationObserver 它會在指定的DOM發生變化時被調用。)
執行順序優先級:SYNC => MICRO => MACRO
所有JS中的異步編程僅僅是根據某些機制來管控任務的執行順序,不存在同時執行兩個任務這一說法
先來看一個例子:
setTimeout(() => { console.log(1); }, 20); setTimeout(() => { console.log(2); }, 0);//=>默認會有最小的等待時間(V8一般是5~6MS) console.time('WHILE'); let i = 0; while (i <= 99999999) { i++; } console.timeEnd('WHILE'); setTimeout(() => { console.log(3); }, 10); console.log(4);
結果輸出如圖:
我們先模擬下瀏覽器的程序執行過程,代碼自上而下執行,碰到第一個程序,先放入主棧(主任務隊列),此時瀏覽器發現這是一個宏任務定時器,把它移出主棧,放入等待任務隊列,再繼續執行下面的代碼,放入主棧執行,發現第二個任務也是宏任務的定時器,放入等待隊列,繼續往下執行,推入主棧,同步任務,循環99999999次之后輸出次數,再執行下一個程序,也移入等待隊列,再執行代碼,發現是同步任務,輸出4,此時主棧空閑,任務隊列到達時間后先進先出的原則,首先第二個任務到達時間,把它放入主棧執行,輸出2,此時本因輸出3,因為第三個程序是10ms到達,第一個是20s到達,但是第三個程序是等待247.849853515625ms后才放入的等待隊列,所以第一個程序先到達,輸出1,最后輸出3。
我們用ajax來看看js的同步與異步的執行順序和機制,AJAX任務開始:SEND,AJAX任務結束:狀態為4
let xhr = new XMLHttpRequest(); xhr.open('GET', 'xxx.txt', false); // 放到等待區的時候,此時狀態是1 xhr.onreadystatechange = () => { console.log(xhr.readyState);//=>4 }; xhr.send();
// 同步ajax,xhr.send時為同步,xhr.send()執行完后狀態為4,任務狀態為4的時候主棧空閑,onreadystatechange監聽到狀態變化,輸出4
let xhr = new XMLHttpRequest(); xhr.open('GET', 'xxx.txt', false); xhr.send(); // 狀態已經為4了 xhr.onreadystatechange = () => {//=>狀態改變才會觸發,放到等待區的時候狀態已經為4了,不會在改變了,所以不會執行這個方法(啥都不會輸出) console.log(xhr.readyState); };
let xhr = new XMLHttpRequest(); xhr.open('GET', 'xxx.txt'); xhr.send();//=>異步操作:執行SEND后,有一個線程是去請求數據,主棧會空閑下來 // 放等待區之前狀態是1 xhr.onreadystatechange = () => { console.log(xhr.readyState);//=> 2 3 4 }; // 主棧又空閑了 // 狀態為2 把函數執行 // 狀態為3 把函數執行 // 狀態為4 把函數執行