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