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