原文連接javascript
對於瀏覽器而言,有多個線程協同合做,以下圖。具體細節能夠參考一幀剖析。java
對於常說的JS單線程引擎也就是指的 Main Therad
。node
注意以上主線程的每一塊未必都會執行,須要看實際狀況。 先把 Parse HTML
-> Composite
的過程稱爲渲染管道流 Rendering pipeline
。git
瀏覽器內部有一個不停的輪詢機制,檢查任務隊列中是否有任務,有的話就取出交給 JS引擎
去執行。github
例如:web
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");
bar();
foo();
baz();
複製代碼
過程:segmentfault
一些常見的 webapi
會產生一個 task
送入到任務隊列中。api
script
標籤XHR
、addEventListener
等事件回調setTimeout
定時器每一個 task
執行在一個輪詢中,有本身的上下文環境互不影響。也就是爲何,script
標籤內的代碼崩潰了,不影響接下來的 script
代碼執行。promise
pop
,便於 JSer
的世界觀改用 shift
)while(true) {
task = taskQueue.shift();
execute(task);
}
複製代碼
input event
、 setTimeout
的 callback
可能維護在不一樣的隊列中。 代碼若是操做 DOM
,主線程還會執行渲染管道流。僞代碼修改以下:while(true) {
+ queue = getNextQueue();
- task = taskQueue.shift();
+ task = queue.shift();
execute(task);
+ if(isRepaintTime()) repaint();
}
複製代碼
button.addEventListener('click', e => {
while(true);
});
複製代碼
點擊 button
產生一個 task
,當執行該任務時,一直佔用主線程卡死,該任務沒法退出,致使沒法響應用戶交互或渲染動態圖等。瀏覽器
改換執行如下代碼
function loop() {
setTimeout(loop, 0);
}
loop();
複製代碼
看似無限循環執行 loop
,setTimeout
到時後產生一個 task
。執行完 loop
即退出主線程。使得用戶交互事件和渲染可以得以執行。
正由於如此,setTimeout
和其餘 webapi
產生的 task
執行依賴任務隊列中的順序。 即便任務隊列沒有其餘任務,也不能作到 0秒
運行,setTimeout
定時器到時間 cb
入任務隊列,在輪詢取出 task
給引擎執行,最少大約 4.7ms
。
function callback() {
moveBoxForwardOnePixel();
requestAnimationFrame(callback);
}
callback()
複製代碼
換成 setTimeout
function callback() {
moveBoxForwardOnePixel();
- requestAnimationFrame(callback);
+ setTimeout(callback, 0);
}
callback()
複製代碼
對比,能夠發現 setTimeout
移動明顯比 rAF
移動快不少(3.5倍左右)。 意味着 setTimeout
回調過於頻繁,這並非一件好事。
渲染管道流不必定發生在每一個 setTimeout
產生的 task
之間,也可能發生在多個 setTimeout
回調以後。 由瀏覽器決定什麼時候渲染而且儘量高效,只有值得更新纔會渲染,若是沒有就不會。
若是瀏覽器運行在後臺,沒有顯示,瀏覽器就不會渲染,由於沒有意義。大多數狀況下頁面會以固定頻率刷新, 保證 60FPS
人眼就感受很流暢,也就是一幀大約 16ms
。頻率高,人眼看不見無心義,低於人眼能發現卡頓。
在主線程很空閒時,setTimeout
回調能每 4ms
左右執行一次,留 2ms
給渲染管道流,setTimeout
一幀內能執行大概 3.5次
。 3.5ms * 4 + 2ms = 16ms
。
setTimeout
調用次數太多 3-4次
,多於用戶可以看到的,也多於瀏覽器可以顯示的,大約3/4是浪費的。 不少老的動畫庫,用 setTimeout(animFrame, 1000 / 60)
來優化。
但 setTimeout
並非爲動畫而生,執行不穩定,會產生飄移或任務太重會推遲渲染管道流。
requestAnimationFrame
正是用來解決這些問題的,使一切整潔有序,每一幀都按時發生。
推薦使用 requestAnimationFrame
包裹動畫工做提升性能。它解決這個 setTimeout
不肯定性與性能浪費的問題,由瀏覽器來保證在渲染管道流以前執行。
0px
移動到 1000px
處,再到 500px
處嗎?button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
box.style.transform = 'translateX(500px)';
});
複製代碼
結果:從 0px
移動到 500px
處。因爲回調任務的代碼塊是同步執行的,瀏覽器不在意中間態。
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(500px)';
+ });
});
複製代碼
結果:依然從 0px
移動到 500px
處。
這是由於在 addEventListener
的 task
中同步代碼修改成 1000px
。 在渲染管道流中的計算樣式執行以前,須要執行 rAF
,最終的樣式爲 500px
。
500px
。button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
requestAnimationFrame(() => {
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(500px)';
+ });
});
});
複製代碼
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
+ getComputedStyle(box).transform;
box.style.transform = 'translateX(500px)';
});
複製代碼
getComputedStyle
會致使強制重排,渲染管道流提早執行,多餘操做損耗性能。
Edge
和 Safari
的 rAF
不符合規範,錯誤的放在渲染管道流以後執行。
DOMNodeInserted
初衷被設計用來監聽 DOM
的改變。
DOMNodeInserted
。document.body.addEventListener('DOMNodeInserted', () => {
console.log('Stuff added to <body>!');
});
for(let i = 0; i < 100; i++) {
const span = document.createElement('span');
document.body.appendChild(span);
span.textContent = 'hello';
}
複製代碼
理想 for 循環完畢後,DOMNodeInserted
回調執行一次。 結果:執行了 200
次。添加 span
觸發 100
次,設置 textContent
觸發 100
。 這就讓使用 DOMNodeInserted
會產生極差的性能負擔。 爲了解決此等問題,建立了一個新的任務隊列叫作微任務 Microtasks
。
常見微任務
微任務是在一次事件輪詢中取出的 task
執行完畢,即 JavaScript
運行棧(stack)中已經沒有可執行的內容了。 瀏覽器緊接着取出微任務隊列中全部的 microtasks
來執行。
loop
會怎樣?function loop() {
Promise.resolve().then(loop);
}
loop();
複製代碼
你會發現,它跟以前的 while
同樣卡死。
如今咱們有了3個不一樣性質的隊列
task
執行,若是產生new task
入隊列。task
執行完畢等待下一次輪詢取出next task
。microtask
,若是產生new microtask
,入隊列,等待執行,直到隊列清空。while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
+ while(microtaskQueue.hasTasks()) {
+ doMicrotask();
+ }
if(isRepaintTime()) repaint();
}
複製代碼
rAF queue
每一幀渲染管道流開始以前一次性執行完全部隊列中的 rAF callback
,若是產生new rAF
等待下一幀執行。while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
while(microtaskQueue.hasTasks()) {
doMicrotask();
}
- if(isRepaintTime()) repaint();
+ if(isRepaintTime()) {
+ animationTasks = animationQueue.copyTasks();
+ for(task in animationTasks) {
+ doAnimationTask(task);
+ }
+
+ repaint();
+ }
}
複製代碼
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 1'));
console.log('Listener 1');
});
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 2'));
console.log('Listener 2');
});
複製代碼
點擊按鈕會是怎麼樣的順序呢?
來分析一下,以上代碼塊爲一個 task 0
。
task 0
執行完畢後,webapi
監聽事件。click
事件,task queue
中入隊 task 1
、task 2
。task 1
執行,Microtask queue
入隊 Microtask 1
。 console
輸出 Listener 1
。task 1
執行完畢。microtask
(目前只有 Microtask 1
),取出執行,console 輸出 Microtask 1
。task 2
執行,Microtask queue
入隊 Microtask 2
。 console
輸出 Listener 2
。task 2
執行完畢。microtask
,取出 Microtask 2
執行,console 輸出 Microtask 2
。答案:Listener 1
-> Microtask 1
-> Listener 2
-> Microtask 2
若是你答對了,那麼恭喜你,超越了 87%
的答題者。
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 1'));
console.log('Listener 1');
});
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 2'));
console.log('Listener 2');
});
+ button.click();
複製代碼
思路同樣分析
task 0
執行到 button.click()
等待事件回調執行完畢。Listener 1
,Microtask queue
入隊 Microtask 1
。console
輸出 Listener 1
。Listener 2
,Microtask queue
入隊 Microtask 2
。console
輸出 Listener 2
。click
函數 return
,結束 task 0
。microtask
,取出 Microtask 1
執行,console 輸出 Microtask 1
。Microtask 2
執行,console 輸出 Microtask 2
。答案:Listener 1
-> Listener 2
-> Microtask 1
-> Microtask 2
在作自動化測試時,須要當心,有時會產生和用戶交互不同的結果。
如下代碼,用戶點擊,會阻止a
連接跳轉嗎?
const nextClick = new Promise(resolve => {
link.addEventListener('click', resolve, { once: true });
});
nextClick.then(event => {
event.preventDefault();
// handle event
});
複製代碼
若是是代碼點擊呢?
link.click();
複製代碼
暫不揭曉答案,歡迎評論區討論。
rAF
callback
node 不須要一直輪詢有沒有任務,清空全部隊列就結束。
常見任務隊列 task queue
常見微任務 microtask queue
process.nextTick
執行優先級高於 Promise
。
while(tasksAreWaiting()) {
queue = getNextQueue();
while(queue.hasTasks()) {
task = queue.shift();
execute(task);
while(nextTickQueue.hasTasks()) {
doNextTickTask();
}
while(promiseQueue.hasTasks()) {
doPromiseTask();
}
}
}
複製代碼
script tag
DOM
相似 node