性能優化小冊 - React 搜索優化:防抖、緩存、LRU

最近要對 react 項目作重構優化等相關的工做,因爲有好長時間沒碰 React 了,今天索性把一個基於關鍵字搜索的 demo 作一下簡單優化,在此記錄如下。html

主要從三個方面進行優化處理:react

    1. 減小事件的觸發頻率 - 對關鍵字鍵入進行 debounce 處理
    1. 減小 HTTP 請求 - 對重複的 HTTP 請求進行緩存攔截
    1. 緩存淘汰策略 - 使用 LRU 優化緩存

減小事件的觸發頻率 - debounce

debounce 旨在時間段內控制事件只在最後一次操做觸發。webpack

debounce 原理:是維護一個計時器,在規定的 delay 時間後觸發函數,在 delay 時間內再次觸發的話,就會取消以前的計時器而從新設置。這樣一來,只有最後一次操做能被觸發。ios

下面是 react 中 debounce 優化的代碼:web

...
handler = e => {
  let val = e.target.value;
  if(val) { 
    this.search(val);
  }
  this.setState(() => ({
    value: e.target.value
  }))
}

debounce = (fn, delay) => {
  let timer = null;
  return function(event) {
    timer && clearTimeout(timer);
    event.persist && event.persist() // 保留引用,已備異步階段訪問
    timer = setTimeout(() => {
      fn.call(this, event)
    }, delay) 
  }
}

onChangeHandler = this.debounce(this.handler, 1000)
...
render() {
  return (
    <div>
      <input
        // 這裏不能設置成 value
        defaulValue={this.state.value}
        onChange={e => this.onChangeHandler(e)}
        placeholder="試着輸入一些文字"
      />
      <div>
        <Suspense fallback="Loading">
          {this.renderMovies}
        </Suspense>
      </div>
    </div>
  );
}

這裏須要注意的是:若是想要異步訪問合成事件對象 SyntheticEvent,須要調用 persist() 方法或者對事件對象進行深拷貝 const event = { ...event } 保留對事件的引用。編程

在 React 事件調用時,React 傳遞給事件處理程序是一個合成事件對象的實例 SyntheticEvent 是經過合併獲得的。 這意味着在事件回調被調用後,SyntheticEvent 對象將被重用而且全部屬性都將被取消。 這是出於性能緣由,所以,您沒法以異步方式訪問該事件。 React合成事件官方文檔
event.persist()
// or
const event: SyntheticEvent = { ...event }

還有一個隱晦點的須要指出,咱們知道若是想要使 input 爲受控元素,正確的作法是:在給 input 綁定 value 時,須要同時綁定 onChange 事件來監聽數據變化,不然就會報以下警告。axios

可是當你異步傳遞 SyntheticEvent 對象時,使用 value 屬性進行綁定的 input,值不會再發生變化(但它還是一個受控元素)。segmentfault

...
event.persist()
timer = setTimeout(() => {
  fn.call(this, event) // 傳遞 event
}, delay) 
...
<input
  defaultValue={this.state.value}
  // value={this.state.value} 使用 value 屬性,值不會發生變化
  onChange={e => this.onChangeHandler(e)}
/>

以下圖:數組

image.png

減小 HTTP 請求

減小 HTTP 請求的手段之一就是將 HTTP 請求結果進行緩存,若是下次請求的 url 未發生變化,則直接從緩存中獲取數據。緩存

import axios from 'axios';
const caches = {}; 
const axiosRequester = () => {
  let cancel;
  return async url => {
    if(cancel) {
      cancel.cancel();
    }
    cancel = axios.CancelToken.source();
    try {
      if(caches[url]) { //若是請求的 url 以前已經提交過,就不在進行請求,返回以前請求回來的數據
        return caches[url];
      }
      const res = await axios.post(url, {
         cancelToken: cancel.token
      })
      const result = res.data.result;
      caches[url] = result;  //將 url做爲 key, result 爲請求回來的數據,存儲起來
      return result;
    } catch(error) {
      if(axios.isCancel(error)) {
        console.log('Request canceled', error.message);
      } else {
        console.log(error.message);
      }
    }
  }
}

export const _search = axiosRequester();

在使用 axios 進行 HTTP 請求時,首先根據 url 判斷數據是否已被緩存,若是命中則直接從緩存中拿數據。若是未被緩存,則發起 HTTP 請求,並將請求回來的結果以鍵值對的形式保存在 caches 對象中。

緩存淘汰策略 - LRU

因爲緩存空間是有限的,因此不能無限制的進行數據存儲,當存儲容量達到一個閥值時,就會形成內存溢出,所以在進行數據緩存時,就要根據狀況對緩存進行優化,清除一些可能不會再用到的數據。

這裏咱們用到 keepAlive 相同的緩存淘汰機制 - LRU。

LRU - 最近最少使用策略

  • 以時間做爲參考,若是數據最近被訪問過,那麼未來被訪問的概率會更高,若是以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,代表最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。

實現 LRU 策略咱們須要一個存儲緩存對象 key 的數組:

const keys = [];

而且須要設置一個閥值,控制緩存棧最大的存儲數量:

const MAXIMUN_CACHES = 20;

還須要一個用來刪除數組 keys 成員項的工具函數 remove

function remove(arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

最後再實現一個 pruneCacheEntry 函數,用來刪除最少訪問的數據(第一項):

// 傳入 keys 數組的第一項
if (keys.length > parseInt(MAXIMUN_CACHES)) {
  pruneCacheEntry(caches, keys[0], keys);
}

...
// 刪除最少訪問的數據
function pruneCacheEntry ( caches, key, keys) {
  caches[key] = null; // 清空對應的數據
  delete caches[key]; // 刪除緩存 key
  remove(keys, key);
}

最終「鍵入防抖」結合 LRU 緩存優化後的搜索功能就像這樣:

同系列文章:

相關文章
相關標籤/搜索