【阿里雲映象】配置阿里雲RepoForge 映象
程序與執行緒 -- 涉及⾯試題:程序與執行緒區別? JS 單執行緒帶來的好處?
- JS 是單執行緒執⾏的,但是你是否疑惑過什麼是執行緒?
- 講到執行緒,那麼肯定也得說⼀下程序。本質上來說,兩個名詞都是 CPU ⼯作時間⽚的⼀個描述。
- 程序描述了 CPU 在運⾏指令及載入和儲存上下⽂所需的時間,放在應⽤上來說就代表了⼀個程式。
- 執行緒是程序中的更⼩單位,描述了執⾏⼀段指令所需的時間。
--- 把這些概念拿到瀏覽器中來說,當你開啟⼀個 Tab ⻚時,其實就是建立了⼀個程序,⼀個程序中可以有多個執行緒,⽐如渲染執行緒、 JS 引擎執行緒、HTTP 請求執行緒等等。當你發起⼀個請求時,其實就是建立了⼀個執行緒,當請求結束後,該執行緒可能就會被銷燬。 - 上⽂說到了 JS 引擎執行緒和渲染執行緒,⼤家應該都知道,在 JS 運⾏的時候可能會阻⽌ UI 渲染,這說明了兩個執行緒是互斥的。這其中的原因是因為 JS 可以修改 DOM ,如果在 JS 執⾏的時候 UI 執行緒還在⼯作,就可能導致不能安全的渲染 UI 。這其實也是⼀個單執行緒的好處,得益於 JS 是單執行緒運⾏的,可以達到節省記憶體,節約上下⽂切換時間,沒有鎖的問題的好處。
執⾏棧 -- 涉及⾯試題:什麼是執⾏棧?
* 可以把執⾏棧認為是⼀個儲存函式調⽤的棧結構,遵循先進後出的原則。
當開始執⾏ JS 程式碼時,⾸先會執⾏⼀個 main 函式,然後執⾏我們的程式碼。
根據先進後出的原則,後執⾏的函式會先彈出棧,在圖中我們也可以發現, foo 函式後執⾏,當執⾏完畢後就從棧中彈出了。
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
⼤家可以在上圖清晰的看到報錯在 foo 函式, foo 函式⼜是在 bar 函式 中調⽤的。
當我們使⽤遞迴的時候,因為棧可存放的函式是有限制的,⼀旦存放了過多的函式且沒有得到釋放的話,就會出現爆棧的問題。
function bar() {
bar()
}
bar()
瀏覽器中的 Event Loop
涉及⾯試題:非同步程式碼執⾏順序?解釋⼀下什麼是 Event Loop ?
* 眾所周知 JS 是⻔⾮阻塞單執行緒語⾔,因為在最初 JS 就是為了和瀏覽器互動⽽誕⽣的。
* 如果 JS 是⻔多執行緒的語⾔話,我們在多個執行緒中處理 DOM 就可能會發⽣問題(⼀個執行緒中新加節點,另⼀個執行緒中刪除節點)。
-
JS 在執⾏的過程中會產⽣執⾏環境,這些執⾏環境會被順序的加⼊到執⾏棧中。如果遇到非同步的程式碼,會被掛起並加⼊到 Task (有多種 task ) 佇列中。⼀旦執⾏棧為空,Event Loop 就會從 Task 佇列中拿出需要執⾏的程式碼並放⼊執⾏棧中執⾏,所以本質上來說 JS 中的非同步還是同步⾏為。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); console.log('script end');
-
不同的任務源會被分配到不同的 Task 佇列中,任務源可以分為 微任務 ( microtask ) 和 巨集任務( macrotask )。
-
在 ES6 規範中,microtask 稱為 jobs , macrotask 稱為 task 。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise((resolve) => { console.log('Promise') resolve() }).then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // script start => Promise => script end => promise1 => promise2 => setTimeout
--- 以上程式碼雖然 setTimeout 寫在 Promise 之前,但是因為 Promise 屬於 微任務 而 setTimeout 屬於巨集任務。
-
微任務
- process.nextTick
- promise
- Object.observe
- MutationObserver
巨集任務
- script
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
-- 巨集任務中包括了 script ,瀏覽器會先執⾏⼀個巨集任務,接下來有 非同步程式碼 的話就先執⾏微任務。
所以正確的⼀次 Event loop 順序是這樣的
- 執⾏同步程式碼,這屬於巨集任務;
- 執⾏棧為空,查詢是否有微任務需要執⾏;
- 執⾏所有微任務 ;
- 必要的話渲染 UI ;
- 然後開始下⼀輪 Event loop ,執⾏巨集任務中的非同步程式碼。
--- 通過上述的 Event loop 順序可知,如果巨集任務中的非同步程式碼有⼤量的計算 並且需要操作 DOM 的話,為了更快的響應界⾯響應,我們可以把操作 DOM 放⼊微任務中。
Node 中的 Event loop
-
Node 中的 Event loop 和瀏覽器中的不相同。
-
Node 的 Event loop 分為 6 個階段,它們會按照順序反覆運⾏。
┌───────────────────────┐
┌─> │ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└─ ─┤ close callbacks │
└───────────────────────┘
timer
timers 階段會執⾏ setTimeout 和 setInterval
⼀個 timer 指定的時間並不是準確時間,⽽是在達到這個時間後儘快執⾏回撥,可能會因 為系統正在執⾏別的事務⽽延遲 I/O
I/O 階段會執⾏除了 close 事件,定時器和 setImmediate 的回撥 poll
poll 階段很重要,這⼀階段中,系統會做兩件事情 執⾏到點的定時器 執⾏ poll 佇列中的事件
並且當 poll 中沒有定時器的情況下,會發現以下兩件事情 如果 poll 佇列不為空,會遍歷回撥佇列並同步執⾏,直到佇列為空或者系統限制 如果 poll 佇列為空,會有兩件事發⽣ 如果有 setImmediate 需要執⾏, poll 階段會停⽌並且進⼊到 check 階段執⾏
setImmediate
如果沒有 setImmediate 需要執⾏,會等待回撥被加⼊到佇列中並⽴即執⾏回撥 如果有別的定時器需要被執⾏,會回到 timer 階段執⾏回撥。 check
check 階段執⾏ setImmediate
close callbacks
close callbacks 階段執⾏ close 事件
並且在 Node 中,有些情況下的定時器執⾏順序是隨機的
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
setTimeout(() => {
console.log('setTimeout'); }, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 這⾥可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於效能
// 因為可能進⼊ event loop ⽤了不到 1 毫秒,這時候會執⾏ setImmediate
// 否則會執⾏ setTimeout
上⾯介紹的都是 macrotask 的執⾏情況, microtask 會在以上每個階段完 成後⽴即執⾏
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
}) }, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
}) }, 0)
// 以上程式碼在瀏覽器和 node 中列印情況是不同的
// 瀏覽器中⼀定列印 timer1, promise1, timer2, promise2
// node 中可能列印 timer1, timer2, promise1, promise2
// 也可能列印 timer1, promise1, timer2, promise2
Node 中的 process.nextTick 會先於其他 microtask 執⾏
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1"); }); }, 0);
process.nextTick(() => {
console.log("nextTick"); });
// nextTick, timer1, promise1
對於 microtask 來說,它會在以上每個階段完成前清空 microtask 隊 列,下圖中的 Tick 就代表了 microtask