手写简易版hox

985 阅读4分钟

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,没啥问题。

2.png

这样做的话使用一次没啥问题,但是要使用多次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, 这样就保证了所有使用的地方的数据一致性。

3.png

继续实现添加订阅,使用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。

4.png

在createModel时render一个Executor组件。

5.png

把model和触发更新的逻辑放在一个Executor组件中进行调度,

6.png

这样子就不会重复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文章的评论。

7.png

总结

这次写的createModel只是简陋版本,方便大家理解hox的原理。 hox的源码也很容易阅读,大家感兴趣的可以自己看看源码。 就到这里了,祝大家新一年里万事如意,拜拜。