React学习日志
一、React 基础
1、React 概述
React 是一个用于构建用户界面的 JavaScript 库
MVC角度,React 仅仅是视图层(V)
特点:
- 声明式
- 基于组件
- 学习一次,随处使用
2、React 基本使用
2.1 React 的安装
npm i react react-dom
- react 包是核心,提供创建元素、组件等功能
- react-dom 包提供 DOM 相关功能
2.2 React 的基本使用
<div id="root"></div>
<!-- 1 引入 js 文件 -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<script>
// 2 创建 react 元素
// 参数:元素名称、元素属性、元素子节点(第三个及其以后得子节点)
// const title = React.createElement('h1', null, 'Hello react !')
const title = React.createElement(
'p',
{ title: '我是标题', id: 'p1' },
'Hello react !',
React.createElement('span', null, '我是span节点')
)
// 3 渲染 react 元素
// 参数:要渲染的react元素、挂载点
ReactDOM.render(title, document.getElementById('root'))
</script>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2Bm27ts-1692620120415)(D:\PerfectCode\前端学习\React\Image\创建元素.PNG)]
3、React 脚手架的使用
3.1 React 脚手架意义
- 脚手架是开发 现代 Web 应用的必备
- 充分利用 Webpack、Babel、ESLint 等工具辅助项目开发
- 零配置,无需手动配置繁琐的工具即可使用
- 关注业务,而不是工具配置
3.2 使用 React 脚手架初始化项目
-
初始化项目:
npx create-react-app 项目名称
-
启动项目,在项目根目录中执行:
npm start
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hmQdorfo-1692620120416)(D:\PerfectCode\前端学习\React\Image\npx.PNG)]
特别:
还可以用 npm init react-app my-app
和 yarn create react-app my-app
创建
3.3 在脚手架中使用 React
- 导入 react 和 react-dom 两个包
import React from 'react'
import ReactDOM from 'react-dom'
- 调用 React.createElement() 方法创建 react 元素
- 调用 ReactDOM.render() 方法渲染 react 元素到页面中
特别地(最新版):
- 导入 react 和 react-dom 两个包
import React from 'react'
import ReactDOM from 'react-dom/client'
-
调用 ReactDOM.createRoot(document.getElementById(‘root’)); 获取 root 节点
-
使用 JSX 语法创建 react 元素
-
调用 root.render() 方法渲染 react 元素到页面中
3.4 最新版
网址:https://reactjs.org/link/switch-to-createroot
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) 使用 TypeScript 语法
root.render(<App tab="home" />);
脚手架中:
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'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
二、JSX
1、JSX 的基本使用
1.1 createElement() 的问题
- 繁琐不简洁
- 不直观
- 不优雅,用户体验不爽
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nvVvSpJw-1692620120417)(D:\PerfectCode\前端学习\React\Image\createElement问题.PNG)]
1.2 JSX 简介
JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML (HTML)格式的代码
优势:声明式语法更加直观、与HTML结构相同,降低学习成本、提高开发效率
1.3 使用步骤
// 1 导入 react
import React from 'react';
import ReactDOM from 'react-dom';
// import ReactDOM from 'react-dom/client';
// 最新版
// const root = ReactDOM.createRoot(document.elementById('root'))
// 2 使用 JSX 语法创建 react 元素
const title = (
<h1 className="title">Hello JSX
<span>这是span</span>
<p/>
</h1>
);
// 3 渲染 react 元素
ReactDOM.render(title, document.getElementById('root'));
// 最新版
root.render(title)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQ5z0ZYh-1692620120417)(D:\PerfectCode\前端学习\React\Image\为什么可以使用JSX.PNG)]
1.4 注意点
-
React 元素的属性名使用驼峰命名法
-
特殊属性名:calss -> calssName、for -> htmlFor、tabindex -> tabIndex
-
没有子节点的 React 元素可以用 /> 结束
-
推荐:使用 小括号包裹 JSX,从而避免 JS 中的自动插入分层陷阱
const dv = (
<div className="hello">
Hello JSX
<p />
</div>
)
2、JSX 中使用 JavaScript 表达式
嵌入 JS 表达式
- 数据存储在 JS 中
- 语法:{ JavaScript 表达式 }
- 注意:语法中是 单大括号,不是爽大括号!
const name = '张三';
const p = <p>这是p</p>;
const sayHi = () => 'Hi~'
const title = (
<h2 className="title">Hello JSX ,
<span>这是span,{name}</span>
{p}, {1}, {'abc'}
{3>5?'大于':'小于'}
<p/>
{sayHi()}
</h2>
);
注意点:
- 单大括号 中可是使用任意的 JavaScript 表达式
- JSX 吱声也是 JS 表达式
- 注意:JS 中的对象是一个例外,一般只会出现在 style 属性中
- 注意:不能再 {} 中出现语句(比如:if/for 等)
{/* 错误演示 */}
{/* <p>{ {a: '6'} }</p> */}
{/* { if (true) { <p>这是if</p> } } */}
{/* { for (let i = 0; i < 10; i++) { <p>这是for</p> } } */}
3、JSX 的条件渲染
- 场景:loading效果
- 条件渲染:根据条件渲染特定的 JSX 结构
- 可以使用
if/else
或三元表达式
或逻辑与运算符
来实现
const isLoading = true;
const loadData = () => {
// if else方式
// if (isLoading) {
// return <div>loading...</div>
// }
// return <div>data...</div>
// 三元表达式
return isLoading ? <div>loading...</div> : <div>data...</div>
// 逻辑与
// return isLoading && <div>loading...</div>
}
const title = (
<h2>
条件渲染:{loadData()}
</h2>
);
4、JSX 的列表渲染
- 如果要渲染一组数据,应该使用数组的 map() 方法
- 注意:渲染列表时应该添加 key 属性,key 属性的值要保证唯一
- 原则:map() 遍历谁,就给谁添加 key 属性
- 注意:尽量避免使用索引号作为 key
const songs = [
{ id: 1, name: '歌曲1' },
{ id: 2, name: '歌曲2' },
{ id: 3, name: '歌曲3' }
]
const list = (
<ul>
{ songs.map(item => <li key={item.id}>{ item.name }</li>) }
</ul>
)
5、JSX 的样式处理
5.1 行内样式——style
<h2 style={{ color:'red', textAlign: 'center' }}>JSX style 样式</h2>
5.2 类名——className ( 推荐 )
<h2 className="title">JSX className 样式</h2>
注意:要提前引入 css 样式文件
三、React 组件基础
1、React 组件介绍
- 组件是 React 的 一等公民,使用 React 就是在用组件
- 组件表示页面中的部分功能
- 组合多个组件实现完整的页面功能
- 特点:可复用 、独立、可组合
2、React 组件的两种创建方式
2.1 使用函数创建组件
- 函数组件:使用 JS 的函数(或箭头函数)创建的组件
- 约定1:函数名称必须以 大写字母开头,React 据此区分 组件 和 普通的 React 元素
- 约定2:函数组件 必须有返回值 ,表示该组件的结构
- 如果返回值为 null ,表示不渲染任何内容
// function HelloMessage(props) {
// return <h1>Hello World!</h1>;
// }
// function HelloMessage(props) {
// return null
// }
const HelloMessage = (props) => <h1>Hello World!</h1>
- 渲染函数组件:用函数名作为组件标签名
- 组件标签可以是单标签也可以是双标签
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<HelloMessage />);
可以使用嵌套组件的方式或者**<React.StrictMode></React.StrictMode>**来同时渲染多个组件
2.2 使用类创建组件
- 类组件:使用 ES6 的 class 创建组件
- 约定1:类名称也必须以 大写字母开头
- 约定2:类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性
- 约定3:类组件必须提供 render() 方法
- 约定4:render() 方法 必须有返回值,表示该组件的结构
- render() 方法如果返回值为 null ,表示不渲染任何内容
// 类组件
class Hello extends React.Component {
render() {
return <div>我是一个类组件</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
<HelloMessage />
<Hello />
</React.StrictMode>)
可以使用嵌套组件的方式或者**<React.StrictMode></React.StrictMode>**来同时渲染多个组件
2.3 抽离为独立 JS 文件
- 创建 Hello.js 组件
- 在 Hello.js 中导入 React
- 创建组件(函数 或者 类)
- 在 Hello.js 中导出该组件
- 在 index.js 中导入 Hello 组件
- 渲染组件
// 导入React
import React from 'react';
// 创建组件
class Hello1 extends React.Component {
render() {
return (
<div>我是一个类组件,这是第一个抽离到js文件中的组件</div>
)
}
}
// 导出组件
export default Hello1
//-----------------index.js-------------------
// 导入组件
import Hello1 from './Hello';
const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
<Hello1 />
</React.StrictMode>)
3、React 事件处理
3.1 事件绑定
- React 事件绑定语法与 DOM 事件语法相似
- 语法:on + 事件名称 = {事件处理程序},比如:onClick = {() => {}}
- 注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus
- 函数和类中略有不同
// 在类组件中绑定事件处理程序
class App1 extends React.Component {
// 事件处理程序
handleClick() {
console.log('点击了按钮');
}
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
// 在函数组件中绑定事件处理程序
const App2 = () => {
function handleClick() {
console.log('点击了按钮');
}
return <button onClick={handleClick}>点击</button>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
<App1 />
<App2 />
</React.StrictMode>)
3.2 事件对象
- 可以通过 事件处理程序的参数 获取到事件对象
- React 中的事件对象叫做:合成事件(对象)
- 合成事件:兼容所有浏览器,无须担心跨浏览器兼容性问题
// 事件对象
class App3 extends React.Component {
handleClick(e) {
e.preventDefault();
console.log(e.target.href);
}
render() {
return (
<a href="https://www.baidu.com" onClick={this.handleClick} style={{textDecoration: 'none', color: 'green'}}>点击</a>
);
}
}
4、有状态组件和无状态组件
- 函数组件又叫做 无状态组件,类组件又叫做 有状态组件
- 状态(state)即 数据
- 函数组件没有自己的状态,只负责 数据展示(静)
- 类组件有自己的状态,负责更新UI,让页面 “动” 起来
5、组件中的 state 和 setState()
5.1 state 的基本使用
- 状态(state)即数据,是组件内部的 私有 数据,只能在组件内部使用
- state 的值是对象,表示一个组件仲可以有多个数据
- 状态是私有的,只能在组件内部使用
- 通过 this.state 来获取状态
class App4 extends React.Component {
/* constructor() {
super()
// 初始化 state
this.state = {
count: 0
}
} */
// 简化写法
state = {
count: 0
}
render() {
return (
<div>
<h2>计数器:{ this.state.count }</h2>
</div>
)
}
}
5.2 setState() 修改状态
- 状态是可改变的
- 语法:this.setState({ 要修改的数据 })
- 注意:不要直接修改 state 中的值,这是错误的!!!
- setState() 作用:1.修改 state 2.更新 UI
- 思想:数据驱动视图
// state 的基本使用
class App4 extends React.Component {
/* constructor() {
super()
// 初始化 state
this.state = {
count: 0
}
} */
// 简化写法
state = {
count: 0
}
render() {
return (
<div>
<h2>计数器:{ this.state.count }</h2>
<button onClick={() => {
this.setState({
count: this.state.count + 1
})
}}>+1</button>
</div>
)
}
}
5.3 从 JSX 中抽离事件处理程序
- JSX 中掺杂过多 JS 逻辑代码,会显得非常混乱
- 推荐:将逻辑抽离到单独的方法中,保证 JSX 结构清晰
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMlzZMIZ-1692620120418)(D:\PerfectCode\前端学习\React\Image\this报错.PNG)]
- 原因:事件处理程序中的 this 的值为 undefined
- 希望:this 指向组件实例(render 方法中的 this,即为组件实例)
// state 的基本使用
class App4 extends React.Component {
/* constructor() {
super()
// 初始化 state
this.state = {
count: 0
}
} */
// 简化写法
state = {
count: 0
}
// 事件处理程序
onIncrement() {
console.log('事件处理程序中的this:', this) // undefined
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h2>计数器:{ this.state.count }</h2>
{/* <button onClick={() => {
this.setState({
count: this.state.count + 1
})
}}>+1</button> */}
<button onClick={this.onIncrement.bind(this)}>+1</button>
{/* 不加bind(this)会报错,因为这个this指向事件处理函数,而不是render */}
</div>
)
}
}
6、事件绑定 this 指向
6.1 箭头函数
- 利用箭头函数自身不绑定 this 的特点
- render()方法中的 this 为组件实例,可以获取到 setState()
- 可以为标签绑定 ref 属性,并通过 this.refs.属性名 来操作dom
// 事件处理程序
onIncrement() {
this.setState({ ... })
}
render () {
return (
{/* 1. 箭头函数解决 */}
<button onClick={() => this.onIncrement()}>+1</button>
)
}
6.2 Function.prototype.bind()
- 利用 ES5 中的 bind 方法,将事件处理程序中的 this 与组件实例绑定到一起
- 两种使用方法
class App4 extends React.Component {
constructor() {
super()
// 事件处理程序中的this指向问题
this.onIncrement = this.onIncrement.bind(this)
// 初始化 state
this.state = {
count: 0
}
}
// 事件处理程序
onIncrement() {
console.log('事件处理程序中的this:', this) // undefined
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h2>计数器:{ this.state.count }</h2>
{/* 2. bind 方法 */}
{/* 直接调用 */}
{/* <button onClick={this.onIncrement.bind(this)}>+1</button> */}
{/* 提前绑定 */}
<button onClick={this.onIncrement}>+1</button>
</div>
)
}
}
6.3 class 的实例方法(推荐)
- 利用箭头函数形式的 class 实例方法
- 注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用
class App4 extends React.Component {
// 初始化 state,简写
state = {
count: 0
}
// 事件处理程序,箭头函数解决this
onIncrement = () => {
console.log('事件处理程序中的this:', this) // undefined
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h2>计数器:{ this.state.count }</h2>
{/* 3. 箭头函数解决 */}
<button onClick={this.onIncrement}>+1</button>
</div>
)
}
}
7、表单处理
7.1 受控组件
- HTML 中的表单元素是可输入的,也就是有自己的可变状态
- 而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改
- React 将 state 与表单元素值 value 绑定到一起,由 state 的值来控制表单元素的值
- 受控组件:其值受到 React 控制的表单元素
步骤:
- 在 state 中添加一个状态,作为表单元素的 value 值(控制表单元素值的来源)
- 给表单元素绑定 change 事件,将表单元素的值 设置为 state 的值(控制表单元素值的变化)
state = { val: '' }
render() {
return (
<div>
<input type="text" value={this.state.val} onChange={this.handleChange} />
<p>{this.state.val}</p>
</div>
)
}
例子:
// 受控组件
class App5 extends React.Component {
state = {
val: '',
content: '',
city: '2',
isChecked: true
}
handleChange = (e) => {
this.setState({
val: e.target.value
})
}
handleContent = (e) => {
this.setState({
content: e.target.value
})
}
handleCity = (e) => {
this.setState({
city: e.target.value
})
}
handleChecked = (e) => {
this.setState({
isChecked: e.target.checked
})
}
render() {
return (
<div>
{/* 文本框 */}
<input type="text" value={this.state.val} onChange={this.handleChange} />
<p>{this.state.val}</p>
<br />
{/* 富文本框 */}
<textarea value={this.state.content} onChange={this.handleContent} />
<p>{this.state.content}</p>
{/* 下拉框 */}
<select value={this.state.city} onChange={this.handleCity}>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
</select>
{/* 复选框 */}
<input type="checkbox" checked={this.state.isChecked} onChange={this.handleChecked} />
</div>
)
}
}
多表单元素优化:
- 给表单元素添加 name 属性,名称与 state 相同
- 根据表单元素类型获取对应的值
- 在 change 事件处理程序中通过 [name] 来修改对应的state
// 受控组件
class App5 extends React.Component {
state = {
val: '',
content: '',
city: '2',
isChecked: true
}
handleForm = (e) => {
// 获取当前DOM对象
const target = e.target
// 根据类型获取值
const value = target.type === 'checkbox' ? target.checked : target.value
// 获取name
const name = target.name
// es6 属性表达式
this.setState({
[name]: value
})
}
render() {
return (
<div>
{/* 文本框 */}
<input type="text" name='val' value={this.state.val} onChange={this.handleForm} />
<p>{this.state.val}</p>
<br />
{/* 富文本框 */}
<textarea name='content' value={this.state.content} onChange={this.handleForm} />
<p>{this.state.content}</p>
{/* 下拉框 */}
<select name='city' value={this.state.city} onChange={this.handleForm}>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
</select>
{/* 复选框 */}
<input type="checkbox" name='isChecked' checked={this.state.isChecked} onChange={this.handleForm} />
</div>
)
}
}
7.2 非受控组件(DOM方式)
- 说明:借助于 ref ,使用原生 DOM 方式来获取表单元素值
- ref 的作用:获取 DOM 或组件
使用步骤:
- 调用 React.createRef() 方法创建一个 ref 对象
constructor() {
super()
this.txtRef = React.createRef()
}
- 将创建好的 ref 对象添加到文本框中
<input type="text" ref={this.txtRef} />
- 通过 ref 对象获取到文本框的值
console.log(this.txtRef.current.value)
class App6 extends React.Component {
constructor() {
super()
// 创建 ref
this.txtRef = React.createRef()
}
// 获取文本框的值
getTxt = () => {
console.log('文本框的值:', this.txtRef.current.value)
}
render() {
return (
<div>
<input type="text" ref={this.txtRef} />
<button onClick={this.getTxt}>获取文本框的值</button>
</div>
)
}
}
7.3 评论列表案例
import React from 'react';
class App extends React.Component {
// 初始化状态
state = {
// 评论列表
comments: [
{id: 1, name: '张三', content: '今天天气真好'},
{id: 2, name: '李四', content: '今天天气真好'},
{id: 3, name: '王五', content: '今天天气真好'},
{id: 4, name: '赵六', content: '今天天气真好'},
{id: 5, name: '田七', content: '今天天气真好'},
],
// 评论人
userName: '',
// 评论内容
userContent: ''
}
// 通过函数来渲染
renderList() {
const { comments } = this.state;
if (comments.length > 0) {
return (
<ul>
{ this.state.comments.map(item => (
<li key={item.id}>
<h3>评论人:{item.name}</h3>
<p>评论内容:{item.content}</p>
</li>
))}
</ul>
)
}
return <div className='no-comment'>暂无评论,快去评论吧~</div>
// return (
// this.state.comments.length > 0
// ? <ul>
// { this.state.comments.map(item => (
// <li key={item.id}>
// <h3>评论人:{item.name}</h3>
// <p>评论内容:{item.content}</p>
// </li>
// ))}
// </ul>
// : <div className='no-comment'>暂无评论,快去评论吧~</div>
// )
}
// 受控表单元素值
handleForm = e => {
const { name, value } = e.target;
this.setState({
[name]: value
})
}
// 添加评论
addComment = () => {
// 获取评论人和评论内容
const { comments, userName, userContent } = this.state;
// 判断是否为空
if (!userName.trim() || !userContent.trim()) {
alert('评论人或评论内容不能为空');
return;
}
// 生成评论对象
const newComment = {
id: Date.now(),
name: userName,
content: userContent
}
// 更新状态, 同时清空输入框
this.setState({
comments: [newComment, ...comments],
userName: '',
userContent: ''
})
}
render() {
const { userName, userContent } = this.state;
return (
<div>
<div>
<input type='text' placeholder='请输入评论人' value={userName} name='userName' onChange={this.handleForm} />
<br />
<textarea cols='30' rows='10' placeholder='请输入评论内容' value={userContent} name='userContent' onChange={this.handleForm} />
<br />
<button onClick={this.addComment}>发表评论</button>
</div>
{/* 通过条件渲染来判断 */}
{/* {
this.state.comments.length > 0
? <ul>
{ this.state.comments.map(item => (
<li key={item.id}>
<h3>评论人:{item.name}</h3>
<p>评论内容:{item.content}</p>
</li>
))}
</ul>
: <div className='no-comment'>暂无评论,快去评论吧~</div>
} */}
{/* 通过函数来渲染 */}
{this.renderList()}
</div>
)
}
}
export default App;
四、React 组件进阶
1、组件通讯介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gs21hZG9-1692620120418)(D:\PerfectCode\前端学习\React\Image\组件通讯.PNG)]
2、组件的props
- 组件是封闭的,要接受外部数据应该通过 props 来实现
- props 的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过参数 props 接收数据,类组件通过 this.props 接收数据
import React from 'react';
import ReactDOM from 'react-dom/client';
/* // 函数组件props
function Hello(props) {
return <h2>Hello, {props.name}--{props.age}</h2>;
} */
// 类组件props
class Hello extends React.Component {
render() {
return <h2>Hello, {this.props.name}--{this.props.age}</h2>;
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Hello name='zs' age={23} />
</React.StrictMode>
);
特点 :
- 可以给组件传递任意类型的数据
- props 是只读的对象,只能读取属性的值,无法修改对象
- 注意:使用类组件时,如果写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取到 props!
/* // 函数组件props
function Hello(props) {
return <h2>Hello, {props.name}--{props.age}</h2>;
} */
// 类组件props
class Hello extends React.Component {
// 推荐在写类组件时,构造函数要携带props参数
constructor(props) {
super(props);
console.log(props);
}
render() {
console.log('props: ', this.props);
return (
<div>
<h2>Hello, {this.props.name}--{this.props.age}</h2>
{this.props.tag}
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Hello
name='zs'
age={23}
colors={['red', 'green']}
fn={() => console.log('这是一个函数')}
tag={<p>这是一个p标签</p>}
/>
</React.StrictMode>
);
3、组件通讯的三种方式
3.1 父组件传递数据给子组件
- 父组件提供要传递的 state 数据
- 给子组件标签添加属性,值为 state 中的数据
- 子组件通过 props 接收父组件中传递的数据
// 父组件
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: 'hello!'
}
}
render() {
return (
<div>
<h2>父组件</h2>
<Child data={this.state.msg} />
</div>
)
}
}
// 子组件
const Child = (props) => {
return (
<div>
<h3>子组件,接收到父组件的数据:{props.data}</h3>
</div>
)
}
3.2 子组件传递数据给父组件
思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
- 父组件提供一个回调函数(用于接收数据)
- 将该函数作为属性的值,传递给子组件
// 父组件
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: 'hello!',
parentMsg: ''
}
}
// 提供回调函数,用来接收数据
getChildData = (data) => {
console.log('父组件接收到子组件的数据:', data)
this.setState({
parentMsg: data
})
}
render() {
return (
<div>
<h2>父组件</h2>
<h3>父组件接收到子组件的数据:{this.state.parentMsg}</h3>
<Child1 getMsg={this.getChildData} />
</div>
)
}
}
// 子组件
class Child1 extends React.Component {
state = {
msg: 'hello! hello!'
}
handleClick = () => {
// 调用父组件传递过来的回调函数,将数据传递给父组件
this.props.getMsg(this.state.msg)
}
render() {
return (
<div>
<button onClick={this.handleClick}>点击向父组件传递数据</button>
</div>
)
}
}
3.3 兄弟组件传递数据
- 将共享状态提升到最近的公共组件中,有公共父组件管理这个状态
- 思想:状态提升
- 公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法
- 要通讯的子组件只需要通过 props 接收状态或操作状态的方法
import React from "react";
// 父组件
class Counter extends React.Component {
// 提供共享状态
state = {
count: 0
}
// 提供修改状态的方法
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<Child1 count={this.state.count} />
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
}
const Child1 = (props) => {
return <h2>计数器:{props.count}</h2>
}
const Child2 = (props) => {
return <button onClick={() => props.onIncrement()}>+1</button>
}
export default Counter;
4、Context
实现跨组件传递数据
使用步骤:
- 调用 React.createContext() 创建 Provider (提供数据)和 Consumer(消费数据)两个组件
- 使用 Provider 组件作为父节点
- 设置 value 属性,表示要传递的数据
- 调用 Consumer 组件接收数据,函数参数接收
import React from "react";
// 创建 context 得到两个组件
const { Provider, Consumer } = React.createContext('default')
class Children extends React.Component {
render () {
return (
// Provider 传递 value
<Provider value="pink">
<div>
<h1>Parent ----- </h1>
<Node />
</div>
</Provider>
)
}
}
const Node = props => {
return (
<div>
<h2>Node -------- </h2>
<SubNode />
</div>
)
}
const SubNode = props => {
return (
<div>
<h3>SubNode -------- </h3>
<Child />
</div>
)
}
const Child = props => {
return (
<div>
<h4>Child ------
{/* Consumer 消费 value */}
<Consumer>
{ data => <span style={{color: data}}> {data} </span> }
</Consumer>
</h4>
</div>
)
}
export default Children
5、props 深入
5.1 children 属性
- children 属性:表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性
- children 属性与普通的 props 一样,值可以是任意值(文本、React元素、组件,甚至是函数)
const AppChildren = props => {
console.log('props: ', props);
return (
<div>
<h1>
组件标签子节点:{props.children}
</h1>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<AppChildren>我是文本子节点</AppChildren>
<AppChildren><button>我是一个标签子节点</button></AppChildren>
{/* <AppChildren>{() => {console.log('我是一个函数子节点');}}</AppChildren> */}
</React.StrictMode>
);
5.2 props 校验
- 对于组件来说,props 是外来的,无法保证组件使用者传入什么格式的数据
- props 校验:允许在创建组件的时候,就指定 props 的类型、格式等
- 作用:捕获使用组件时因为 props 导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤:
- 安装包 prop-types(yarn add prop-types / npm i prop-types)
- 导入 prop-types 包
- 使用 组件名.propTypes = {} 来给组件的 props 添加检验规则
- 校验规则 通过 PropTypes 对象来指定
// 导入 PropTypes
import PropTypes from 'prop-types';
// props 校验
const AppYz = props => {
const arr = props.colors
const lis = arr.map((item, index) => <li key={index}>{item}</li>)
return <ul>{lis}</ul>
}
// 添加校验规则
AppYz.propTypes = {
colors: PropTypes.array
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<AppYz colors={['red', 'green']} />
</React.StrictMode>
);
约束规则:
- 常见类型:array、bool、func、number、object、string
- React 元素类型:element
- 必选项:isRequired
- 特定结构对象:shape({})
const App1 = props => {
return (
<div>
<h1>props校验: </h1>
</div>
)
}
App1.propTypes = {
a: PropTypes.number,
fn: PropTypes.func.isRequired,
tag: PropTypes.element,
filter: PropTypes.shape({ // 指定为对象结构
area: PropTypes.string,
price: PropTypes.number
}),
}
5.3 props 的默认值
- 场景:分页组件 --> 每页显示条数
- 作用:给 props 设置默认值,在未传入 props 时生效
const App2 = props => {
return (
<div>
<h1>props校验: {props.pageSize}</h1>
</div>
)
}
// 设置默认值
App2.defaultProps = {
pageSize: 10,
}
6、组件的生命周期
6.1 组件的生命周期概述
- 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
- 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
- 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
- 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机
- 只有 类组件 才有生命周期
6.2 生命周期的三个阶段
注意:
组件的一次更新流程,在视图真正刷新之前的部分都是可能被多次调用的,因而这些部分中不能出现副作用,开发环境下会刻意触发两次以使得开发者能注意到误用的副作用。
- 创建时(挂载阶段)
- 执行时机:组件创建时(页面加载时)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g3ladAO9-1692620120418)(D:\PerfectCode\前端学习\React\Image\React生命周期.PNG)]
- 执行顺序:
constructor() --> render() --> componentDidMount
钩子函数 | 触发时机 | 作用 |
---|---|---|
constructor | 创建组件时,最先执行 | 1.初始化state 2.为事件处理程序绑定 this |
render | 每次组件渲染都会触发 | 渲染 UI ( 注意:不能直接调用 setState() ) |
componentDIdMount | 组件挂载(完成DOM渲染)后 | 1.发送网络请求 2.DOM 操作 |
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
// 处理this指向
// this.handleClick = this.handleClick.bind(this);
console.warn('生命周期函数:constructor');
}
componentDidMount() {
const btn = document.querySelector('#btn');
console.log(btn);
console.warn('生命周期函数:componentDidMount');
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
// 强制更新
// this.forceUpdate()
}
render() {
console.warn('生命周期函数:render');
// 不要再render中调用setState,会造成死循环
// this.setState({
// count: this.state.count + 1
// })
return (
<div>
<Counter count={this.state.count} />
<button id="btn" onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}
- 更新时(更新阶段)
- 执行时机:1.setState() 2.forceUpdate() 3.组件接收到新的 props
- 说明:以上三者任意一种变化,组件就会重新渲染
- 执行顺序:render() --> componentDidUpdate()
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染UI(与 挂载阶段 是同一个render) |
componentDidUpdate | 组件更新(完成DOM渲染)后 | 1.发送网络请求 2.DOM 操作 注意:如果要setState()必须放在一个 if 条件中 |
class Counter extends React.Component {
render() {
console.warn('生命周期钩子函数:Counter的 render');
return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
}
// 注意:如果要调用 setState,必须放在一个条件判断中
// 否则会造成死循环
componentDidUpdate(prevProps, prevState, snapshot) {
console.warn('生命周期钩子函数:Counter的 componentDidUpdate');
const title = document.querySelector('#title');
console.log(title);
// 比较更新前后的props是否相同,来决定是否重新渲染组件
console.log('上一次的props:', prevProps);
console.log('当前的props:', this.props);
if (prevProps.count !== this.props.count) {
this.setState({})
// 发送ajax请求
}
}
}
- 卸载时(卸载阶段)
- 触发时机:组件从页面中消失
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载(从页面消失) | 执行清理工作(比如:清理定时器等) |
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
// 处理this指向
// this.handleClick = this.handleClick.bind(this);
console.warn('生命周期函数:constructor');
}
componentDidMount() {
const btn = document.querySelector('#btn');
console.log(btn);
console.warn('生命周期函数:componentDidMount');
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
// 强制更新
// this.forceUpdate()
}
render() {
console.warn('生命周期函数:render');
// 不要再render中调用setState,会造成死循环
// this.setState({
// count: this.state.count + 1
// })
return (
<div>
{ this.state.count > 3 ? <h1>豆豆被打死了~</h1> : <Counter count={this.state.count} /> }
<button id="btn" onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}
class Counter extends React.Component {
componentDidMount() {
// 开启定时器
this.timerId = setInterval(() => {
console.log('定时器正在执行啦~')
}, 500)
}
render() {
console.warn('生命周期钩子函数:Counter的 render');
return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
}
// 注意:如果要调用 setState,必须放在一个条件判断中
// 否则会造成死循环
componentDidUpdate(prevProps, prevState, snapshot) {
console.warn('生命周期钩子函数:Counter的 componentDidUpdate');
const title = document.querySelector('#title');
console.log(title);
// 比较更新前后的props是否相同,来决定是否重新渲染组件
console.log('上一次的props:', prevProps);
console.log('当前的props:', this.props);
if (prevProps.count !== this.props.count) {
this.setState({})
// 发送ajax请求
}
}
// 组件卸载之前调用生命周期函数
componentWillUnmount() {
console.warn('生命周期钩子函数:Counter的 componentWillUnmount');
// 清理定时器
clearInterval(this.timerId)
console.log('定时器被清空了!');
}
}
6.3 不常用的钩子函数介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fa59FRSZ-1692620120419)(D:\PerfectCode\前端学习\React\Image\React生命周期图.png)]
注意:componentWillMount、componentWillReceiveProps、componentWillUpdate已经被弃用,不推荐使用
新版:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PYvAFaaI-1692620120419)(D:\PerfectCode\前端学习\React\Image\React新版生命周期图.png)]
getDerivedStateFromProps、getSnapshotBeforeUpdate不推荐使用
7、render-props 和高阶组件
7.1 React组件复用概述
- 处理方式:复用相似功能
- 复用什么:1.state 2.操作 state 的方法(组件状态逻辑)
- 两种方式:1. render props 模式 2.高阶组件(HOC)
- 注意:这两种方式 不是新的API,而是利用 React 自身特点的编码技巧,演化而成的固定模式(写法)
7.2 render props 模式
思路分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DraeQJtJ-1692620120419)(D:\PerfectCode\前端学习\React\Image\render props思路.PNG)]
使用步骤:
- 创建 mouse 组件,在组件中提供复用的状态逻辑代码(1.状态 2.操作状态的方法)
- 将要 复用的状态 作为 props.render(state) 方法的参数,暴露到组件外部
- 使用 props.render() 的 返回值 作为要渲染的内容
import React from 'react';
/*
render props 模式
*/
// 创建 Mouse 组件
class Mouse extends React.Component {
// 鼠标位置 state
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
render() {
// return null
return this.props.render(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
<Mouse render={mouse => {
return <p>鼠标位置:{mouse.x}, {mouse.y}</p>
}} />
{/* 猫捉老鼠 */}
<Mouse render={mouse => {
return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
}} />
</div>
);
}
}
export default App;
children 代替 render 属性
- 注意:并不是该模式叫 render props 就必须使用名为 render 的 prop,实际上可以使用任意名称的 prop
- 把 prop 是一个函数并且告诉组件要渲染什么内容的技术叫做:renderprops 模式
- 推荐:使用 children 代替 render 属性
import React from 'react';
/*
render props 模式
*/
// 创建 Mouse 组件
class Mouse extends React.Component {
// 鼠标位置 state
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
render() {
// return null
// return this.props.render(this.state)
return this.props.children(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
{/* <Mouse render={mouse => {
return <p>鼠标位置:{mouse.x}, {mouse.y}</p>
}} /> */}
{/* children 方式 */}
<Mouse>
{mouse => {
return <p>鼠标位置:{mouse.x}, {mouse.y}</p>
}}
</Mouse>
{/* 猫捉老鼠 */}
{/* <Mouse render={mouse => {
return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
}} /> */}
{/* children 方式 */}
<Mouse>
{mouse => {
return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
}}
</Mouse>
</div>
);
}
}
export default App;
代码优化
- 推荐:给 render props 模式添加 props 校验
// 添加 props 校验
Mouse.propTypes = {
children: PropTypes.func.isRequired
}
- 应该在组件卸载时解除 mousemove 事件绑定
// 在 Mouse 组件卸载时,移除事件监听
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
7.3 高阶组件
概述:
- 目的:实现状态逻辑使用
- 采用 包装(装饰)模式,比如说:手机壳
- 高阶组件就相当于手机壳,通过包装组件,增强组件功能
思路分析:
- 高阶组件(HOC,Higher-OrderComponent)是一个函数,接受要包装的组件,返回增强后的组件
- 高阶组件内部 创建一个类组件,在这个类组件中 提供复用的状态逻辑 代码,通过 prop 将复用的状态传递给被包装组件 WrappedComponent
使用步骤:
- 创建一个函数,名称约定 以 with 开头
- 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 再该组件中,渲染参数组件,同时将状态通过 prop 传递给参数组件
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
import React from 'react';
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 组件卸载时,移除事件监听
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
return (
<WrappedComponent {...this.state} />
)
}
}
// 返回增强后的组件
return Mouse
}
// 用来测试高阶组件
const Position = props => {
return <p>鼠标当前位置:(x: {props.x}, y:{props.y} )</p>
}
// 猫捉老鼠组件
const Cat = props => {
return <img src='' alt="猫" style={{ position: 'absolute', left: props.x - 64, top: props.y - 64 }} />
}
// 获取增强后的组件
const MousePosition = withMouse(Position)
// 调用高阶组件来增强猫捉老鼠组件
const MouseCat = withMouse(Cat)
class ComponentGao extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
{/* 渲染增强后的组件 */}
<MousePosition />
<MouseCat />
</div>
)
}
}
export default ComponentGao;
设置 diaplayName
-
使用高阶组件存在的问题:得到的两个组件名称相同
-
原因:默认情况下,React 使用 组件名称 作为 displayName
-
解决方式:为高阶组件设置 displayName 便于调试时区分不同组件
-
displayName的作用:用于设置调试信息(React Developer Tools 信息)
-
设置方式:
// 设置高阶组件的名称 displayName Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}` function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
传递 props
-
问题:props 丢失
-
原因:高阶组件没有往下传递 props
-
解决方式:渲染 WrappedComponent 时,将 state 和 this.props 一起传递给组件
-
传递方式:
<WrappedComponent {...this.state} {...this.props} />
import React from 'react';
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 组件卸载时,移除事件监听
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
console.log('Mouse', this.props);
return (
<WrappedComponent {...this.state} {...this.props} />
)
}
}
// 设置高阶组件的名称 displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
// 返回增强后的组件
return Mouse
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => {
console.log('Position', props);
return <p>鼠标当前位置:(x: {props.x}, y:{props.y} )</p>
}
// 猫捉老鼠组件
const Cat = props => {
return <img src='' alt="猫" style={{ position: 'absolute', left: props.x - 64, top: props.y - 64 }} />
}
// 获取增强后的组件
const MousePosition = withMouse(Position)
// 调用高阶组件来增强猫捉老鼠组件
const MouseCat = withMouse(Cat)
class ComponentGao extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
{/* 渲染增强后的组件 */}
<MousePosition a="1" />
<MouseCat />
</div>
)
}
}
export default ComponentGao;
五、React 原理
1、setState() 说明
1.1 更新数据
- setState() 是 异步 更新数据的
- 注意:使用该语法时,后面的 setState() 不要依赖于前面的 setState();(也就是合并成一个 setState() )
- 可以多次调用 setState(),只会触发一次重新渲染
1.2 推荐语法
- 推荐:使用 setState( (state,props) => {}) 语法
- 参数 state:表示最新的 state
- 参数 props:表示最新的 props
import React from 'react';
class App extends React.Component {
state = {
count: 1
}
handleClick = () => {
// 此处,更新 state
/* this.setState({
count: this.state.count + 1 // 1
})
this.setState({
count: this.state.count + 1 // 1 + 1
}) */
// 推荐语法
// 异步的
this.setState((state, props) => {
return {
count: state.count + 1 // 1 + 1
}
})
// 第二次调用 setState 时,会使用上一次的 state
this.setState((state, props) => {
console.log('第二次调用: ', state.count) // 2
return {
count: state.count + 1 // 2 + 1
}
})
console.log('count: ', this.state.count) // 1 拿到的还是上一次的 state
}
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
export default App;
1.3 第二个参数
- 场景:在状态更新(页面完成重新渲染)后立即执行某个操作
- 语法:setState(updater[,callback])
this.setState((state, props) => {
console.log('第二次调用: ', state.count) // 2
return {
count: state.count + 1 // 2 + 1
}
}, () => { // 回调函数,状态更新完成后执行
console.log('状态更新完成: ', this.state.count);
console.log(document.getElementById('title').innerHTML);
document.title = '更新完成'
})
2、JSX 语法的转化过程
- JSX 仅仅是 createElement() 方法的语法糖(简写语法)
- JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法
- React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXLgvJPd-1692620120420)(D:\PerfectCode\前端学习\React\Image\JSX语法.PNG)]
3、组件更新机制
- setState() 两个作用:1.修改 state 2.更新组件(UI)
- 过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染 当前组件子树 (当前组件及其所有子组件)
4、组件性能优化
4.1 减轻 state
- 减轻 state:只存储根组件渲染相关的数据(比如:count/列表数据/loading等)
- 注意:不用做渲染的数据不要放在 state 中,比如定时器 id 等
- 对于这种需要多个方法中用到的数据,应该放在 this 中
4.2 避免不必要的重新渲染
-
组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
-
问题:子组件没有任何变化时也会被重新渲染
-
解决方式:使用 **钩子函数 shouldComponentUpdate(nextProps,nextState)**两个参数表示最新的内容
-
作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染
-
触发时机:更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate --> render)
class Hello extends Component { shouldComponentUpdate() { // 根据条件,决定是否重新渲染组件 return false } render() {...} }
-
案例:随机数
import React from 'react'; /* 组件性能优化 */ // 生成随机数 class App1 extends React.Component { state = { num: 0 } handleClick = () => { this.setState(() => { return { num: Math.floor(Math.random() * 3) } }) } // 1. 因为两次生成的随机数可能是一样的,所以需要优化,利用nextState参数判断 shouldComponentUpdate(nextProps, nextState) { console.log('最新状态:', nextState, '当前状态:', this.state); /* if (nextState.num === this.state.num) { return false } return true */ return nextState.num !== this.state.num } render() { console.log('App1 render'); return ( <div> <NumBox num={this.state.num} /> <button onClick={this.handleClick}>生成随机数</button> </div> ) } } class NumBox extends React.Component { // 2. 优化,利用nextProps参数判断 shouldComponentUpdate(nextProps, nextState) { return nextProps.num !== this.props.num } render() { console.log('NumBox render'); return ( <div> <h2>随机数:{this.props.num}</h2> </div> ) } } export default App1;
4.3 纯组件
-
纯组件:PureComponent 与 React.Component 功能相似
-
区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子函数,不需要手动比较
-
原理:纯组件内部通过分别 对比 前后量词 props 和 state 的值,来决定是否重新渲染组件
import React from 'react'; /* 组件性能优化 */ // 生成随机数 class App1 extends React.PureComponent { state = { num: 0 } handleClick = () => { this.setState(() => { return { num: Math.floor(Math.random() * 3) } }) } render() { console.log('App2 render'); return ( <div> <NumBox num={this.state.num} /> <button onClick={this.handleClick}>生成随机数</button> </div> ) } } class NumBox extends React.PureComponent { render() { console.log('NumBox render'); return ( <div> <h2>随机数:{this.props.num}</h2> </div> ) } } export default App1;
-
说明:纯组件内部的对比是 shallow compare(浅层对比)
-
对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
-
对于 引用类型 来说:只比较对象的引用(地址)是否相同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmupZmtW-1692620120420)(D:\PerfectCode\前端学习\React\Image\纯组件原理.PNG)]
-
注意:state 或 props 中的属性值为引用类型时,应该创建新数据,不要直接原数据!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jleQPIY9-1692620120420)(D:\PerfectCode\前端学习\React\Image\纯组件引用类型做法.PNG)]
import React from 'react';
/*
组件性能优化
*/
// 生成随机数
class App3 extends React.PureComponent {
state = {
obj: {
num: 0
}
}
handleClick = () => {
// 正确做法:创建一个新对象,修改新对象中的数据
// es6语法:对象的扩展运算符,并且修改新对象中的数据
const newObj = { ...this.state.obj, num: Math.floor(Math.random() * 3) };
this.setState(() => {
return {
obj: newObj
}
})
// 错误做法:直接修改原始对象state中的数据
/* const newObj = this.state.obj;
newObj.num = Math.floor(Math.random() * 3);
this.setState(() => {
return {
obj: newObj
}
}) */
}
render() {
console.log('App2 render');
return (
<div>
<NumBox num={this.state.obj.num} />
<button onClick={this.handleClick}>生成随机数</button>
</div>
)
}
}
class NumBox extends React.PureComponent {
render() {
console.log('NumBox render');
return (
<div>
<h2>随机数:{this.props.num}</h2>
</div>
)
}
}
export default App3;
5、虚拟 DOM 和 Diff 算法
- React 更新视图的思想是:只要 state 变化就重新渲染视图
- 特点:思路非常清晰
- 问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中?不是
- 理想状态:部分更新,只更新变化的地方
- 问题:React 是如何做到部分更新的?虚拟 DOM 配合 Diff 算法
虚拟DOM:本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)
执行过程:
- 初次渲染师,React 会根据初始 state(Model),创建一个 虚拟 DOM 对象(树)
- 根据虚拟 DOM 生成真正的 DOM,渲染到页面中
- 当数据变化后(setState()),重新根据新的数据,创建新的虚拟 DOM 对象(树)
- 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容
- 最终,React 直降 变化的内容 更新(patch)到 DOM 中,重新渲染到页面
代码演示:
- 组件 render() 调用后,根据 状态 和 JSX结构 生成虚拟 DOM 对象
class NumBox extends React.PureComponent {
render() {
// 虚拟DOM对象, el
const el = (
<div>
<h2>随机数:{this.props.num}</h2>
</div>
)
console.log(el);
return el
}
}
六、React 路由
1、React 路由介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDNo0sC1-1692620120421)(D:\PerfectCode\前端学习\React\Image\路由介绍.PNG)]
2、路由的基本使用
2.1 使用步骤
-
安装:
yarn add react-router-dom
/npm i react-router-dom
-
导入路由的三个核心组件:Router/Routes/Route/Link
import { BrowserRouter as Router, Routes, Route, Link } from ‘react-router-dom’
-
使用 Router 组件 包裹整个应用(重要)
-
使用 Link 组件 作为导航菜单(路由入口)
-
使用 Route 组件 配置路由规则和要展示的组件(路由出口),v6版本必须要使用 Routes 组件 包裹
import React from 'react';
// 2. 导入组件
import { BrowserRouter as Router, Route, Link, Routes } from 'react-router-dom';
/*
react-router-dom 的基本使用
*/
const About = () => {
return <h2>关于</h2>
}
const User = () => {
return <h2>用户</h2>
}
const App = () => {
return (
// 3. Router 组件是路由的根组件,所有的路由组件都应该包裹在 Router 组件中
<Router>
<div>
<h1>React路由基础</h1>
<ul>
{/* 4. 指定路由的入口 */}
<li><Link to="/about">关于</Link></li>
<li><Link to="/user">用户</Link></li>
</ul>
<hr />
{/* 5. 指定路由出口 */}
{/* v6 必须用 Routes 包裹 */}
<Routes>
{/* v6 版本把component改为element */}
<Route path="/about" element={<About />} />
<Route path="/user" element={<User />} />
</Routes>
<hr />
</div>
</Router>
);
}
export default App;
2.2 常用组件说明
-
Router 组件:包裹整个应用,一个 React 应用只需要使用一次
-
两种常用 Router:HashRouter 和 BrowserRouter
-
HashRouter:使用 URL 的 hash 值实现( #/*** )
-
(推荐)BrowserRouter:使用 H5 的 history API 实现( /*** )
-
Link 组件:用于指定导航链接(a 标签)
to 属性:浏览器地址栏中的 pathname(location.pathname)
-
Route 组件:指定路由展示的组件相关信息
path 属性:路由规则
element 属性:展示的组件
Route 组件写在哪,渲染出来的组件就展示在哪
3、路由的执行过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLED1EJn-1692620120421)(D:\PerfectCode\前端学习\React\Image\路由执行过程.PNG)]
4、编程式导航
-
编程式导航:通过 JS 代码来实现页面跳转
-
history 是 React 路由提供的,用于获取 浏览器历史记录 的相关信息
-
push(path):跳转到指定页面(有历史记录),参数 path 表示要跳转的路径(v5版本)
this.props.history.push(‘/login’)
-
//V5版本 import { useHistory } from "react-router-dom"; const history = useHistory(); history.path() history.go(n); // 前进或者后退到某个页面,n代表页数 history.goback(); // 返回 history.forward(); // 前进 history.replace(); // 没有历史记录 //V6版本 import {useNavigate} from 'react-router-dom'; const navigate = useNavigate(); navigate("/home"); // 跳转路由 navigate('/home', {replace: true}); navigate(-1); //后退 navigate(1); //前进
注意一:v5和v6版本略有不同,可参考下面内容
react-router-dom V6版本使用 - 简书 (jianshu.com)
注意二:useNavigate只能用在函数组件中,要想class组件也能使用,可以包装,如下:
高阶组件包装使得hook在class组件中使用
import { useNavigate } from 'react-router-dom'
// 高阶组件包装
function widthHook(WrapCompontent) {
// 设置别名
WrapCompontent.displayName = `WithHook${getDisplayName(WrapCompontent)}`
return function QiLincompont() {
const navigate = useNavigate()
return <WrapCompontent to={navigate}></WrapCompontent>
}
}
// 设置别名
function getDisplayName(WrapCompontent) {
return WrapCompontent.displayname || WrapCompontent.name || 'Component'
}
export default widthHook
此时高阶组件函数已经写好了,只需要对自己写的组件进行包装即可**(看个人习惯,可以把 to 改为 navigate)**
import React from 'react'
import withHook from 'withHook'
class IndexSwiper extends React.PureComponent {
toPath = (path) => {
this.props.to(path)
// this.props.to(path,{replace:true})
}
render() {
return <button onClick={ this.toPath('/news') }>路由跳转</button>
}
}
const QilinCompont= withHook(IndexSwiper)
export default QilinCompont
改改路径可以直接使用
案例:
import React from 'react';
import widthHook from './widthHook';
// 2. 导入组件
import { BrowserRouter as Router, Route, Link, Routes, useNavigate } from 'react-router-dom';
/*
react-router-dom 的基本使用
*/
const About = () => {
return <h2>关于</h2>
}
const User = () => {
return <h2>用户</h2>
}
const Home = () => {
const navigate = useNavigate();
const handleBack = () => {
// 使用编程式导航,返回登录页
navigate(-1); //后退
}
return (
<div>
<h2>首页</h2>
<button onClick={handleBack}>返回</button>
</div>
)
}
class Login extends React.Component {
handleLogin = () => {
// 使用编程式导航
// to 是高级组件封装 navigate 的方法
// 带历史记录的跳转
this.props.to(`/home`)
// 不带历史记录的跳转
// this.props.to(`/home`,{replace:true})
}
render() {
return (
<div>
<h2>登录页面</h2>
<button onClick={this.handleLogin}>登录</button>
</div>
)
}
}
const LoginWithHook = widthHook(Login)
const App = () => {
return (
// 3. Router 组件是路由的根组件,所有的路由组件都应该包裹在 Router 组件中
<Router>
<div>
<h1>React路由基础</h1>
<p><Link to="/login">去登录</Link></p>
<ul>
{/* 4. 指定路由的入口 */}
<li><Link to="/about">关于</Link></li>
<li><Link to="/user">用户</Link></li>
</ul>
<hr />
{/* 5. 指定路由出口 */}
{/* v6 必须用 Routes 包裹 */}
<Routes>
{/* v6 版本把component改为element */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user" element={<User />} />
<Route path="/login" element={<LoginWithHook />} />
<Route path="/home" element={<Home />} />
</Routes>
<hr />
</div>
</Router>
);
}
export default App;
5、默认路由(重定向)、路由嵌套
5.1 默认路由(重定向)
- 进入页面就会匹配的路由
- path:/
<Route path=“/” element={<Home />} />
- 也可以使用重定向来实现默认路由
//V5版本
import { Redirect } from 'react-router-dom';
//重定向到首页
return <Redirect to="/home" component={Home} />
//V6版本
import { Navigate } from 'react-router-dom';
//重定向到首页
return <Navigate to="/home" element={<Home />} />
5.2 路由嵌套
- 父:
../*
- 子:没有
/
// 路由使用
<Routes>
<Route path='/home/*' element={<Home />}
<Route path='home-page' element={<div>我是嵌套内容</div>} />
<Route/>
</Routes>
- 使用
Outlet
组件,此组件是一个占位符,告诉 React Router 嵌套的内容应该放到哪里。
//路由中
<Routes>
<Route path='/user/*' element={<User />}
<Route path='user-item' element={<div>我是嵌套子项</div>} />
<Route/>
</Routes>
//User组件中
import {Outlet} from 'react-router-dom';
const User = () => {
return
<section>
<h1>我是外容器</h1>
<Outlet />
</section>
}
export default User;
- useRoutes代替react-router-config
export const routes = [
{
path: '/',
element: <Layout />,
children: [
{
path: 'home',
meta: {
title: '首页',
icon: ‘’,
},
children: [
{
path: 'homepage1',
element: <Homepage />,
meta: {
title: 'homepage1',
}
}
]
}
]
},
{
path: '/login',
element: <Login />,
meta: {
title: '登录',
hideMenu: true
}
},
{
path: '*',
element: <Page404 />,
meta: {
title: '404',
hideMenu: true
}
},
];
const Routes = () => (
useRoutes(routes)
)
export default Routes;
6、匹配模式
6.1 模糊匹配模式(v5)
- 问题:当 Link 组件的 to 属性值为 ‘/login’ 时,为什么默认路由 也会被匹配成功
- 默认情况下,React 路由是 模糊匹配模式
- 模糊匹配模式:只要 pathname 以 path 开头就会被匹配成功(to以path开头)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OphzwQJi-1692620120421)(D:\PerfectCode\前端学习\React\Image\模糊匹配.PNG)]
6.2 精确匹配
注意:不用Route标签包裹子组件,可以直接使用element属性,并且不需要exact来表示精准匹配,V6版本内部算法改变,它默认就是匹配完整路径,先后顺序不再重要,它能够自动找出最优匹配路径
-
v5 中给 Route 组件添加 exact 属性,让其变为 精确匹配模式
-
精确匹配:只有当 path 和 pathname 完全匹配才会展示相应路由
推荐:使用v6版本