复习 & 总结
背景
在开发 React 组件时,我们要尽可能避免在每个组件中编写重复的代码。而是应该将这部分逻辑抽象出来,形成通用的功能,并在不同组件之间共享这些功能。
Mixin
Mixin 是 React 初期用来解决代码复用问题而提出的,后来因为隐式依赖和命名冲突的问题,逐步被 HOC 和 Hooks 所取代。
Mixin 并不是 React 独有的概念,而是一种普遍适用于面向对象编程领域的设计模式。
继承分为单继承和多继承问题,多继承是相当危险的,会带来继承歧义的问题。比如『钻石问题』。但只使用单继承,又无法实现一些繁琐的抽象。Mixin 就是实现多继承的一种模式。
为什么不推荐使用 Mixin
- 组件和 Mixin 的关系是非显示的,混乱的,组件可以调用 Mixin 方法,Mixin 中也可以使用组件中定义的状态和方法,两者之间的联系仅仅时组件声明的 Mixin 数组
- 由于 JavaScript 是动态语言,因此语言层面无法追踪和管理这些依赖
- 场景1:由于 Mixin 可以使用组件的 state,但后续迭代中,你可能会修改组件的 state,这时候你会考虑到该 state 被某一个 Mixin 使用了吗?如果该迭代由其他同事开发,他就更难发现某个角落有个 Mixin 在默默使用这个 state。就算你发现了要修改某个 Mixin,但是你有什么办法保证这个 Mixin 没有被别人组件使用呢。你的修改可能会破坏掉另外一个使用该 Mixin 的组件,但你对此可能一无所知
- 场景2:如果你在组件中使用某个方法,但组件中没有发现该发现,基本可以认定是 Mixin 混入的,当你发现组件的 Mixin 列表中声明了很多个 Mixin 时,你一定很奔溃。更可怕的是一个 Mixin 可以混入另外的 Mixin,理论上来讲,查找的层次可以是无限的
- 场景3:如果你是 Mixin 的开发者,你可能永远不知道你添加一个方法会带来什么后果,可能会和使用该组件的某个函数同名,从而出现命名冲突的情况
HOC
从函数式编程中,高阶函数演变过来的,本身是一个函数,接收组件作为参数,并返回一个新的组件。
Mixin 是面向对象语言的模式,HOC 就是函数式编程的模式
简单 HOC 演示
const withLoading = WrappedComponent => {
const ComponentWithLoading = props => {
const [isLoadingVisible, setLoadingVisible] = useState(false)
return (
<>
{isLoadingVisible ? <div className="loading">Loading...</div> : null}
<WrappedComponent {...props} setLoadingVisible={setLoadingVisible} />
</div>
)
}
return ComponentWithLoading
}
HOC 与 Mixin 相比的具体优势
- HOC 这种模式是将组件进行更高一层的封装,组件本身并不会受到任何入侵
- HOC 中组件和功能是解耦的,HOC 可以被应用到任何组件中,组件也可以被任何 HOC 包裹,随意拆分和组合
HOC 具体应用
- 日志收集
- 状态传递
- 身份认证
- UI 动作响应
以上常用的功能都可以被抽象成各种各样的 HOC。
HOC 存在的问题
- JSX 嵌套地狱,并且其中大部分节点都是无意义的
- 不要轻易更改 HOC 嵌套的顺序,HOC 的本质是组合,组合就一定会涉及到执行的先后顺序,如果破坏了顺序,可能会就会出问题
- props 传递问题,多个 HOC 组合时,需要将顶层的 props 一层一层传递到最内部的组件,在传递的过程中,props 可能会在每一层 HOC 中被加工,这样的加工是孤立的,当前的 HOC 不知道别的 HOC 对 props 做了什么,别的 HOC 也不知道当前的 HOC 对 props 做了什么,这就带来了修改冲突
Render Props
开发者可以将一些通用的状态和逻辑封装在特定组件中,之后通过 Render Props 的渲染函数将这些通用功能复用到其他组件中。
Render Props 就是为父组件的 props 添加一个返回组件的函数,并在父组件 render 的时候执行这个函数
实时追踪鼠标位置示例
const MouseTracker = props => {
const [position, setPosition] = useState({x: 0, y: 0})
const handleMouseMove = event => {
setPosition({
x: event.clientX,
y: event.clientY
})
}
return (
<div onMouseMove={handleMouseMove}>
{props.render(position)}
</div>
)
}
// 具体使用
const App = () => {
return <MouseTracker render={position => <Picture mouse={position}>} />
}
props 上的任何函数都可以执行渲染行为,甚至是函数子节点
上一节了解了 HOC,该功能也可以使用 HOC 实现如下,你可以直观对比下
const withMousePosition = WrappedComponent => {
const ComponentWithMousePosition = props => {
const [position, setPosition] = useState({x: 0, y: 0})
const handleMouseMove = event => {
setPosition({
x: event.clientX,
y: event.clientY
})
}
return (
<div onMouseMove={handleMouseMove}>
<WrappedComponent {...props} mousePosition={position} />
</div>
)
}
return withMousePosition
}
// 具体使用
const PictureWithMousePosition = withMousePosition(<Picture />)
const App = () => {
return <PictureWithMousePosition />
}
既然 HOC 也可以达到目的,那么为什么不采用 HOC,而会出现 Render Props 这种模式呢
- HOC 嵌套地狱会给调试带来困难
- HOC props 传递问题,如果两个组件先后向 props 注入了同名的属性,会产生命名冲突问题
- Render Props 正好解决了上述两个问题
- 不会额外增加任何用于包装的组件,所有被渲染的组件都是真实有意义的,比如上面的例子 HOC 还会增加 withMousePosition 这一层组件
- 不会出现 props 传递异常问题,因为命名有组件自身决定
- Render Props 最大的不同点:组件的组合时机
- HOC 是静态执行
- 由于每执行一次都会产生新的组件,因此 HOC 都会提前在组件外部执行
- 由于在组件外执行,因此无法获取,组件状态,无法感知组件的声明周期,因此如果想根据组件状态,判断执行那种 HOC 组合是不可能的
- Render Props 是动态执行
- 可以很好的利用组件的声明周期和状态,能够在渲染时动态决定执行哪些组件
- HOC 是静态执行
Render Props 存在的问题
- Callback 嵌套地狱问题
- 与 PureComponent 产生冲突所引发的性能问题,因此在使用 Render Props 需要注意将 Render 函数变成不可变引用
Hooks
Hooks 并不会像 Mixin 那样入侵你的组件,也不会像 HOC 哪些打乱组件的结构,用一种很自然的方式,将逻辑从组件中提取出来。
关于 Hooks 已经在其他文章说的够多了,就不赘述了。