React 源码版本: v16.9.0 源码注释笔记:airingursb/react
如何复用和扩展 React 组件的状态逻辑?具体而言,有以下五种方案:
下面,我们一一介绍五种方案的实现。
Mixins 混合,其将一个对象的属性拷贝到另一个对象上面去,其实就是对象的融合,它的出现主要就是为了解决代码复用问题。
扩展:说到对象融合,
Object.assign
也是常用的方法,它跟 Mixins 有一个重大的区别在于 Mixins 会把原型链上的属性一并复制过去(因为for...in
),而Object.assign
则不会。
由于现在 React 已经不再支持 Mixin 了,所以本文不再赘述其如何使用。至于以前在 React 中如何使用 Mixin ,请参考这篇文章:React Mixin 的使用 | segmentfault
Mixins 虽然能解决代码复用的问题,但是其会产生许多问题,甚至弊大于利,由此 React 现在已经不支持 Mixins 了。具体而言,有以下几个缺点:
FluxListenerMixin
定义 handleChange()
和 WindowSizeMixin
定义 handleChange()
,则不能同时使用它们,甚至我们也无法在自己的组件上定义具有此名称的方法。说到类组件的代码逻辑复用,熟悉 OOP 的同学肯定第一时间想到了类的继承,A 组件只要继承 B 组件就可以复用父类中的方法。但同样的,我也相信使用 React 的同学不会用继承的方法去复用组件的逻辑。
这里主要的考虑是代码质量问题,如果两个组件本身业务比较复杂,做成继承的方式就很不好,阅读子组件代码的时候,对于那么不明就里的、没有在该组件中声明的方法还需要跑到去父组件里去定位,而 React 希望一个组件只专注于一件事。
另外,如果重写子组件的生命周期,那父组件的生命周期会被覆盖,这也是我们在开发中不愿意看到的。
Facebook 对在 React 中使用继承这件事“深恶痛绝”,官网在 Composition vs Inheritance 一文中写到:“在 Facebook,我们在成百上千个组件中使用 React,我们并没有发现需要使用继承来构建组件层次的情况。”
的确,函数式编程和组件式编程思想某种意义上是一致的,它们都是“组合的艺术”,一个大的函数可以有多个职责单一的函数组合而成。同样的,组件也是如此。我们做 React 开发时,总是会不停规划组件,将大组件拆分成子组件,对组件做更细粒度的控制,从而保证组件的纯净性,使得组件的职责更单一、更独立。组合带来的好处就是可复用性、可测试性和可预测性。
因此,优先考虑组合,才去考虑继承,并且 Facebook 在官网的文章中推荐使用 HOC 去实现组件的逻辑复用(详见《Higher-Order Components》),那下面我们就来看一看 HOC 到底是什么。
HOC,Higher-Order Component,即高阶组件。虽然名字很高级,但其实和高阶函数一样并没有什么神奇的地方。
回顾一下高阶函数的定义:
其实高阶组件也就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。需要注意的是高阶组件是一个函数,并不是一个组件。可见 HOC 其实就是一个装饰器,因此也可以使用 ES 7 中的装饰器语法,而本文为了代码的直观性就不使用装饰器语法了。
扩展阅读:装饰器提案 proposal-decorators | GitHub
高阶组件也有两种实现:
由于继承官方不推崇,继承式的 HOC 可能会原始组件的逻辑而并非简单的复用和扩展,因此继承式的 HOC 依然有许多弊端,我们这里就列一段代码展示一下,但就不展开讲了。
// 继承式 HOC
import React, { Component } from 'react'
export default const HOC = (WrappedComponent) => class NewComponent extends WrappedComponent {
componentWillMount() { console.log('这里会修改原始组件的生命周期') }
render() { const element = super.render() const newProps = { ...this.props, style: { color: 'red' }} return React.cloneElement(element, newProps, element.props.children) }}
可以看到继承式的 HOC 也确实可以复用和扩展原始组件的逻辑。而代理式的 HOC 更加简单,接下来举个例子来看看。
这里有两个组件 Profile 和 Home,两个组件都被 Container 包裹,且每个 Container 的样式一样并且都有一个 title。这里我们希望 Profile 和 Home 都可以复用 Container 的样式和结构,现在我们用 HOC 实现一下:
// app.js
import React from "react";import ReactDOM from "react-dom";import Profile from "./components/Profile";import Home from "./components/Home";import "./styles.css";
function App() { return ( <div className="App"> <Profile name={"Airing"} /> <Home /> </div> );}const rootElement = document.getElementById("root");ReactDOM.render(<App />, rootElement);
// Container.js
import React, { Component } from "react";import "../styles.css";
export default title => WrappedComponent => class Container extends Component { render() { return ( <div className="container"> <header className="header"> {title} </header> <div> <WrappedComponent url={"https://me.ursb.me"} {...this.props} /> </div> </div> ); } };
// Profile.js
import React, { Component } from "react";import WrappedComponent from "./WrappedComponent";
class Profile extends Component { render() { return ( <> <p>Author: {this.props.name}</p> <p>Blog: {this.props.url}</p> <p>Component A</p> </> ); }}
export default WrappedComponent("Profile")(Profile);
// Home.js
import React, { Component } from "react";import WrappedComponent from "./WrappedComponent";
class Home extends Component { render() { return ( <> <p>Component B</p> </> ); }}
export default WrappedComponent("Home")(Home);
可以发现这里的 HOC 其实本质上是原始组件的一个代理,在新组件的 render 函数中,将被包裹组件渲染出来,除了 HOC 自己要做的工作,其余功能全都转手给了被包裹的组件。
而 Redux 的 connect
函数其实也是 HOC 的一个应用。
ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component);
等同于
// connect是一个返回函数的函数(就是个高阶函数)const enhance = connect(mapStateToProps, mapDispatchToProps);// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store// 关联起来的新组件const ConnectedComment = enhance(Component);
另外,还有 antd 的 Form 也是用 HOC 实现的。
const WrappedNormalLoginForm = Form.create()(NormalLoginForm);
虽然 HOC 在组件逻辑复用上提供了很多便利,也有许多项目会使用这种模式,但 HOC 还是存在一些缺点的:
displayName
/** * 使用高阶组件,我们可以代理所有的props,但往往特定的HOC只会用到其中的一个或几个props。 * 我们需要把其他不相关的props透传给原组件 */
function visible(WrappedComponent) { return class extends Component { render() { const { visible, ...props } = this.props; if (visible === false) return null; return <WrappedComponent {...props} />; } }}
下图对比了 Mixin 和 HOC 的差异:(图源:【React深入】从Mixin到HOC再到Hook)
Render Props 其实很常见,比如 React Context API:
class App extends React.Component { render() { return ( <ThemeProvider> <ThemeContext.Consumer> {val => <div>{val}</div>} </ThemeContext.Consumer> </ThemeProvider> ) } }
React 的 props 并没有限定类型,它可以是一个函数,于是就有了 render props,这种模式也很常见。它的实现思路很简单,把原来该放组件的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。
但是,这会产生和 HOC 一样的 Wrapper Hell 问题。
而以上的问题,使用 Hooks 均可以得到解决,Hooks 可谓是组件逻辑复用扩展的完美方案。具体而言,有以下优点:
至于 Hooks 的具体实现,我们下一篇文章中再谈。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有