逻辑复用
继进阶技巧记录之后,再起一篇,专门记录逻辑复用的技巧与跨组件通信方法
逻辑复用
复用的目标:组件状态逻辑
复用的内容:state 状态、操作 state 状态的方法
在 Hooks 推出之前,组件的状态逻辑复用经历了:mixins、HOC、render-props 等模式
注意:这几种方式不是 API,而是利用 React 自身特点的编码技巧,演化而成的固定写法模式
已废弃的 mixins
React 的 mixins 跟 Vue2 的 mixins 配置项很类似,都是采用组件混合的方式进行的,但缺点也很明显:组合混乱、命名冲突、维护复杂。因此 React 现在已废弃 mixins
HOC
HOC 是通过装饰器模式,实现组件状态逻辑复用的,接收要包装的组件,返回增强后的组件。
高阶组件命名约定以 with 开头,如:withMouse、withRouter 等
原理:高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过 prop 将复用的状态传递给被包装组件
注意点:
- 设置 displayName 方便在 devTools 中展示名称
- 注意传递 props,不传递 props 会导致增强组件丢失 props
更多记录详见高阶组件
render-props
模式
render-props
将要复用的状态逻辑代码封装到一个组件中,通过一个值为函数的 prop 对外暴露数据,实现状态逻辑复用
不使用 prop 函数的情况下,在组件标签内部声明的 jsx 元素或者 jsx 函数,在组件的 render 函数中,默认从 children 属性中获取
Hooks
最新方案:详见Hook
Hooks
为什么要有我们先分析 Hooks 出现之前 React 存在的问题
- 组件的状态逻辑复用
已废弃的 mixins 的问题:数据来源不清晰、命名冲突
HOC、render-props
的问题:重构组件结构,JSX 嵌套地狱
- class 组件自身的问题
this 指向存在学习门槛
关联代码需要被拆分到不同生命周期函数中
不利用代码压缩和优化,也不利于 TS 类型推导
控制反转
案例
const { useState } = React;
const Children = () => {
console.log("Children rendered");
return <p>I am Children</p>;
};
const Father = () => {
const [count, setCount] = useState(0);
console.log("Father rendered");
return (
<div>
<p>I am Father tag, {count}</p>
<button onClick={() => setCount(count + 1)}>Add Count</button>
<br />
<Children />
</div>
);
};
export default Father;
打开 F12 可以看到,父子组件初次渲染会各打印一次信息,随后每次 button 点击都会重新触发渲染,每点一次就各打印一次信息
这是因为父组件 state 更新触发重新渲染,连带子组件一起重新渲染,但是实际上子组件是一个静态 p 标签,并没有冲渲染的需要,那么这个组件更新就是没有必要的。如果子组件的渲染开销比较大,就可能引发严重的性能问题
我们的第一反应可能是给子组件增加React.memo
转为 props 浅比较,但是秉承能不用就不用的原则,我们选择其他的办法:控制反转(Inversion of Control)
因为<br/>
标签与子组件并不需要来自父组件的状态,所以对上面的代码进行改动,在父组件和子组件之间添加一个 IOC 组件:
const { useState } = React;
const Children = () => {
console.log("Children rendered");
return <p>I am Children</p>;
};
const FatherIoc = ({ children }) => {
const [count, setCount] = useState(0);
return (
<div>
<p>I am Father tag, {count}</p>
<button onClick={() => setCount(count + 1)}>Add Count</button>
{children}
</div>
);
};
const Father = () => {
console.log("Father rendered");
return (
<>
<FatherIoc>
<br />
<Children />
</FatherIoc>
</>
);
};
export default Father;
再打开 F12,无论点击多少次 button,控制台都只停留在页面第一次渲染时打印的信息,说明多次更新都没有触发父组件和子组件的重新渲染
分析
这个案例实际上通过巧妙的render-props
隔离了依赖 state,将子组件挂在 children 上,IOC 组件的更新既不触发外层 Father 组件 rerender,也不触发插槽 Children 组件 rerender
一句话描述:因为 Children 来自于父组件,子组件的重新渲染并不会导致其也重新渲染
完整描述:
- 从 Father 组件开始,Father 满足前 4 个条件,但是其子组件 FatherIoc 存在更新,不满足第 5 个条件,所以进入复用逻辑但不会跳过子组件的对比,此时父节点的 props 也不进入更新,原值赋予给新的子节点
- FatherIoc 子组件的 state 发生了变化,不进入复用逻辑,重新调用生成新 Fiber 节点
- 更新前后父节点的 props 完全相同,children 节点不会重新创建。
<br/>
和<Children>
实际位于父组件树,并非 FatherIoc 的子组件,判断更新条件 5 个条件均满足,跳过对比直接复用
而没有 IOC 组件的原始组件中更新是:
Father 的 state 发生变化,调用生成新的 Fiber 节点,子组件的 props 虽然是空对象,但是 JSX 转换 createElement 的时候,新的空对象与旧的空对象不等,继而重新创建,触发重新渲染