hox 号称下一代状态管理工具,使用方式简单,内部实现轻量。公司内部也有项目逐渐在使用,自己也跟随着学了一下写了个小demo。
1、hox的使用
先创建一个自定义hook,作为参数传递给createModel就可以直接在页面中导入使用了,直接附上官方的教程。
定义model
import { createModel } from 'hox';
/* 任意一个 custom Hook */
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}
export default createModel(useCounter)
使用model
import useCounterModel from "../models/useCounterModel";
function App(props) {
const counter = useCounterModel();
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
2、手写简易版createModel
从createModel使用方式上来看,createModel接收一个hook作为参数,返回一个新的useModelHook, useModelHook可以在页面中使用并返回定义的model,并且在多个页面中使用都是共享同一份model。
要说原理的话引用尤大大的话。
尤雨溪:就是个singleton
自己研究了发现使用了闭包形成单例 + 发布订阅模式。
下面我们就一起来探索一下,先创建createModel 函数,返回一个hook。
下文中的model都指useCounter返回的 { count, decrement, increment }; useModelHook 指 createModel 返回的 hook
export default function createModel(hook) {
return () => {} //返回一个hook
}
返回的hook中我们要获取参数hook返回的model,并将其返回给组件中使用。
export default function createModel(hook) {
return () => {
const data = hook()
return data
}
}
按照上面介绍的使用方式,创建model,在页面中使用一下,控制台输出counter,没啥问题。
这样做的话使用一次没啥问题,但是要使用多次useCounterModel, 在触发increment改变counter的时候因为每次使用useCounterModel都会创建一个新model, 这会导致每个counter都是独立的,这与全局数据共享相违背。
function App() {
const counter = useCounterModel(); // Increment 点击后 coutner 更新
const counter1 = useCounterModel(); // counter1 不更新
return (
<div>
<p>{counter.count}</p>
<p>{counter1.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
为了保证使用的都是同一份model, hox是实现了一个container实现发布订阅, 并且在useModelHook中定义了一个state来保存同一份container中的data, 先来实现一下container。
const container = {
data: null,
subscribers: new Set(),
notify() {
for (const subscriber of this.subscribers) {
subscriber(this.data);
}
}
}
container.data存放自定义hook中返回的model,每次使用useModelHook
都会在内部创建一个subscriber函数用来更新自身的state,
container.subscribers用来存放这些函数,
在任意地方改变model都会触发这些subscriber来重新设置各自的state,
这样就保证了所有使用的地方的数据一致性。
继续实现添加订阅,使用useEffect添加依赖container, 使用createModel后创建一个container,被返回的hook闭包使用,引用地址不变, 所以当使用useCounterModel的时候就会添加一个subscriber至container.subscribers,而不会被重复添加。
function createModel(hook) {
const container = {
data: null,
subscribers: new Set(),
notify() {
for (const subscriber of this.subscribers) {
subscriber(this.data);
}
}
}
return () => {
const data = hook()
const [state, setState] = useState(data) // 将公共的data存为自己私有的state
useEffect(() => {
if (!container) return;
const subscriber = (val) => setState(val) // 每次自定义hook的状态变化都会设置自身的状态变化
container.subscribers.add(subscriber);
return () => container.subscribers.delete(subscriber);
}, [container]);
return state
}
}
订阅完就要想想怎么在model变化的时候触发这些订阅了。 选择useLayoutEffect在页面渲染之前触发所有的订阅,等待所有的内部状态改变后更新页面 ,减少不必要的更新。
function createModel(hook) {
const container = {
data: null,
subscribers: new Set(),
notify() {
for (const subscriber of this.subscribers) {
subscriber(this.data);
}
}
}
return () => {
const data = hook()
const [state, setState] = useState(data)
useLayoutEffect(() => {
container.data = data;
container.notify();
}, [JSON.stringify(data)])
useEffect(() => {
if (!container) return;
const subscriber = (val) => setState(val)
container.subscribers.add(subscriber);
return () => container.subscribers.delete(subscriber);
}, [container]);
return state
}
}
页面中使用一下,两个counter都变化了,至此就一个简单的createModel就完成了。 但在测试的时候发现一个问题,点击increment页面会re-render多次。 若是定义了多个model性能很差,hox的官方则是利用了ReactReconciler重新写了个render。
在createModel时render一个Executor组件。
把model和触发更新的逻辑放在一个Executor组件中进行调度,
这样子就不会重复re-render了,我直呼🐂🖊。
3、番外
Vue.js团队核心成员开发的新一代状态管理器Pinia.js。使用Composition Api进行重新设计的,也被视为下一代Vuex。
看一下基本使用
//src/store/modules/counter.ts
import {defineStore} from "pinia"
export const useCounterStore = defineStore("counter",{
state:()=>{
return {
count:0
}
},
actions:{
increment(){
this.count++
}
}
})
vue文件中
<template>
<div>count:{{counter.count}}</div>
</template>
<script lang="ts" setup>
import { useCounterStore } from '@store/modules/counter';
const counter = useCounterStore();
counter.increment()
</script>
使用方式上怎么有点和hox神似。不过Pinia提供的api更加丰富,最近比较火热可以尝试一下。
回头看了一眼知乎上介绍hox文章的评论。
总结
这次写的createModel只是简陋版本,方便大家理解hox的原理。 hox的源码也很容易阅读,大家感兴趣的可以自己看看源码。 就到这里了,祝大家新一年里万事如意,拜拜。