reselect源碼閱讀

  • 2020 年 1 月 14 日
  • 筆記

reselect源碼閱讀

之前就聽聞了reselect是一個用於react性能優化的庫,並且源碼只有100多行。可謂短小精悍,今天來閱讀一波膜拜大佬們的思想

import { createSelector } from 'reselect'    const shopItemsSelector = state => state.shop.items  const taxPercentSelector = state => state.shop.taxPercent    const subtotalSelector = createSelector(    shopItemsSelector,    items => items.reduce((acc, item) => acc + item.value, 0)  )    const taxSelector = createSelector(    subtotalSelector,    taxPercentSelector,    (subtotal, taxPercent) => subtotal * (taxPercent / 100)  )    export const totalSelector = createSelector(    subtotalSelector,    taxSelector,    (subtotal, tax) => ({ total: subtotal + tax })  )    let exampleState = {    shop: {      taxPercent: 8,      items: [        { name: 'apple', value: 1.20 },        { name: 'orange', value: 0.95 },      ]    }  }    console.log(subtotalSelector(exampleState)) // 2.15  console.log(taxSelector(exampleState))      // 0.172  console.log(totalSelector(exampleState))    // { total: 2.322 }

官網demo如上,通過介紹可以知道,subtotalSelector taxSelector totalSelector在傳進去的state不變的情況下,第二次調用不會重新計算,而是會取前一次的計算結果。在涉及到大量運算的時候,例如redux中,可以避免全局state某一小部分改變而引起這邊根據小部分state進行計算的重新執行。起到性能優化的作用。下面開始閱讀探讀部分

先說幾個簡單的工具函數吧

首先是默認的比較函數,代表比較方式,可以根據業務需求換的。默認是進行全等比較

/**   * 默認的比較函數,只進行一層全等比較。根據業務需要可以自己訂製。   * @param {*} a   * @param {*} b   */  function defaultEqualityCheck(a, b) {      return a === b    }

比較兩個參數是否完全相等的庫…見注釋。記住不要為了裝逼而棄用for循環

  /**     * 比較前後兩次的參數是否完全相等     * @param {*} equalityCheck 比較規則,默認是使用defaultEqualityCheck全等比較     * @param {*} prev 前一次的參數     * @param {*} next 這次的參數     */    function areArgumentsShallowlyEqual(equalityCheck, prev, next) {      if (prev === null || next === null || prev.length !== next.length) {        return false      }      // 學到了。使用for循環是因為return後不會繼續for後面的,forEach和every是會繼續的,所以以後不要為了裝逼而拋棄for循環了      // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.      const length = prev.length      for (let i = 0; i < length; i++) {          // 只要遇到一個參數不想等就退出,判斷返回false        if (!equalityCheck(prev[i], next[i])) {          return false        }      }        return true    }

獲取依賴,就是保證inputSelectors的每一項都是函數。否則就報錯

  /**     * 用來保證inputSelectors的每一項都是函數,非函數就報錯。然後返回每一個inputSelector組成的數組(依賴)     * @param {*} funcs     */    function getDependencies(funcs) {        // 因為可以兩種形式傳入,所以處理下        // createSelector(...inputSelectors | [inputSelectors], resultFunc)      const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs        if (!dependencies.every(dep => typeof dep === 'function')) {        const dependencyTypes = dependencies.map(          dep => typeof dep        ).join(', ')        throw new Error(          'Selector creators expect all input-selectors to be functions, ' +          `instead received the following types: [${dependencyTypes}]`        )      }        return dependencies    }

下面是默認記憶化函數defaultMemoize

就是說有個func函數,每次去調用它的時候,先去比較它的參數是否和上一次參數相同,相等就返回上一次結果,不等就重新計算一遍, 並把結果和參數存到lastArgs、lastResult中,這裡是用到了閉包來保存的。

  /**     * 默認的記憶化函數     * @param {*} func     * @param {*} equalityCheck 比較的函數,默認情況下是判斷是否全等     */    export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {      let lastArgs = null      let lastResult = null      // 這裡為了性能原因。使用arguments而不是rest運算符      // we reference arguments instead of spreading them for performance reasons      return function () {        if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {          // 參數改變了重新計算一遍          lastResult = func.apply(null, arguments)        }          lastArgs = arguments // 記錄下這次的參數值        return lastResult      }    }

demo中引入的createSelector其實是export const createSelector = createSelectorCreator(defaultMemoize)這樣創建的

下面看看createSelectorCreator的實現

1、memoize是自定義的記憶化函數,默認是上面說到的defaultMemoize。我們可以根據自己的業務需求進行訂製

2、它的內部也使用了defaultMemoize進行優化。就是對於相同的state,不需要每一次都一次調用inputSelect去一個個獲取對應的值

  /**     *     * @param {*} memoize     * @param {*} memoizeOptions     */    export function createSelectorCreator(memoize, ...memoizeOptions) {      return (...funcs) => { // createSelector的本體        let recomputations = 0        const resultFunc = funcs.pop() // 最後一個。。就是那個計算函數        const dependencies = getDependencies(funcs)// funs是inputSelectors          // resultFunc經過包裝。具備了記憶功能        const memoizedResultFunc = memoize(          function () {              // 這裡的arguments是每一項是inputSelector後的結果              // 把inputSelector後的結果傳進resultFunc              // recomputations 用了記錄調用了多少次            recomputations++            return resultFunc.apply(null, arguments)          },          ...memoizeOptions        )          // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.        // reselect內部的一個優化        // 作用就是對於相同的state,不需要每一次都一次調用inputSelect去一個個獲取對應的值        const selector = defaultMemoize(function () {            // arguments應該是state          const params = []          const length = dependencies.length            for (let i = 0; i < length; i++) {            // 對每一個inputSelect結合state取出相應的值            params.push(dependencies[i].apply(null, arguments))          }            //最後供memoizedResultFunc計算使用Ss s          return memoizedResultFunc.apply(null, params)        })          // 對外提供的幾個方法          // 返回那個函數        selector.resultFunc = resultFunc          //返回計算次數        selector.recomputations = () => recomputations          // 用來重置        selector.resetRecomputations = () => recomputations = 0        return selector      }    }

reselect這個庫還是很吊的…主要是利用了必=閉包來保存變數,比較函數前後兩次的參數值,參數值不同就重新計算。參數相同就返回之前的結果