更贴合hooks的状态管理--hox

4,004 阅读6分钟

前言

在之前的React Hooks:初探·实践提到过hooks更符合React编程模型,其中custom hooks更是让人眼前一亮,只能说用过的都说香,但是一直没找到贴合hooks的状态管理,虽然hooks的设计挺多来自于redux,但是redux基本和hooks没有交集,至于mobx,也推出了基于hooks的如useLocalStore、useObserve等Hooks,但是在我的使用过程中也没有感觉引入Hooks带了很大便利,我的个人看法是redux和mobx更强调的集中式管理状态,而Hooks更强调状态分享。最近找到了两个比较有意思的状态管理工具,一个是来自fackbook官方实验室(非React官方)的recoil和来自蚂蚁金服的hox,两个都接触了一下,觉得recoil要理解的概念和API挺多,没有那么香,下面重点介绍一下我觉得既简单又更贴合hooks的hox

是什么

hox是蚂蚁金服推出的下一代react状态管理器。

我觉得官方这么自信的原因有两点,第一点就是只有一个API——createModel,这个和文档一样简单的API确实能够吸引不少眼球,为上手提供了条件;第二点就是hox完美的拥抱了react hooks,而hooks又是react官方不断安利的,有react官方作为重要支撑,综上所述,所以单方面宣布下一代还是可以接受的。

为什么

为什么说hox更提盒hooks的状态管理呢?下面结合源码说一下我的个人看法!

对于使用而言,hox基本上只需要和一个API——createModel打交道,这一点还是值得点个赞的。(当然还有withModel(在class组件使用)等API,不过感觉都是买一送一的感觉)。对于设计而言,hox很完美地利用了hooks自身强大地状态管理,但是hooks是针对单一组件而言,所以hox巧妙地利用了react reconciler,通过自定义渲染器把散落各个组件的状态以model为单位收集起来,有点像是把hooks的定义往外层提,或者说是额外创建一个组件树,通过自定义hooks和自定义渲染器实现了共享hooks,通过container容器在状态更新时通知给各个组件。

直接上源码(分三块解析):

import { ModelHook, UseModel } from "./types";
import { Container } from "./container";
import { Executor } from "./executor";
import React, { useEffect, useRef, useState } from "react";
import { render } from "./renderer";

export function createModel<T, P>(hook: ModelHook<T, P>, hookArg?: P) {
  /**
  * 第一块:以model为一个单位实例化一个对应的容器进行数据管理
  * Container主要做了两个事情,通过ES6的Set结构存储了观察者,再暴露notify方法在通知全部观察者更新,就是
  * 下面useModel方法中useEffect里定义的subscriber函数
  */
  const container = new Container(hook);
  
  /**
  * 第二块:基于react reconciler + Executor组件自定义生成了一个渲染器,通过观察者模式在状态更新时通知全部观察者,就是去执行Set结构里存储的subscriber函数
  * react reconciler用法没什么特殊的,可以直接看文档
  * Executor组件主要执行了两个函数,一个是createModel传进来的hook,另一个是hook返回的data作为参数去执行onUpdate回调
  */
 
  render(
    <Executor
      onUpdate={val => {
        container.data = val;
        container.notify();
      }}
      hook={() => hook(hookArg)}
    />
  );
  
  // 第三块:返回一个自定义的hooks,
  // ①通过useState将container的状态和React自身的状态管理挂钩,通过暴露depsFn函数来控制是否更新状态
  // ②通过useEffect注册/移除了观察者
  // ③通过defineProperty劫持当前自定义hooks的data属性,来暴露container的data,做到只读取当前的状态
  const useModel: UseModel<T> = depsFn => {
    const [state, setState] = useState<T | undefined>(() =>
      container ? (container.data as T) : undefined
    );
    const depsFnRef = useRef(depsFn);
    depsFnRef.current = depsFn;
    const depsRef = useRef<unknown[]>([]);
    useEffect(() => {
      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);
      return () => {
        container.subscribers.delete(subscriber);
      };
    }, [container]);
    return state!;
  };
  Object.defineProperty(useModel, "data", {
    get: function() {
      return container.data;
    }
  });
  return useModel;
}

function compare(oldDeps: unknown[], newDeps: unknown[]) {
  if (oldDeps.length !== newDeps.length) {
    return true;
  }
  for (const index in newDeps) {
    if (oldDeps[index] !== newDeps[index]) {
      return true;
    }
  }
  return false;
}

怎么用

hox用起来还是比较省心的,可以直接参考文档

说多无益,show me the code! 下面是基于redux和hox实现的todoList

redux: 基于reudx官方例子 + React Profiler

hox: 基于hox + React Profiler

写在最后

  1. 使用hox的心智模型,应该以特定功能为一个单元去划分model

还是我之前说的Hooks的主要成本不在于api的学习,而是在于心智模型的转变。这一点同样适用于hox,在redux和mbox我们更关注的是stroe中状态的变化,但是在hox中我们更应该关注每一个model,说白了就是被createModel包装过后的那个custom hooks,更倾向特定的功能逻辑。像上面的todoList,我们在写hooks应该奔着实现添加todo,可以改变todo状态,还有筛选功能的todoList。

  1. 性能对比和优化

上面的两个例子都用到了React Profiler(基于React Profiler写了一个useProfiler)来作为简单的性能比对,虽然hox提供了depsFn来优化性能,在比对了两个例子的所有操作,觉得基于hox的TodoList就组件而言多了一些不必要的渲染,例如第一次执行addTodo的操作,hox版本渲染了三个组件(AddTodo, TodoList, FilterFooter),而redux则渲染了一个组件(VisibleTodoList);第二次addTodo的操作,hox版本也渲染了两个组件(AddTodo, TodoList)。这一点我也是琢磨了许久,思来想去没想到好的优化办法,个人觉得是hox的设计本身导致的问题。(也可能是写法有问题,需要进一步的探索,望高人指点)。但是我觉得只要划分好model,用好depsFn这个利器,hox的写法和复用性还是可以完胜redux的,所以还是挺香的。

  1. 关注issue

从学习看源码到写这篇文章的过程,一直关注hox的issue,感觉有点不温不火,感觉大多数人都处于观望状态,现在最新一条的issue是关于hox v2的,这条issue挺有看头的,由于v1版本存在挺多的弊端,v2打算配合context,这也意味着使用者需要手动写provider,但是相对其他的基于context的库,这个api也相对简单。因此结合issuse和hox的发展形势,个人觉得小型项目(如活动页等用完即走的项目)可以考虑接入一下,大型项目就感觉没必要了,应该保持持续地关注,希望hox能越走越远,如果能持续搭上hooks的快车,感觉hox还是挺有前途的!再退一步说,即使hox没能跻身状态管理的前列,它的设计思想和源码也是值得一读的,值得学习!

good!