所謂單線程,就是 同一個時間只能作一件事。JavaScript從誕生之初就是做爲瀏覽器的一種腳本語言,其主要用途是與用戶互動,以及 操做DOM,而這就決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程, 一個線程在某個DOM節點上添加內容, 另外一個線程刪除了這個節點,這個時候瀏覽器就不知道該如何處理了, 究竟是應該在節點上添加內容仍是應該刪除這個節點呢?
雖然爲了利用CPU的多核計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是 子線程徹底受主線程控制, 且不得操做DOM,因此, 這個新標準並無改變JavaScript單線程的本質。
任務隊列是指 task queue,因爲JavaScript是單線程的,因此 全部任務必須進行排隊依次進行處理。而任務又分爲 同步任務和 異步任務, 同步任務直接進入主線程中進行排隊, 異步任務則進入任務隊列中進行排隊,同步任務是在 主線程的調用棧中執行的, 只有主線程的調用棧被清空的時候, 纔會執行任務隊列中的任務,這也就是所說的 JavaScript的運行機制。
一般異步操做都會進入到任務隊列中,好比setTimeout()、setInterval(),這裏須要注意的就是 瀏覽器是多線程的,主要爲 UI渲染線程、 JS引擎線程、 GUI線程(主要用於處理事件交互),其中, JS引擎線程和UI渲染線程是互斥的,即, 若是JS引擎主線程在執行,那麼UI將沒法進行渲染,由於 JS引擎線程是能夠進行DOM操做的,只有互斥才能保證不會出現UI引擎在渲染的同時,JS引擎線程同時在修改DOM,如頁面中有一個按鈕,點擊按鈕後會開始一段耗時比較長的計算,這裏要求實現點擊按鈕後按鈕文字顯示"計算中",計算完成後,按鈕文字顯示"計算完成"。
<body> <button id="btn">點我</button> </body>
let btn = document.getElementById("btn"); function long_running() { console.log("long_running"); var result = 0; for(var i = 0; i < 1000; i++) { for(var j= 0; j < 1000; j++) { for(var k=0; k< 1000; k++) { result = result + i + j + k; } } } btn.innerHTML = "計算完成"; } btn.addEventListener("click", (e) => { btn.innerHTML = "計算中..."; long_running(); });
運行如上代碼,咱們能夠發現點擊按鈕後並無先變成"計算中",而後再變成"計算完成",而是點擊以後無變化,而後等計算完成後直接變成了"計算完成"。由於btn.innerHTML = "計算中..."; 是進行DOM操做, 使用的UI渲染線程,此時, JS引擎線程調用棧還未清空(還須要往下執行js),因此 還不能當即執行,而後執行long_running(),long_running()不是異步任務,不進入到任務隊列中,直接進入到主線程的調用棧中執行,因爲耗時比較長,等long_running()執行完成後,主線程調用棧被清空,UI渲染引擎開始執行,因此直接顯示"計算完成"了,要實現上述效果,咱們須要給long_running()添加一個延時,讓其進入到任務隊列中,不要佔用主線程調用棧,讓btn.innerHTML = "計算中..."先執行,再進行計算。如:
btn.addEventListener("click", (e) => { btn.innerHTML = "計算中..."; setTimeout(() => { long_running(); }, 0); });
添加延時後,long_running();也進入到了任務隊列中,因此會先執行btn.innerHTML = "計算中...";再執行long_running();等計算完成後再更新爲"計算完成"。
異步任務又分爲 宏認爲和 微任務。宏任務包括 總體代碼script, setTimeout, setInterval, 宏認爲進入宏任務隊列,而且 宏任務隊列能夠有多個;微任務包 Promise的then(回調), process.nextTick, 微任務進入微任務隊列,而且 微任務隊列只有一個,當宏任務隊列的中的任務所有執行完之後,會查看微任務隊列中是否有微任務,好比 在執行宏任務的時候產生了微任務,那麼 會先執行微任務隊列中的全部微任務,若是微任務隊列中沒有微任務,那麼直接執行下一個宏任務隊列,重複執行以前的執行步驟,從而造成事件環。
① 示例1promise
setTimeout(() => console.log('setTimeout1'), 0); //1宏任務 setTimeout(() => { //2宏任務 console.log('setTimeout2'); Promise.resolve().then(() => { console.log('promise3'); Promise.resolve().then(() => { console.log('promise4'); }) console.log(5) }) setTimeout(() => console.log('setTimeout4'), 0); //4宏任務 }, 0); setTimeout(() => console.log('setTimeout3'), 0); //3宏任務 Promise.resolve().then(() => {//1微任務 console.log('promise1'); })
首先總體代碼首先產生了一、二、3三個宏任務,進入宏任務隊列,而後執行到最後一行Promise的時候產生了一個微任務,進入微任務隊列,由於 總體代碼是一個宏任務, 宏任務結束後會檢查微任務隊列中是否有任務,發現有一個,因此首先輸出promise1,微任務清空後,接着執行下一個宏任務,雖然一下產生了三個宏任務,可是因爲時間都是0,因此這三個宏任務其實至關因而一個大的宏任務,能夠合在一塊兒,如:
setTimeout(() => { console.log('setTimeout1'); // 宏任務1 console.log('setTimeout2'); Promise.resolve().then(() => { console.log('promise3'); Promise.resolve().then(() => { console.log('promise4'); }) console.log(5) }) setTimeout(() => console.log('setTimeout4'), 0); console.log('setTimeout3') // 宏任務3 }, 0);
因此接着執行這個大的宏任務,輸出setTimeout1,setTimeout2,setTimeout3,而後執行宏任務的過程當中產生了一個微任務和一個宏任務,因此接着執行這個微任務,輸出promise3,5,而後執行微任務的過程當中又產生了一個微任務,而後繼續執行微任務輸出promise4,此時微任務清空完畢,執行最後一個宏任務,輸出setTimeout4。
②示例2瀏覽器
new Promise((resolve,reject)=>{ console.log("promise1") resolve() }).then(()=>{ console.log("then1-1") new Promise((resolve,reject)=>{ console.log("promise2") resolve() }).then(()=>{ console.log("then2-1") }).then(()=>{ console.log("then2-2") }) }).then(()=>{ console.log("then1-2") })
首先執行總體代碼,當即輸出promise1,而後產生了一個微任務,沒有宏任務,接着執行產生的微任務,輸出then1-1,promise2,執行微任務的過程當中又產生了一個微任務,進入到微任務隊列,外層第一個then執行完成,接着執行第二個then又產生了一個微任務,因而添加到微任務隊列,此時微任務隊列中有兩個微任務了,即內層的第一個then,和外層的第二個then,故依次輸出then2-一、then1-2,在執行內層第一個then的過程當中又產生了一個微任務,繼續添加到微任務隊列,而後輸出then2-2
首先promise.then和process.nextTick屬於微任務,setTimeout和setImmediate屬於宏任務,而且 process.nextTick的優先級要高於promise.then, setTimeout的優先級高於setIImmediate。
setImmediate(function(){ // 宏任務1 console.log(1); },0); setTimeout(function(){ // 宏任務2 console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ // 微任務1 console.log(5); }); console.log(6); process.nextTick(function(){ // 微任務2 console.log(7); }); console.log(8);
首先執行總體代碼,產生了兩個宏任務,而後建立Promise執行同步代碼輸出3和4,此時產生了一個微任務1,接着輸出6,而後再產生了一個微任務2,接着輸出8,此時總體代碼執行完畢,而後檢測微任務隊列並執行,此時微任務隊列中有兩個,雖然微任務2後面添加進去,可是微任務2是由process.nextTick建立具備更高優先級,因此先執行微任務2,依次輸出7和5,接着再執行宏任務,因爲setTimeout比setImmediate具備更高優先級,因此先執行宏任務2,依次輸出2和1,故最終結果爲三、四、六、八、七、五、二、1。
① 示例1多線程
console.log(1); setTimeout(function() { // 宏任務1 console.log('2'); process.nextTick(function() { // 微任務3 console.log('3'); }); new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { // 微任務4 console.log('5'); }); }, 0); process.nextTick(function() { // 微任務1 console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { // 微任務2 console.log('8') }); setTimeout(() => { // 宏任務2 console.log('9'); process.nextTick(function() { // 微任務5 console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { // 微任務6 console.log('12'); }); }, 0);
首先執行總體代碼,輸出1,併產生宏任務1,接着產生一個微任務1,接着建立Promise執行同步代碼輸出7,併產生微任務2,最後產生一個宏任務2;接着狀況微任務隊列,微任務隊列中有兩個任務,故依次輸出6和8;而後再執行宏任務隊列,因爲宏任務1和2時間都是0,因此能夠看作是一個大的宏任務,先輸出2,併產生微任務3,接着建立Promise執行同步代碼輸出4,而後產生微任務4,繼續執行宏任務2,輸出9,產生微任務5,接着建立Promise執行同步代碼輸出11,併產生微任務6,此時宏任務1和2執行完畢,接着須要清空微任務隊列,微任務隊列中有三、四、五、6,因爲process.nextTick優先級高於Promise.then,因此先輸出3和10,而後再輸出5和12,故最終輸出結果爲一、七、六、八、二、四、九、十一、三、十、五、12
② 示例2異步
async function async1() { console.log('async1 start') await async2(); console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(() => { console.log('setTimeout0') },0) setTimeout(() => { console.log('setTimeout3') },3) setImmediate(() => { console.log("setImmediate"); }); async1(); new Promise((resolve) => { console.log("promise1"); resolve(); console.log("promise2"); }).then(() => { console.log("promise3"); }); process.nextTick(() => { console.log("nextTick"); }); console.log("scritp end.");
這道題主要考察的是async函數的執行原理, async函數會返回一個Promise對象,當函數執行的時候,一旦遇到await就會當即返回,可是要等到await後的代碼執行完成後才能回到主線程,即接着執行函數外的同步代碼,函數外的同步代碼執行完成後再回到async函數內接着執行,而且之間若是產生了微任務,那麼須要先清空微任務。
首先執行總體代碼,輸出 script start,而後setTimeout0、setTimeout三、setImmediate進入到宏任務隊列,接着執行async1函數,輸出 async1 start,而後遇到await,async1函數當即返回,可是還要等到await以後的代碼async2執行完畢,async2執行完成輸出 async2,此時回到主線程繼續執行,即執行Promise中的同步代碼,輸出 promise1、 promise2,而後產生一個promise3微任務,接着nextTick也進入到微任務對列,接着輸出 scritp end,此時主線程執行完畢,即主線程調用棧已經被清空,接着檢測是否有微任務隊列,發現有,開始執行微任務隊列,產生了nextTick和promise3兩個微任務,而且nextTick優先級更高,依次輸出 nextTick、 promise3,此時再次回到async1()執行剩餘的代碼,輸出 async1 end,接着再執行宏任務隊列中的代碼,setTimeout0和setImmediate時間都是0,而且setTimeout0優先級更高,依次輸出 setTimeout0、 setImmediate,最後輸出setTimeout3。