補(bǔ)充:JS中其實(shí)是沒(méi)有線程概念的,所謂的單線程也只是相對(duì)于多線程而言。JS的設(shè)計(jì)初衷就沒(méi)有考慮這些,針對(duì)JS這種不具備并行任務(wù)處理的特性,我們稱之為“單線程”。
JS單線程是指一個(gè)瀏覽器進(jìn)程中只有一個(gè)JS的執(zhí)行線程,同一時(shí)刻內(nèi)只會(huì)有一段代碼在執(zhí)行。
舉個(gè)通俗例子,假設(shè)JS支持多線程操作的話,JS可以操作DOM,那么一個(gè)線程在刪除DOM,另外一個(gè)線程就在獲取DOM數(shù)據(jù),這樣子明顯不合理,這算是證明之一。
來(lái)看段代碼??
function foo() { ? ?console.log("first");
setTimeout(( function(){ ? ? ? ?console.log( 'second' );
}),5);
}
for (var i = 0; i < 1000000; i++) {
foo();
}復(fù)制代碼
打印結(jié)果就是首先是很多個(gè)first,然后再是second。
異步機(jī)制是瀏覽器的兩個(gè)或以上常駐線程共同完成的,舉個(gè)例子,比如異步請(qǐng)求由兩個(gè)常駐線程,JS執(zhí)行線程和事件觸發(fā)線程共同完成的。
先給出結(jié)論
CSS
不會(huì)阻塞DOM
解析,但會(huì)阻塞DOM
渲染。CSS
會(huì)阻塞JS執(zhí)行,并不會(huì)阻塞JS文件下載CSS
控制的屬性,瀏覽器是需要計(jì)算的,也就是依賴于CSS
。瀏覽器也無(wú)法感知腳本內(nèi)容到底是什么,為避免樣式獲取,因而只好等前面所有的樣式下載完后,再執(zhí)行JS
。先給出結(jié)論??
由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時(shí)渲染界面(即 JavaScript 線程和 UI 線程同時(shí)運(yùn)行),那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。 因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置?「GUI 渲染線程與 JavaScript 引擎為互斥」的關(guān)系。 當(dāng) JavaScript 引擎執(zhí)行時(shí) GUI 線程會(huì)被掛起,GUI 更新會(huì)被保存在一個(gè)隊(duì)列中等到引擎線程空閑時(shí)立即被執(zhí)行。 當(dāng)瀏覽器在執(zhí)行 JavaScript 程序的時(shí)候,GUI 渲染線程會(huì)被保存在一個(gè)隊(duì)列中,直到 JS 程序執(zhí)行完成,才會(huì)接著執(zhí)行。 因此如果 JS 執(zhí)行的時(shí)間過(guò)長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞的感覺(jué)。
DOMContentLoaded
?事件前執(zhí)行,如果缺少?src
?屬性(即內(nèi)嵌腳本),該屬性不應(yīng)被使用,因?yàn)檫@種情況下它不起作用帶async的腳本一定會(huì)在load事件之前執(zhí)行,可能會(huì)在DOMContentLoaded之前或之后執(zhí)行。
如果 script 標(biāo)簽中包含 defer,那么這一塊腳本將不會(huì)影響 HTML 文檔的解析,而是等到HTML 解析完成后才會(huì)執(zhí)行。而 DOMContentLoaded 只有在 defer 腳本執(zhí)行結(jié)束后才會(huì)被觸發(fā)。
我覺(jué)得這個(gè)題目說(shuō)法上可能就是行不通,不能這么說(shuō),如果了解的話,都知道will-change只是一個(gè)優(yōu)化的手段,使用JS改變transform也可以享受這個(gè)屬性帶來(lái)的變化,所以這個(gè)說(shuō)法上有點(diǎn)不妥。
所以圍繞這個(gè)問(wèn)題展開話,更應(yīng)該說(shuō)建議推薦使用CSS動(dòng)畫,至于為什么呢,涉及的知識(shí)點(diǎn)大概就是重排重繪,合成,這方面的點(diǎn),我在瀏覽器渲染流程中也提及了。
盡可能的避免重排和重繪,具體是哪些操作呢,如果非要去操作JS實(shí)現(xiàn)動(dòng)畫的話,有哪些優(yōu)化的手段呢?
比如??
createDocumentFragment
進(jìn)行批量的 DOM 操作節(jié)流的意思是讓函數(shù)有節(jié)制地執(zhí)行,而不是毫無(wú)節(jié)制的觸發(fā)一次就執(zhí)行一次。什么叫有節(jié)制呢?就是在一段時(shí)間內(nèi),只執(zhí)行一次。
規(guī)定在一個(gè)單位時(shí)間內(nèi),只能觸發(fā)一次函數(shù)。如果這個(gè)單位時(shí)間內(nèi)觸發(fā)多次函數(shù),只有一次生效。
抓取一個(gè)關(guān)鍵的點(diǎn):就是執(zhí)行的時(shí)機(jī)。要做到控制執(zhí)行的時(shí)機(jī),我們可以通過(guò)「一個(gè)開關(guān)」,與定時(shí)器setTimeout結(jié)合完成。
?function throttle(fn, delay) { ? ? ? ? ? ?let flag = true,
timer = null; ? ? ? ? ? ?return function (...args) { ? ? ? ? ? ? ? ?let context = this; ? ? ? ? ? ? ? ?if (!flag) return;
flag = false;
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args);
flag = true;
}, delay);
};
};復(fù)制代碼
在事件被觸發(fā)n秒后再執(zhí)行回調(diào),如果在這n秒內(nèi)又被觸發(fā),則重新計(jì)時(shí)。
核心思想:每次事件觸發(fā)都會(huì)刪除原有定時(shí)器,建立新的定時(shí)器。通俗意思就是反復(fù)觸發(fā)函數(shù),只認(rèn)最后一次,從最后一次開始計(jì)時(shí)。
代碼:
?function debounce(fn, delay) { ? ? ? ? ? ?let timer = null
return function (...args) { ? ? ? ? ? ? ? ?let context = this
if(timer) ? clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(context, args)
},delay)
}
}復(fù)制代碼
_.debounce
?和?_.throttle
?方法,可以使用 Lodash 的自定義構(gòu)建工具,生成一個(gè) 2KB 的壓縮庫(kù)。使用以下的簡(jiǎn)單命令即可:npm i -g lodash-cli
npm i -g lodash-clilodash-cli include=debounce,throttle復(fù)制代碼
_.debounce
?方法:// 錯(cuò)誤$(window).on('scroll', function() {
_.debounce(doSomething, 300);
});// 正確$(window).on('scroll', _.debounce(doSomething, 200));復(fù)制代碼
debounced_version.cancel()
,lodash 和 underscore.js 都有效。let debounced_version = _.debounce(doSomething, 200);
$(window).on(‘scroll’, debounced_version);// 如果需要的話debounced_version.cancel();復(fù)制代碼防抖
節(jié)流
動(dòng)畫幀率可以作為衡量標(biāo)準(zhǔn),一般來(lái)說(shuō)畫面在 60fps 的幀率下效果比較好。
換算一下就是,每一幀要在 16.7ms (16.7 = 1000/60) 內(nèi)完成渲染。
我們來(lái)看看MDN對(duì)它的解釋吧??
window.requestAnimationFrame() 方法告訴瀏覽器您希望執(zhí)行動(dòng)畫并請(qǐng)求瀏覽器在下一次重繪之前調(diào)用指定的函數(shù)來(lái)更新動(dòng)畫。該方法使用一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)會(huì)在瀏覽器重繪之前調(diào)用。— MDN
當(dāng)我們調(diào)用這個(gè)函數(shù)的時(shí)候,我們告訴它需要做兩件事:
rAF(requestAnimationFrame) 最大的優(yōu)勢(shì)是「由系統(tǒng)來(lái)決定回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)」。
具體一點(diǎn)講就是,系統(tǒng)每次繪制之前會(huì)主動(dòng)調(diào)用 rAF 中的回調(diào)函數(shù),如果系統(tǒng)繪制率是 60Hz,那么回調(diào)函數(shù)就每16.7ms 被執(zhí)行一次,如果繪制頻率是75Hz,那么這個(gè)間隔時(shí)間就變成了 1000/75=13.3ms。
換句話說(shuō)就是,rAF 的執(zhí)行步伐跟著系統(tǒng)的繪制頻率走。它能保證回調(diào)函數(shù)在屏幕每一次的繪制間隔中只被執(zhí)行一次(上一個(gè)知識(shí)點(diǎn)剛剛梳理完「函數(shù)節(jié)流」),這樣就不會(huì)引起丟幀現(xiàn)象,也不會(huì)導(dǎo)致動(dòng)畫出現(xiàn)卡頓的問(wèn)題。
另外它可以自動(dòng)調(diào)節(jié)頻率。如果callback工作太多無(wú)法在一幀內(nèi)完成會(huì)自動(dòng)降低為30fps。雖然降低了,但總比掉幀好。
與setTimeout動(dòng)畫對(duì)比的話,有以下幾點(diǎn)優(yōu)勢(shì)
規(guī)范中似乎是這么去定義的:
這樣子分析的話,似乎很合理嘛,為什么要在重新渲染前去調(diào)用呢?因?yàn)閞AF作為官方推薦的一種做流暢動(dòng)畫所應(yīng)該使用的API,做動(dòng)畫不可避免的去操作DOM,而如果是在渲染后去修改DOM的話,那就只能等到下一輪渲染機(jī)會(huì)的時(shí)候才能去繪制出來(lái)了,這樣子似乎不合理。
rAF
在瀏覽器決定渲染之前給你最后一個(gè)機(jī)會(huì)去改變 DOM 屬性,然后很快在接下來(lái)的繪制中幫你呈現(xiàn)出來(lái),所以這是做流暢動(dòng)畫的不二選擇。
至于宏任務(wù),微任務(wù),這可以說(shuō)起來(lái)就要展開篇幅了,暫時(shí)不在這里梳理了。
跟?_.throttle(dosomething, 16)
?等價(jià)。它是高保真的,如果追求更好的精確度的話,可以用瀏覽器原生的 API 。
可以使用 rAF API 替換 throttle 方法,考慮一下優(yōu)缺點(diǎn):
優(yōu)點(diǎn)
缺點(diǎn)
根據(jù)經(jīng)驗(yàn),如果 JavaScript 方法需要繪制或者直接改變屬性,我會(huì)選擇?requestAnimationFrame
,只要涉及到重新計(jì)算元素位置,就可以使用它。
涉及到 AJAX 請(qǐng)求,添加/移除 class (可以觸發(fā) CSS 動(dòng)畫),我會(huì)選擇?_.debounce
?或者?_.throttle
?,可以設(shè)置更低的執(zhí)行頻率(例子中的200ms 換成16ms)。