解構reselect

reselect

version: 4.0.0react

描述

reselect是可以緩存函數運行結果的一個緩存函數生成器,其接收一個函數返回一個新的具備緩存能力的函數redux

用途

  1. 用來緩存須要大量計算的結果
  2. 用來緩存頻繁的重複運算的結果

解析

核心緩存函數,實現很簡單
主要是把新舊參數是否不變的判斷規則交給了equalityCheck數組

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}

默認的比較函數只是定義了一個簡單的規則,若是要深度對比對象或者數組,須要自行實現緩存

function defaultEqualityCheck(a, b) {
  return a === b
}

在判斷新舊參數是否相同的過程當中,須要對舊的參數進行遍歷,將equalityCheck規則應用到每一個參數的對比上app

function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null || prev.length !== next.length) {
    return false
  }

  // 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++) {
    if (!equalityCheck(prev[i], next[i])) {
      return false
    }
  }

  return true
}

至此就已經得到了一個緩存函數生成器defaultMemoize,用法相似這樣函數

function add(a, b) {
    return a + b
}
let memoryAdd = defaultMemoize(add)
memoryAdd(1, 2)
// 直接返回結果
memoryAdd(1, 2)

可是reselect進行了進一步的擴展,定義了一個建立緩存函數的工廠,返回一個使用memoize來緩存結果的函數,並且該函數對傳入的參數作了處理oop

export function createSelectorCreator(memoize, ...memoizeOptions) {
  // 返回一個函數,該函數接收多個函數並最終返回selector
  return (...funcs) => {
    let recomputations = 0
    // 最後一個函數用來計算結果
    const resultFunc = funcs.pop()
    // 除最後一個外的函數,都將做爲參數傳遞給resultFunc
    const dependencies = getDependencies(funcs)
    // resultFunc的緩存函數
    const memoizedResultFunc = memoize(
      function () {
        // 用來統計resultFunc真正被調用的次數
        recomputations++
        // apply arguments instead of spreading for performance.
        return resultFunc.apply(null, arguments)
      },
      // 傳遞給memory,對於defaultMemoize來講可用來覆蓋默認的defaultEqualityCheck比較規則
      ...memoizeOptions
    )

    // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
    // selector是一個memoize生成的緩存函數,若是每次調用的參數相同則會直接返回結果
    const selector = memoize(function () {
      const params = []
      const length = dependencies.length

      // 當調用selector時將arguments傳入每一個dependencie,並將運行結果push進params裏,而後傳給resultFunc計算最終結果
      for (let i = 0; i < length; i++) {
        // apply arguments instead of spreading and mutate a local list of params for performance.
        params.push(dependencies[i].apply(null, arguments))
      }

      // apply arguments instead of spreading for performance.
      // 這裏並無直接調用resultFunc,而是調用對resultFunc進一步緩存獲得的memoizedResultFunc,
      // 也就是說若是selector調用時的參數arguments發生了改變,可是dependencies計算後的結果先後參數依然相等,則直接返回結果,
      // 這裏主要是處理arguments是引用類型時,可能基於數據不可變原則,arguments可能只是引用發生了改變,其內部的values並未發生改變,那麼就能夠直接返回ResultFunc的運行結果
      return memoizedResultFunc.apply(null, params)
    })

    // 得到最終計算函數
    selector.resultFunc = resultFunc
    // 得到參數函數列表
    selector.dependencies = dependencies
    // 獲取實時的計算次數
    selector.recomputations = () => recomputations
    // 重置recomputations
    selector.resetRecomputations = () => recomputations = 0
    return selector
  }
}

因此createSelectorCreator的使用方式應該以下this

export const createSelector = createSelectorCreator(defaultMemoize)
// createSelector返回的就是上面的selector
let memoryFunc = createSelector(
    user => user.name,
    user => user.age,
    user => user.gender,
    (name, age, gender) => [name, age, gender].join('')
)
// memoryFunc接收的參數會被傳遞給每一個前置函數,好比user,也能夠傳遞多個參數進去
let user = {
    name: 'a',
    age: 22,
    type: 'male'
}
memoryFunc(user)
// 在react中此處的user就是redux的state

若是默認的===比較不知足需求,則能夠本身實現一個比較函數並建立本身的createSelector函數編碼

function customEqualityCheck(a, b) { // doSomething }
const createCustomSelector = createSelectorCreator(defaultMemoize, customEqualityCheck)

上面還有個getDependencies的函數主要是作參數處理設計

function getDependencies(funcs) {
  // 從這裏能夠看出依賴項也能夠放進一個數組中
  const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs
  // 若是dependencies中存在不是函數的參數,則拋出異常
  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
}

此外reselect還存在createStructuredSelector,是一個基於createSelector進一步封裝用來結構化輸出的函數,接受一個自定義結構的對象obj,其每一個key對應的value都必須是個函數,返回結果是一個和obj具備相同key的對象,其每一個key對應的值爲obj上相應的key的value函數執行結果。

export function createStructuredSelector(selectors, selectorCreator = createSelector) {
  if (typeof selectors !== 'object') {
    throw new Error(
      'createStructuredSelector expects first argument to be an object ' +
      `where each property is a selector, instead received a ${typeof selectors}`
    )
  }
  const objectKeys = Object.keys(selectors)
  return selectorCreator(
    // selector是個對象,selectorCreator接收的參數所有爲函數,或者一個函數數組加一個函數,這裏將selector的values放入數組中做爲依賴參數
    objectKeys.map(key => selectors[key]),
    (...values) => {
      // composition貫穿整個reduce遍歷
      return values.reduce((composition, value, index) => {
        // 讀取的objectKeys[index]也就是key值,value是依賴函數的執行結果
        composition[objectKeys[index]] = value
        return composition
      }, {})
    }
  )
}

createStructuredSelector是至關於省略了最後一個函數,可是若是須要在最後一個函數中處理額外的邏輯,就不知足需求了,應該用createSelector

let selector = createStructuredSelector({
    name: user => user.firstName + user.lastName,
    age: user => user.age,
    description: user => user.details.join(' '),
})
selector({
    firstName: 'redux',
    lastName: 'reselect',
    age: 0,
    details: [ 'oneoneone' ]
})
// 輸出結構
=> {
    name: 'reduxreselect',
    age: 0,
    description: 'oneoneone',
}
// 上面這段代碼用createSelector來寫是這樣的
let selector = createSelector({
    name: user => user.firstName + user.secondName,
    age: user => user.age,
    description: user => user.details.join(' '),
    (name, age, description) => { name, age, description }
})

理解

  1. createSelectorCreator出現的目的是什麼

在createSelectorCreator中主要的功能是定製參數和再次緩存resultFunc,若是按照只是須要一個能緩存值的函數defaultMemoize已經達到了需求,而createSelectorCreator中可以接受多個函數,前面全部函數做爲最後一個函數的參數,
爲什麼這樣設計
若是是用defaultMemoize,代碼可能以下

const state = {}
let memoriedFuncA = defaultMemoize(function(state) {
    // get taskC
    // todo taskA
    return ''
})
let memoriedFuncB = defaultMemoize(function(state) {
    // get taskC
    // todo taskB
    return ''
})
// 上面的代碼很明顯能夠將公共模塊分離出來
let memoriedFuncC = defaultMemoize(function(state) {
    // todo taskC
    return ''
})
let memoriedFuncA = defaultMemoize(function(state) {
    let resultc = memoriedFuncC(state)
    // todo taskA
    return ''
})
let memoriedFuncB = defaultMemoize(function(state) {
    let resultc = memoriedFuncC(state)
    // todo taskB
    return ''
})

可是這樣memoriedFuncC是硬編碼到memoriedFuncA、memoriedFuncB中,考慮耦合問題能夠將memoriedFuncC做爲參數傳入,而memoriedFuncC自己是個函數,而且可能存在多個依賴,天然就想到傳入多個函數

let memoriedFuncB = defaultMemoize(function(state, taskC) {
    let resultc = taskC(state)
    // todo taskB
    return ''
})
// createSelectorCreator對參數進行了整理
let memoriedFuncC = createSelector(
    state => 'c'
)
let memoriedFuncA = createSelector(
    memoriedFuncC,
    c => 'a'
)
let memoriedFuncB = createSelector(
    memoriedFuncC,
    c => 'b'
)

總結

reselect主要是實現函數緩存運行結果的功能,其代碼主要分爲如下幾塊

  1. 緩存函數defaultMemoize,判斷先後參數是否相等來決定執行仍是直接返回結果
  2. 分離出先後參數對比的邏輯areArgumentsShallowlyEqual
  3. 分理出判斷是否相等的邏輯defaultEqualityCheck
  4. 定義工廠createSelectorCreator,定製了參數格式也就是調用方式
  5. 基於createSelector對結構化輸出進行了封裝(createStructuredSelector)
相關文章
相關標籤/搜索