useSelector和reselect幹了什麼

先說結論react

useSelector 幹了什麼

調用此 Hook API 時會在 store 上註冊監聽器。 當 Store::state 變化時,組件會 checkForUpdates,利用 equalityFn 判斷是否進行更新。git

兩個 feature:

  1. 訂閱 Store,當 state 變化時,自動 mapState,返回的 childState 會被渲染到視圖上
  2. equalityFn 等效於 shouldComponentUpdate

不足的地方

沒有對 selector 函數作 memorize 優化github

能夠利用 useCallback 優化 selector 嗎?redux

不能。selector 的入參是 Store::State,既然使用了 react-redux 就儘可能不要訪問 store,而 useCallback 須要 deps,即 Store::State (對於 selector 就是入參),這裏沒有辦法直接拿到。數組

解決方案,使用 reselect 對 selector 作 memorize 處理。(對 selector 入參作判斷)緩存

reselect 幹了什麼

對 selector 函數(等效於 mapXXXToProps 函數)作 memorize 優化 若是 selector 的入參沒有發生變化,則返回上一次執行的緩存markdown

源碼細節

useSelectorWithStoreAndSubscription

React-Redux 庫中 useSelector 函數的核心部分react-router

// selector:(storeState) => childState
// equalityFn: <T>(newChildState:T, oldChildState:T) => boolean
// useSelectorWithStoreAndSubscription:
// <T>(selector: (storeState) => T, equalityFn: (newProps:T, oldProps:T) => boolean, ...) => T
// 對於Provider使用store,下層組件使用contextSub。
function useSelectorWithStoreAndSubscription( selector, equalityFn, store, contextSub ) {
  // forceUpdate
  const [, forceRender] = useReducer(s => s + 1, 0)

  const subscription = useMemo(() => new Subscription(store, contextSub), [
    store,
    contextSub
  ])

  const latestSelector = useRef() // selector的引用
  const latestSelectedState = useRef() // mapStateToProps以後獲得的State的引用

  let selectedState

  // 這裏和connectAdvanced中計算actualChildProps的道理同樣
  if (selector !== latestSelector.current) {
    // selector相似mapStateToProps
    selectedState = selector(store.getState())
  } else {
    // selector沒變化,則使用緩存
    selectedState = latestSelectedState.current
  }

  useEffect(() => {
    latestSelector.current = selector
    latestSelectedState.current = selectedState
  })

  useEffect(() => {
    function checkForUpdates() {
      // 執行selector即mapStateToProps
      const newSelectedState = latestSelector.current(store.getState())

      // 比較新舊State即 shouldComponentUpdate
      if (equalityFn(newSelectedState, latestSelectedState.current)) {
        return // shouldComponentUpdate判斷爲state沒變化 則放棄此次update
      }

      latestSelectedState.current = newSelectedState

      // forceUpdate
      forceRender({})

      // 說一下爲何是`force`
      // setState函數只有傳入新的值纔會re-render
      // 例如setState(array.reverse()),這個不會引發update,由於Array.prototype.reverse不純
      // 這裏強制傳入了一個新對象,即setState({}),一定會引發update
    }

    // checkForUpdates註冊到Provider::subscription
    // 爲何是Provider?請看components/Provider.js
    // 不嚴格的講,也能夠說是註冊到store listeners裏
    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    // 初始化selector更新一次
    checkForUpdates()

    return () => subscription.tryUnsubscribe() // 清理effect。取消訂閱
  }, [store, subscription])

  return selectedState
}
複製代碼

createSelectorCreator

// memoize: (func, equalityCheck) => (...args) => Result
// createSelectorCreator: (memorize, ...memoizeOptions) =>
// (...inputSelectors, resultFunc) => State => Result
export function createSelectorCreator(memoize, ...memoizeOptions) {
  // funcs: [[inputSelectors], resultFunc]
  // funcs: [...inputSelectors, resultFunc]
  return (...funcs) => {
    let recomputations = 0

    // 拿到funcs中最後一個函數
    const resultFunc = funcs.pop()

    // funcs: [inputSelectors] | [[inputSelectors]]
    // dependencies: InputSelector[] = funcs
    const dependencies = getDependencies(funcs)

    // 獲得resultFunc通過memorize優化後的版本
    const memoizedResultFunc = memoize((...args) => {
      recomputations++
      return resultFunc(...args)
    }, ...memoizeOptions)

    // 每一個inputSelector的入參都是相同的
    // 因此將全部inputSelectors的入參統一塊兒來作memorize優化
    const selector = memoize((...args) => {
      const params = []
      const length = dependencies.length

      for (let i = 0; i < length; i++) {
        // 遍歷每一個inputSelector執行
        // 並將結果收集到params裏
        params.push(dependencies[i](...args))
      }

      // 將收集到的params傳給resultFunc執行
      // 返回resultFunc執行後的結果
      return memoizedResultFunc(...params)
    })

    selector.resultFunc = resultFunc
    selector.dependencies = dependencies
    selector.recomputations = () => recomputations
    selector.resetRecomputations = () => (recomputations = 0)
    return selector
  }
}
複製代碼

源碼比較簡單,主要看看 memorize 部分閉包

defaultMemoize

// 對函數func進行memorize優化
// 利用equalityCheck對入參作緩存驗證
// defaultMemoize: (func, equalityCheck) => (...args) => Result
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs /**: any[] **/ = null
  let lastResult /**: any[] **/ = null
  return (...args) => {
    // 利用比較函數equalityCheck對比lastArgs和args(兩個數組)
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, args)) {
      // 若是不一致,則從新執行func
      lastResult = func(args)
    }

    // 若是lastArgs和args一致
    lastArgs = args

    // 返回閉包中的緩存
    return lastResult
  }
}

// 兩個數組之間的diff
// 利用比較函數equalityCheck對比prev和next
// equalityCheck: (prev: any, next: any) => boolean
// areArgumentsShallowlyEqual: (equalityCheck, prevs: any[], nexts: any[]) => boolean
function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null || prev.length !== next.length) {
    return false
  }

  const length = prev.length
  for (let i = 0; i < length; i++) {
    if (!equalityCheck(prev[i], next[i])) {
      return false
    }
  }

  return true
}
複製代碼

reselect 中 memorize 的不足

在 reselect 中默認的 memorize 函數依靠閉包來作緩存,缺點是不能記錄屢次。ide

怎麼才能記錄屢次呢?舉個例子

在 react-router 庫中 compilePath 的 memo 優化

github.com/ReactTraini…

const cache = {}
const cacheLimit = 10000 // 緩存最大限制
let cacheCount = 0

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {})

  // memorize優化
  if (pathCache[path]) return pathCache[path]

  // 省略n多代碼...
}
複製代碼

若是 path 參數有對應的緩存記錄,則直接返回緩存。這裏能夠記錄不少次,最大上限是 10k 個不一樣的 path(實際上不會有這麼多的路由)。屬於空間換時間的優化策略。

如何正確使用 useSelector

首先知道,使用了 useSelector 的組件就會訂閱 store(useSelector 是 connect 函數的替代品)。useSelector 第二個參數至關於 shouldComponentUpdate。

使用了 useSelector 獲得的返回值須要經過調用 dispatch 來更新。(參見 useDispatch)

而後,useSelector 不會避免 selector 函數重複執行。須要使用 reselect 庫對 selector 函數作優化。

相關文章
相關標籤/搜索