先说结论react
调用此 Hook API 时会在 store 上注册监听器。 当 Store::state 变化时,组件会 checkForUpdates,利用 equalityFn 判断是否进行更新。git
没有对 selector 函数作 memorize 优化github
能够利用 useCallback 优化 selector 吗?redux
不能。selector 的入参是 Store::State,既然使用了 react-redux 就尽可能不要访问 store,而 useCallback 须要 deps,即 Store::State (对于 selector 就是入参),这里没有办法直接拿到。数组
解决方案,使用 reselect 对 selector 作 memorize 处理。(对 selector 入参作判断)缓存
对 selector 函数(等效于 mapXXXToProps 函数)作 memorize 优化 若是 selector 的入参没有发生变化,则返回上一次执行的缓存markdown
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 } 复制代码
// 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 部分闭包
// 对函数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 函数依靠闭包来作缓存,缺点是不能记录屡次。ide
怎么才能记录屡次呢?举个例子
在 react-router 库中 compilePath 的 memo 优化
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 的组件就会订阅 store(useSelector 是 connect 函数的替代品)。useSelector 第二个参数至关于 shouldComponentUpdate。
使用了 useSelector 获得的返回值须要经过调用 dispatch 来更新。(参见 useDispatch)
而后,useSelector 不会避免 selector 函数重复执行。须要使用 reselect 库对 selector 函数作优化。