React 最新正式版已经支持了 Hooks API,先快速过一下新的 API 和大概的用法。

// useState,简单粗暴,setState可以直接修改整个state
const [state,setState] = useState(value);

// useEffect,支持生命周期
useEffect(()=>{
    // sub
    return ()=>{
        // unsub
    }
},[]);

// useContext,和 React.createConext() 配合使用。
// 父组件使用 Context.Provider 生产数据,子组件使用 useContext() 获取数据。
const state = useContext(myContext);

// useReducer,具体用法和redux类似,使用dispatch(action)修改数据。
// reducer中处理数据并返回新的state
const [state, dispatch] = useReducer(reducer, initialState);

// useCallback,返回一个memoized函数,第二个参数类似useEffect,只有参数变化时才会更改。
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

// useMemo,返回一个memoized值,只有第二个参数发生变化时才会重新计算。类似 useCallback。
// useCallback(fn,inputs) 等效 useMemo(() => fn,inputs)。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

// useRef,返回一个可变的ref对象
const refContainer = useRef(initialValue);
// useImperativeMethods,详情自行查阅文档
// useMutationEffect,类似useEffect,详情自行查阅文档
// useLayoutEffect,类似useEffect,详情自行查阅文档

那么在新的 API 支持下,如何做全局的状态管理呢?

首先我们看到官方提供了 useReducer 方法,可以实现类似redux的效果。

但是这个方案有一个明显的问题,这里定义的state是和组件绑定的,和useState一样,无法和其他组件共享数据。其实useReducer内部也是用useState实现的。

另外一个方案,基于useContext,同时配合useReducer一起使用。

我们知道,React.createContext()是一种生产消费者的模式,我们可以在组件树顶层使用Context.Provider生产/改变数据,在子组件使用Context.Consumer消费数据。

那么基于这一点,我们可以在顶层组件使用const [state,dispatch] = useReducer(reducer, initialState),然后将返回的state以及dispatch方法传递给Context.Provider,变通的实现数据共享,大约类似下面的代码:

const myContext = React.createContext();
const ContextProvider = ()=>{
    const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <myContext.Provider value={{ state, dispatch }}>
      {props.children}
    </myContext.Provider>
  );
};

然后就可以在子组件使用const { state, dispatch } = useContext(myContext);获取到全局的statedispatch了。

但是,这个方案的缺陷是,当数据太大,组件太多,会直接导致渲染性能下降。

每一次state的变化,都会从顶层组件传递下去,性能影响比较大。

当然也有一些优化手段,比如使用memo()或者useMemo(),又或者拆分更细粒度的context,对应不同的数据模块,再包装成不同的ContextProvider,只是这样略显繁琐了。

那么还有木有其他的方案呢?

我们知道在使用hooks时,顺序非常重要,并且不允许在条件分支、循环嵌套等代码中使用。

稍微了解一下 hooks 的实现原理,每一个无状态组件在调用 Hooks API 时,会将 state 存储在当前组件对象的一个叫做memoizedState的属性中,类似这样:

// 假设一个屋状态组件被解析成这样一个对象:
const obj = {
    ..., // 其他的属性
    memoizedState:{
        memoizedState, // 存储第一次调用 useState 的 state
        next: {
            memoizedState, // 存储第二次调用 useState 的 state
            next:{
                memoizedState, // 第三次
                next, // 继续,如果有更多次调用的话。
            }
        }
    }
}

那么我们是不是通过实现一个发布订阅的模式,来将各个不同的组件和中心化的store之间建立一个关联关系?

流程如下:

  1. store 内部实现一个useModel方法,内部调用useState,但返回全局的store.statestore.dispatch。这一步实现了全局数据共享。
  2. store 内部将每次调用useState返回的setState方法储存到一个队列。
  3. store的数据发生变化时,按顺序调用队列里的setState方法,触发每个子组件的渲染。
  4. 根据调用hooks是有顺序的这个特点,利用useEffect方法,安全的订阅和取消订阅,避免组件销毁了仍然通知组件数据变化。

一个粗糙的例子:

// Model 类
class Model {
    state:{
        name: 'lilei'   
    },
    actions:{},
    queue: [],
    constructor({initialState,actions}){
        this.state = initialState;
        this.actions = {};
        Object.keys(actions).forEach((name)=>{
            this.actions[name] = (...args)=>{
                this.state = actions[name].apply(this,args);
                this.onDataChange();
            }
        });
    },
    useModel(){
        const [, setState] = useState();
        // 使用useEffect实现发布订阅
      useEffect(() => {
        const index = this.queue.length;
        this.queue.push(setState); // 订阅
        return () => { // 组件销毁时取消
          this.queue.splice(index, 1);
        };
      });
        return [this.state, this.actions];
    },
    onDataChange(){
        const queues = [].concat(this.queue);
        this.queue.length = 0;
        queues.forEach((setState)=>{
            setState(this.state); // 通知所有的组件数据变化
        });
    }
}

// models/user.js
const user = new Model({
    initialState,
    actions:{
        changeName(name){
            return {name};
        }
    }
});

// 组件
import {useModel} from '../models/user';
const Person = ()=>{
    const [state,actions] = useModel();
    return (
        <div>
            <span> My name is {state.name}.</span>
            <button onClick={()=> actions.changeName('han meimei.')}>btn1</button>
        </div>
    )
};

我实现了一个稍微完整一些的例子,代码也不复杂,大约100行的样子,其中支持了异步方法、loading的处理,有兴趣的可以移步github看看,地址在这里:

GitHub - yisbug/react-hooks-model

目前的实现还比较粗糙,也借鉴了很多其他方案。 但相比其他方案来说,如果理解了 Hooks 的用法,上手是非常简单的,没有太多的概念。

并且基于这个思路,也可以很容易的扩展一些功能,实现一个比较完整的model层,再通过一个单例的store统一管理所有model,例如实现这样的效果:

import {useModel} from 'store';
const [userState, user] = useModel('user');
const [articleState, article] = useModel('article');
userEffect(()=>{
    user.getUserInfo();
    article.getList();
},[]);

这里只是提供了一些思路,除了以上,还有木有其他的方案呢?欢迎留言讨论哈~

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐