React 最新正式版已经支持了 Hooks API,先快速过一下新的 API 和大概的用法。
// useState,简单粗暴,setState可以直接修改整个stateconst [state,setState] = useState(value);
// useEffect,支持生命周期useEffect(()=>{
// sub return ()=>{
// unsub }
},[]);
// useContext,和 React.createConext() 配合使用。// 父组件使用 Context.Provider 生产数据,子组件使用 useContext() 获取数据。const state = useContext(myContext);
// useReducer,具体用法和redux类似,使用dispatch(action)修改数据。// reducer中处理数据并返回新的stateconst [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 (
{props.children}
);
};
然后就可以在子组件使用const { state, dispatch } = useContext(myContext);获取到全局的state和dispatch了。
但是,这个方案的缺陷是,当数据太大,组件太多,会直接导致渲染性能下降。
每一次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之间建立一个关联关系?
流程如下:store 内部实现一个useModel方法,内部调用useState,但返回全局的store.state和store.dispatch。这一步实现了全局数据共享。
store 内部将每次调用useState返回的setState方法储存到一个队列。
当store的数据发生变化时,按顺序调用队列里的setState方法,触发每个子组件的渲染。
根据调用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.jsconst user = new Model({
initialState,
actions:{
changeName(name){
return {name};
}
}
});
// 组件import {useModel} from '../models/user';
const Person = ()=>{
const [state,actions] = useModel();
return (
My name is {state.name}.
actions.changeName('han meimei.')}>btn1
)
};
我实现了一个稍微完整一些的例子,代码也不复杂,大约100行的样子,其中支持了异步方法、loading的处理,有兴趣的可以移步github看看,地址在这里:
目前的实现还比较粗糙,也借鉴了很多其他方案。 但相比其他方案来说,如果理解了 Hooks 的用法,上手是非常简单的,没有太多的概念。
并且基于这个思路,也可以很容易的扩展一些功能,实现一个比较完整的model层,再通过一个单例的store统一管理所有model,例如实现这样的效果:
import {useModel} from 'store';
const [userState, user] = useModel('user');
const [articleState, article] = useModel('article');
userEffect(()=>{
user.getUserInfo();
article.getList();
},[]);
这里只是提供了一些思路,除了以上,还有木有其他的方案呢?欢迎留言讨论哈~