1、新建react项目
1-1、前期准备
(1)首先确保已经安装node.js
需要在本地开发计算机上安装 Node 8.16.0 或 Node 10.16.0 或更高版本(但服务器上不需要)
(2)下载create-react-app
全局安装:
npm install -g create-react-app
如果之前已经下载create-react-app可以先卸载再安装最新版本
1-2、创建react项目
# 查看自己的 npm 版本
npm --version# 第一种新建项目方式——npm 5.2+ 时,以下命令会安装最新版 CRA
npx create-react-app my-app
# 第一种新建项目方式——npm 版本小于等于 5.1 时
create-react-app my-app# 第二种新建项目方式
npm 6+ 开始支持 npm init <initializer>
npm init react-app my-app# 第三种新建项目方式
yarn create react-app my-app
例子:npx create-react-app my-app
出现这样表示创建项目成功
cd my-app
npm start
1-3、目录结构
+ node_module ------ 第三方资源
+ public ------ 静态资源文件夹
+ favicon.ico ------ 网站页面图标
+ index.html ------ 主页面
+ logo192.png ------ logo 图
+ logo512.png ------ logo 图
+ manifest.json ------ 应用加壳的配置文件
+ robots.txt ------ 爬虫协议文件
+ src ------ 源码文件夹
+ App.css ------ App 组件的样式
+ App.js ------ App 组件
+ App.test.js ------ 用于给 App 组件做测试
+ index.css ------ 全局样式
+ index.js ------ 入口文件
+ logo.svg ------ logo 图
+ reportWebVitals.js ------ 页面性能分析文件(需要 web-vitals 库的支持)
+ setupTests.js ------ 组件单元测试的文件(需要 jest-dom 库的支持)
1-4、文件说明
(1)public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想窗口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器,兼容性较差) -->
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app"/>
<!-- 用于指定网页添加到手机主屏幕后到图标(仅支持 apple 手机) -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时到配置 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
(2)src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
// React.StrictMode 标签会自动校验 react 语法,遇到一些将要遗弃或不推荐使用的语法,会提示
// 不加 React.StrictMode 标签,直接使用 App 组件也没啥影响
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
注意:旧版本的 src/index.js 中,渲染组件是通过方法ReactDOM.render(<App/>,el)实现的:
import React from "react";
import App from "./App"
// 旧版本引入 ReactDOM ,然后 执行 ReactDOM.render()
import ReactDOM from "react-dom"
ReactDOM.render(<App />, document.getElementById("root"))
而新版本18.0.2是通过ReactDOM.createRoot(el).render(<App/>)实现的:
import React from "react";
import App from "./App"
// 新版本引入方式,利用 ReactDOM.createRoot() 创建节点,然后执行 render 函数
import ReactDOM from "react-dom/client"
ReactDOM.createRoot(document.getElementById("root"))
.render(
<App/>
)
(3)ReactDom是一个第三方的模块
位置:index.js;这个文件是整个项目的入口
作用:可以帮助我们把一个组件挂载到某一个dom节点上
以下是将App这个组件挂载到id等于root的节点上进行显示
2、开发
2-1、组件命名
React通过组件的思想,将界面拆分成一个个可复用的模块,每一个模块就是一个React 组件。一个React 应用由若干组件组合而成,一个复杂组件也可以由若干简单组件组合而成。
React 组件可以用好几种方式声明,可以是一个包含 render() 方法的类,也可以是一个简单的函数,不管怎么样,它都是以props作为输入,返回 React 元素作为输出。
组件可以以js
为后缀,也可以以 jsx 为后缀,以 jsx 为后缀可以明显区别于其他功能性的 js 文件。
2-2、引入 React 和 Component
(1)只引 React,定义类组件时使用 React.Component
import React from "react";
// 定义类式组件
export default class Hello extends React.Component {
render() {
return (
<h1 className={hello.title}>Hello 组件</h1>
)
}
}
(2)解构引入 Component,定义类组件时直接使用 Component
// React 中使用了默认暴露和分别暴露,所以可以使用下面的引入方式
// import React, { Component } from "react";
import { Component } from "react";
// 定义类式组件
export default class Welcome extends Component {
render() {
return (
<h1 className="title">Welcome组件</h1>
)
}
}
能使用以上引用方式是因为 React 中使用了 默认暴露 和 分别暴露
class React {
}
// 分别暴露 Component
export class Component {
}
React.Component = Component
// 默认暴露 React
export default React
--------------------------------------
// 其他文件引用时可以这样:
import React, { Component } from "react";
import { Component } from "react";
2-3、创建组件
(1)函数式组件
//组件名称总是以大写字母开始。
//定义模版的函数名首字母必须大写
function Welcome(props){
//添加其他事件函数这么加:function otherFun(){·····}
return <div>my name is {props.name},{props.age}</div>;
}
(2)类组件
//组件名首字母必须大写
class Welcome extends React.Component{
//添加其他事件函数这么加:myWay(){···}
render(){
return (<div>my name is {this.props.name},{this.props.age}</div>);
}
};
(3)React.createClass()
/*组件首字母必须大写,eg:Greeting的G*/
var Greeting = React.createClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>;
}
});
2-4、引入 ReactDOM
(1)新版本18.0.2
要从 react-dom/client中引入 ReactDOM,用法如下:
import React from "react";
// 新版本引入 ReactDOM,渲染节点时使用 ReactDOM.createRoo(el).render(<App/>)
import ReactDOM from "react-dom/client"
import App from "./App";
ReactDOM.createRoot(document.getElementById("root"))
.render(<App/>)
(2)旧版本中
要从 react-dom 中引入 ReactDOM,用法如下:
import React from "react";
import ReactDOM from "react-dom"
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"))
2-5、渲染一个组件
(1)js函数式组件
function Welcome(props){
return <div>my name is {props.name},{props.age}</div>;
}
const element = (
<Welcome name='bty' age='12'/>
);
ReactDOM.render(
element,
document.getElementById('root')
);
输出:my name is bty,12
(2)类组件
//组件名首字母必须大写
class Welcome extends React.Component{
render(){
return (<div>my name is {this.props.name},{this.props.age}</div>);
}
};
//const element必须在定义组件之后
const element = (
<Welcome name='bty' age="12"/>
);
ReactDOM.render(
element,
document.getElementById('root')
);
输出:my name is bty,12
(3)工作步骤:
1、调用ReactDOM.render(),并向其中传入< Welcome name=’bty’ age=’12’/>元素
2、React 调用 Welcome 组件,并向其中传入了 {name: ‘bty’,age=’12’} 作为 props 对象
3、Welcome 组件返回 < div>my name is {props.name},{props.age}
4、 React DOM 迅速更新 DOM ,使其显示为 < div>my name is bty,12
2-6、组件嵌套
function Myhi(props){
return <h1>I‘m {props.name}</h1>
}
class Welcome extends React.Component{
render(){
return(
<div>
<p>start</p>
<Myhi name="bty"/>
<Myhi name="ssh"/>
<Myhi name="mama"/>
</div>)
}
};
ReactDOM.render(
<Welcome/>,
document.getElementById('root')
);
输出:
start
I‘m bty
I‘m ssh
I‘m mama
组件嵌套是如何渲染的?
拿上面例子说,
(1)首先调用ReactDOM.render(),向其中传入< Welcome/>组件;
(2)渲染时:组件< Welcome/>知道如何渲染< p>标签,但不知道如何渲染< Myhi/>,所以再继续调用解析对应组件< Myhi/>,并以{name:'···'}作为props对象传给Myhi组件,返回React 元素,
(3)像上面这样一直做递归,直到返回的React 元素中只包含DOM节点为止。
这样React 就能获取到页面的完整DOM结构信息,渲染的工作自然就ok了
2-7、组件的实例
React 组件是一个函数或类,实际工作时,发挥作用的是React 组件的实例对象。只有组件实例化后,每一个组件实例才有了自己的props和state,才持有对它的DOM节点和子组件实例的引用。
在传统的面向对象的开发方式中,实例化的工作是由开发者自己手动完成的,但在React中,组件的实例化工作是由React自动完成的,组件实例也是直接由React管理的。换句话说,开发者完全不必关心组件实例的创建、更新和销毁。
在类组件中 的实例,就是用 this 引用的那个对象,对于保存本地状态以及介入生命周期函数是有用的。
注:组件必须是 纯函数
无论你用函数或类的方法来声明组件, 它都无法修改其自身 props. 思考下列 sum (求和)函数:
function sum(a, b) {
return a + b;
}
这种函数称为 “纯函数” ,因为它们不会试图改变它们的输入,并且对于同样的输入,始终可以得到相同的结果。
反之, 以下是非纯函数, 因为它改变了自身的输入值:
function withdraw(account, amount) {
account.total -= amount;
}
严格的规则:
所有 React 组件都必须是纯函数,并禁止修改其自身 props
2-8、css 模块化
按照截图方式在同一个组件中引用多个组件,如果 Hello 和 Welcome 组件存在相同类的不同样式时,后者会覆盖前者,所以需要模块化样式,使其互不影响。
怎么做?
(1)将 .css 文件改为 .module.css 文件
(2)引入 css 文件时,使用 import hello from "./Hello.module.css" 代替 import "./Hello.module.css"
组件标签中使用 hello.title, <h1 className={hello.title}>Hello 组件</h1>
编译出来是如下效果:
2-9、组件通信
react的props、state、ref
参考文章:React03-props 和 state 详解_react state和props-CSDN博客
如果是在App.js引入则是全局引入,不用没个组件都引入
(1)父组件=》子组件通信props
子组件是类组件,新建文件ChildByProps1.js,代码如下:
import React,{Component} from "react";
export default class ChildByProps1 extends Component {
constructor(props){
super(props);
}
render(){
return (
<span>子组件1:{this.props.txt}</span>
)
}
}
子组件是函数组件,新建文件ChildByProps2.js,代码如下:
// 例子1
export default function ChildByProps2(props){
const {txt} = props;
return <span>子组件2:{txt}</span>
}
//例子2
export default function ChildByProps2(props){
return <span>子组件2:{props.txt}</span>
}
父组件:
import React from 'react';
import './App.css';
import ChildByProps1 from './Component/ClassComponent/Parent2ChildByProps';
import ChildByProps2 from './Component/FunctionComponent/Parent2ChildByProps';
function App() {
return (
<div className='container'>
<p>父传子Props</p>
<ChildByProps1 txt={"类组件"}></ChildByProps1>
<ChildByProps2 txt="函数组件"></ChildByProps2>
</div>
)
}
export default App;
(2)父组件使用refs来直接调用子组件实例的方法
useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值
在大多数情况下,应当避免使用ref这样的命令式代码
useImperativeHandle应当与forwardRef一起使用
- useImperativeHandle使用简单总结:
- 作用:减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
- 参数1:父组件传递的ref属性
- 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
函数组件(useRef):
useImperativeHandle、forwardRef、useRef()
1、函数式
函数子组件:
import React, { useState,useImperativeHandle, forwardRef } from "react";
import {message } from "antd";
function ChildByRef2({},ref){
const showMsg = (msg)=>{
message.info(msg);
}
//将方法暴露给父组件使用
//@param: ref:父组件传递的ref属性,参数2返回一个对象,以供父组件通过ref.current调用对象中的方法
useImperativeHandle(ref,()=>({
show: showMsg
}));
return (
<></>
)
}
export default forwardRef(ChildByRef2);
函数父组件:
import ChildByRef2 from './Component/FunctionComponent/Parent2ChildByRef';
function App() {
const myRef2 = useRef();
//使用子组件暴露的方法
const myclick2 = ()=>{
myRef2.current.show("我是函数组件");
}
return(
<div className='container'>
<ChildByRef2 ref={myRef2}></ChildByRef2>
<Button onClick={myclick2}>函数父调用函数子组件的方法</Button>
</div>
)
}
显示:
2、类组件
类子组件:
import { message } from "antd";
import React from "react";
export default class ChildByRef1 extends React.Component{
constructor(props){
super(props)
}
show = (msg)=>{
message.info(msg)
}
render() {
return (
<></>
)
}
}
类父组件:
import { Button } from "antd";
import React,{ Component, createRef } from "react";
import ChildByRef1 from "../Parent2ChildByRef";
export default class Parent1 extends Component{
constructor(props) {
super(props)
this.report = React.createRef();
this.state={}
}
render(){
return(
<div>
<Button onClick={()=>{this.report.current.show("子组件是类组件")}}>
父组件是类组件
</Button>
<ChildByRef1 ref={this.report}></ChildByRef1>
</div>
)
}
}
显示:
(3)子组件=》父组件callback回调函数
函数子组件
import React from "react";
import { Button } from "antd";
export default function ChildByCallback2({myClick}){
const handleClick = ()=>{
myClick("我是函数子组件")
}
return (
<Button type="primary" onClick={handleClick}>click me</Button>
)
}
类子组件
import { Button } from "antd";
import React from "react";
export default class ChildByCallback1 extends React.Component{
constructor(props){
super(props);
}
handleClick = ()=>{
this.props.myClick("我是类子组件")
}
render(){
return (
<Button type="primary" style={{marginTop:"5px"}}
onClick={this.handleClick}>点我</Button>
)
}
}
App.js
import ChildByCallback2 from './Component/FunctionComponent/Child2ParentByCallBack';
import ChildByCallback1 from './Component/ClassComponent/Child2ParentByCallBack';
function App() {
const handleClick = (data)=>{
message.success(data)
}
return (
<div>
<Divider>子传父Callback</Divider>
<ChildByCallback2 myClick={handleClick}></ChildByCallback2>
<ChildByCallback1 myClick={handleClick}></ChildByCallback1>
</div>
)
}
(4)兄弟组件通信
利用共同父组件(状态提升:将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态)。为了方便,这里使用了钩子函数useState。
兄弟A组件
import React from "react";
export default function SiblingA({count}){
return (
<span>A的count值{count}</span>
)
}
兄弟B组件
import React from "react";
import { Button } from "antd";
export default function SiblingB({updateFunc}){
const clickFunc = ()=>{
updateFunc(value=>{
return value+1
})
}
return (
<Button onClick={clickFunc}>B修改A的值</Button>
)
}
App.js
import SiblingA from './Component/FunctionComponent/SiBlingA';
import SiblingB from './Component/FunctionComponent/SiblingB';
function App(){
const [count,countUpdate] = useState(0)
return (
<div>
<Divider>兄弟组件利用共同父组件(状态提升)</Divider>
<SiblingA count={count}></SiblingA>
<SiblingB updateFunc={countUpdate}></SiblingB>
</div>
)
}
(5)跨级组件通信
Context
1、调用 React. createContext() 创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件。
2、使用 Provider 组件作为父节点。
3、设置 value 属性,表示要传递的数据。
4、调用 Consumer 组件接收数据。
为了方便,先新建一个文件构建Context对象
import React from "react";
const MyContext = React.createContext();
export default MyContext;
父组件
import React from "react";
import Son from "../Son";
export default function Father(){
return (
<div>
父组件
<Son></Son>
</div>
)
}
子组件
import React from "react";
import Sun from "../Sun";
export default function Son(){
return (
<div>
子组件
<Sun></Sun>
</div>
)
}
孙组件
import React from "react";
import MyContext from "../../../utils/MyContext";
export default function Sun(){
return (
<MyContext.Consumer>
{
({color,changeColor})=>
<div style={{color:color}}>
子孙组件
<button onClick={changeColor}>换颜色</button>
</div>
}
</MyContext.Consumer>
)
}
App.js
import Father from './Component/FunctionComponent/Father';
import MyContext from './utils/MyContext';
function App() {
const [color,colorUpdate] = useState("green");
return(
<div>
<Divider>跨级组件Context</Divider>
<MyContext.Provider value={{color,
changeColor:()=>{if(color!=="blue"){colorUpdate("blue")}else{colorUpdate("green")}}}}>
APP组件
<Father></Father>
</MyContext.Provider>
</div>
)
}
(6)全局变量
子组件
import React from "react";
export default class ComponentByGlobal extends React.Component {
state = {
value:window.a
}
constructor(props){
super(props)
}
componentDidUpdate(prevState){
if(prevState.value!==this.state.value){
console.log("window.a is changed to " + this.state.value);
}
}
render(){
return (
<div>
window.a的值为{this.state.value}
<button onClick={()=>{this.setState({value:window.a})}}>点击更新</button>
</div>
)
}
}
App.js
import ComponentByGlobal from './Component/ClassComponent/GlobalVariable';
function App(){
window.a=1
return (
<div>
<Divider>全局变量Global Variables</Divider>
<button onClick={()=>{window.a+=10; console.log(window.a)}}>更改全局变量window.a</button>
<ComponentByGlobal></ComponentByGlobal>
</div>
)
}
(7)props.children
调用组件时,填充在组件标签中的内容
function Person(props) {
return (
<div>
<h3>hello world</h3>
{props.children}
</div>
);
}
export default function () {
return (
<Person>
<h3>hello web</h3>
<h3>hello</h3>
</Person>
);
}
2-10、路由
(1)路由表配置:参数地址栏显示
<Route path="/list/:id" component={List} />
html:<Link to='/list/2' >跳转列表页面</Link>
Js: this.props.history.push('/list/2');
List页面接收:
console.log(this.props.match.params.id)//传递过来的所有参数
(2)query方法:参数地址栏不显示,刷新地址栏,参数丢失
html:
<Link to={{ pathname: '/list', query: { name: 'xlf' } }}>跳转列表页面</Link>
Js方式:this.props.history.push({ pathname: '/list', query: { name: ' sunny' } })
List页面接收:
console.log(this.props.location.query.name)//传递过来的所有参数
(3)state方法:参数地址栏不显示,刷新地址栏,参数不丢失,但是该方法只支持BrowerRouter,不支持HashRouter
Html: <Link to={{ pathname: '/list', state: { name: 'xlf' } }}>跳转列表页面</Link>
js:this.props.history.push({ pathname: '/list', state: { name: 'sunny' } })
List页面接收
console.log(this.props.location.state.name)//传递过来的所有参数
参考文章:
8种React组件间通信方式(类组件+函数组件)_react组件通信-CSDN博客
react路由跳转、传参3中方式及区别_react的history.push传参-CSDN博客
3、一些补充
3-1、antd使用
1、在项目目录下下载antd
npm install antd --save
2、在你的 React 组件中引入 Ant Design 的组件
import { Button } from 'antd';
3-2、react 关闭eslint
在package.json中,将eslintConfig中的“react-app”删除即可
(如果无效,将"react-app/jest"一并删除)
"eslintConfig": {
"extends": [
"react-app", // 删除这个
"react-app/jest" // 上面的删除后,如果无效,也可以把这个删掉
]
},
3-3、什么时候用类组件什么时候用函数组件
函数组件: 这些是简单的JavaScript函数,接受props作为输入并返回JSX元素。它们通常用于表示或无状态组件。
类组件: 这些是继承自React.Component或React.PureComponent的ES6类。它们有一个render()方法,您可以在其中使用JSX定义组件的UI结构。类组件用于需要管理状态或具有生命周期方法的组件。
随着React Hooks的引入,函数组件获得了使用状态和生命周期方法的能力,使得函数组件与类组件之间的区别显著减少。但是,在React应用程序中选择适当的组件类型仍然非常普遍。
以下是一些指导原则:
-
简单的 UI 组件:对于简单的 UI 组件,如按钮、图标、展示静态内容的组件等,使用函数组件通常更简洁明了。
-
状态管理:如果组件需要内部状态或生命周期方法,则类组件可能更合适。但是,自 React 16.8 版本引入 Hooks 后,函数组件也可以使用状态和其他 React 特性,因此你可以考虑在函数组件中使用 Hooks 来管理状态。
-
性能:通常来说,函数组件比类组件具有更好的性能,因为函数组件本身没有实例化过程,并且函数本身具有更轻量的内存开销。但是在实践中,这种性能差异可能不太明显,除非你的应用程序具有非常大量的组件。
-
团队约定:有时候,团队可能会有自己的偏好或约定,比如全面采用函数组件或类组件。
至于为什么 Ant Design (antd) 的示例代码大多数都是函数组件,可能有几个原因:
- 简洁性:函数组件相对于类组件来说,代码量更少,更易于阅读和编写。
- 性能:函数组件通常比类组件性能更好。
- Hooks 支持:antd 可能更倾向于使用函数组件,因为函数组件更容易与 React Hooks 配合使用,而 Hooks 是 React 生态系统的一部分,也是未来发展的方向。
- 函数式编程范式:函数组件符合函数式编程的范式,这与 React 的设计理念有一定的契合。
总的来说,选择函数组件还是类组件取决于你的具体情况和偏好。在现代 React 应用程序中,函数组件在很多情况下是首选,但有些情况下类组件仍然是必需的,比如需要使用生命周期方法或是需要继承 React.Component 的情况。
参考文章:
https://www.cnblogs.com/ygyy/p/18191538
在React中的函数组件和类组件——附带示例的对比_react使用函数还是类组件更好-CSDN博客
3-4、react生命周期
react生命周期可以分为三个阶段
1、挂载阶段
当组件实例被创建并挂载到DOM中时,会执行这些生命周期方法
(1)constructor(props)
- 构造函数,在创建组件时调用,用于初始化状态和绑定事件处理方法。
- 在构造函数中通常做两件事情:初始化组件的状态和绑定事件处理方法。
- 如果不需要初始化state或绑定方法,则不需要显式定义构造函数。
(2)static getDerivedStateFromProps(props, state)
- 静态方法,用于根据props更新state。
- 在组件实例化时和接收新props时调用,返回一个对象来更新当前state,如果不需要更新可以返回null。
- 在React 16.4版本及以上,setState和forceUpdate也会触发该生命周期方法。
(3)render()
- 渲染方法,准备渲染组件的UI结构。
- 根据状态state和属性props渲染组件,返回需要渲染的内容。
- 只做一件事情:返回需要渲染的内容,不要在这个方法内部做其他业务逻辑。
(4)componentDidMount()
- 在组件第一次渲染完成后调用,通常用于执行一次性操作,如数据获取、订阅事件等。
- 可以执行依赖于DOM的操作、发送网络请求、添加订阅消息等。
- 避免在componentDidMount中直接调用setState,因为会触发额外的渲染。
import React, { Component } from 'react';
class SampleComponent extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
preCounter: 0
};
}
static getDerivedStateFromProps(props, state) {
if (props.counter !== state.preCounter) {
return {
counter: props.counter,
preCounter: props.counter
};
}
return null;
}
componentDidMount() {
// 在组件挂载后,将计数数字变为1
this.setState({
counter: 1
});
}
render() {
return (
<div>
<h1>Hello, world! {this.state.counter}</h1>
</div>
);
}
}
export default SampleComponent;
2、更新阶段
更新阶段(Updating)涉及到组件在接收到新的props或者状态发生变化时的一系列生命周期方法
(1)static getDerivedStateFromProps(props, state)
- 当组件接收到新的props时调用,在这个方法中可以根据新的props来更新组件的state。
- 通过返回一个对象来更新当前的state,如果不需要更新则返回null。
(2)shouldComponentUpdate(nextProps, nextState)
- 用于决定组件是否需要重新渲染,可以通过返回false来阻止不必要的渲染。
- 在这个方法中可以比较当前的props和state与下一个props和state的值,来决定是否需要更新。
(3)render()
- 重新渲染组件的UI结构,根据最新的props和state生成组件的内容。
(4)getSnapshotBeforeUpdate(prevProps, prevState)
- 在最近一次渲染输出(提交到DOM上)之前调用,可以用于获取当前DOM的快照信息。
- 返回的值将作为第三个参数传递给componentDidUpdate方法。
(5)componentDidUpdate(prevProps, prevState, snapshot)
- 组件更新完成后调用,通常用于处理更新后的操作,如数据同步、DOM操作等。
- 可以在这个方法中执行依赖于更新后DOM状态的操作。
在更新阶段,组件可能会多次重新渲染,这些生命周期方法帮助我们控制组件的更新行为,提高性能并确保组件状态的正确性。通过合理地使用这些方法,我们可以在组件更新时做出相应的处理,保持组件的稳定性和效率。
import React, { Component } from 'react';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
console.log('Component is being constructed');
}
static getDerivedStateFromProps(props, state) {
// 根据props更新state
}
shouldComponentUpdate(nextProps, nextState) {
// 控制组件是否重新渲染
return true;
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 在组件更新前获取某些信息,如滚动位置
}
componentDidUpdate(prevProps, prevState, snapshotValue) {
// 组件更新后的操作
}
}
3、卸载阶段
在卸载阶段,只有一个生命周期函数,即componentWillUnmount()。这个方法会在组件即将被卸载和销毁之前被调用。
(1)componentWillUnmount()
- 清除定时器
- 取消网络请求
- 取消在componentDidMount()中创建的订阅等操作
import React, { Component } from 'react';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
console.log('Component is being constructed');
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
componentWillUnmount() {
// 组件卸载前的操作
console.log('Component is about to unmount');
}
}
4、错误处理阶段
在React中,有两个生命周期方法用于处理组件内部错误的情况
(1)static getDerivedStateFromError(error)
- 当子组件抛出错误时调用,用于更新父组件的state以渲染降级UI(Fallback UI)。
- 这个静态方法允许父组件捕获子组件抛出的错误,并在发生错误时更新状态,以渲染备用UI来展示用户。
(2)componentDidCatch(error, info)
- 在后代组件抛出错误后被调用,用于捕获组件内部的JavaScript错误、网络请求失败等异常情况,进行错误处理。
- 接收两个参数:
- error:表示抛出的错误对象。
- info:一个对象,其中包含有关组件引发错误的堆栈信息,通常包含componentStack等信息。
5、react常见生命周期
(1)挂载阶段
- 首先执行constructor构造方法,用于创建组件实例。
- 接着执行render方法,返回需要渲染的内容。
- React将渲染内容挂载到DOM树上。
- 挂载完成后会执行componentDidMount生命周期函数,允许执行一些初始化操作,如数据获取或订阅事件。
(2)更新阶段
- 当组件接收到新的props或调用setState、forceUpdate时,会重新调用render方法。
- render方法返回的内容将会重新挂载到DOM树上。
- 挂载完成后会执行componentDidUpdate生命周期函数,可以执行更新后的操作,如数据同步或DOM操作。
(3)卸载阶段
移除组件时,执行componentWillUnmount生命周期函数,用于清理工作,如清除定时器或取消订阅
6、react主要生命周期
(1)getDefaultProps:在组件创建之前调用一次,用来初始化组件的Props(属性)。
(2)getInitialState:用于初始化组件的state(状态)值。
(3)componentWillMount:在React旧版本中存在,但在React 16中已被废弃。在组件创建后、render之前调用,已不推荐在此阶段执行任何操作。
(4)render:是唯一必须实现的生命周期方法。根据props和state返回一个jsx元素,用于渲染组件到界面上。有时也可返回null或false。
(5)componentDidMount:在组件挂载后立即调用,表示组件挂载完成。适合执行需要DOM操作或发起网络请求的操作,因为此时组件已经插入DOM树中。这也是推荐用于发起网络请求的时机。
参考文章:前端React篇之React的生命周期有哪些?_react 生命周期-CSDN博客
3-5、React Hooks
Hooks简介
Hooks,可以翻译成钩子
在类组件中钩子函数就是生命周期函数,Hooks 主要用在函数组件
类组件的不足:
- 在一个钩子里可能有很多业务代码。
- 一个业务很可能出现在多个钩子里。
- class 组件中的 this 指向问题。
类组件通过在构造器中直接使用 this.state = {} 给组件设置状态值,而函数组件不行。因为函数执行完后会销毁内容,在函数内声明的变量就会自定销毁,所以函数组件无法设置状态,于是 react 提供了 Hooks。
Hooks 的分类
Hook 分为2种,基础 Hook 和额外的 Hook。
基础 Hook:
- useState
- useEffect
- useContext
额外的 Hook:
- useReducer
- useCallback
- useMemo
- useRef
所有 Hook 都以 use 开头。
Hook 的使用
所有 Hook 都在 react 模块下,使用时需进行引入。
import { useState } from 'react';
1、useState
(1) useState 向组件引入新的状态,这个状态会被保留在 react 中。
(2) useState 只有一个参数,这个参数是初始状态值,它可以是任意类型。
(3) useState 可以多次调用,意味着可以为组件传入多个状态。
(4) useState 返回值是一个数组,第一个元素就是我们定义的 state,第二个元素就是修改这个 state 的方法。接收 useState 的返回值使用数组结构语法,我们可以随意为 state 起名字。修改 state 的方法必须是 set + 状态名首字母大写构成,不按照约定写就会报错。
(5) useState 的参数也可以是一个函数,这个函数的返回值就是我们的初始状态。
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
注意:修改状态方法
修改状态的方法 set + 状态变量名:
- 这个方法的参数可以是值(替换方法),也可以是一个函数。如果是函数,那么这个函数的参数就是初始状态值,这个方法就是更新方法。
- 这个方法是异步的。
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
在点击按钮后,计数器显示数字变为了2,但页面的标题还是1
解决办法:
使用useRef存储数据,useEffect监听数据变化,并进行更新。
2、useEffect
主要作用就是将副作用代码添加到函数组件内。所谓的副作用代码就是 dom 更改、计时器、数据请求等。
使用场景:
useEffect(() => {}) 这种写法代表两个生命周期函数 componentDidMount 和 componentDidUpdate。
useEffect(() => {}, []) 这种写法代表生命周期函数 componentDidMount。
useEffect(() => () => {}) 这种写法代表组件卸载之前 componentWillUnmount 和 componentDidUpdate 两个生命周期函数。
(1)useEffect(() => {})
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('hahaha');
});
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
在页面加载完成和每次点击按钮让计数器自增后,控制台都会打印 hahaha。
useEffect Hook 实现了生命周期函数 componentDidMount 和 componentDidUpdate 的功能。
(2)useEffect(() => {}, [])
function Counter() {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('hahaha');
}, []);
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
</div>
);
}
useEffect 钩子函数传入了一个空数组作为第2个参数,当页面加载完成时控制台会打印 hahaha,点击按钮更新计数器后不再打印。
实现了生命周期函数 componentDidMount 的功能。
(3)useEffect(() => () => {})
function Counter(props) {
const [count, setCount] = useState(() => 1);
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => () => {
console.log('hahaha');
}, []);
return (
<div>
<h3>{count}</h3>
<button onClick={() => handleCount(count)}>+</button>
<button onClick={() => props.root.unmount()}>卸载组件</button>
</div>
);
}
点击按钮更新计数器和点击卸载组件时控制台都会打印 hahaha。
实现了生命周期函数 componentWillUnmount 和 componentDidUpdate 的功能。
(4)useEffect 第二个参数的使用
useEffect 钩子函数的第二个参数,正常添加空数组,代表的生命周期是 componentDidMount。即使我们修改了 state,useEffect 也只会调用一次。
如果我们想让某个 state 发生改变的时候,继续调用 useEffect,就需要把这个状态添加到第二个参数的数组中。
function Counter() {
const [count, setCount] = useState(() => 1);
const [person, setPerson] = useState({ name: 'zhangsan' });
function handleCount(a) {
setCount(a + 1);
document.title = a;
}
useEffect(() => {
console.log('计数器改变');
}, [count]); // count 一旦发生改变,就会执行 useEffect
return (
<div>
<h3>{count}</h3>
<h3>{person.name}</h3>
<button onClick={() => handleCount(count)}>+</button>
<button onClick={() => setPerson({ name: 'lisi' })}>更改person</button>
</div>
);
}
useEffect 第二个参数传递了 count,那么将会在 count 状态更新时才会执行传入的函数。
(5)useEffect 的异步处理
// 案例
function Counter() {
function asyncFn() {
setTimeout(() => {
console.log('hahaha');
}, 1000);
}
useEffect(() => {
asyncFn();
});
return (
<div>
</div>
);
}
声明了一个异步函数,然后在 useEffect 中调用,预览正常,在页面加载完成1秒后打印 hahaha
在 useEffect 中如果使用了异步函数,那就需要定义一个自调用函数。如:
function Counter() {
function asyncFn() {
setTimeout(() => {
console.log('hahaha');
}, 1000);
}
useEffect(() => {
(async () => {
await asyncFn();
})();
});
return (
<div>
</div>
);
}
3、useContext
useContext 用于父组件向子孙组件传递数据,不需要再使用通过 props 从父组件向子组件逐级传递数据。
如果组件多层嵌套,使用 props 来传值显得极其复杂,这时就需要使用 useContext。
前面通信方式已经介绍过了
4、useReducer
useReducer 是 useState 的替代方案,将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
因为对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人头大。
可以通过三个步骤将 useState 迁移到 useReducer:
- (1) 将设置状态的逻辑修改成 dispatch 的一个 action;
- (2) 编写 一个 reducer 函数;
- (3) 在组件中 使用 reducer。
function Counter() {
const initState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return { count: state };
}
}
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
5、useMemo
useMemo 是一种性能优化的手段,主要用途就是对状态 state 的记忆,并且还具有缓存功能,类似于 vue 中的计算属性。还有一个作用,避免在代码中参与大量运算。
useMemo 接收2个参数,第1个参数为执行运算的函数,第2个参数为要监控的状态。
function Counter() {
const [count, setCount] = useState(0);
const value = useMemo(function () {
return count * 10;
}, [count]); // 数组中的元素就是 useMemo 监控的状态
return (
<div>
<h3>{count}</h3>
<h3>{value}</h3>
<button onClick={() => setCount(count + 1)}>按钮</button>
</div>
);
}
6、useRef
useRef 用于获取组件中的 dom 对象
前面通信方式已经介绍过了
7、memo
memo 是一个高阶组件(参数又是一个组件),功能是对组件进行记忆。
// 案例
function App() {
const [count, setCount] = useState(0);
const fn = function () {
console.log('hahaha');
};
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
function Heads(props) {
console.log('我被渲染了');
return <button>按钮</button>;
}
我们定义了一个父组件 App 和一个子组件 Heads。
在 count 变化时,子组件重新被渲染。
当前组件内的视图没有发生改变,但被重渲染了,这时就需要借助 memo。
function App() {
const [count, setCount] = useState(0);
const fn = function () {
console.log('hahaha');
};
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
console.log('我被渲染了');
return <button>按钮</button>;
});
memo 的参数是一个组件,返回值为一个高阶组件,可以对传入的组件进行记忆,不修改就不会重新渲染。
但上述案例中,点击按钮时子组件还是会重新渲染。原因在于父组件给子组件传递了一个 fn,当点击按钮时,父组件重新渲染导致 fn 被赋值,fn 修改就会导致子组件重新渲染。
要实现前面的案例传入 fn 不让子组件重新渲染,需要使用 useCallback Hook。
8、useCallback
useCallback 是一个允许我们在多次渲染中缓存函数的 React Hook,它返回一个 memoized 回调函数。
useMemo 是对数据的记忆,useCallback 是对函数的记忆。
useCallback 有2个参数,第1个参数为要缓存的函数,第2个参数是一个数组,表示在哪些响应值(包括 props 、state 和所有在组件内部直接声明的变量和函数)变化时更新函数。
// 案例
function App() {
const [count, setCount] = useState(1);
const fn = useCallback(function () {
return count;
}, []);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});
这里我们使用 useCallback 将函数 fn 进行缓存,这时再去点击增加按钮,将不会再重新渲染子组件
function App() {
const [count, setCount] = useState(1);
const fn = useCallback(function () {
return count;
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
<Heads fn={fn}></Heads>
</div>
);
}
const Heads = memo(function (props) {
return <button onClick={() => console.log(`我被渲染了${props.fn()}次`)}>按钮</button>;
});
useCallback 中加入了第二个参数,数组中有一个元素 count,表示在 count 变化时更新函数 fn。这时在点击增加按钮,就会重新渲染子组件。
9、自定义 Hook
自定义 Hook 就是自己封装的函数功能和 react 中内置的 Hook 进行结合,用于组件间共享逻辑
自定义 Hook 必须以 use 开头
const useGet = function ({ path, params }) {
const [data, setData] = useState({});
useEffect(() => {
axios.get(path, params).then(res => {
setData(res.data);
});
}, []);
return data;
};
// App.jsx
function App() {
const data = useGet({ path: 'https://conduit.productionready.io/api/articles', params: {}});
if (!data.articles) {
return <div>
请求中...
</div>;
}
return (
<div>
{data.articles.map(item => <p>{item.title}</p>)}
</div>
);
}
参考文章: