声明式编程是一种编程范式,它侧重于描述 xx 做什么
而不是 怎么做 xx
。与命令式编程(描述如何一步步完成任务)相对,声明式编程更关注结果的表达,而不是实现的细节。
假设我们要从数据表中获取一个年龄大于 18 的,可以编写如下 SQL 代码:
SELECT name FROM users WHERE age > 18;
在上面的这段 sql 中,它声明了要从 users 表中选择所有 age 大于 18 的用户的 name,而不涉及如何遍历数据表的细节。
React 通过 JSX(或 JavaScript 函数)来声明 UI 组件的结构,而不是命令式地描述如何一步一步地构建 UI。开发者只需要描述最终的 UI 应该是什么样子,而不需要关心具体的 DOM 操作。
const App = () => (
<div>
<h1>Moment!</h1>
<button onClick={() => alert('Moment!')}>点击</button>
</div>
);
在这段代码中,我们声明了一个包含标题和按钮的界面,但没有涉及如何在 DOM 中插入这些元素。
在 react 生态圈里面,组件的设计非常自由。因为 react 本身只提供组件渲染以及生命周期、hooks,至于用户交互、数据交互、组件状态之间的关系,并没有强关联。
React 的设计理念与声明式编程紧密相关。React 鼓励开发者以声明式的方式构建 UI 和管理状态,这不仅使代码更加简洁和易于维护,还提高了代码的可读性和可测试性。React 组件的设计自由度体现了声明式编程的核心思想,即关注结果而非过程,通过高层次的抽象来隐藏实现细节。
组件设计模式
要区分组件的设计模式,这里需要搞清楚基于场景的设计分类,在 React 社区中把组件分成了两类,一类是把只作展示、独立运行、不额外增加功能的组件,称之为傻瓜组件,另一类是把处理业务逻辑与数据状态的组件称之为有聪明组件,它一定包含至少一个灵巧组件或展示组件。
傻瓜组件是只负责 UI 呈现的组件,它们通过接收 props 来渲染内容,不管自己的状态,也不包含复杂的逻辑,这种组件通常是函数组件,它的特点主要有以下几个方面:
-
无状态:哑组件不管理自己的状态,只通过 props 获取数据。
-
可复用:因为没有业务逻辑和状态管理,哑组件更容易复用。
-
易于测试:由于只依赖输入的 props,哑组件的测试变得非常简单。
-
纯函数:大多数哑组件都可以写成纯函数。
如下代码所示:
const Moment = ({ name, email }) => (
<div className="moment">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
export default Moment;
在上面的这些代码中,Moment 组件就是一个傻瓜组件,它通过 props 接收 name 和 email,并用这些数据渲染用户卡片。
对于傻瓜组件,React 也提供了两个不同的方法来提供用户使用来提升傻瓜组件的性能,在类组件中,我们可以使用 PureComponent 自动对 props 和 state 进行浅比较,并在只有当 props 或 state 发生变化时才重新渲染组件。
import React, { PureComponent } from 'react';
class Moment extends PureComponent {
render() {
const { name, email } = this.props;
return (
<div className="moment">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
}
export default Moment;
在类组件中,除了 PureComponent 之外,我们还可以使用 shouldComponentUpdate 这个声明周期方法来判断当前的 props 是否和上一次的相同,如果相同的话则不需要重新渲染:
import React, { Component } from 'react';
class Moment extends Component {
shouldComponentUpdate(nextProps) {
if (this.props.name !== nextProps.name || this.props.email !== nextProps.email) {
return true;
}
return false;
}
render() {
const { name, email } = this.props;
return (
<div className="moment">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
}
export default Moment;
值得注意都是,PureComponent 中 shouldComponentUpdate 对 props 做得只是浅层比较,不是深层比较,如果 props 是一个深层对象,就容易产生问题。(shouldComponentUpdate 可以对 props 进行一次递归来进行对比可以实现深层比较)
如果你使用都是函数式组件,并且是 16.6.0 之后的版本,那么我们可以使用 React.memo 来实现对该组件的包裹,因此,上面的组件可以编写出这样:
import React, { memo } from 'react';
const deepEqual = (obj1, obj2) => {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
};
const Moment = memo(
({ name, email }) => (
<div className="moment">
<h2>{name}</h2>
<p>{email}</p>
</div>
),
deepEqual,
);
export default Moment;
React.memo 接收两个参数:第一个参数是要优化的函数组件,第二个参数是一个可选的比较函数。这个比较函数接收两个参数,分别是 prevProps 和 nextProps,返回一个布尔值,表示 props 是否相等。如果未提供该函数,memo 将使用浅层比较来决定是否重新渲染组件。
高阶组件(Higher-Order Components, HOC)
高阶组件是一种用于复用组件逻辑的模式。HOC 是一个函数,它接收一个组件并返回一个新的组件,通过这种方式可以增强或修改原组件的行为。它是一种设计模式,这种设计模式是由 React 自身的特性产生的结果。
高阶组件主要解决了以下问题,具体如下:
-
复用逻辑: 高阶组件就像是一个加工 React 组件的工厂,你需要向该工厂提供一个坯子,它可以批量地对你送进来的组件进行加工,包装处理,还可以根据你的需求定制不同的产品。
-
强化 props: 高阶组件返回的组件,可以劫持上一层传过来的 props,染回混入新的 props,来增强组件的功能。
-
控制渲染: 劫持渲染是 hoc 中的一个特性,在高阶组件中,你可以对原来的组件进行条件渲染、节流渲染、懒加载等功能。
HOC 的实现方式
正向属性代理(Forwarding Props Proxy)和反向继承(Inverse Inheritance)是两种用于高阶组件(Higher-Order Components, HOC)实现的方法。它们主要用于增强或修改被包裹组件的行为和特性。
正向属性代理(Forwarding Props Proxy)
所谓正向属性代理就是通过包裹一个组件并直接传递(或增强)其 props 来实现的。正向属性代理 HOC 通过直接渲染被包裹的组件,并且将所有的 props 传递给被包裹组件。
实际上这种方式生成的高阶组件就是原组件的父组件,父组件对子组件进行一系列强化操作。
对比原始组件,高阶组件增强的方面主要有以下几个方面:
-
可操作所有传入的 props: 可以对其传入的 props 进行条件渲染,例如权限控制等。
-
可以操作组件的生命周期。
-
可操作组件的 static 方法,但是需要手动处理,或者引入第三方库。
-
获取 refs。
-
抽象 state。
操作传入的 props
高阶组件可以接收传入的 props,对其进行修改、增强或条件渲染。例如,HOC 可以根据权限控制 props 的传递,从而实现权限控制。
import React from 'react';
const withAuthorization = (WrappedComponent, allowedRoles) => {
return class extends React.Component {
render() {
const { role } = this.props;
if (allowedRoles.includes(role)) {
return <WrappedComponent {...this.props} />;
} else {
return <div>Access Denied</div>;
}
}
};
};
const Component = (props) => <div>{props.content}</div>;
const AdminComponent = withAuthorization(Component, ['admin']);
const App = () => {
return (
<div>
{/* 正常访问 */}
<AdminComponent role="admin" content="Hello, Moment!" />
<br />
{/* 拒绝访问 */}
<AdminComponent role="user" content="嘿嘿嘿" />
</div>
);
};
export default App;
在上面这个代码示例中,withAuthorization HOC 检查用户角色是否在允许的角色列表中,只有在满足条件时才渲染被包裹的组件。AdminComponent 将在角色为 admin
时正常渲染 Component,否则显示 Access Denied
信息。如果仍然无法正常渲染,请确保 role 属性正确传递,并检查控制台是否有任何错误信息。
操作组件的生命周期
高阶组件可以通过包裹组件来添加或修改生命周期方法,以增强组件的行为。
import React from 'react';
const sendTrackingData = (event) => {
console.log(`记录事件: ${event}`);
};
const withTracking = (WrappedComponent) => {
return class extends React.Component {
componentDidMount() {
console.log('组件已挂载');
sendTrackingData('组件已挂载');
}
componentDidUpdate(prevProps) {
console.log('组件已更新');
sendTrackingData('组件已更新');
}
componentWillUnmount() {
console.log('组件将卸载');
sendTrackingData('组件将卸载');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
const MyComponent = (props) => <div>{props.text}</div>;
const TrackedComponent = withTracking(MyComponent);
const App = () => <TrackedComponent text="Moment!" />;
export default App;
最终结果如下图所示:
在这个示例中,withTracking 高阶组件会在组件的挂载、更新和卸载时记录日志并发送埋点数据。你可以根据实际需要,修改 sendTrackingData 函数来实现具体的埋点逻辑,例如发送 HTTP 请求到服务器。
操作组件的 static 方法
高阶组件可以访问和操作被包裹组件的静态方法,但需要手动处理或借助第三方库。
操作组件的静态方法有几个常见的需求和场景:
-
实用工具方法:静态方法可以用于定义一些实用工具函数,这些函数不依赖于实例化的组件。例如,一个组件可能有一个静态方法来格式化数据或处理特定逻辑。
-
与类相关的元数据:静态方法可以用于获取与组件类相关的元数据,而不需要实例化组件。例如,一个表单组件可能有一个静态方法来返回表单的默认配置。
-
状态管理:在某些情况下,静态方法可以用于管理跨多个实例的共享状态。这种方法常用于全局状态管理或单例模式。
-
高阶组件(HOC)增强:当使用高阶组件时,可能需要将原始组件的静态方法复制到增强组件上,以确保这些方法在增强组件上也能被调用。
我们来实现一个拷贝静态方法,实现代码如下所示:
import React from 'react';
const withStaticEnhancement = (WrappedComponent) => {
class Enhancer extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
// 获取所有静态属性(包括不可枚举属性)
Object.getOwnPropertyNames(WrappedComponent).forEach((key) => {
if (key !== 'prototype' && key !== 'name' && key !== 'length') {
const descriptor = Object.getOwnPropertyDescriptor(WrappedComponent, key);
Object.defineProperty(Enhancer, key, descriptor);
}
});
return Enhancer;
};
class MyComponent extends React.Component {
static someStaticMethod() {
return '静态方法被调用了';
}
render() {
return <div>{this.props.text}</div>;
}
}
const EnhancedComponent = withStaticEnhancement(MyComponent);
const App = () => {
// 调用静态方法
const staticMethodResult = EnhancedComponent.someStaticMethod();
return (
<div>
<h1>{staticMethodResult}</h1>
<br />
<EnhancedComponent text="Moment!" />
</div>
);
};
export default App;
最终结果如下图所示:
获取 Refs 实例
使用高阶组件后,获取到的 ref 实例实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的 ref,那么这种情况下我们就需要通过高阶组件来转发 ref 了。
import React from 'react';
const withRefForwarding = (WrappedComponent) => {
const forwardRefHOC = React.forwardRef((props, ref) => {
return <WrappedComponent {...props} forwardedRef={ref} />;
});
return forwardRefHOC;
};
class Moment extends React.Component {
changeBackgroundColor = (color) => {
if (this.div) {
this.div.style.backgroundColor = color;
}
};
render() {
const { forwardedRef, ...rest } = this.props;
return (
<div
ref={(div) => {
this.div = div;
if (forwardedRef) {
forwardedRef.current = this;
}
}}
style={{ width: '200px', height: '200px', border: '1px solid black' }}
{...rest}
>
{this.props.text}
</div>
);
}
}
const EnhancedComponent = withRefForwarding(Moment);
class App extends React.Component {
constructor(props) {
super(props);
this.enhancedComponentRef = React.createRef();
}
componentDidMount() {
if (this.enhancedComponentRef.current) {
this.enhancedComponentRef.current.changeBackgroundColor('red');
}
}
render() {
return <EnhancedComponent ref={this.enhancedComponentRef} text="Moment!" />;
}
}
export default App;
最终结果如下图所示:
这个示例通过高阶组件(HOC)和 React.forwardRef 将 ref 传递给被包裹的组件,使父组件能够访问和调用子组件的方法。高阶组件 withRefForwarding 接收一个组件,使用 React.forwardRef 将 ref 转发给被包裹的组件 Moment。在 Moment 组件中,forwardedRef 被用来将组件实例赋值给 ref,使得父组件可以通过 ref 访问和操作该实例。最终,App 组件使用这个增强后的组件 EnhancedComponent,并在 componentDidMount 中调用其方法来更改背景颜色。
反向继承(Inverse Inheritance)
反向继承(Inverse Inheritance)是 React 高阶组件(HOC)设计模式中的一种。与正向属性代理(Forwarding Props Proxy)不同,反向继承通过继承被包裹组件来增强或修改其行为。通过这种方式,高阶组件可以访问并重写被包裹组件的方法、生命周期钩子和状态。
实现反向继承的步骤主要有以下几个方面:
-
创建高阶组件:定义一个高阶组件函数,该函数接收一个组件作为参数并返回一个新的组件。
-
继承被包裹组件:在高阶组件内部,创建一个类继承被包裹组件。
-
重写或增强方法和生命周期钩子:在高阶组件中,可以重写或增强被包裹组件的方法和生命周期钩子。
-
渲染被包裹组件:调用父类(即被包裹组件)的 render 方法以渲染其内容。
如下小节是使用使用反向继承模式的示例,它展示了如何通过高阶组件来增强被包裹组件的行为。
劫持原组件生命周期
使用反向继承(Inversion of Control, IoC)的方式来实现一个劫持生命周期的高阶组件,可以让我们在高阶组件中访问和操作被包裹组件的生命周期方法。
以下是如何使用反向继承来实现一个高阶组件,以劫持生命周期并在挂载、更新和卸载时记录日志
import React from 'react';
const withLifecycleLogging = (WrappedComponent) => {
return class extends WrappedComponent {
componentDidMount() {
if (super.componentDidMount) {
super.componentDidMount();
}
console.log('组件已挂载');
}
componentDidUpdate(prevProps, prevState) {
if (super.componentDidUpdate) {
super.componentDidUpdate(prevProps, prevState);
}
console.log('组件已更新');
}
componentWillUnmount() {
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
console.log('组件将卸载');
}
render() {
return super.render();
}
};
};
class Moment extends React.Component {
changeBackgroundColor = (color) => {
if (this.div) {
this.div.style.backgroundColor = color;
}
};
render() {
return (
<div
ref={(div) => (this.div = div)}
style={{ width: '200px', height: '200px', border: '1px solid black' }}
>
{this.props.text}
</div>
);
}
}
const EnhancedComponent = withLifecycleLogging(Moment);
class App extends React.Component {
componentDidMount() {
if (this.enhancedComponentRef) {
this.enhancedComponentRef.changeBackgroundColor('lightblue');
}
}
render() {
return (
<EnhancedComponent ref={(ref) => (this.enhancedComponentRef = ref)} text="你好,世界!" />
);
}
}
export default App;
这个高阶组件 withLifecycleLogging 通过反向继承的方式劫持了被包裹组件的生命周期方法,在组件挂载、更新和卸载时记录日志。它还允许在外部调用原组件的方法,例如更改 div 元素的背景颜色。
状态管理
通过反向继承(Inversion of Control)方式实现的高阶组件 withStateManagement 用于管理状态。它能够在高阶组件中初始化、更新和清理状态,同时保留被包裹组件的原有功能,确保被包裹组件能够正常运行并获得增强的状态管理能力。
import React from 'react';
const withStateManagement = (WrappedComponent) => {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
managedState: 'moment',
};
}
updateManagedState = (newState) => {
this.setState({ managedState: newState });
};
componentDidMount() {
if (super.componentDidMount) {
super.componentDidMount();
}
console.log('组件已挂载,初始状态:', this.state.managedState);
}
componentDidUpdate(prevProps, prevState) {
if (super.componentDidUpdate) {
super.componentDidUpdate(prevProps, prevState);
}
console.log('组件已更新,当前状态:', this.state.managedState);
}
componentWillUnmount() {
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
console.log('组件将卸载');
}
render() {
const { forwardedRef, ...rest } = this.props;
return (
<WrappedComponent
{...rest}
ref={forwardedRef}
managedState={this.state.managedState}
updateManagedState={this.updateManagedState}
/>
);
}
};
};
// 包装组件以转发 ref
const forwardRefHOC = (WrappedComponent) => {
return React.forwardRef((props, ref) => {
return <WrappedComponent {...props} forwardedRef={ref} />;
});
};
class Moment extends React.Component {
render() {
const { text, managedState, updateManagedState } = this.props;
return (
<div style={{ width: '200px', height: '200px', border: '1px solid black' }}>
<p>{text}</p>
<p>Managed State: {managedState}</p>
<button onClick={() => updateManagedState('mmm')}>更新状态</button>
</div>
);
}
}
const EnhancedComponent = forwardRefHOC(withStateManagement(Moment));
class App extends React.Component {
render() {
return <EnhancedComponent text="Moment!" />;
}
}
export default App;
具体效果如下图所示:
点击之后状态会变成 mmm
。
这个高阶组件 withStateManagement 通过反向继承的方式扩展了被包裹组件的功能。它初始化并管理一个 managedState 状态,并提供 updateManagedState 方法来更新该状态。高阶组件还在组件的挂载、更新和卸载生命周期阶段输出日志,并调用原组件的对应生命周期方法(如果存在)。此外,它将 managedState 和 updateManagedState 作为属性注入到被包裹组件中,使其能够访问和操作这些状态和方法。这种方式可以在不修改原组件的情况下增强其功能。
render props 模式
Render Props 是指在组件的 prop 中传递一个函数,这个函数返回一个 React 元素,并且这个函数可以在组件的 render 方法中被调用。
Render Props 是 React 中一种共享组件之间代码的技巧。Render Props 是一个用于告知组件需要渲染什么内容的函数 prop。通过这种模式,父组件可以控制子组件的渲染逻辑,而子组件则专注于渲染 UI。
import React, { useState } from 'react';
function Cat({ mouse }) {
return (
<img
src="https://cdn.pixabay.com/photo/2023/05/27/22/56/kitten-8022452_1280.jpg"
style={{
position: 'absolute',
left: mouse.x,
top: mouse.y,
width: '200px',
height: '200px',
}}
/>
);
}
function Mouse({ render }) {
const [mouse, setMouse] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setMouse({
x: event.clientX,
y: event.clientY,
});
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(mouse)}
</div>
);
}
function MouseTracker() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={(mouse) => <Cat mouse={mouse} />} />
</div>
);
}
export default MouseTracker;
该代码最结果如下图所示:
该组件通过 Mouse 组件使用 render prop 将鼠标位置传递给父组件。父组件 MouseTracker 使用 Mouse 组件并通过 render prop 传递一个函数,该函数接收鼠标位置并返回 Cat 组件。这样,Cat 组件根据鼠标位置动态渲染图片,实现了图片跟随鼠标移动的效果。
除此之外,render props 还有一个另外的写法,如下代码所示:
import React, { useState } from 'react';
function Cat({ mouse }) {
return (
<img
src="https://cdn.pixabay.com/photo/2023/05/27/22/56/kitten-8022452_1280.jpg"
style={{
position: 'absolute',
left: mouse.x,
top: mouse.y,
width: '200px',
height: '200px',
}}
/>
);
}
function Mouse({ children }) {
const [mouse, setMouse] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setMouse({
x: event.clientX,
y: event.clientY,
});
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{React.Children.map(children, (child) => React.cloneElement(child, { mouse }))}
</div>
);
}
function MouseTracker() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse>
<Cat />
</Mouse>
</div>
);
}
export default MouseTracker;
通过这种方式同样也能实现相同的效果。
值得注意的是,这种设计模式目前组件库里用的多,比如 select option 那种,就需要 React children 去读取 option 然后获取选项列表,但是平时开发不会搞封装性非常高的组件,也不容易维护。
反向状态回传
这个组件的设计模式通过使用 render props 将状态提升到当前组件中,即将容器组件内的状态传递给父组件。具体示例代码如下所示:
import React, { useRef, useEffect } from 'react';
const Home = (props) => {
console.log(props);
const dom = useRef();
const getDomRef = () => dom.current;
const handleClick = () => {
console.log('小黑子');
};
const { children } = props;
return (
<div ref={dom}>
<div>{children({ getDomRef, handleClick })}</div>
<div>{React.Children.map(children, (node) => node)}</div>
</div>
);
};
const App = () => {
const childRef = useRef(null);
useEffect(() => {
const dom = childRef.current();
dom.style.background = 'red';
dom.style.fontSize = '100px';
}, [childRef]);
return (
<div>
<Home admin={true}>
{({ getDomRef, handleClick }) => {
childRef.current = getDomRef;
return <div onClick={handleClick}>你小子</div>;
}}
</Home>
</div>
);
};
export default App;
具体效果如下所示:
在上面的这个代码中,它通过 render props 将子组件 Home 的状态和方法提升到父组件 App 中。Home 组件使用 useRef 获取 DOM 引用,并通过 children prop 将引用和点击处理方法传递给父组件。App 组件通过这些方法访问和操作 Home 组件的 DOM 元素及其状态。
HOC 和 Render Props 优缺点以及使用场景
从上面的内容中我们应该可以得知 HOC 和 Render Props 的优缺点和使用场景大概有是什么了。接下来我们好好总结一下。
高阶组件(HOC)
高阶组件是参数为组件,返回值为新组件的函数。它的优点主要有以下几个方面:
-
代码复用:可以在多个组件间复用逻辑。
-
抽象能力:通过 HOC 可以将公共的逻辑抽象出来,减少代码重复。
-
灵活性:可以根据需要包装任意组件,增加功能。
它的缺点也比较明显,主要有以下几个方面:
-
嵌套过深:多层 HOC 嵌套会导致组件树不易读,调试困难。
-
属性冲突:HOC 可能会传递与被包装组件相冲突的 props。
-
静态方法丢失:HOC 无法访问被包装组件的静态方法。
他的使用场景主要以下这些方面:
-
逻辑复用:多组件需要共享相同的逻辑。
-
条件渲染:根据条件动态添加功能。
-
增强组件:为现有组件增加新的功能。
Render Props
Render Props 是一种用于告知组件需要渲染什么内容的技术,通过将一个函数作为 prop 传递给组件,这个函数会返回一个 React 元素。
它的优点主要有以下几个方面:
-
灵活性:可以根据父组件的状态动态渲染内容。
-
代码清晰:逻辑和 UI 分离,组件职责单一。
-
无嵌套:避免了多层嵌套的问题,使代码更易读。
它的缺点也是跟 HOC 一样,比较明显,主要体现为以下几点:
-
性能问题:频繁创建新的函数可能会影响性能。
-
代码复杂度:大量使用 Render Props 可能导致代码难以理解。
-
维护成本:在多个地方使用相同逻辑时,更新逻辑需要修改多个组件。
根据它的特点,使用场景主要以下几个使用场景:
-
动态渲染:需要根据不同状态渲染不同内容。
-
逻辑分离:希望将逻辑和 UI 分离,保持组件职责单一。
-
复杂 UI:需要在多个地方复用相同的渲染逻辑。
选择 HOC 还是 Render Props
HOC 和 Render Props 都是强大的设计模式,各有优缺点和适用场景。选择使用哪种模式取决于具体的需求和组件的复杂度。HOC 适合逻辑复用和条件渲染,Render Props 适合动态渲染和逻辑分离。
-
逻辑复用:如果需要在多个组件间复用相同的逻辑,可以考虑使用 HOC。
-
动态渲染:如果需要根据状态动态渲染不同内容,可以考虑使用 Render Props。
-
组件层级:如果担心组件层级过深,可以使用 Render Props 来避免多层嵌套。
当然,这些大部分场景仅适用于只有这两个选项可以选择的时候,后面所讲到的两个能直接忽略掉很多选择。
提供者模式(Provider Pattern)
在 React 中,props 是组件之间通讯的主要手段,但是,有一种场景单纯靠 props 来通讯是不恰当的,那就是两个组件之间间隔着多层其他组件,下面是一个简单的组件树示例图:
提供者模式通过创建一个“提供者”(Provider)组件,将状态或依赖传递给其子组件树中的任意组件。这种模式使得状态管理更加集中和便捷,避免了通过逐级传递 props 的麻烦。
在上图中,组件 A 需要传递信息给组件 X,如果通过 props 的话,那么从顶部的组件 A 开始,要把 props 传递给组件 B,然后组件 B 传递给组件 D,最后组件 D 再传递给组件 X。
其实组件 B 和组件 D 完全用不上这些 props,但是又被迫传递这些 props,这明显不合理,要知道组件树的结构会变化的,将来如果组件 B 和组件 D 之间再插入一层新的组件,这个组件也需要传递这个 props,这就麻烦无比。可见,对于跨级的信息传递,我们需要一个更好的方法。
提供者模式(Provider Pattern)是一种在 React 中非常常用的设计模式,特别是在需要跨多个组件共享状态或依赖时。它通常与 React 的 Context API 一起使用。
假设我们有一个应用需要在多个组件中共享用户信息,我们可以使用提供者模式来实现。
import React, { createContext, useState, useContext } from 'react';
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: 'moment', age: 18 });
return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>;
};
const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser必须在UserProvider中使用');
}
return context;
};
const Moment = () => {
const { user, setUser } = useUser();
const updateUser = () => {
setUser({ name: '77', age: 20 });
};
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Age: {user.age}</p>
<button onClick={updateUser}>Update User</button>
</div>
);
};
const App = () => {
return (
<UserProvider>
<Moment />
</UserProvider>
);
};
export default App;
上面的代码通过提供者模式创建了一个用户上下文(UserContext),并使用 UserProvider 组件来管理和共享用户状态。在 Moment 组件中,通过自定义 Hook(useUser)访问和更新用户状态,使得组件可以方便地获取和修改用户信息。应用主组件 App 将 UserProvider 包裹在整个应用外层,以确保所有子组件都能访问用户状态。
最终输出结果如下图所示,当我们点击的时候内容会改变为 { name: 77, age: 20}
:
提供者模式相对于高阶组件(HOC)和 Render Props 的主要优势在于简化状态共享、减少样板代码、清晰的数据流以及更简洁的组件结构。通过 Context API,提供者模式可以轻松地在组件树的深层次共享状态,而无需逐级传递 props,避免了繁琐的嵌套和传递逻辑。
提供者模式只需创建一个 Provider 组件并在需要的地方使用 Context 即可,使得代码更加简洁直观。数据流更加集中和清晰,调试和维护也更加方便。此外,直接在需要的组件中使用 Context 使得组件结构更加简单,避免了 HOC 和 Render Props 带来的额外层级和嵌套。因此,提供者模式在简化状态管理和共享方面具有显著优势。
如果 Context 的值频繁变化,可能会导致不必要的重渲染,从而影响性能。这个时候我们可以考虑 zustand 等管理工具,因为它们可以结合 immer 来实现不可变状态。
Hooks(React Hooks Pattern)
React Hooks 提供了一种更加灵活和简洁的方式来编写有状态的功能组件,使得代码更容易理解和维护。通过使用 Hooks,开发者可以在函数组件中使用状态和其他 React 特性,而不需要编写类组件。
Hooks 是一组特殊的函数,它们允许你在函数组件中使用状态(state)和生命周期方法。最常用的 hooks 包括:
-
useState:用于声明状态变量。
-
useEffect:用于在组件生命周期的不同阶段执行副作用操作(例如,数据获取、订阅、手动更改 DOM 等)。
除了这些内置的 hooks 之外,React 还允许你创建自定义 hooks,以封装和复用逻辑,这个和前面的 HOC 有点类似。
自定义 Hooks 是一个以 use 开头的函数,可以调用其他 Hooks。以下是一个简单的自定义 Hook 的示例:
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
};
// 使用自定义 Hook 的组件
const DataFetcher = () => {
const { data, loading } = useFetch('https://www.bilibili.com/');
return <div>{loading ? 'Loading...' : <pre>{JSON.stringify(data, null, 2)}</pre>}</div>;
};
export default DataFetcher;
在上面的这些代码中我们定义了一个自定义 Hook 为 useFetch ,用于从给定的 URL 获取数据,并在组件中使用该数据和加载状态。DataFetcher 组件调用 useFetch,从 Bilibili 获取数据并显示加载状态或数据内容。整个过程通过 useState 和 useEffect 来管理数据和副作用。
Hooks 组件模式的优势
-
简化代码结构:Hooks 使函数组件能够管理状态和生命周期,避免了复杂的类组件,代码更加简洁易读。
-
更好的状态管理:通过使用多个 useState 和 useReducer,可以更细粒度地管理状态。
-
副作用管理:useEffect 提供了一种统一的方式来处理副作用操作,如数据获取、订阅和手动 DOM 操作。
-
逻辑复用:自定义 Hooks 允许开发者将逻辑提取到可复用的函数中,提高了代码的复用性和可维护性。
尽管 Hooks 组件模式有很多优点,但当使用 useEffect 管理副作用时,容易出现依赖数组管理不当的问题。如果依赖数组没有正确设置,可能会导致副作用函数不必要地重新执行或遗漏更新。
类组件中的一些生命周期方法(如 componentDidMount 和 componentDidUpdate)在 Hooks 中被 useEffect 合并,这可能会导致一些场景下缺少细粒度的生命周期控制。
总的来说,Hooks 组件模式通过提供一种更自然的方式来管理状态和副作用,使函数组件具备了类组件的所有功能,同时保持了代码的简洁性和灵活性。尽管 Hooks 组件模式带来了很多便利,但开发者在使用时需要注意管理好状态和副作用,以避免潜在的性能和调试问题。同时,需要适度地使用自定义 Hooks,避免过度抽象。
总结
总之,那些现在看上去落后的技术,在当时都面临着特定的困境。这些技术在其发展的过程中,由于当时的开发环境、浏览器兼容性和社区支持等因素的制约,无法像今天的一些现代技术那样高效和便捷。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗