天天看點

10 分鐘掌握浏覽器運作 JS 的順序

前言

不知道你有沒有遇到過類似這樣的問題,某些代碼亂序執行或樣式的更改後不生效?你是不是曾經把代碼包在 setTimeout 裡面來解決類似的問題?是不是這種方式不太可靠?然後你就不斷調試 timeout 值以至于看起來好像沒問題了?接下來我們将一起來看一下這其中到底發生了什麼。

程序和線程

我們先來區分一下程序和線程

  • 程序是 cpu 資源配置設定的最小機關(是能擁有資源和獨立運作的最小機關)
  • 線程是 cpu 排程的最小機關(線程是建立在程序的基礎上的一次程式運作機關,一個程序中可以有多個線程)

通俗地講:程序就像是一個有邊界的生産廠間,而線程就像是廠間内的一個個員工,可以自己做自己的事情,也可以互相配合做同一件事情,是以一個程序可以建立多個線程。

了解了程序與線程了差別後,接下來對浏覽器進行一定程度上的認識:

浏覽器的多程序

它主要包括以下程序:

10 分鐘掌握浏覽器運作 JS 的順序
  • 浏覽器是多程序的
  • 浏覽器之是以能夠運作,是因為系統給它的程序配置設定了資源(CPU、記憶體)
  • 簡單點了解,每打開一個 Tab 頁,就相當于建立了一個獨立的浏覽器程序。

渲染程序(浏覽器核心)

對于普通的前端操作來說,最重要的是渲染程序,頁面的渲染,JS 的執行,事件的循環,都在這個程序内進行。接下來我們重點分析這個程序。注意:浏覽器的渲染程序是多線程的。

接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

10 分鐘掌握浏覽器運作 JS 的順序

單線程的 JS

JavaScript 單線程指的是浏覽器中負責解釋和執行 JavaScript 代碼的隻有一個主線程,即為 JS 引擎線程,每次隻能做一件事。

我們知道一個 AJAX 請求,主線程在等待它響應的同時是會去做其它事的,浏覽器先在事件表注冊 AJAX 的回調函數,響應回來後回調函數被添加到任務隊列中等待執行,不會造成線程阻塞,是以說 JS 處理 AJAX 請求的方式是異步的。

JavaScript 的單線程,與它的用途有關。作為浏覽器腳本語言,其主要用途是與使用者互動,以及操作 DOM。這決定了它隻能是單線程,否則會帶來很複雜的同步問題。假設同時有兩個線程,一個線程在某個 DOM 節點上添加内容,另一個線程删除了這個節點,那就亂套了。

我們可以來看下面的例子:

function foo() {
    bar()
    console.log('foo')
}

function bar() {
    baz()
    console.log('bar')
}

function baz() {
    console.log('baz')
}

foo()

// baz
// bar
// foo
           
function foo() {
    console.log('foo');
}

setTimeout(() => {
    console.log('setTimeout');
}, 0);

foo();

// foo
// setTimeout: 0s
           

任務隊列

任務隊列是指一個事件的隊列(消息隊列),先進先出的資料結構,排在前面的事件,優先被主線程讀取。隻要執行棧上任務一清空,就會被主線程讀取,任務隊列上首位的事件就自動進入主線程。

任務又分成兩種,一種是同步任務,另一種是異步任務。

  • 同步任務,在主線程形成一個執行棧,排隊執行的任務,隻有前一個任務執行完畢,才能執行後一個任務;
  • 異步任務,不進入主線程,而進入任務隊列的任務,隻有任務隊列通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。

另外事件循環中的異步任務隊列有兩種:macroTask(宏任務)隊列和 microTask(微任務)隊列。

宏任務隊列

  • DOM 事件
  • XHR
  • 定時器(setTimeout / setInterval / setImmediate)

可以通過 setTimeout(func) 即可将 func 添加到宏任務隊列中(使用場景:将計算耗時長的任務切分成小塊,以便于浏覽器有空處理使用者事件,以及顯示耗時進度)。

微任務隊列

  • Promise 事件
  • MutationObserver
  • process.nextTick 事件(Node.js)

可以通過 queueMicrotask(func) 将 func 添加到微任務隊列中。

事件循環

JS 主線程循環往複地從任務隊列(callback queue / task queue)中讀取任務,執行任務,其中運作機制稱為事件循環(event loop)。

在事件表 Web APIs 中會注冊各類事件線程處理各種事件,然後将處理好的回調事件放入對應的任務隊列(宏任務、微任務)中。如果執行棧裡的任務執行完成,即執行棧為空的時候(JS 引擎線程空閑),事件觸發線程才會從任務隊列取出一個任務(即異步的回調函數)放入執行棧中執行。具體的流程可以參考下圖:

10 分鐘掌握浏覽器運作 JS 的順序

運作機制

  • 執行一個宏任務(棧中沒有就從事件隊列中擷取)
  • 執行過程中如果遇到微任務,就将它添加到微任務的任務隊列中
  • 宏任務執行完畢後,立即執行目前微任務隊列中的所有微任務(依次執行)
  • 目前宏任務執行完畢,開始檢查渲染,然後 GUI 線程接管渲染
  • 渲染完畢後,JS 線程繼續接管,開始下一個宏任務(從事件隊列中擷取)
10 分鐘掌握浏覽器運作 JS 的順序

setTimeout

我們一起來看下面的代碼:

console.log('script start');

setTimeout(() => {
  console.log('setTimeout'); // 調用 setTimeout 函數,将其回調函數放入宏任務隊列
}, 0);

console.log('script end');

// script start
// script end
// setTimeout
           

Promise

  • Promise 本身是同步的立即執行函數, 當在 executor 中執行 resolve 或者 reject 的時候,此時是異步操作,會先執行 then/catch 等(即 then/catch 裡面的東西才是異步執行的部分)。當主棧完成後,才會去任務隊列中調用 resolve/reject 中存放的回調方法執行,傳回一個 Promise 執行個體。

多說無益,我們來看下面的代碼:

console.log('script start');

Promise.resolve().then(() => {
  console.log('promise1'); // Promise 将其回調函數 then 放入微任務隊列
}).then(() => {
  console.log('promise2'); // 同上
});

console.log('script end');

// script start
// script end
// promise1
// promise2
           

async/await

  • async/await 本質上還是基于 Promise 的封裝。
  • async 總是傳回一個 Promise。
  • async 函數在 await 之前的代碼都是同步執行的,可以了解為 await 之前的代碼相當于 Promise executor 中的代碼,await 之後的所有代碼都是在 Promise.then 中的回調。
  • await 傳回 Promise 對象的處理結果。如果後面等待的不是 Promise 對象,則傳回該值本身。
async function foo() {
    console.log(1)
    await 2
    console.log(3)
}

等價于

function foo() {
    console.log(1)
    return Promise.resolve(2).then(console.log(3))
}
           

接着,我們來看一下下面的代碼:

console.log('script start');

async function async1() {
  console.log('async1 start')
  await async2() 							// 執行 async2,暫停整個 async 函數的執行并讓出執行棧
  console.log('async1 end')		// Promise.then 将其加入微任務隊列
}

async function async2() {
  await console.log('async2')
}

async1()
console.log('script end');

// script start
// async1 start
// async2
// script end
// async1 end
           

牛刀小試

最後放幾道題大家一起來考察一下自己的掌握程度吧。

第一題

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

async function async1() {
  console.log('async1 start');
  await async2()
  console.log('async1 end');
}

async function async2() {
  await console.log('async2');
}

async1();
console.log('script end');


// script start
// async1 start
// async2
// script end
// promise1
// promise2
// async1 end
// setTimeout
           

第二題

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0)

new Promise((resolve) => {
  console.log('promise3');
  resolve();
}).then(() => {
  console.log('promise4');
});

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  new Promise((resolve) => {
	  console.log('promise1');
	  resolve();
  }).then(() => {
	  console.log('promise2');
  });
}

async1();
console.log('script end');


// script start
// promise3
// async1 start
// promise1
// script end
// promise4
// promise2
// async1 end
// setTimeout
           

第三題

console.log('script start')

setTimeout(() => {
    console.log('setTimeout3')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

async function async1() {
    console.log('async1')
    await setTimeout2()
    setTimeout(() => {
        console.log('setTimeout1')
    }, 0)
}

async function setTimeout2() {
    setTimeout(() => {
		console.log('setTimeout2')
	},0)
}

async1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})

console.log('script end')


// script start
// async1
// promise2
// script end
// promise1
// promise2.then
// promise3
// setTimeout3
// setTimeout2
// setTimeout1
           

完結

繼續閱讀