hox做数据存储的方案是通过闭包实现的:
hox api只有一个 createModel(hook); 文档连接:github.com/umijs/hox
我模仿hox的源码写了一个简化版的数据存储,主要是为了分析原理,去掉了传入hook的逻辑处理(container)// 这里的数据是写死的,为了方便梳理分析过程,等等继续深入分析源码传入hook的处理。:
CreateModel 部分
export function myCreateModel() {
const element = document.createElement('div');
let state = {
count: 1,
decrement: () => {},
increment: () => {},
notify: val => {},
};
ReactDOM.render(
<Executor
onUpdate={val => {
state = Object.assign(state, val);
state.notify(val);
}}
/>,
element
);
function useModel() {
let [data, setData] = useState(state);
useEffect(() => {
state.notify = val => setData(val) as any;
}, [state]);
return data;
}
return useModel;
}
export const useMyFn = myCreateModel();
代码说到useModel之前:
这里我先用一个临时变量state保存数据。
然后实例化一个函数组件Executor挂载到空div上,且Executor接受一个更新state的函数取名onUpdate。
再来看看Executor做了什么:
对hooks不熟悉的可以看这里:zh-hans.reactjs.org/docs/hooks-…
Executor 部分
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react';
export function Executor<T>(props) {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
const data = {
count,
decrement,
increment,
};
props.onUpdate(data);
return null as ReactElement;
}
Executor使用了useState来保存count,并且有两个更新count的fn,除了notify方法,data与上方的state属性一致。
这里其实挂载的是一个null,但是把Executor的逻辑保存了起来,利用了react的hooks特性,每次Executor的count发生变化时,都会触发内部逻辑。即执行传入的onUpdate方法。
而onUpdate方法会将Executor的data通过Object.assign方法赋值给createModel的临时变量state,从而更新了createModel中的state。
继续看createModel后续做了什么:
声明了useModel方法,useModel是一个自定义hook,用useState保存了临时变量 state作为data的初始化。且将state的notify属性改为更新自身的data。函数最后返回data。
createModel最后将useModel返回出去。
也就是说在你用createModel包了一层之后,其实后续使用的是useModel这个hook。
createModel利用闭包,将state数据以及Executor触发更新的执行器保存下来之后返回一个useModel。然后useModel会返回自身的data,这个data是与state数据保持一致的。就做到了数据存储。
这里有一篇作者写的简要的原理分析:zhuanlan.zhihu.com/p/89518937 会对整个hox原理有个大致的认知。
看一下使用例子:
示例
import React from 'react';
import { useMyFn } from './myHox';
export default function App(props) {
const data = useMyFn();
return (
<div>
<button onClick={data.decrement}>Decrement</button>
<p>{data.count}</p>
<br />
</div>
);
}
这个组件使用了useMyFn也就是myCreateModel() ---→ 其实就是useModel。
这里的data即是useModel返回的data。但点击按钮的时候,触发了data.decrement函数,如果你对Executor还有印象的话,其实Executor的decrement函数,会将count减1,此时Executor的useState的数据发生了变更,触发react的renderWithHooks函数,会走一遍Executor的逻辑,触发useEffect,进而更新了外部闭包的state以及useModel的data。
而useModel的data会呈现到视图中,渲染视图更新。
上面就是createModel核心的数据存储变更的工作流程。
此时我们已经实现了静态数据存储,也就是createModel写死的state。当我们需要保存动态的数据时,只需要将state改成动态传入的参数(必须为hook,只有hook才能触发react的renderWithHooks函数),再对onUpdate做些更改即可。
此时的createModel还无法动态传入自定义hook。接下来看看源码是什么样的:
源码多出来的部分是干嘛用的:
CreateModel 源码
import { ModelHook, UseModel } from "./types";
import { Container } from "./container";
import ReactDOM from "react-dom";
import { Executor } from "./executor";
import React, { useEffect, useRef, useState } from "react";
export function createModel<T>(hook: ModelHook<T>) {
const element = document.createElement("div");
const container = new Container(hook);
ReactDOM.render(
<Executor
onUpdate={val => {
container.data = val;
container.notify();
}}
hook={hook}
/>,
element
);
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;
}
这里多出了一个container,看一下container的源码:
Container 源码
export class Container<T = unknown> {
constructor(public hook: ModelHook<T>) {}
subscribers = new Set<Subscriber<T>>();
data!: T;
notify() {
for (const subscriber of this.subscribers) {
subscriber(this.data);
}
}
}
container的源码很简单,是一个类,构造函数是传入的hook。有个data属性,有个notify方法,看起来很像订阅发布的功能。
container分析完了,接着看Executor发生了上面变化:
Executor 源码
export function Executor<T>(props: {
hook: ModelHook<T>;
onUpdate: (data: T) => void;
}) {
const data = props.hook();
const initialLoad = useRef(false);
useMemo(() => {
// notify the initial value
props.onUpdate(data);
initialLoad.current = false;
}, [])
useEffect(()=>{
if (initialLoad.current) {
// notify the following value changes
props.onUpdate(data);
} else {
initialLoad.current = true;
}
})
return null as ReactElement;
}
可以看到,Executor只有data不一样,此时的data是传入的自定义hook逻辑执行完之后的返回值。
以及传入的onUpdate发生了变化:将新的val值更新到container的data属性,并且调用container的notify方法。
到目前可以知道,createModel先是创建了一个container的实例,用于保存传入自定义hook的返回值(container的构造函数就是hook)
接着创建了一个Executor组件实例,同时组件实例自身也拥有一个hook(即传入的hook)保存在Executor的data(其实和之前写死的hook逻辑一个道理),当data发生改变时会触发Executor的逻辑执行。
接着看看useModel做了什么:
useModel 部分
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;
container.subscribers.add(subscriber);
return () => {
container.subscribers.delete(subscriber);
};
}, [container]);
return state!;
};
这里为了方便看清楚逻辑去掉了subscriber这个函数的定义。
useModel先是用useState保存container的data,然后定义了一个变量depsFnRef,用来保存使用useModel传入的函数,传入的函数作为判断state是否变更的依赖。
然后useEffect里面每当container发生变化时,就往container的subscribers增加一个subscriber,组件销毁的时候就会delete掉这个subscriber。
最后useModel返回state,也就是container.data的副本。
来看看subscribe这个函数定义:
subscribe
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;
}
}
判断是否有传入的变更依赖,没有直接设置当前的state为val,有的话就判断变更依赖是否改变,如改变则setState为val。
其实subscribe函数就是用来更新useModel的state。
subscribe会在container调用notify的时候触发,也就是在Executor触发onUpdate的时候触发,也就是在Executor的data发生改变的时候触发。Executor的data会通过onUpdate传入到container的data,进而传入到useModel的state。
就是通过Executor和container的hook逻辑一致,通过闭包保存了container的实例,来保存数据;通过Executor的实例组件来触发更新,最终反应到useModel上。
当我们用createModel包裹了我们的自定义hook之后,所使用的其实是createModel返回的useModel。
createModel通过闭包存储了数据,useModel则会同步这个数据,从而实现数据存储。
最后
最终的使用示例:官方demo
创建一个自定义hook,并且使用createModel API
import { createModel } from "hox";
import { useState } from 'react';
export 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);
在组件中引用这个向外导出的hook
import useCounterModel from './state';
function Myref() {
const counter = useCounterModel();
const onButtonClick = () => {
counter.increment();
};
return (
<>
<p>{counter.count}</p>
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
demo仓库:github.com/mango-lzp/h…
是一个create-react-app项目 加了hox,可以clone下来自己体验一下。
更新
///
更新:最新的代码改了Executor和createModel中ReactDom.render的部分。
Executor去掉了没有作用的判断,以及render使用了react-reconciler库代替原先的ReactDom.render。 hox核心逻辑(闭包暂存+订阅派发)没有变动。