我已经困扰了几年,关于这个问题,以下是我的理解。我不断地完善这些想法,因此请持保留态度,如果感觉错误或不清楚,请毫不犹豫地进行评论或澄清。同时,像任何通用的或基于模式的解决方案一样,它并不总是值得付出成本或适合某种情况。
组件仅涉及数据下传和事件上传
这里的基本前提是,任何给定组件的实现代码应仅使用来自其他代码的纯数据和回调来工作。所谓“来自其他代码”,指的是prop、钩子的输出或纯计算的结果。所谓“纯数据”,是指任何可变实例都应该被“投影”为不可变的子集,以供组件消耗,或者至少被视为不透明,使得可变的细节被忽略。
简而言之:甚至不要让组件“知道”可变模型的存在!让其他事物为它们做点简单的事情。组件应专注于呈现给定的数据,并通过回调向上报告事件。
对于你的示例,这意味着既不应该让 App
也不应该让 CarViewer
直接操作 Car
。
使用Hooks处理模型
那么,基于这个规则,像 Car
这样的模型如何成为组件可以使用的东西呢?Props和纯函数无法解决问题,只能将其推回;毕竟,数据仍然必须来自某处。这只留下了hooks。
但是,“使用hook”过于模糊,没有实际意义。我们需要知道a)首先如何获得您手头的模型,b)如何将其转换为组件允许使用的内容。
维护模型实例
模型从哪里来,我们如何保留它?归根结底,这始于业务逻辑:使用构造函数或工厂函数构建新模型,或从一些外部提供者(如DI容器)中提取共享实例。在你的例子中,这是 new Car(data)
。
一旦实例可用,使用 useRef
存储它。不要将其存储在 useState
中,因为您不会直接渲染该值,并且将需要以其他方式考虑更改。
确保钩子恰好在需要时创建(并清理!)模型。具体细节因用例而异,但此处是有关管理非状态实例的提示,考虑到计划中的更改react挂载行为。
对于您的简单示例,我只需创建一个如下的钩子:
function useCar(initialData) {
const carRef = useRef();
if (!carRef.current) {
carRef.current = new Car(initialData);
}
return carRef.current;
}
维护模型预测
好了,现在您最终拥有了一个包含访问详情的模型实例。组件如何在不违反主要规则的情况下使用它呢?一个钩子必须将相关的模型值和函数投影到不可变的计算子值中,并将其存储在状态中。诀窍在于每当投影值发生更改时,需要使用重新计算的投影更新此状态。这里有两个常用的选项。
第一种完整的选项是让您的模型发布/发出更改事件,以便您可以监听并在每次更改发生时重新计算投影。我喜欢使用 rxjs
来实现这一点。不幸的是,这需要将更改事件考虑在内来设计模型,但如果更改可能来自于视图层次之外(例如来自于推送通知),那么这可能是必要的。
如果无法依赖模型通知您有关更改的信息,则必须拦截自己的更改。为此,您可以包装已投影更改的函数,以便它们直接在更新状态后更新状态。下面是示例代码:
function useCarProjection(car) {
const [projection, setProjection] = useState(projectCar(car));
return {
...projection,
changeColor: (newColor) => {
car.changeColor(newColor);
setProjection(projectCar(car));
},
};
}
function projectCar(car) {
return { color: car.data.color };
}
值得注意的是,我将这个钩子作为参数传递汽车,以便它不必直接访问或存储汽车。您需要第三个钩子来组合这两个钩子,或者您可以将两个钩子合并为一个钩子,以简化情况。