摘要:所以回來(lái)后就想著補(bǔ)一篇文章針對(duì)時(shí)間切片展開詳細(xì)的討論。所以時(shí)間切片的目的是不阻塞主線程,而實(shí)現(xiàn)目的的技術(shù)手段是將一個(gè)長(zhǎng)任務(wù)拆分成很多個(gè)不超過(guò)的小任務(wù)分散在宏任務(wù)隊(duì)列中執(zhí)行。
上周我在FDConf的分享《讓你的網(wǎng)頁(yè)更絲滑》中提到了“時(shí)間切片”,由于時(shí)間關(guān)系當(dāng)時(shí)并沒(méi)有對(duì)時(shí)間切片展開更細(xì)致的討論。所以回來(lái)后就想著補(bǔ)一篇文章針對(duì)“時(shí)間切片”展開詳細(xì)的討論。
從用戶的輸入,再到顯示器在視覺(jué)上給用戶的輸出,這一過(guò)程如果超過(guò)100ms,那么用戶會(huì)察覺(jué)到網(wǎng)頁(yè)的卡頓,所以為了解決這個(gè)問(wèn)題,每個(gè)任務(wù)不能超過(guò)50ms,W3C性能工作組在LongTask規(guī)范中也將超過(guò)50ms的任務(wù)定義為長(zhǎng)任務(wù)。
關(guān)于這50毫秒我在FDConf的分享中進(jìn)行了很詳細(xì)的講解,沒(méi)有聽到的小伙伴也不用著急,后續(xù)我會(huì)針對(duì)這次分享的內(nèi)容補(bǔ)一篇文章。
在線PPT地址:ppt.baomitu.com/d/b267a4a3
所以為了避免長(zhǎng)任務(wù),一種方案是使用Web Worker,將長(zhǎng)任務(wù)放在Worker線程中執(zhí)行,缺點(diǎn)是無(wú)法訪問(wèn)DOM,而另一種方案是使用時(shí)間切片。
什么是時(shí)間切片
時(shí)間切片的核心思想是:如果任務(wù)不能在50毫秒內(nèi)執(zhí)行完,那么為了不阻塞主線程,這個(gè)任務(wù)應(yīng)該讓出主線程的控制權(quán),使瀏覽器可以處理其他任務(wù)。讓出控制權(quán)意味著停止執(zhí)行當(dāng)前任務(wù),讓瀏覽器去執(zhí)行其他任務(wù),隨后再回來(lái)繼續(xù)執(zhí)行沒(méi)有執(zhí)行完的任務(wù)。
所以時(shí)間切片的目的是不阻塞主線程,而實(shí)現(xiàn)目的的技術(shù)手段是將一個(gè)長(zhǎng)任務(wù)拆分成很多個(gè)不超過(guò)50ms的小任務(wù)分散在宏任務(wù)隊(duì)列中執(zhí)行。
上圖可以看到主線程中有一個(gè)長(zhǎng)任務(wù),這個(gè)任務(wù)會(huì)阻塞主線程。使用時(shí)間切片將它切割成很多個(gè)小任務(wù)后,如下圖所示。
可以看到現(xiàn)在的主線程有很多密密麻麻的小任務(wù),我們將它放大后如下圖所示。
可以看到每個(gè)小任務(wù)中間是有空隙的,代表著任務(wù)執(zhí)行了一小段時(shí)間后,將讓出主線程的控制權(quán),讓瀏覽器執(zhí)行其他的任務(wù)。
使用時(shí)間切片的缺點(diǎn)是,任務(wù)運(yùn)行的總時(shí)間變長(zhǎng)了,這是因?yàn)樗刻幚硗暌粋€(gè)小任務(wù)后,主線程會(huì)空閑出來(lái),并且在下一個(gè)小任務(wù)開始處理之前有一小段延遲。
但是為了避免卡死瀏覽器,這種取舍是很有必要的。
如何使用時(shí)間切片
時(shí)間切片是一種概念,也可以理解為一種技術(shù)方案,它不是某個(gè)API的名字,也不是某個(gè)工具的名字。
事實(shí)上,時(shí)間切片充分利用了“異步”,在早期,可以使用定時(shí)器來(lái)實(shí)現(xiàn),例如:
btn.onclick = function () { someThing(); // 執(zhí)行了50毫秒 setTimeout(function () { otherThing(); // 執(zhí)行了50毫秒 }); };
上面代碼當(dāng)按鈕被點(diǎn)擊時(shí),本應(yīng)執(zhí)行100毫秒的任務(wù)現(xiàn)在被拆分成了兩個(gè)50毫秒的任務(wù)。
在實(shí)際應(yīng)用中,我們可以進(jìn)行一些封裝,封裝后的使用效果類似下面這樣:
btn.onclick = ts([someThing, otherThing], function () { console.log("done~"); });
當(dāng)然,關(guān)于ts這個(gè)函數(shù)的API的設(shè)計(jì)并不是本文的重點(diǎn),這里想說(shuō)明的是,在早期可以利用定時(shí)器來(lái)實(shí)現(xiàn)“時(shí)間切片”。
ES6帶來(lái)了迭代器的概念,并提供了生成器Generator函數(shù)用來(lái)生成迭代器對(duì)象,雖然Generator函數(shù)最正統(tǒng)的用法是生成迭代器對(duì)象,但這不妨我們利用它的特性做一些其他的事情。
Generator函數(shù)提供了yield關(guān)鍵字,這個(gè)關(guān)鍵字可以讓函數(shù)暫停執(zhí)行。然后通過(guò)迭代器對(duì)象的next方法讓函數(shù)繼續(xù)執(zhí)行。
對(duì)Generator函數(shù)不熟悉的同學(xué),需要先學(xué)習(xí)Generator函數(shù)的用法。
利用這個(gè)特性,我們可以設(shè)計(jì)出更方便使用的時(shí)間切片,例如:
btn.onclick = ts(function* () { someThing(); // 執(zhí)行了50毫秒 yield; otherThing(); // 執(zhí)行了50毫秒 });
可以看到,我們只需要使用yield這個(gè)關(guān)鍵字就可以將本應(yīng)執(zhí)行100毫秒的任務(wù)拆分成了兩個(gè)50毫秒的任務(wù)。
我們甚至可以將yield關(guān)鍵字放在循環(huán)里:
btn.onclick = ts(function* () { while (true) { someThing(); // 執(zhí)行了50毫秒 yield; } });
上面代碼我們寫了一個(gè)死循環(huán),但依然不會(huì)阻塞主線程,瀏覽器也不會(huì)卡死。
基于生成器的ts實(shí)現(xiàn)原理
通過(guò)前面的例子,我們會(huì)發(fā)現(xiàn)基于Generator的時(shí)間切片非常好用,但其實(shí)ts函數(shù)的實(shí)現(xiàn)原理非常簡(jiǎn)單,一個(gè)最簡(jiǎn)單的ts函數(shù)只需要九行代碼。
function ts (gen) { if (typeof gen === "function") gen = gen() if (!gen || typeof gen.next !== "function") return return function next() { const res = gen.next() if (res.done) return setTimeout(next) } }
代碼雖然全部只有9行,關(guān)鍵代碼只有3、4行,但這幾行代碼充分利用了事件循環(huán)機(jī)制以及Generator函數(shù)的特性。
創(chuàng)造出這樣的代碼我還是很開心的。
上面代碼核心思想是:通過(guò)yield關(guān)鍵字可以將任務(wù)暫停執(zhí)行,從而讓出主線程的控制權(quán);通過(guò)定時(shí)器可以將“未完成的任務(wù)”重新放在任務(wù)隊(duì)列中繼續(xù)執(zhí)行。
避免把任務(wù)分解的過(guò)于零碎
使用yield來(lái)切割任務(wù)非常方便,但如果切割的粒度特別細(xì),反而效率不高。假設(shè)我們的任務(wù)執(zhí)行100ms,最好的方式是切割成兩個(gè)執(zhí)行50ms的任務(wù),而不是切割成100個(gè)執(zhí)行1ms的任務(wù)。假設(shè)被切割的任務(wù)之間的間隔為4ms,那么切割成100個(gè)執(zhí)行1ms的任務(wù)的總執(zhí)行時(shí)間為:
(1 + 4) * 100 = 500ms
如果切割成兩個(gè)執(zhí)行時(shí)間為50ms的任務(wù),那么總執(zhí)行時(shí)間為:
(50 + 4) * 2 = 108ms
可以看到,在不影響用戶體驗(yàn)的情況下,下面的總執(zhí)行時(shí)間要比前面的少了4.6倍。
保證切割的任務(wù)剛好接近50ms,可以在用戶使用yield時(shí)自行評(píng)估,也可以在ts函數(shù)中根據(jù)任務(wù)的執(zhí)行時(shí)間判斷是否應(yīng)該一次性執(zhí)行多個(gè)任務(wù)。
我們將ts函數(shù)稍微改進(jìn)一下:
function ts (gen) { if (typeof gen === "function") gen = gen() if (!gen || typeof gen.next !== "function") return return function next() { const start = performance.now() let res = null do { res = gen.next() } while(!res.done && performance.now() - start < 25); if (res.done) return setTimeout(next) } }
現(xiàn)在我們測(cè)試下:
ts(function* () { const start = performance.now() while (performance.now() - start < 1000) { console.log(11) yield } console.log("done!") })();
這段代碼在之前的版本中,在我的電腦上可以打印出 215 次 11,在后面的版本中可以打印出 6300 次 11,說(shuō)明在總時(shí)間相同的情況下,可以執(zhí)行更多的任務(wù)。
再看另一個(gè)例子:
ts(function* () { for (let i = 0; i < 10000; i++) { console.log(11) yield } console.log("done!") })();
在我的電腦上,這段代碼在之前的版本中,被切割成一萬(wàn)個(gè)小任務(wù),總執(zhí)行時(shí)間為 46秒,在之后的版本中,被切割成 52 個(gè)小任務(wù),總執(zhí)行時(shí)間為 1.5秒。
總結(jié)
我將時(shí)間切片的代碼放在了我的Github上,感興趣的可以參觀下:github.com/berwin/time…
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/6661.html
摘要:第一章數(shù)據(jù)類型隱式方法利用快速生成類方法方法通過(guò)下標(biāo)找元素自動(dòng)支持切片操作可迭代方法與如果是一個(gè)自定義類的對(duì)象,那么會(huì)自己去調(diào)用其中由你實(shí)現(xiàn)的方法。若返回,則會(huì)返回否則返回。一個(gè)對(duì)象沒(méi)有函數(shù),解釋器會(huì)用作為替代。 第一章 python數(shù)據(jù)類型 1 隱式方法 利用collections.namedtuple 快速生成類 import collections Card = collec...
摘要:第一章數(shù)據(jù)類型隱式方法利用快速生成字典方法方法通過(guò)下標(biāo)找元素自動(dòng)支持切片操作可迭代方法與如果是一個(gè)自定義類的對(duì)象,那么會(huì)自己去調(diào)用其中由你實(shí)現(xiàn)的方法。若返回,則會(huì)返回否則返回。一個(gè)對(duì)象沒(méi)有函數(shù),解釋器會(huì)用作為替代。 第一章 python數(shù)據(jù)類型 1 隱式方法 利用collections.namedtuple 快速生成字典 import collections Card = coll...
摘要:如下圖所示渲染性能保證主動(dòng)交互讓用戶感覺(jué)流暢一般超過(guò)認(rèn)為是長(zhǎng)任務(wù)會(huì)阻塞的運(yùn)行如下是兩種解決方案。下面是另外一種使頁(yè)面流暢的方法時(shí)間分片。 流暢性 本篇是基于 FDCon2019 上《讓你的網(wǎng)頁(yè)更絲滑by劉博文》的復(fù)盤文。該課題也是博主感興趣的領(lǐng)域, 后續(xù)會(huì)結(jié)合 React 的 Schedule 與該文進(jìn)行進(jìn)一步整合, 個(gè)人博客 被動(dòng)交互: animation 主動(dòng)交互: 鼠標(biāo)、鍵盤 ...
摘要:計(jì)算列表所有元素的和,其元素類型必須是數(shù)值型的整數(shù)浮點(diǎn)數(shù)返回一個(gè)排序的列表,但并不改變?cè)斜?。只有列表所有元素為才返回。列表的?nèi)置方法前面我們說(shuō)的是語(yǔ)言的內(nèi)置函數(shù),這里我們講的是列表本身的內(nèi)置方法。 Python的基本數(shù)據(jù)類型有整數(shù),浮點(diǎn)數(shù),布爾,字符串,它們是最基本的數(shù)據(jù)。在實(shí)際編程中,我們要經(jīng)常組織由很多基本數(shù)據(jù)組成的集合,這些集合的不同組織方式就是:數(shù)據(jù)結(jié)構(gòu),今天講的是數(shù)據(jù)結(jié)構(gòu)中...
閱讀 3512·2023-04-26 02:41
閱讀 2646·2023-04-26 00:14
閱讀 3109·2021-08-11 10:22
閱讀 1448·2019-12-27 11:38
閱讀 3668·2019-08-29 18:34
閱讀 2466·2019-08-29 12:13
閱讀 3036·2019-08-26 18:26
閱讀 2087·2019-08-26 16:49