這篇借助於同事准備的技術分享,其他技術文章,書本知識,自己的理解梳理而成
高級程序設計第三版:
js 是一門單線程的語言,運行於單線程的環境中,例如定時器等並不是線程,定時器僅僅只是計划代碼在未來的某個時間執行,瀏覽器負責排序,指派某段代碼在某個時間點運行
的優先級
1.為什么規定瀏覽器必須是單線程?
JS主要用途之一是操作DOM,如果JS同時有兩個線程,同時對同一個dom進行操作,一個需要刪除dom,一個需要添加dom,這時瀏覽器應該聽哪個線程的,如何判斷優先級,所以為了簡化操作,規定js是一門單線程的語言。
2.有關於js是單線程的理解
所謂的"JS是單線程的"是指解釋和執行JS代碼的線程,只有一個,一般稱之為“主線程”,而瀏覽器並不是單線程的,是多線程並且是多進程的,而對於前端最關心的還是渲染進程.
- GUI渲染線程
● 負責渲染瀏覽器界面,解析HTML、CSS,構建DOM樹和RenderObject樹,布局和繪制
● 當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行
● GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中,等到JS引擎空閑時立即被執行。 - JS引擎線程
● 也稱JS內核,負責處理JS腳本程序。例如V8引擎
● JS引擎一直等待着任務隊列中任務的到來,然后加以處理,一個Tab頁(Renderer進程)中無論什么時候都只有一個JS引擎線程在運行JS程序
● GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,頁面渲染就不連貫。 - 定時觸發器線程
● 傳說中的setInterval和setTimeout所在的線程
● 定時器線程其實只是一個計時的作用,他並不會真正執行時間到了的回調,真正執行這個回調的還是JS主線程。當時間到了,定時器線程就通知事件觸發線程,讓事件觸發線程將setTimeout的回調事件添加到待處理任務隊列的尾部,等待JS引擎的處理。
● W3C在HTML5標准中規定,要求setTimeout中低於4ms的時間間隔算4ms - 事件觸發線程
● 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(可以理解為:JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
● 當JS引擎執行setTimeout時(或者是來自瀏覽器內核的其他線程,如鼠標點擊、ajax異步請求等),當這些事件滿足觸發條件被觸發時,該線程就會將對應回調事件添加到添加到待處理任務隊列的尾部,等待JS引擎的處理
● 由於JS是單線程關系,所以這些待處理任務隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行) - 異步http請求線程
● 這個線程負責處理異步的ajax請求,當請求完成后如果設置有回調函數,他也會通知事件觸發線程,然后事件觸發線程將這個回調再放入任務隊列中尾部,等待JS引擎執行
3.單線程如何實現異步?
大家都知道JS是單線程的腳本語言,在同一時間,只能做同一件事,為了協調事件、用戶交互、腳本、UI渲染和網絡處理等行為,防止主線程阻塞,設計者給JS加了一個事件循環(Event Loop)的機制
3.1理解什么是執行上下文?
可以看這篇文章https://amberzqx.com/2020/02/04/JavaScript%E7%B3%BB%E5%88%97%E4%B9%8B%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E5%92%8C%E6%89%A7%E8%A1%8C%E6%A0%88/
當 JavaScript 代碼執行一段可執行代碼(executable code)時,會創建對應的執行上下文(execution context)。執行上下文(可執行代碼段)總共有三種類型:
全局執行上下文(全局代碼):不在任何函數中的代碼都位於全局執行上下文中,只有一個,瀏覽器中的全局對象就是 window 對象,this 指向這個全局對象。
函數執行上下文(函數體):只有調用函數時,才會為該函數創建一個新的執行上下文,可以存在無數個,每當一個新的執行上下文被創-建,它都會按照特定的順序執行一系列步驟。
Eval 函數執行上下文(eval 代碼): 指的是運行在 eval 函數中的代碼,很少用而且不建議使用
執行上下文又包括三個生命周期階段:創建階段 → 執行階段 → 回收階段
JS引擎創建了執行上下文棧(執行棧)來管理執行上下文。可以把執行上下文棧認為是一個存儲函數調用的棧結構,遵循先進后出,后進先出的原則,就像下面的漢諾塔,第一個最大的先進去,當拿出來的時候肯定是最后一個出來的,最小的那個后進去,拿出來的時候是最先拿出來的~因為JS執行中最先進入全局環境,所以處於"棧底的永遠是全局執行上下文"。而處於"棧頂的是當前正在執行函數的執行上下文"
舉個例子:
const firstFunction = () => {
console.log('1');
secondFunction();
console.log('2');
}
const secondFunction = () => {
console.log('3');
}
firstFunction();
console.log(4)
// 1324
//從上到下的執行
//圖一:從上到下執行,先是全局作用域,那就是棧底第一個
//圖二: firstFunction的調用,打印出1,現在棧頂是secondFunction,因為函數里面還沒有執行完,所以還沒有被銷毀
//圖三: secondFunction的調用,打印3,secondFunction,因為函數里面執行完,所以要被銷毀到圖四
//再執行棧頂firstFunction里面的2到圖5
看上面的圖是不是對應漢諾塔放進去,拿出來的一個過程
3.2理解同步任務,異步任務,任務隊列
JavaScript 是一個單線程序的解釋器,因此一定時間內只能執行一段代碼。為了控制要執行的代碼,就有一個 JavaScript 任務隊列。這些任務會按照將它們添加到隊列的順序執行如果隊列是空的,那么添加的代碼會立即執行;如果隊列不是空的,那么它就要等前面的代碼執行完了以后再執行.同步任務指的是,在主線程上,排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程,而進入“任務隊列”(task queue)的任務,只有“任務隊列”通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
3.3 js的事件循環機制
具體來說,異步運行機制如下:
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)述過程會不斷重復,也就是常說的Event Loop(事件循環)
3.4宏任務微任務
js的任務又分為宏任務和微任務(btw,不要歸類為同步任務異步任務和宏任務微任務扯上聯系,拋開同步異步的概念去理解宏任務微任務)
前端常見的宏任務,微任務分類:
macro-task(宏任務):包括整體代碼script,setTimeout,setInterval,setImmediate(node或者小眾瀏覽器支持)
micro-task(微任務):Promise,process.nextTick(node 環境支持)
注意:
Promise 新建后就會立即執行,也就是說new Promise構造函數是同步任務,但Promise的注冊的then回調和catch回調才是微任務
宏任務:可以理解是每次執行棧執行的代碼就是一個宏任務,所有宏任務都是添加到任務隊列,所以”任務隊列又叫宏任務隊列”,這個任務隊列由事件觸發線程來單獨維護的
微任務
可以理解是在當前宏任務執行結束后立即執行的任務
每一次事件循環,是先執行宏任務,再執行宏任務里面的微任務,看到里面兩個字了嗎?????每一次事件循環只執行一個宏任務
注意:
1.微任務隊列里邊的優先級process.nextTick()>Promise.then()
2.setInterval,setImmediate的執行順序后續補充,目前前端幾乎用不到setImmediate,不要慌
總結:
每一次循環稱為 tick, 每一次tick的任務如下:
1、執行一個宏任務(執行棧中沒有就從任務隊列中獲取)
2、宏任務執行過程中如果遇到微任務,就將它添加到微任務隊列中
3、宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
4、重復1到3步驟
宏任務 > 所有微任務 > 宏任務>微任務,也就是每一次事件循環,是先執行宏任務,再執行宏任務里面的微任務,下一輪還是先執行宏任務,再執行下一輪的微任務
練習題目:
example1
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
}).then(() => {
console.log('4')
})
console.log('5')
// 1 5 3 4 2
//第一輪循環,執行宏任務script里面的同步任務,遇到微任務掛起,先執行15
// 執行完宏任務,執行微任務 3, 4 微任務執行完畢了就是下一輪事件循環的開始
// 第二輪循環 執行 settimout里面的2
example2
es6那本書---Promise 新建后就會立即執行
console.log('1')
new Promise((resolve) => {
console.log('2')
setTimeout(()=>{
console.log('4')
resolve()
}, 0)
}).then(() => {
console.log('3')
})
// 1,2,4,3
// 第一輪: 宏 script 打印出:12
// 第二輪: 宏 setTimeout 打印出: 4,3
// 這里要注意Promise的注冊的then回調和catch回調才是微任務,所以resolve()是在setTimeout里面調的
// 屬於第二輪的微任務
example3
new Promise((resolve) => {
console.log('1')
setTimeout(() => {
console.log('2')
}, 1000)
resolve()
}).then(() => {
console.log('3')
})
// 132
// 基於example2,可以明白,resolve()在setTimeout之外調用,還是屬於第一輪宏任務里面的微任務
example4
console.log('1')
new Promise((resolve) => {
console.log('2')
resolve()
console.log('3')
}).then(() => {
console.log('4')
})
console.log('5')
// 第一輪 宏: script 打印: 1235 微:4
// 12354
// 要注意的是3不用等resolve回調完再執行哦,因為並沒有還可以繼續執行,await可以阻塞下面的執行
example5
console.log('1')
setTimeout(() => {
console.log('2')
new Promise((resolve) => {
console.log('3')
resolve()
}).then(() => {
console.log('4')
})
})
new Promise((resolve) => {
console.log('5')
resolve()
}).then(() => {
console.log('6')
})
console.log('7')
setTimeout(() => {
console.log('8')
new Promise((resolve) => {
console.log('9')
resolve()
}).then(() => {
console.log('10')
})
})
// 第一輪循環 宏:script 打印: 157 微:resolve() 打印:6
// 第二輪循環 宏:第一個setTimeout 打印:23 微:resolve() 打印:4
// 第三輪: 宏:第二個setTimeout 打印:89 微:resolve() 打印:10
// 15762348910
example6 在node下面執行,這個例子一般,不值得一看
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 第一輪 宏:script 打印:17 微: process.nextTick以及Promise.then 打印: 6 8
// 第二輪:宏: 第一個settimeout 打印: 24 微:process.nextTick以及Promise.then 打印:35
// 第三輪: 宏: 第二個settimeout 打印: 9 11 微: process.nextTick以及Promise.then 打印: 10 12
// 1768 2435 9 11 10 12
example6
async function async1(){
console.log('1')
await async2()
console.log('2')
}
async function async2(){
console.log('3')
}
console.log('4')
setTimeout(function(){
console.log('5')
},0)
async1();
new Promise(function(resolve){
console.log('6')
resolve();
}).then(function(){
console.log('7')
})
console.log('8')
// 第一輪事件循環 宏:script 打印:4 1 3 6 8 微: await之后的結果 以及resolve() 打印: 2 7
// 第二輪事件循環 宏:setTimeout 打印: 5
// 4 1 3 6 8 2 7 5
//注意 await xx的時候,相當於xx這里直接創建了一個new promise,所以async2函數是new promise,會立即執行, await的結果是promise.then的結果,並且沒有成功finish會阻塞下面的執行,所以2會在微任務拿到結果之后執行
還有一個例子,前端目前用不到哈,后續更新:
console.log('1')
setTimeout(() => {
console.log('2')
process.nextTick(() => {
console.log('3')
})
new Promise((resolve) => {
console.log('4')
resolve()
}).then(() => {
console.log('5')
})
})
new Promise((resolve) => {
console.log('7')
resolve()
}).then(() => {
console.log('8')
})
console.log('9')
process.nextTick(() => {
console.log('10')
})
setImmediate(() => {
console.log('15')
process.nextTick(() => {
console.log('16')
})
new Promise((resolve) => {
console.log('17')
resolve()
}).then(() => {
console.log('18')
})
})
setTimeout(() => {
console.log('11')
new Promise((resolve) => {
console.log('12')
resolve()
}).then(() => {
console.log('13')
})
process.nextTick(() => {
console.log('14')
})
})
1,7,9,10,8,
2,4,3,5
11 12 14 13
15 17 16 18