經過定時器、時間分片、Web Worker優化長任務

提示

但願你能瞭解什麼是 Event Loop(事件循環),以及對 Web Worker 有所瞭解,以便更容易吸取javascript

什麼是長任務

W3C 性能組規定:執行時長大於 50ms 的任務,定義爲長任務html

那麼咱們如何對長任務進行優化?前端

setTimeout

js 是單線程語言,它的做用主要用於操做DOM。 js執行也很是簡單(從上往下執行),但 js 裏也有異步方法,好比 xhr 、 setTimeout 等等。java

setTimeout 的做用是:將當前任務推入任務隊列,當主線程同步的代碼執行完成後判斷 setTimeout 設置的時間有沒有到,若是到了將任務推出隊列執行。git

下面是 Event Loop 示例圖:github

因爲長任務執行時間長,會阻塞主線程,用戶能感受到頁面卡頓,因此咱們常常會採用 setTimeout 把長任務推入任務隊列,等到同步代碼執行完成後再處理長任務:web

const t = setTimeout(() =>{
    clearTimeout(t);
    // 長任務
},0)
複製代碼

注意點

因爲前端處理長任務的場景並很少,通常由服務端處理完後給到前端,因此 setTimeout 能解決大部分的長任務問題,可是該方法不能用於時間過長的任務,好比一個任務須要秒級時長,用戶仍然會感受頁面卡死。api

下面這個場景也會致使卡頓:瀏覽器

#box {
        width: 100px;
        height: 100px;
        background: green;
      }
      
<div id="box"></div>

// 動畫 大概須要2s跑完
var start = null;
var element = document.getElementById('box');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) { window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); setTimeout(function() { // 500ms的長任務 var now = performance.now(); while (now + 500 > performance.now()) {}
}, 100);
複製代碼

上面代碼中使用 setTimeout 來處理長任務,可是頁面中有個須要 2s 跑完的動畫效果,當執行到 setTimeout 時,動畫仍在運行,這時在隊列裏的長任務被推出並執行致使主線程阻塞,動畫出現卡頓。bash

使用時間分片

時間分片並非某個 api,而是一種技術方案,它能夠把長任務分割成若干個小任務執行,並在執行小任務的間隔中把主線程的控制權讓出來,這樣就不會致使UI卡頓。

React 的 Fiber 技術核心思想也是時間分片,Vue 2.x 也用了時間分片,只不過是以組件爲單位來實施分片操做,因爲收益不高 Vue 3 把時間分片移除了。

演示

爲了好理解,先寫段長任務代碼,將主線程阻塞 1秒鐘:

const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {}
console.log('done!');

複製代碼

該段腳本霸佔主線產長達 1s 的時間 咱們能夠封裝一個 ts 方法,讓這個長任務被分割成多個小任務執行:

ts(function* (){
    const start = performance.now();
    let count = 0;
    while (performance.now() - start < 1000) {
        yield;
    }
    console.log('done!');
})()
複製代碼

先看看效果吧:

從圖裏看到,一個長任務被切成了諾幹個小任務,在每一個小任務間隔中把主線程的控制權交出來,這樣就不會致使頁面卡頓

基於 Generator 函數實現時間分片方法

基於 Generator 函數的執行特性,咱們很容易使用它來實現一個時間分片函數:

function ts(gen) {
  if (typeof gen === 'function') gen = gen();
  if (!gen || typeof gen.next !== 'function') return;
  return function next() {
    const start = performance.now();
    const res = null;
    do {
      res = gen.next();
    } while (!res.done && performance.now() - start < 25);
    if (res.done) return;
    setTimeout(next);
  };
}
複製代碼

上面代碼中,作了一個 do while:若是當前任務執行時間低於 25ms 則多個任務一塊兒執行,不然做爲一個任務執行,一直到 res.done = true 爲止。

代碼核心思想:經過 yield 關鍵字能夠將任務暫停執行,並讓出主線程的控制權;經過setTimeout將未完成的任務從新放在任務隊列中執行

使用 Web Worker 優化

文章開頭提過 js 是單線程,但瀏覽器不是。 Web Worker 容許咱們在後臺建立獨立於主線程的其餘線程,因此咱們能夠把一些費時費力的長任務交給 Web Worker。

使用

因爲共享線程瀏覽器支持狀況較差,本章咱們只介紹專用線程。

咱們建立一個文件夾,並在裏面建立 index.html 和 worker.js 目錄以下:

.
├── index.html
└── worker.js
複製代碼

index.html 代碼:

<input type="text" id="ipt" value="" />
<div id="result"></div>

<script> const ipt = document.querySelector('#ipt'); const worker = new Worker('worker.js'); ipt.onchange = function() { // 經過postMessage發送消息 worker.postMessage({ number: this.value }); }; // 經過onmessage接收消息 worker.onmessage = function(e) { document.querySelector('#result').innerHTML = e.data; }; </script>
複製代碼

worker.js 代碼:

// self 相似主線程中的 window
self.onmessage = function(e) {
  self.postMessage(e.data.number * 2);
};
複製代碼

總結

  • 對於大多數場景 setTimeout 夠用,但要注意使用場景
  • 須要更長時間來執行的任務,能夠使用時間分片或者交給 Web Worker 來解決

參考

相關文章
相關標籤/搜索