Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

声明式编程是一种编程范式,它侧重于描述 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 来渲染内容,不管自己的状态,也不包含复杂的逻辑,这种组件通常是函数组件,它的特点主要有以下几个方面:

  1. 无状态:哑组件不管理自己的状态,只通过 props 获取数据。

  2. 可复用:因为没有业务逻辑和状态管理,哑组件更容易复用。

  3. 易于测试:由于只依赖输入的 props,哑组件的测试变得非常简单。

  4. 纯函数:大多数哑组件都可以写成纯函数。

如下代码所示:

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 自身的特性产生的结果。

高阶组件主要解决了以下问题,具体如下:

  1. 复用逻辑: 高阶组件就像是一个加工 React 组件的工厂,你需要向该工厂提供一个坯子,它可以批量地对你送进来的组件进行加工,包装处理,还可以根据你的需求定制不同的产品。

  2. 强化 props: 高阶组件返回的组件,可以劫持上一层传过来的 props,染回混入新的 props,来增强组件的功能。

  3. 控制渲染: 劫持渲染是 hoc 中的一个特性,在高阶组件中,你可以对原来的组件进行条件渲染、节流渲染、懒加载等功能。

HOC 的实现方式

正向属性代理(Forwarding Props Proxy)和反向继承(Inverse Inheritance)是两种用于高阶组件(Higher-Order Components, HOC)实现的方法。它们主要用于增强或修改被包裹组件的行为和特性。

正向属性代理(Forwarding Props Proxy)

所谓正向属性代理就是通过包裹一个组件并直接传递(或增强)其 props 来实现的。正向属性代理 HOC 通过直接渲染被包裹的组件,并且将所有的 props 传递给被包裹组件。

实际上这种方式生成的高阶组件就是原组件的父组件,父组件对子组件进行一系列强化操作。

对比原始组件,高阶组件增强的方面主要有以下几个方面:

  1. 可操作所有传入的 props: 可以对其传入的 props 进行条件渲染,例如权限控制等。

  2. 可以操作组件的生命周期。

  3. 可操作组件的 static 方法,但是需要手动处理,或者引入第三方库。

  4. 获取 refs。

  5. 抽象 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 方法

高阶组件可以访问和操作被包裹组件的静态方法,但需要手动处理或借助第三方库。

操作组件的静态方法有几个常见的需求和场景:

  1. 实用工具方法:静态方法可以用于定义一些实用工具函数,这些函数不依赖于实例化的组件。例如,一个组件可能有一个静态方法来格式化数据或处理特定逻辑。

  2. 与类相关的元数据:静态方法可以用于获取与组件类相关的元数据,而不需要实例化组件。例如,一个表单组件可能有一个静态方法来返回表单的默认配置。

  3. 状态管理:在某些情况下,静态方法可以用于管理跨多个实例的共享状态。这种方法常用于全局状态管理或单例模式。

  4. 高阶组件(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)不同,反向继承通过继承被包裹组件来增强或修改其行为。通过这种方式,高阶组件可以访问并重写被包裹组件的方法、生命周期钩子和状态。

实现反向继承的步骤主要有以下几个方面:

  1. 创建高阶组件:定义一个高阶组件函数,该函数接收一个组件作为参数并返回一个新的组件。

  2. 继承被包裹组件:在高阶组件内部,创建一个类继承被包裹组件。

  3. 重写或增强方法和生命周期钩子:在高阶组件中,可以重写或增强被包裹组件的方法和生命周期钩子。

  4. 渲染被包裹组件:调用父类(即被包裹组件)的 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)

高阶组件是参数为组件,返回值为新组件的函数。它的优点主要有以下几个方面:

  1. 代码复用:可以在多个组件间复用逻辑。

  2. 抽象能力:通过 HOC 可以将公共的逻辑抽象出来,减少代码重复。

  3. 灵活性:可以根据需要包装任意组件,增加功能。

它的缺点也比较明显,主要有以下几个方面:

  1. 嵌套过深:多层 HOC 嵌套会导致组件树不易读,调试困难。

  2. 属性冲突:HOC 可能会传递与被包装组件相冲突的 props。

  3. 静态方法丢失:HOC 无法访问被包装组件的静态方法。

他的使用场景主要以下这些方面:

  1. 逻辑复用:多组件需要共享相同的逻辑。

  2. 条件渲染:根据条件动态添加功能。

  3. 增强组件:为现有组件增加新的功能。

Render Props

Render Props 是一种用于告知组件需要渲染什么内容的技术,通过将一个函数作为 prop 传递给组件,这个函数会返回一个 React 元素。

它的优点主要有以下几个方面:

  1. 灵活性:可以根据父组件的状态动态渲染内容。

  2. 代码清晰:逻辑和 UI 分离,组件职责单一。

  3. 无嵌套:避免了多层嵌套的问题,使代码更易读。

它的缺点也是跟 HOC 一样,比较明显,主要体现为以下几点:

  1. 性能问题:频繁创建新的函数可能会影响性能。

  2. 代码复杂度:大量使用 Render Props 可能导致代码难以理解。

  3. 维护成本:在多个地方使用相同逻辑时,更新逻辑需要修改多个组件。

根据它的特点,使用场景主要以下几个使用场景:

  1. 动态渲染:需要根据不同状态渲染不同内容。

  2. 逻辑分离:希望将逻辑和 UI 分离,保持组件职责单一。

  3. 复杂 UI:需要在多个地方复用相同的渲染逻辑。

选择 HOC 还是 Render Props

HOC 和 Render Props 都是强大的设计模式,各有优缺点和适用场景。选择使用哪种模式取决于具体的需求和组件的复杂度。HOC 适合逻辑复用和条件渲染,Render Props 适合动态渲染和逻辑分离。

  1. 逻辑复用:如果需要在多个组件间复用相同的逻辑,可以考虑使用 HOC。

  2. 动态渲染:如果需要根据状态动态渲染不同内容,可以考虑使用 Render Props。

  3. 组件层级:如果担心组件层级过深,可以使用 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 包括:

  1. useState:用于声明状态变量。

  2. 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 组件模式的优势

  1. 简化代码结构:Hooks 使函数组件能够管理状态和生命周期,避免了复杂的类组件,代码更加简洁易读。

  2. 更好的状态管理:通过使用多个 useState 和 useReducer,可以更细粒度地管理状态。

  3. 副作用管理:useEffect 提供了一种统一的方式来处理副作用操作,如数据获取、订阅和手动 DOM 操作。

  4. 逻辑复用:自定义 Hooks 允许开发者将逻辑提取到可复用的函数中,提高了代码的复用性和可维护性。

尽管 Hooks 组件模式有很多优点,但当使用 useEffect 管理副作用时,容易出现依赖数组管理不当的问题。如果依赖数组没有正确设置,可能会导致副作用函数不必要地重新执行或遗漏更新。

类组件中的一些生命周期方法(如 componentDidMount 和 componentDidUpdate)在 Hooks 中被 useEffect 合并,这可能会导致一些场景下缺少细粒度的生命周期控制。

总的来说,Hooks 组件模式通过提供一种更自然的方式来管理状态和副作用,使函数组件具备了类组件的所有功能,同时保持了代码的简洁性和灵活性。尽管 Hooks 组件模式带来了很多便利,但开发者在使用时需要注意管理好状态和副作用,以避免潜在的性能和调试问题。同时,需要适度地使用自定义 Hooks,避免过度抽象。

总结

总之,那些现在看上去落后的技术,在当时都面临着特定的困境。这些技术在其发展的过程中,由于当时的开发环境、浏览器兼容性和社区支持等因素的制约,无法像今天的一些现代技术那样高效和便捷。

最后分享两个我的两个开源项目,它们分别是:

这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗

最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666