- react:版本 18.2.0
- node: 版本18.19.1
- 脚手架:版本 5.0.1
一、类组件
(一) 一个干净的脚手架
【1】使用已经被废弃的 CRA (create-react-app)
-
create-react-app 已经被废弃,且目前使用会报错,官方已经不推荐使用,但是可以通过以下步骤能够运行项目,以下步骤可能某天也会出错,请谨慎使用。如果要使用 typescript 那也 create-react-app 因为出错而有更多问题。
-
使用如下命令创建脚手架
npm create-react-app 项目名称
或者
npm init react-app 项目名称
-
使用上边的命令会默认安装 react 19 的项目,并且会报一个错误如下图,这个错误可以忽略,只需要将 package.json 文件中的
"react": "^19.0.0"
和"react-dom": "^19.0.0"
改为"react": "^18.0.0"
和"react-dom": "^18.0.0"
,然后删除package-lock.json
文件,再执行npm install
命令即可;
-
将 public 文件夹下 除了 favicon.ico 和 index.html文件外的文件都删除,index.html 文件中如下的两句话也需要删除
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-
src 文件夹下只留下 App.js、index.js、index.css(也可以删除),index.css 文件内容清空
-
src/index.js 文件内容如下
import React from 'react'; // <React.StrictMode> 被删掉,这个也不用保留 import ReactDOM from 'react-dom/client'; import './index.css'; // 如果文件被删掉,这个也不要保留 import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( /* <React.StrictMode> 可以被删掉,代表严格模式 */ <React.StrictMode> <App /> </React.StrictMode> );
-
src/App.js 文件内容如下(可以将App.js 改为 App.jsx 更好的适配 VSCode)
function App() { return <div>Hello React</div>; } export default App;
【2】使用 vite 创建项目
-
Vite 需要 Node.js 版本 18+ 或 20+。vite 官网
-
使用如下命令创建一个项目(如果需要 TypeScript 支持,可以选择 react-ts 模板:–template react-ts),目前默认已将开始安装 react 19 版本
# npm 7+,需要添加额外的 --: npm create vite@latest 项目名称 -- --template react
-
如果你需要 react18,将 package.json 文件中的
"react": "^19.0.0"
和"react-dom": "^19.0.0"
改为"react": "^18.0.0"
和"react-dom": "^18.0.0"
"dependencies": { "react": "^19.0.0", // => "^18.0.0" "react-dom": "^19.0.0" // => "^18.0.0" },
-
进入到项目目录,执行下面的命令,安装依赖
npm install
-
执行如下命令,运行项目
npm run dev
-
纯净的项目:需要将 src 文件夹下的 assets 文件夹删除掉,App.css、index.css 文件删除掉
-
main.jsx 中去除对 index.css 文件的引用
import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.jsx' createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode>, )
-
App.jsx 文件修改如下
import React, { memo } from 'react' const App = memo(() => { return ( <div>App</div> ) }) export default App
(二) 相关概念
- 类组件的定义有如下要求:
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
- 在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。
- 使用 class 定义一个组件:
- constructor 是可选的,我们通常在constructor中初始化一些数据;
- this.state 中维护的就是我们组件内部的数据;
- render() 方法是 class 组件中唯一必须实现的方法;
- 当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素:
- 通常通过 JSX 创建。
- 例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件;
- 无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
- 数组或 fragments:使得 render 方法可以返回多个元素。
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型、 null 和 undefined:什么都不渲染。
- React 元素:
(三) 简单案例
- 创建一个干净的脚手架
- 修改 src/App.js 文件,内容如下
// 类组件 // 方式一: // import React from 'react'; // class App extends React.Component { // } // 方式二: import { Component } from 'react'; class App extends Component { constructor() { super(); this.state = { message: 'Hello React', }; } render() { // 1. 返回 React 元素 // const { message } = this.state; // return <h2>{message}</h2>; // 2. 返回 数组 // return ['张三', '里斯'] // return [{name: '张三'}] // 报错 // return [ // <h1>h1元素</h1>, // <h2>h2元素</h2>, // ] // 3. 返回字符串或数值类型 // return '123' // return 456 // 4. 布尔类型或 null // return undefined // return null // return true return false } } export default App;
二、生命周期
(一) 相关概念
- 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
- React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
- 装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 卸载过程(Unmount),组件从DOM树中被移除的过程;
- React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- componentDidMount函数:组件已经挂载到DOM上时,就会回调;
- componentDidUpdate函数:组件已经发生了更新时,就会回调;
- componentWillUnmount函数:组件即将被移除时,就会回调
- getDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新state;
- getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
- shouldComponentUpdate:该生命周期函数很常用,可以用来进行性能优化
- 可以通过 https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/ 网站查看更详细的生命周期
- 下面是一些常见的生命周期:

(二) 生命周期的作用
【1】Constructor
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
- constructor中通常只做两件事情:
-
- 通过给 this.state 赋值对象来初始化内部的state;
-
- 为事件绑定实例(this);
-
【2】componentDidMount
- componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用
- componentDidMount中通常进行的操作
-
- 依赖于DOM的操作可以在这里进行;
-
- 在此处发送网络请求就最好的地方;(官方建议)
-
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
-
【3】componentDidUpdate
- componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
-
- 当组件更新后,可以在此处对 DOM 进行操作;
-
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;
-
【4】componentWillUnmount
- componentWillUnmount() 会在组件卸载及销毁之前直接调用。
-
- 在此方法中执行必要的清理操作;例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
-
(三) 简单案例
-
创建一个干净的脚手架
-
修改 src/App.js (将js后缀改为jsx) 文件,内容如下
import { Component } from 'react'; import Son from './Son'; class App extends Component { constructor() { super(); this.state = { msg: 'Hello World', isShowSon: true, }; console.log('父组件constructor'); } render() { const { msg, isShowSon } = this.state; console.log('父组件render'); return ( <div> 父组件:{msg} <button onClick={() => this.btnClick()}>父组件修改</button> <button onClick={() => this.changeSonShow()}> {isShowSon ? '隐藏' : '显示'}子组件 </button> {isShowSon && <Son />} </div> ); } btnClick() { console.log('父组件执行 setState'); this.setState({ msg: 'Hello React', }); } changeSonShow() { this.setState({ isShowSon: !this.state.isShowSon, }); } componentDidMount() { console.log('父组件componentDidMount'); } componentDidUpdate() { console.log('父组件componentDidUpdate'); } componentWillUnmount() { console.log('父组件componentWillUnmount'); } } export default App;
-
在 App.jsx 同目录下,创建一个子组件 Son.jsx,内容如下
import { Component } from 'react'; class Son extends Component { constructor() { super(); this.state = { msg: 'Hello Son', }; console.log('子组件constructor'); } render() { const { msg } = this.state; console.log('子组件render'); return ( <div> 子组件:{msg} <button onClick={() => this.btnClick()}>子组件修改</button> </div> ); } btnClick() { console.log('子组件执行 setState'); this.setState({ msg: 'Hi Son', }); } componentDidMount() { console.log('子组件componentDidMount'); } componentDidUpdate() { console.log('子组件componentDidUpdate'); } componentWillUnmount() { console.log('子组件componentWillUnmount'); } } export default Son;
-
生命周期
三、组件间传递信息
(一) 父组件传递信息给子组件
【1】 传递方式
- 父组件通过
属性=值
的形式来传递给子组件数据; - 子组件通过
props
参数获取父组件传递过来的数据;
【2】 对 props 传递的数据类型进行校验(propTypes )
-
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说
- 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证
- 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证
-
从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
-
如果某个 prop 没有传递,可以使用 defaultProps 设置默认值,有两种方式可以设置默认值,但是两个方式不能共存,具体可以看用例
-
更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
import PropTypes from 'prop-types'; MyComponent.propTypes = { // 你可以将属性声明为 JS 原生类型,默认情况下 // 这些属性都是可选的。 optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalSymbol: PropTypes.symbol, // 任何可被渲染的元素(包括数字、字符串、元素或数组) // (或 Fragment) 也包含这些类型。 optionalNode: PropTypes.node, // 一个 React 元素。 optionalElement: PropTypes.element, // 一个 React 元素类型(即,MyComponent)。 optionalElementType: PropTypes.elementType, // 你也可以声明 prop 为类的实例,这里使用 // JS 的 instanceof 操作符。 optionalMessage: PropTypes.instanceOf(Message), // 你可以让你的 prop 只能是特定的值,指定它为 // 枚举类型。 optionalEnum: PropTypes.oneOf(['News', 'Photos']), // 一个对象可以是几种类型中的任意一个类型 optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]), // 可以指定一个数组由某一类型的元素组成 optionalArrayOf: PropTypes.arrayOf(PropTypes.number), // 可以指定一个对象由某一类型的值组成 optionalObjectOf: PropTypes.objectOf(PropTypes.number), // 可以指定一个对象由特定的类型值组成 optionalObjectWithShape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }), // An object with warnings on extra properties optionalObjectWithStrictShape: PropTypes.exact({ name: PropTypes.string, quantity: PropTypes.number }), // 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保 // 这个 prop 没有被提供时,会打印警告信息。 requiredFunc: PropTypes.func.isRequired, // 任意类型的必需数据 requiredAny: PropTypes.any.isRequired, // 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。 // 请不要使用 `console.warn` 或抛出异常,因为这在 `oneOfType` 中不会起作用。 customProp: function(props, propName, componentName) { if (!/matchme/.test(props[propName])) { return new Error( 'Invalid prop `' + propName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } }, // 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。 // 它应该在验证失败时返回一个 Error 对象。 // 验证器将验证数组或对象中的每个值。验证器的前两个参数 // 第一个是数组或对象本身 // 第二个是他们当前的键。 customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) { if (!/matchme/.test(propValue[key])) { return new Error( 'Invalid prop `' + propFullName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } }) };
【3】 用例
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import ChildComponent from './ChildComponent'; class App extends Component { constructor() { super(); this.state = { msg: '父组件传递给子组件一个变量', moves: ['星际穿越', '流浪地球'], }; } render() { const { msg, moves } = this.state; return ( <div> <ChildComponent parentMsg={msg} parentMoves={moves} defaultMsg="msg的一条信息" /> </div> ); } } export default App;
-
子组件 ChildComponent.jsx 文件内容如下,如果设置了 isRequired 没有传递数据则会在浏览器控制台中报错
import React, { Component } from 'react'; import PropTypes from "prop-types" export default class ChildComponent extends Component { // 默认值方式二 react19 已经不推荐: // static defaultProps = { // defaultMsg2: '默认值方式二' // } // 如果 constructor 只是进行如下操作,可以省略 constructor(props) { // 等同于 this.props = props super(props); } render() { const { parentMsg, parentMsg2, parentMoves, defaultMsg } = this.props; return ( <div style={{ border: '1px solid #ddd' }}> <h3>这里是子组件:</h3> <p>{parentMsg}</p> {/* 不传默认是 undefined */} <p>{parentMsg2 + ''}</p> <ul> {parentMoves && parentMoves.map((item) => <li key={item}>{item}</li>)} </ul> <p>{defaultMsg}</p> <p>{this.props.defaultMsg2}</p> </div> ); } } // 为 props 设置类型校验 ChildComponent.propTypes = { // 字符串类型 defaultMsg: PropTypes.string.isRequired, defaultMsg2: PropTypes.string, parentMsg: PropTypes.string.isRequired, // 任意类型且必须传递 parentMsg2: PropTypes.any.isRequired, // 数组类型且必须传递 parentMoves: PropTypes.array.isRequired, }; // 提供默认值方式一: ChildComponent.defaultProps = { defaultMsg: '默认值方式一', };
(二) 子组件传递信息给父组件
- React中子组件同样是通过props传递消息给父组件,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import ChildComponent from './ChildComponent'; class App extends Component { constructor() { super(); this.state = { count: 1, }; } changeCount(count) { this.setState({ count: this.state.count + count, }); } render() { return ( <div> <h1>{this.state.count}</h1> {/* 将一个函数传递给子组件,用于子组件调用 */} <ChildComponent addClick={(count) => this.changeCount(count)} /> </div> ); } } export default App;
- 子组件 ChildComponent.jsx 文件内容如下
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ChildComponent extends Component { onClick(count) { // 调用父组件传递过来的函数 this.props.addClick(count); } render() { return ( <div> <button onClick={() => this.onClick(1)}>+1</button> <button onClick={() => this.onClick(-1)}>-1</button> </div> ); } } // 验证父组件传递给子组件的属性类型 ChildComponent.propTypes = { addClick: PropTypes.func.isRequired, };
四、React中的插槽(slot)
- React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的children子元素;
- props属性传递React元素;
(一) children 实现插槽
- 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。
- 弊端1:如果组件的开始标签和结束标签之间的内容有多个,props.children则会是一个数组,如果只有一个则会是当前的内容元素
- 弊端2:通过索引值获取传入的元素很容易出错,不能精准的获取传入组件;
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import NavBar from './NavBar'; export default class App extends Component { render() { return ( <div> {/* 导航标签中间的元素都会传递给 NavBar 组件的props.children 中*/} {/* 如果导航标签只有一个元素,则会直接将元素放在 props.children 中 */} {/* <NavBar> <div>首页</div> </NavBar> */} {/* 如果导航标签有多个元素,props.children 是一个数组 */} <NavBar> <div>返回</div> <div>标题</div> <div>更多</div> </NavBar> </div> ); } }
-
导航组件(子组件) NavBar.jsx 内容如下
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class NavBar extends Component { render() { const { children } = this.props; console.log(children); return ( <div className="nav-bar"> {/* 传入元素 */} {/* <div className="element">{children}</div> */} {/* 传入数组 */} <div className="left">{children[0]}</div> <div className="center">{children[1]}</div> <div className="right">{children[2]}</div> </div> ); } } // 可以进行传入的内容的校验(非必要) NavBar.propTypes = { // 只要一个元素 // children: PropTypes.element, // 多个元素 children: PropTypes.array, };
(二) props 实现插槽
- 使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import NavBar from './NavBar'; import Child from './Child'; export default class App extends Component { render() { const btn = <button>按钮</button>; return ( <div> {/* 可以使用多种形式 */} {/* leftSolt 可以随意命名,只要NavBar组件中定义了对应的属性,就可以使用 */} <NavBar leftSolt={<div>返回</div>} centerSolt={btn} rightSolt={<Child />} /> </div> ); } }
-
子组件 Child.jsx 内容如下
import React, { Component } from 'react' export default class Child extends Component { render() { return ( <div>子组件</div> ) } }
-
导航组件(子组件) NavBar.jsx 内容如下
import React, { Component } from 'react'; export default class NavBar extends Component { render() { const { leftSolt, centerSolt, rightSolt } = this.props; return ( <div className="nav-bar"> <div className="left">{leftSolt}</div> <div className="center">{centerSolt}</div> <div className="right">{rightSolt}</div> </div> ); } }
(三) 实现作用域插槽
- 可以参考 vue 对于作用域插槽的描述.
- 简单来说就是:父组件的某个子组件需要用到另一个子组件中的信息
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import NavBar from './NavBar'; import MoveName from './MoveName'; export default class App extends Component { constructor() { super(); this.state = { moves: ['星际穿越', '流浪地球'], }; } render() { const { moves } = this.state; return ( <div> {/* moveEl 属性通过在 Navbar 组件中被调用,传递参数给 MoveName 组件, */} {/* 然后再通过 props 将传递的参数给 MoveName 组件 */} <NavBar moves={moves} moveEl={(name) => <MoveName key={name} name={name} />} /> </div> ); } }
-
子组件 NavBar.jsx 内容如下
import React, { Component } from 'react'; export default class NavBar extends Component { render() { const { moves, moveEl } = this.props; // 该组件通过调用 moveEl 函数,将数据传递父组件 return <div>{moves.map((move) => moveEl(move))}</div>; }
-
子组件 MoveName.jsx 内容如下
import React, { Component } from 'react'; export default class MoveName extends Component { render() { const { name } = this.props; return <div>{name}</div>; } }
五、非父子组件共享数据
(一) context (上下文)
- 官网:https://zh-hans.legacy.reactjs.org/docs/context.html
【1】相关概念
-
如果层级更多的话,使用 props 一层层传递数据是非常麻烦,并且代码是非常冗余的
-
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
-
React提供了一个API:
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
-
如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。
【2】Context 相关API
(1) React.createContext
- 创建一个需要共享的Context对象:
- 一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
(2) Context.Provider
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
(3) Class.contextType
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值;
- 你可以在任何生命周期中访问到它,包括 render 函数中;
(4) Context.Consumer
- 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的 context 值,返回一个 React 节点;
- 什么时候使用Context.Consumer呢
- 1.当使用value的组件是一个函数式组件时
- 2.当组件中需要使用多个Context时;
(5)用例
-
用例文件结构
-
创建一个干净的脚手架
-
在src 文件夹下新建一个文件夹 context ,并在context 文件夹里边新建 theme-context.js 文件和 user-context.js 作为公共数据存放的文件
- theme-context.js 文件内容如下
import React from 'react'; export const themes = { light: { foreground: '#000000', background: '#eeeeee', color: '#000', }, dark: { foreground: '#ffffff', background: '#222222', color: '#fff', }, }; // 创建context对象的 // 为当前的 theme 创建一个 context(“dark”为默认值)。 export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, });
- user-context.js 文件内容如下
import React from 'react'; export const user = { id: '001', name: '张三', age: 18, gender: '男', }; // 创建context对象的,将 user 作为默认值 export const UserContext = React.createContext(user);
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import { ThemeContext, themes } from './context/theme-context'; import { UserContext } from './context/user-context'; import Themed from './Themed'; import Toolbar from './Toolbar'; import User from './User'; export default class App extends Component { constructor() { super(); this.toggleTheme = () => { console.log(123); this.setState((state) => ({ themeContext: { theme: state.themeContext.theme === themes.dark ? themes.light : themes.dark, toggleTheme: this.toggleTheme, }, user: this.state.user, })); }; this.state = { themeContext: { // 这个字段名要和 ThemeContext 默认值的字段名保持一致 theme: themes.light, toggleTheme: this.toggleTheme, }, user: { id: '002', name: '里斯', age: 19, gender: '女', }, }; } render() { return ( <div> {/* 在 ThemeContext.Provider 中的子元素无论层级多深,都可以获取到value所设置的值 */} {/* 在 ThemeContext.Provider 设置了 value 该标签内部的 Toolbar 会使用所设置的value值 */} <ThemeContext.Provider value={this.state.themeContext}> <UserContext.Provider value={this.state.user}> <Themed /> </UserContext.Provider> </ThemeContext.Provider> {/* Toolbar和User组件可以使用到 ThemeContext、UserContext 所设置的默认值 */} <div> <Toolbar /> <br /> <User /> </div> </div> ); } }
-
在src文件夹下新建用户信息展示页面 User.jsx 文件
import React, { Component } from 'react'; import { ThemeContext } from './context/theme-context'; import { UserContext } from './context/user-context'; // 类组件获取多个context export default class User extends Component { render() { // 注:可以使用 React.forwardRef 来实现类似于函数组件 useContext 方法的效果 return ( <div> <ThemeContext.Consumer> {(themeContext) => ( <UserContext.Consumer> {(user) => ( <div style={{ backgroundColor: themeContext.theme.background, color: themeContext.theme.color, }} > user组件:{user.name}--{user.age}--{user.gender} </div> )} </UserContext.Consumer> )} </ThemeContext.Consumer> </div> ); } }
-
在src文件夹下新建工具栏页面 Toolbar.jsx 文件
import { useContext } from 'react'; import { ThemeContext } from './context/theme-context'; import { UserContext } from './context/user-context'; // 函数组件获取单个context function Toolbar(props) { // 方式一:已过时 // 多个context的可以参考 user.jsx 文件 // return ( // <ThemeContext.Consumer> // {(theme) => ( // <div style={{ backgroundColor: themeContext.theme.background, color: themeContext.theme.color }}> // toolbar 默认(dark) 主题 // </div> // )} // </ThemeContext.Consumer> // ); // 方式二:推荐 // useContext 方式只能在 函数组件中使用 const themeContext = useContext(ThemeContext); const user = useContext(UserContext); return ( <div style={{ backgroundColor: themeContext.theme.background, color: themeContext.theme.color, }} > toolbar组件: 默认(dark) 主题 <br /> {user.name} </div> ); } export default Toolbar;
-
在src文件夹下新建主题页面 Themed.jsx 文件
import React, { Component } from 'react'; // 引入 UserContext 对象 import { UserContext } from './context/user-context'; import ThemedButton from './Themed-button'; // 类组件组件获取单个context class Themed extends Component { render() { // 2. this.context 指向到 ThemeContext 对象 let user = this.context; return ( <div> {user.name} <ThemedButton /> </div> ); } } // 1. 将 ThemeContext 传递到主题按钮组件 Themed.contextType = UserContext; export default Themed;
-
在src文件夹下新建主题页面中的按钮页面 Themed-button.jsx 文件
import React, { Component } from 'react'; // 引入 ThemeContext,UserContext 对象 import { ThemeContext } from './context/theme-context'; import { UserContext } from './context/user-context'; // 类组件组件获取单个context // 并调用传递的切换主题的函数 class ThemedButton extends Component { render() { let props = this.props; // 2. this.context 指向到 ThemeContext 对象 let themeContext = this.context; console.log(themeContext); return ( <UserContext.Consumer> {(user) => ( <div> {user.name} <button {...props} onClick={themeContext.toggleTheme} style={{ backgroundColor: themeContext.theme.background, color: themeContext.theme.color }} > themed-button组件:dark主题 </button> </div> )} </UserContext.Consumer> ); } } // 1. 将 ThemeContext 传递到主题按钮组件 ThemedButton.contextType = ThemeContext; export default ThemedButton;
(二) EventBus(事件总线)
- 利用 node 的 api events,
- EventBus相当于一个全局的仓库,任何组件都可以去这个仓库里获取事件,它的工作原理是发布/订阅方法,通常称为 Pub/Sub 。
- 使用事件总线总的来说分三步:发布事件、监听事件、移除事件
-
发布事件:eventBus.emit(“事件名称”, 参数列表);
-
监听事件:eventBus.on(“事件名称”, 监听函数);
-
移除事件:eventBus.off(“事件名称”, 监听函数);
-
-
创建一个干净的脚手架
-
创建 events.js 文件作为事件总线存放文件,内容如下
import EventEmitter from 'events'; const eventBus = new EventEmitter(); export default eventBus;
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import Child1 from './Child1'; import Child2 from './Child2'; export default class app extends Component { constructor() { super(); this.state = { isShowChild1: true, }; } render() { return ( <div> {this.state.isShowChild1 && <Child1 />} <button onClick={() => { this.setState({ isShowChild1: false, }); }} > 销毁Child1 </button> <Child2 /> </div> ); } }
-
创建子组件Child1.jsx 用于监听事件总线
import React, { Component } from 'react'; import eventBus from './events'; export default class Child1 extends Component { constructor() { super(); this.state = { params: [], }; // 方案三: this.childEventBusFn3 = this.childEventBusFn2.bind(this); } render() { return ( <div> <p>Child1</p> {this.state.params.join(',')} </div> ); } // 方案一:使用箭头函数 childEventBusFn = (val1, val2) => { console.log('监听函数被调用1'); this.setState({ params: [val1, val2], }); }; // 下面的写法,如果直接作为监听函数的回调方法会找不到 this childEventBusFn2(val1, val2) { console.log('监听函数被调用2'); console.log(this); this.setState({ params: [val1, val2], }); } componentDidMount() { // 监听事件总线 // 方案一: // eventBus.on('childEventBus', this.childEventBusFn); /** * 方案二:有问题 * this.childEventBusFn2.bind(this) 会返回一个新的函数, * 在移除事件总线时不是同一个回调函数,导致监听事件移除失败 * */ // eventBus.on('childEventBus', this.childEventBusFn2.bind(this)); // 方案三: eventBus.on('childEventBus', this.childEventBusFn3); } // 组件销毁时,移除事件总线 componentWillUnmount() { console.log('组件被销毁,移除事件总线'); // 方案一对应的移除事件总线的方法 // eventBus.off('childEventBus', this.childEventBusFn); // 方案二对应的移除事件总线的方法 // eventBus.off('childEventBus', this.childEventBusFn2); // 方案三对应的移除事件总线的方法 eventBus.off('childEventBus', this.childEventBusFn3); } }
-
创建子组件Child2.jsx 用于发送事件总线
import React, { Component } from 'react'; import eventBus from './events'; export default class Child2 extends Component { render() { return ( <button onClick={() => { //发出事件:eventBus.emit(“事件名称”, 参数列表); eventBus.emit('childEventBus', '参数1', '参数2'); }} > Child2发送事件 </button> ); } }
六、setState
【1】 概念
- 开发中我们并不能直接通过修改组件的state的值来让界面发生更新:
- 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
- React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
- 我们必须通过setState来告知React数据已经发生了变化;
- 在组件中的setState方法是从Component中继承过来的。
- setState 为异步更新
- setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果
- setState设计为异步,可以显著的提升性能;
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
- 最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
- state和props不能保持一致性,会在开发中产生很多的问题;
- 在React 18 之前:
- 在组件生命周期或React合成事件中,setState是异步;
- 在setTimeout或者原生dom事件中,setState是同步;
【2】 三种用法
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; export default class App extends Component { constructor(props) { super(props); this.state = { message: 'hello world', count: 0, }; } setMessage() { // // 方式一: // this.setState({ // message: 'hello react', // }); // 方式二: // props: 组件获取的props // this.setState((state, props) => { // console.log(state, props); // return { // count: state.count + 1, // }; // }); // 方式三: // this.setState 是异步的, // 可以传递第二个参数作为回调函数,在数据修改之后回调函数执行 this.setState( (state, props) => { // 调用2 console.log('调用2', state, props); return { count: state.count + 1, }; }, () => { // 调用3:数据更新后被调用 console.log('调用3', this.state); console.log(arguments); } ); // 调用1 console.log('调用1', this.state); } render() { const { message, count } = this.state; return ( <div> <p> {message}:{count} </p> <button onClick={() => this.setMessage()}>修改message/count</button> </div> ); } }
七、React 性能优化
- 在 React 中如果即使只有父组件中的数据发生变化,其子组件和孙组件等都将重新被调用 render() 方法,所有的组件都需要重新render,进行diff算法,性能必然是很低的
(一)shouldComponentUpdate 生命周期函数
- React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU)
- 该方法有两个参数:
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState 修改之后,最新的state属性
- 该方法返回值是一个boolean类型:
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么就不需要调用render方法;
- 默认返回的是true,也就是只要state发生改变,就会调
- 可以通过对比修改前后的 props 和 state 来判断是否需要更新组件
【1】用例
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下,该页面提供两个按钮,用于判断 数据修改时和没修改时子组件是否被重新渲染
import React, { Component } from 'react'; import Child1 from './Child1'; import Child2 from './Child2'; export default class App extends Component { constructor() { super(); this.state = { message: 'hello world', count: 0, }; } changeMsg() { // 数据并没有发生改变, // 但是 render 函数还会被调用 this.setState({ message: 'hello world', }); } changeCount() { // 数据发生改变, this.setState({ count: 2, }); } componentDidUpdate() { // 数据不改变该生命周期函数也会被调用 console.log('app componentDidUpdate 被调用'); } render() { const { message, count } = this.state; console.log('App render 被调用'); return ( <div> <p>{message}</p> <p>{count}</p> <button onClick={() => this.changeMsg()}> App修改message(数据不改变) </button> <button onClick={() => this.changeCount()}> App修改count(数据改变) </button> <hr /> {/* 不做任何处理 */} <Child1 count={count} /> <hr /> {/* 设置 shouldComponentUpdate 生命周期函数 */} <Child2 count={count} /> </div> ); } }
-
新建子组件 Child1.jsx,用于判断不使用 shouldComponentUpdate 时的渲染方式,内容如下
import React, { Component } from 'react'; export default class Child1 extends Component { componentDidUpdate() { console.log('child1 componentDidUpdate 被调用'); } render() { console.log('child1 render 被调用'); return <div>child1不做任何处理</div>; } }
-
新建子组件 Child2.jsx,用于判断使用 shouldComponentUpdate 时的渲染方式,内容如下
import React, { Component } from 'react'; export default class Child2 extends Component { constructor(props) { super(props); this.state = { message: 'Hi child2', }; } componentDidUpdate() { console.log('child2 componentDidUpdate 被调用'); } shouldComponentUpdate(nextProps, nextState) { console.log(this.props, nextState); // 通过判断前后数据变化来决定是否要调用render 函数 return ( this.props.count !== nextProps.count || this.state.message !== nextState.message ); } changeMsg() { // 数据并没有发生改变, this.setState({ message: 'Hi child2', }); } changeMsg2() { // 数据并没有发生改变, this.setState({ message: 'Bye child2', }); } render() { console.log('child2 render 被调用'); const { message } = this.state; return ( <div> <p>{message}</p> <button onClick={() => this.changeMsg()}> Child2修改message(数据未改变) </button> <button onClick={() => this.changeMsg2()}> Child2修改message(数据改变) </button> </div> ); } }
(二)pureComponent 类
- 组件通过继承pureComponent 类,可以自动进行判断props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
- pureComponent 类是通过
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
方法来判断的,shallowEqual是进行浅层比较 - 仅在你的 props 和 state 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。
- React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。
- 建议在新代码中使用函数式组件,而不是 类式组件。当你将组件从类式组件转换为函数式组件时,将其包装在 memo 中
【1】用例
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 内容如下
import React, { PureComponent } from "react"; import Child1 from "./Child1"; export default class App extends PureComponent { constructor() { super(); this.state = { title: "Hello World", names: ["aaa", "bbb", "ccc"], }; } changeTitle() { this.setState({ title: "Hello React", }); } notModifyTitle() { this.setState({ title: "Hello World", }); } addNames() { const { names } = this.state; names.push("ddd"); console.log(names); // 没有更新 child1 // purecomponent 进行了浅比较 this.setState({ names: names, }); } changeNames() { this.setState({ names: ["eee"], }); } render() { const { title, names } = this.state; return ( <div> <p>{title}</p> <p>{names}</p> <Child1 title={title} names={names} /> <button onClick={() => this.notModifyTitle()}>不修改title</button> <button onClick={() => this.changeTitle()}>修改title</button> <button onClick={() => this.addNames()}>增加names</button> <button onClick={() => this.changeNames()}>修改names</button> </div> ); } }
- 子组件 Child1.jsx 文件内容如下
import React, { PureComponent } from "react"; export default class Child1 extends PureComponent { constructor(props) { super(props); } componentDidUpdate() { console.log("Child1组件更新"); } render() { console.log("Child1组件render"); return <div style={{border:'1px solid red'}}> <p>{this.props.title}</p> <p>{this.props.names}</p> </div>; } }
(三)高阶组件memo
- React.memo 的工作原理与 PureComponent 类似,它只会检查 props 的顶层引用是否相同,而不会深入检查其内部内容。因此,如果你传递的对象或数组的内容发生了变化,但引用保持不变,React.memo 认为 props 没有变化,从而不会触发重新渲染。
【1】用例
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { PureComponent } from "react"; import Child1 from "./Child1"; import Child2 from "./Child2"; export default class App extends PureComponent { constructor() { super(); this.state = { title: "Hello World", names: ["aaa", "bbb", "ccc"], }; } changeTitle() { this.setState({ title: "Hello React", }); } notModifyTitle() { this.setState({ title: "Hello World", }); } addNames() { const { names } = this.state; names.push("ddd"); console.log(names); // 没有更新 child1 // purecomponent 进行了浅比较 this.setState({ names: names, }); } changeNames() { this.setState({ names: ["eee"], }); } render() { const { title, names } = this.state; return ( <div> <p>{title}</p> <p>{names}</p> <Child1 title={title} names={names} /> <Child2 title={title} names={names} /> <button onClick={() => this.notModifyTitle()}>不修改title</button> <button onClick={() => this.changeTitle()}>修改title</button> <button onClick={() => this.addNames()}>增加names</button> <button onClick={() => this.changeNames()}>修改names</button> </div> ); } }
-
子组件 child1.jsx 内容如下
export default function Child1(props) { console.log("Child1组件render"); return <div style={{border:'1px solid red'}}> <p>{props.title}</p> <p>{props.names}</p> </div>; }
-
子组件 child2.jsx 内容如下
import React, { memo } from "react"; const Child2 = memo(function (props) { console.log("Child2组件 render" ); return <div style={{border:'1px solid blue'}}> <p>{props.title}</p> <p>{props.names}</p> </div>; }); export default Child2;
八、Ref 获取 DOM
- 在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。
(一)获取ref
- 创建refs来获取对应的DOM目前有三种方式:
- 方式一:传入字符串 【过时】
- 通过
this.refs.字符串
获取对应的元素;
- 通过
- 方式二:传入一个对象 【推荐】【react19 将被废弃】
- 对象是通过 React.createRef() 方式创建出来的;
- 使用时获取到创建的对象其中有一个
current
属性就是对应的元素;
- 方式三:传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象;
【1】用例
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; export default class App extends Component { constructor() { super(); this.myRef2 = React.createRef(); this.myRef3 = null } getDOM() { // 方式一:(被警告) console.log(this.refs.myRef); // 方式二: console.log(this.myRef2.current); // 方式三: console.log(this.myRef3); } render() { return ( <div> <p ref="myRef">一条信息</p> <p ref={this.myRef2}>二条信息</p> <p ref={(el)=> this.myRef3 = el}>三条信息</p> <button onClick={() => this.getDOM()}>获取ref</button> </div> ); } }
(二)ref 的类型
- ref 的值根据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
- 当 ref 属性用于自定义 类组件时,ref 对象接收组件的挂载实例作为其 current 属性;
- 不能在函数组件上使用 ref 属性,因为他们没有实例;
- 可以使用
React.forwardRef
创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。 React.forwardRef
接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。
- 可以使用
【1】用例
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import Child1 from './Child1'; import Child2 from './Child2'; export default class App extends Component { constructor() { super(); this.myRef = React.createRef(); this.myChild1Ref = React.createRef(); this.myChild2Ref = React.createRef(); } getRef(){ console.log(this.myRef.current); console.log(this.myChild1Ref.current); this.myChild1Ref.current.setMsg() console.log(this.myChild2Ref.current); } render() { return ( <div> <p ref={this.myRef}>基础元素</p> <Child1 ref={this.myChild1Ref}/> <Child2 ref={this.myChild2Ref}/> <button onClick={() => this.getRef()}>获取Ref</button> </div> ); } }
-
用类方式创建组件 Child1.jsx
import React, { PureComponent } from 'react'; export default class Child1 extends PureComponent { constructor() { super(); this.state = { msg: '将会被修改', }; } setMsg() { this.setState({ msg: '已修改', }); } render() { return ( <div> <p>Child1</p> <p>{this.state.msg}</p> </div> ); } }
-
用函数方式创建组件 Child2.jsx
import React from 'react'; const Child2 = React.forwardRef(function (props, ref) { return ( <div> <p>Child2</p> <p ref={ref}>只能获取一个元素</p> </div> ); }); export default Child2;
九、表单
(一) 受控组件
-
在受控组件是指其输入值由 React 组件的状态(state)进行控制的组件。在受控组件中,表单元素的值由组件的状态驱动,任何对输入值的更改都需要通过状态更新来实现。
-
在受控组件中,表单元素的 value 属性与组件的状态紧密相连。每当用户输入数据时,组件会通过事件处理函数更新状态,并将新的状态传递给表单元素。
元素 绑定值 回调方法 回调方法中的值 <input type=“text” /> value=“string” onChange event.target.value <input type=“checkbox” /> checked={boolean} onChange event.target.checked <input type=“radio” /> checked={boolean} onChange event.target.checked <textarea /> value=“string” onChange event.target.value <select /> value=“options value” onChange event.target.value
【1】用例
- 对文本框,单选按钮,多选按钮(单个/ 多个),下拉框(单选/多选) 进行处理
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; export default class App extends Component { constructor() { super(); this.state = { username: '', password: '', gender: '', hobbies: [ { value: 'baseketball', label: '篮球', isChecked: false, }, { value: 'football', label: '足球', isChecked: false, }, { value: 'pinpang', label: '乒乓球', isChecked: false, }, ], isAggree: false, fruit: '', fruits: [], }; } handleSubmit(event) { // 阻止表单提交 event.preventDefault(); // 获取表单数据 console.log(this.state); // 处理多选的值 const hobbies = this.state.hobbies .filter((hobby) => hobby.isChecked) .map((hobby) => hobby.value); console.log(hobbies); // 提交表单数据 /、 } handleInputChange(event) { const key = event.target.name; this.setState({ [key]: event.target.value, }); } handleCheckboxChange(event) { this.setState({ isAggree: event.target.checked, }); } handleHobbiesChange(event, index) { // 结构出一个新的hobbies const hobbies = [...this.state.hobbies]; hobbies[index].isChecked = event.target.checked; this.setState({ hobbies: hobbies, }); } handleFruitsChange(event) { const options = Array.from(event.target.selectedOptions); const values = options.map(option => option.value) this.setState({ fruits: values }) } render() { const { username, password, isAggree, hobbies, fruit, fruits } = this.state; return ( <div> <form onSubmit={(e) => this.handleSubmit(e)}> {/* 文本 */} <label htmlFor="username"> 用户名: <input type="text" id="username" name="username" value={username} onChange={(e) => this.handleInputChange(e)} /> </label> <br /> {/* 文本 */} <label htmlFor="password"> 密码: <input type="password" id="password" name="password" value={password} onChange={(e) => this.handleInputChange(e)} /> </label> <br /> {/* 单选框 */} <div> 性别: <label htmlFor="man"> <input type="radio" id="man" name="gender" value="man" onChange={(e) => this.handleInputChange(e)} /> 男 </label> <label htmlFor="woman"> <input type="radio" id="woman" name="gender" value="woman" onChange={(e) => this.handleInputChange(e)} /> 女 </label> </div> {/* 复选框 */} <div> 爱好: {hobbies.map((hobby, index) => ( <label key={hobby.value} htmlFor={hobby.value}> <input type="checkbox" name="hobbies" id={hobby.value} value={hobby.value} checked={hobby.isChecked} onChange={(e) => this.handleHobbiesChange(e, index)} /> {hobby.label} </label> ))} </div> {/* 复选框 */} <label htmlFor="agree"> <input id="agree" type="checkbox" name="agree" checked={isAggree} onChange={(e) => this.handleCheckboxChange(e)} /> 同意协议 </label> <br /> {/* 单选下拉框 */} <select name="fruit" value={fruit} onChange={(e) => this.handleInputChange(e)} > <option value="">...</option> <option value="apple">苹果</option> <option value="banana">香蕉</option> <option value="orange">橘子</option> </select> <br /> {/* 多选下拉框 */} <select name="fruit" value={fruits} onChange={(e) => this.handleFruitsChange(e)} multiple > <option value="">...</option> <option value="apple">苹果</option> <option value="banana">香蕉</option> <option value="orange">橘子</option> </select> <br /> <button type="submit">提交</button> </form> </div> ); } }
(二) 非受控组件
- 非受控组件是指其输入值不由 React 组件的状态控制,而是通过直接操作 DOM 元素来获取输入值。通常来说,非受控组件使用 React 的 ref 来访问 DOM 元素并获取其值。
- 在非受控组件中,表单输入的值存在于 DOM 元素中,而不是 React 组件的状态。要获取当前的输入值,通常使用 ref 来引用该元素。
- 在非受控组件中通常使用defaultValue来设置默认值;
【1】用例
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; export default class App extends Component { constructor() { super(); this.state = { username: '张三', password: '', }; this.usernameRef = React.createRef(); this.passwordRef = React.createRef(); } // 监听表单文本框的事件 componentDidMount() { this.usernameRef.current.addEventListener('input', (event) => { console.log('用户名文本框事件触发'); // // 获取文本框的值 // const username = event.target.value; // // 设置到state中 // this.setState({ username }); }); } handleSubmit(event) { // 阻止表单提交 event.preventDefault(); // 获取表单数据 console.log('用户名: ' + this.usernameRef.current.value); console.log('密码: ' + this.passwordRef.current.value); // 提交表单数据 // ... } render() { const { username, password } = this.state; return ( <div> <form onSubmit={(e) => this.handleSubmit(e)}> {/* 设置默认值 */} <label htmlFor="username"> 用户名: <input type="text" id="username" name="username" ref={this.usernameRef} defaultValue={username} /> </label> <br /> <label htmlFor="username"> 用户名: <input type="password" id="password" name="password" ref={this.passwordRef} defaultValue={password} /> </label> <br /> <button type="submit">提交</button> </form> </div> ); } }
十、高阶组件
-
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
-
具体而言,高阶组件是参数为组件,返回值为新组件的函数。
-
HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;
(一) props 增强
- 虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
【1】简单用例
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 中有三个组件,App、Child1、Child2,并对Child1、Child2 进行 props 增强
import React, { Component } from 'react'; import enhancedUserInfo from './enhancedProps'; // 在同一个文件中新建另一个组件 // 和单独一个文件一个组件是一样的 class Child1 extends Component { render() { return ( <div> Child1: {this.props.name}-{this.props.age} </div> ); } } // 增强Child1 const NewChild1 = enhancedUserInfo(Child1); // 在同一个文件中新建另一个组件 // 和单独一个文件一个组件是一样的 function Child2(props) { return ( <div> Child2:{props.name}-{props.age}-{props.gender} </div> ); } // 增强Child2 const NewChild2 = enhancedUserInfo(Child2); export default class App extends Component { render() { return ( <div> <NewChild1 /> {/* gender 本质是传递给了增强组件NewComponent */} {/* NewComponent 中又传递给了 Child2 组件 */} <NewChild2 gender="男" /> </div> ); } }
- 新建一个 enhancedProps.js 文件
import React, { Component } from 'react'; /** * 创建一个高阶函数,将用户信息传递给组件 * oginalComponent: 组件 */ function enhancedUserInfo(OginalComponent) { class NewComponent extends Component { constructor(props) { super(props); this.state = { userInfo: { name: '张三', age: 19, }, }; } render() { // 将传递的 props 通过 props 传递给组件 // 将用户信息通过 props 传递给组件 return ( <OginalComponent {...this.props} {...this.state.userInfo} ></OginalComponent> ); } } return NewComponent; } export default enhancedUserInfo;
【2】用例2
通过高阶组件和通过 Context 将主题传递给子组件
-
创建一个干净的脚手架
-
新建一个文件夹context,并在其中创建文件 theme_context.js 文件, 用于存放主题 context
import React from 'react'; export const themes = { dark: { foreground: '#ffffff', background: '#222222', color: '#fff', }, }; // 创建context对象的 // 为当前的 theme 创建一个 context(“dark”为默认值)。 export const ThemeContext = React.createContext({ theme: themes.dark });
-
创建一个文件夹 hoc,用于存放高阶组件,并创建一个文件 theme_hoc.js 用于创建给子组件传递主题相关数据
import { ThemeContext } from '../context/theme_context'; export default function themeHoc(OriginComponment) { // function NewComponment(props) {} // return NewComponment; // 等同于 return (props) => { return ( <div> <ThemeContext.Consumer> {(themeContext) => ( <OriginComponment {...props} theme={themeContext.theme} ></OriginComponment> )} </ThemeContext.Consumer> </div> ); }; }
-
父组件 App.js(App.jsx) 内容如下
import React, { Component } from 'react'; import Child from './Child'; export default class App extends Component { render() { return ( <div> <Child /> </div> ); } }
-
子组件Child.jsx 内容如下
import React, { Component } from 'react'; import themeHoc from './hoc/theme_hoc'; class Child extends Component { render() { return ( <div style={{ color: this.props.theme.color, background: this.props.theme.background, }} > Child </div> ); } } export default themeHoc(Child);
(二) 渲染判断鉴权
- 判断用户是否登录
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx)
import React, { PureComponent } from 'react'; import Child from './Child'; export default class App extends PureComponent { constructor() { super(); const token = localStorage.getItem('token'); this.state = { isLogin: !!token, }; } loginClick() { const token = localStorage.setItem('token', '123'); this.setState({ isLogin: true, }); } render() { return ( <div> { !this.state.isLogin? <button onClick={() => this.loginClick()}>登录</button> : '' } <Child /> </div> ); } }
-
创建 hoc 文件夹,用于存放高阶组件,并在其中创建login_auth.js 文件
export default function loginAuth(OginalComponent) { // function NewComponment(props) {} // return NewComponment; // 等同于 return (props) => { const token = localStorage.getItem('token'); if (token) { return <OginalComponent {...props}></OginalComponent>; } else { return <h2>请先登录:</h2>; } }; }
-
创建子组件 Child.jsx
import React, { PureComponent } from 'react'; import loginAuth from './hoc/login_auth'; class Child extends PureComponent { render() { return <div>Child</div>; } } export default loginAuth(Child);
十一、Portals 的使用
-
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
-
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
ReactDOM.createPortal(child, container)
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
- 第二个参数(container)是一个 DOM 元素;
-
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点;然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
-
一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框
-
创建一个干净的脚手架
-
父组件 App.js(App.jsx) 内容如下
import React, { PureComponent } from "react"; import Modal from "./Modal"; export class App extends PureComponent { render() { return ( <div style={{ height: "500px", width: "1000px", backgroundColor: "#f5f5f5" }} > <p>App</p> <Modal title="我是Modal" content="一个被放在body下的div元素" /> </div> ); } } export default App;
-
新建一个组件 Modal.jsx 内容如下
import React, { PureComponent } from "react"; import { createPortal } from "react-dom"; export class Modal extends PureComponent { render() { const ele = ( <div style={{ backgroundColor: "#888", width: "300px", height: "300px", position: "fixed", top: "25%", left: "25%", }} > <h2>{this.props.title}</h2> <p>{this.props.content}</p> </div> ); // 元素将被挂载到body下 return createPortal(ele, document.body); } } export default Modal;
十二、Fragments
- React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
- React还提供了Fragment的短语法:
- 它看起来像空标签
<> </>
; - 但是,如果我们需要在Fragment中添加key,那么就不能使用短语法
- 它看起来像空标签
- key 是唯一可以传递给 Fragment 的属性。
- 创建一个干净的脚手架
- 父组件 App.js(App.jsx) 内容如下
import React, { Fragment, PureComponent } from "react"; export class App extends PureComponent { constructor() { super(); this.state = { moves: ["电影1", "电影2", "电影3"], }; } render() { // 一般情况下,在顶层如果有多个元素,需要被 // 同一个父元素包裹起来,如果不希望有多余的父元素, // 可以使用Fragment return ( <Fragment> <p>元素1</p> <p>元素2</p> {/* Fragment 可以被省略,但必须使用 <> </> */} <> <p>元素3</p> <p>元素4</p> </> {/* Fragment 上可以使用key,这时不可以使用省略语法*/} <ul> {this.state.moves.map((item) => ( <Fragment key={item}>{item}</Fragment> ))} </ul> </Fragment> ); } } export default App;
十三、StrictMode 严格模式
- StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
- 它为其后代元素触发额外的检查和警告;
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
- StrictMode 不接受任何参数。
- 你可以为应用程序的任何部分启用严格模式。
- 但是检测,到底检测什么呢?
-
- 识别不安全的生命周期:
-
- 使用过时的ref API
-
- 检查意外的副作用
- 一个组件如果在严格模式下,生命周期和render和constructor 都会被执行两次,用于检测如果组件中的某个函数如果没有在组件销毁时被卸载,而多次执行的情况;
-
- 使用废弃的findDOMNode方法
- 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
-
- 检测过时的context API
- 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;目前这种方式已经不推荐使用
-