JS時間分片技術解決長任務導致的頁面卡頓
起因
同事遇到一個動畫展示的問題,就是下面要執行一個運算量很大的函數,他要加載一個 loading,但他發現把 loading 的元素 display: block; 頁面中也不會立刻出現 loading 動畫,出現動畫的時候是運算函數執行完畢之後。
處理辦法
有兩種方法去處理這種耗時任務,第一種就是 webWorker,但是一些 dom 的操作做不瞭,於是就想到瞭通過 generator 函數來解決,下面先簡單瞭解下事件循環。
事件循環
微任務:
1. Promise.then
2. Object.observe
3. MutaionObserver
宏任務:
1. script(整體代碼)
2. setTimeout
3. setInterval
4. I/O
5. postMessage
6. MessageChannel
瀏覽器渲染時機
除去特殊情況,頁面的渲染會在微任務隊列清空後,宏任務執行前,所以我們可以讓推入主執行棧的函數執行到一定時間就去休眠,然後在渲染之後的宏任務裡面叫醒他,這樣渲染或者用戶交互都不會卡頓瞭!
原始代碼
我們先模擬一個 js 長任務
代碼
// style @keyframes move { from { left: 0; } to { left: 100%; } } .move { position: absolute; animation: move 5s linear infinite; } // dom <div class="move">123123123</div> // script function fnc () { let i = 0 const start = performance.now() while (performance.now() - start <= 5000) { i++ } return i } setTimeout(() => { fnc() }, 1000)
效果
如下圖,動畫運行 1s 的時候,js 函數開始運行,動畫會先停止渲染,然後等 js 主執行棧空閑之後動畫才繼續進行。
函數改造
我們把原來的函數改造為 generator 函數
代碼
// generator 處理原來的函數 function * fnc_ () { let i = 0 const start = performance.now() while (performance.now() - start <= 5000) { yield i++ } return i } // 簡易時間分片 function timeSlice (fnc, cb = setTimeout) { if(fnc.constructor.name !== 'GeneratorFunction') return fnc() return async function (...args) { const fnc_ = fnc(...args) let data do { data = fnc_.next(await data?.value) // 每執行一步就休眠,註冊一個宏任務 setTimeout 來叫醒他 await new Promise( resolve => cb(resolve)) } while (!data.done) return data.value } } setTimeout(async () => { const fnc = timeSlice(fnc_) const start = performance.now() console.log('開始') const num = await fnc() console.log('結束', `${(performance.now() - start)/ 1000}s`) console.log(num) }, 1000)
效果
動畫根本不受影響,fps 一直很穩定,因為我們把耗時任務拆成很多個塊來執行。
優化時間分片
上面的時間分片函數每執行一步,就會休眠,然後通過一個宏任務來喚醒他,但是這樣的執行效率肯定是比較低的,我們再優化一下執行的效率,提升連續執行時間。
代碼
// 精準時間分片 function timeSlice_ (fnc, time = 25, cb = setTimeout) { if(fnc.constructor.name !== 'GeneratorFunction') return fnc() return function (...args) { const fnc_ = fnc(...args) let data return new Promise(async function go (resolve, reject) { try { const start = performance.now() do { data = fnc_.next(await data?.value) } while (!data.done && performance.now() - start < time) if (data.done) return resolve(data.value) cb(() => go(resolve, reject)) } catch(e) { reject(e) } }) } } setTimeout(async () => { const fnc1 = timeSlice_(fnc_) let start = performance.now() console.log('開始') const num = await fnc1() console.log('結束', `${(performance.now() - start)/ 1000}s`) console.log(num) }, 1000);
效果
我們把函數分成瞭較大的塊,這樣函數執行的效率就會變高,fps 會稍微收到影響,但是在接受范圍內。
對比優化前後
我們對比一下優化時間分片函數前後的效果
代碼
setTimeout(async () => { const fnc = timeSlice(fnc_) const fnc1 = timeSlice_(fnc_) let start = performance.now() console.log('開始') const a = await fnc() console.log('結束', `${(performance.now() - start)/ 1000}s`) console.log('開始') start = performance.now() const b = await fnc1() console.log('結束', `${(performance.now() - start)/ 1000}s`) console.log(a, b) }, 1000);
效果
對比優化後的時間分片函數,是之前效率的 4452 倍,我們做的隻是提升瞭函數連續執行時間。
最後
generator 函數中 yield 的位置非常關鍵,需要放到耗時的地方,優化後的時間分片函數也提供瞭 time 變量,你可以根據實際情況來改變你的 time 值。
以上就是JS時間分片技術解決長任務導致的頁面卡頓的詳細內容,更多關於js時間分片長任務分解的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- vue中Promise的使用方法詳情
- Vue中的同步調用和異步調用方式
- es7中的async、await使用方法示例詳解
- JS promise 的回調和 setTimeout 的回調到底誰先執行
- 一篇文章帶你瞭解vue.js的事件循環機制