Published on

use-context-selector源码解读&思考

Authors
  • avatar
    Name
    noodles
    每个人的花期不同,不必在乎别人比你提前拥有

React可以通过Context来解决跨组件的属性共享问题,但是Context方案会存在性能问题,Context更新会触发使用该Context的组件渲染. React提出过Context selectors的提案,use-context-selector就通过实现Context selectors 来解决Context的性能问题.

use-context-selector简单使用


    import { createContext, useContextSelector } from 'use-context-selector';
    // 创建Context 这个使用方式跟React提供的createContext是一样的
    const Context = createContext(null);

    const Counter1 = () => {
        // 通过useContextSelector获取想要的属性
        const count1 = useContextSelector(Context, (v) => v[0].count1);
        // 获取设置Context的函数,可以看上去有reducer的感觉
        const setState = useContextSelector(Context, (v) => v[1]);
        // 更新调用
        const increment = () =>
            setState((s) => ({
            ...s,
            count1: s.count1 + 1,
            }));
        return (
            <div>
                <span>Count1: {count1}</span>
                <button type="button" onClick={increment}>
                    +1
                </button>
                {Math.random()}
            </div>
        );
    };
    const StateProvider = ({ children }) => (
        <Context.Provider value={useState({ count1: 0})}>
            {children}
        </Context.Provider>
    );

    const App = () => (
        <StateProvider>
            <Counter1 />
        </StateProvider>
    );

在上面的例子中Context的value是一个useState的返回值,这里就留下一个疑问,use-context-selector是如何实现订阅更新的呢??!!

use-context-selector原理

use-context-selector前后对比
react的context方案中,子组件通过useContext获取Context的值和触发更新.Context的值变化的时候会触发所有使用该Context组件的更新. use-context-selector中提供Ref的方式来存储Context的值,通过订阅的方式实现更新,这样就实现了渲染的优化

Provider值生成/订阅逻辑

// 通过ref来存储context的值
const contextValue = useRef(undefined);
if (!contextValue.current) {
  const listeners = new Set();
  const update = (fn, options) => {
    versionRef.current += 1;
    const action = {
      n: versionRef.current,
    };
    listeners.forEach((listener) => listener(action));
    fn();
  };
  contextValue.current = {
    [CONTEXT_VALUE]: {
      /* "v"alue     */ v: valueRef,
      /* versio"n"   */ n: versionRef,
      // 订阅列表
      /* "l"isteners */ l: listeners,
      // context更新函数
      /* "u"pdate    */ u: update,
    },
  };
}
// 传给Provider的是useRef创建的contextValue
return createElement(
  ProviderOrig,
  { value: contextValue.current },
  children,
);

useContextSelectord订阅更新实现

    const {
        v: { current: value },
        n: { current: version },
        l: listeners,
    } = contextValue;
    // 通过selector从contextValue中获取想要的属性
    const selected = selector(value);
    // 生成useReducer  每个组件都对应自己的useReducer 
    // 只有selected变化的时候 当前组件才会更新
    const [state, dispatch] = useReducer(
        (prev, action) => {
            if (!action) {
                return [value, selected];
            }
            if ('p' in action) {
                throw action.p;
            }
            if (action.n === version) {
                if (Object.is(prev[1], selected)) {
                    return prev;
                }
                return [value, selected];
            }
            try {
                if ('v' in action) {
                    if (Object.is(prev[0], action.v)) {
                        return prev;
                    }
                    const nextSelected = selector(action.v);
                    if (Object.is(prev[1], nextSelected)) {
                        return prev;
                    }
                    return [action.v, nextSelected];
                }
            } catch {
            }
            return [...prev];
        },
        [value, selected]
    );

    if (!Object.is(state[1], selected)) {
        dispatch();
    }
    useIsomorphicLayoutEffect(() => {
        // 添加订阅
        listeners.add(dispatch);
            return () => {
                listeners.delete(dispatch);
            };
    }, [listeners]);
    // 返回useReducer中的值 这个值会关联更新
    return state[1];

一点思考

use-context-selector会在一些场景下用于Context的渲染优化,在看它的源码实现上也比较巧妙做到了不需要对原来的Context使用方式做较大改变就能 实现代码的升级.这点在在进行代码重构或者架构设计的时候可以借鉴.

  • 在代码重构的时候去hack中间层,减少对代码大范围的改动
  • 在架构设计上,分层且封装中间层逻辑,降低代码对特定库或者使用方式的依赖.