React Hooks 状态管理器 Hox 源码解读

·  阅读 1386

背景

React Hooks 正式发布已经快 2 年了,它的特性让前端打工人又多了一种搬砖姿势,特别是它的函数式编程风格、声明式副作用处理、逻辑层抽取能力,以及灵活的自定义 Hooks 能力,用起来特别香!那该用哪一款状态管理器和它搭配呢,我发现蚂蚁正好开源了一款:hox,完全是按 hooks 方式实现的状态管理器,于是拉下源码小分析一把,往下看。

代码结构

看到代码,发现写得相当简洁,核心代码 5 个源文件,主要逻辑一共就50多行。

来看看结构和模块功能:

  • container:共享状态的数据容器,实现方式是消息订阅和通知;
  • create-model:核心模块,创建 model 的全部逻辑都在这里了,实现方式是工厂模式;
  • executor:model hooks 执行器,用于更新全局状态,注意:它设计成一个组件,在 create-model 时渲染,这样生命周期就可以由 react 管理;
  • renderer:渲染器,因为上面的 executor 是一个组件,需要在 create-model 时渲染,这里单独抽出来,为了可以跨平台使用(RN、小程序,SSR等)。注意:这里用的是 react-reconciler,这是 react 的虚拟 dom 引擎,react 代码由它来创建和更新虚拟 dom 树,所以,executor 最终并没有渲染出来,只是一个独立的虚拟 dom 节点,为了跨平台,不过也增加了包的大小
  • with-model:这是兼容类组件用的,使用了高阶组件模式,代码比较简单,大部分是 ts 包装类型声明;

引用一个网络流程图:

核心源码解读

我们就看三个文件:container、executor和create-model。

container

这个类有两个属性和一个方法,一个简单的消息订阅/通知实现。

export class Container<T = unknown> {
  constructor(public hook: ModelHook<T>) {}
  // 消息订阅者表
  subscribers = new Set<Subscriber<T>>();
  data!: T;

  // 消息通知
  notify() {
    for (const subscriber of this.subscribers) {
      subscriber(this.data);
    }
  }
}
复制代码

executor

这里的主要流程就是:Executor 在 render 时,就会调用 hook 函数,然后调用更新回调响应。目的就是在 model 状态改变后,触发更新回调,具体细节往下看 create-model。

export function Executor<T>(props: {
  hook: () => ReturnType<ModelHook<T>>;
  onUpdate: (data: T) => void;
}) {
  // 执行 hooks
  const data = props.hook();
  // 触发更新回调
  props.onUpdate(data);
  return null as ReactElement;
}
复制代码

create-model

压轴来了,我上面说的 50 行核心代码就是它了!用于创建 model,useModel 部分也在里面。

它是个高阶函数,它的目的是创建并返回一个 hooks,把 container、executor 和 model 连接起来,成为全局状态。

它有两个参数:

  • hook:它是你定义的 model,比如:用户信息。

它也是一个自定义 Hooks,所以要用 Executor 去包装。

它内部一般声明状态和操作,引用一个官方示例:

function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  return {
    count,
    decrement,
    increment
  };
}
复制代码
  • 第二个参数,hookArg:你想传给 model 的参数值,比如 useCounter 有默认值,我们就可以用这种方式传递。

看注释。

// 它会返回一个自定义 Hooks:useModel
export function createModel<T, P>(hook: ModelHook<T, P>, hookArg?: P) {
  // 实例化了一个数据容器,这个就是用来存放全局状态用的,并且它是设计成消息订阅/通知模式
  // 通过闭包来持久化
  const container = new Container(hook);
  
  // 渲染 model hooks 执行器,用于更新全局状态,它是一个独立虚拟 dom
  render(
    <Executor
      onUpdate={val => {
        container.data = val;
        container.notify();
      }}
      hook={() => hook(hookArg)}
    />
  );
  
  // useModel,它是自定义 Hooks
  // 可以把它看作类似于 useState 的东西
  // 我们上面可以看到 Model 本身也是一个 Hooks,为什么 Model 还要再包装一层,这里就是为了把 container、executor 和 model 连接起来,成为全局状态
  const useModel: UseModel<T> = depsFn => {
    // 这里的 state 就是使用者所用的状态,类型是:model hooks 的返回值
    const [state, setState] = useState<T | undefined>(() =>
      container ? (container.data as T) : undefined
    );
    
    // 这里如果有声明 deps 函数,就用 deps 函数的结果来决定是否该更新状态
    // deps 用 ref 存储
    const depsFnRef = useRef(depsFn);
    depsFnRef.current = depsFn;
    const depsRef = useRef<unknown[]>([]);
    useAction(() => {
      if (!container) return;
      
      // 这里就是订阅
      function subscriber(val: T) {
        if (!depsFnRef.current) {
          setState(val);
        } else {
          const oldDeps = depsRef.current;
          const newDeps = depsFnRef.current(val);
          if (compare(oldDeps, newDeps)) {
            setState(val);
          }
          depsRef.current = newDeps;
        }
      }
      
      // 添加订阅
      container.subscribers.add(subscriber);
      
      // container 变化时,销毁旧的订阅
      return () => {
        container.subscribers.delete(subscriber);
      };
    }, [container]);
    
    // 返回的就是 model hooks 的返回值
    return state!;
  };
  
  // 使用劫持,挂载 data 到 model 上
  Object.defineProperty(useModel, "data", {
    get: function() {
      return container.data;
    }
  });
  return useModel;
}
复制代码
分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改