react概述
什么是react
react是一个用于构建用户界面的JavaScript库
react的特点
- 声明式:只需要描述HTML看起来是什么样子
- 基于组件:组件是react中最重要的内容,组件是页面中一部分的内容
- 学习一次,随处使用:可以在多个领域使用
react基本使用
安装react
安装命令:npm i react react-dom
- react包是核心,提供创建元素,组件等功能。
- react-dom包提供操作DOM相关功能。
react的使用
-
引入react和react-dom
-
创建React元素
-
渲染react元素到页面中
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<!-- 引入js文件 -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<script>
// 创建react元素
// 参数一:元素名称
// 参数二:元素属性{title: '我是标题'}
// 第三个及其以后的参数:元素子节点
const title = React.createElement('h1', null, 'Hello React')
// 渲染react元素
// 参数一:要渲染的react元素
// 参数二:挂载点
ReactDOM.render(title, document.querySelector('#root'))
</script>
</body>
</html>
react脚手架的使用
react脚手架意义
脚手架是开发现代Web应用的必备
充分利用Webpack Babel ESLint等工具辅助项目开发
零配置,无需手动配置繁琐的工具即可使用
关注业务,而不是工具配置
使用React脚手架初始化项目
初始化项目命令:
npx create-react-app my-app
启动项目:切换到项目根目录下使用命令
npm start
在脚手架中使用react
-
导入react和react-dom两个包
-
调用React.createElement()方法创建React元素
-
调用ReactDOM.render()方法渲染React元素到页面中
找到src下index.js文件删除所有内容后编写如下内容
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 创建react元素
const title = React.createElement('h1', null, "Hello React 脚手架")
// 渲染react元素
ReactDOM.render(title, document.querySelector('#root'))
JSX的基本使用
createElement()方法的问题
- 繁琐不简洁
- 不直观,无法快速看出元素结构
- 不优雅,程序员的苦恼
JSX简介
JSX是JavaScript XML的简写,表示在JavaScript中写XML(HTML)格式的代码
优势:声明式语法更加直观、与HTML结构相同,降低学习成本、提升开发效率
使用步骤
- 使用JSX语法创建React元素
- 使用ReactDOM.render()方法渲染React元素到页面中
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// JSX创建react元素
const title = <h1>Hello JSX</h1>
// 渲染react元素
ReactDOM.render(title, document.querySelector('#root'))
为什么脚手架中可以使用JSX语法
默认使用babel进行编译处理,然后在浏览器环境中使用
注意点
-
React元素的属性名使用驼峰命名法
-
特殊属性名:class->className、for->htmlFor、tabindex->tabIndex
-
React元素没有子节点则可以 /> 结束
-
推荐使用小括号包裹JSX语法,从而避免一些问题
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// JSX创建react元素
const title = (
<h1 className="title">Hello JSX</h1>
)
// 渲染react元素
ReactDOM.render(title, document.querySelector('#root'))
JSX中使用JavaScript表达式
当数据存储在JavaScript中时,我们需要在JSX中使用JavaScript表达式
语法:{JavaScript表达式}
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 数据
const name = "Jack"
// JSX创建react元素
const dv = (
<div>
<h1 className="title">Hello JSX</h1>
<div>你好,我叫{name}</div>
</div>
)
// 渲染react元素
ReactDOM.render(dv, document.querySelector("#root"))
注意点:
- 单大括号中可以使用任意的JavaScript表达式
- JSX自身也是JavaScript表达式
- JavaScript的对象是一个例外,一般只会出现style属性中
- 不能在{}出现语句(if/for)
JSX的条件渲染
根据条件渲染特定的JSX结构
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
/*
条件渲染
*/
const isLoading = false;
const loadData = () => {
if(isLoading) {
return <div>loading...</div>
}else {
return <div>数据加载完成,此处显示加载完的数据</div>
}
}
const title = (
<h1>
条件渲染:
{ loadData() }
</h1>
)
ReactDOM.render(title, document.querySelector('#root'))
JSX的列表渲染
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
/*
列表渲染
*/
const songs = [
{id: 1, name: '痴心绝对'},
{id: 2, name: '像我这样的人'},
{id: 3, name: '南山南'}
]
const list = (
<ul>
{ songs.map(item => <li key={ item.id }>{ item.name }</li>) }
</ul>
)
ReactDOM.render(list, document.querySelector('#root'))
- 如果要渲染一组数据,可以使用数组的map()方法
- 渲染列表时应该添加key属性,key属性的值要保证唯一
- map遍历谁,就给谁添加key属性
- 尽量避免使用索引号作为key
JSX的样式处理
行内样式
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
/*
样式处理
*/
// 行内样式
const list = (
<h1 style={ {color: 'red'} }>
JSX样式处理
</h1>
)
ReactDOM.render(list, document.querySelector('#root'))
外联样式
新建css文件
.title {
color: red;
background-color: aqua;
}
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 导入css
import './css/index.css'
/*
样式处理
*/
// 外联样式
const list = (
<h1 className='title'>
JSX样式处理
</h1>
)
ReactDOM.render(list, document.querySelector('#root'))
React组件
React组件介绍
组件是React的"一等公民",使用React就是在用组件
组件表示页面中的部分功能
组合多个组件实现完整的页面功能
特点:可复用,独立,可组合
React组件的两种创建方式
使用函数创建组件
- 函数名称必须以大写字母开头
- 函数组件必须有返回值,表示该组件的结构
- 如果返回值为null,表示不渲染任何内容
渲染函数组件:使用函数名作为组件标签名
组件标签可以是单标签也可以是双标签
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
/*
函数组件
*/
function Hello() {
return ( <div>这是我的第一个函数组件</div> )
}
// 渲染函数组件
ReactDOM.render(<Hello />, document.querySelector('#root'))
使用类创建组件
-
类名称必须以大写字母开头
-
类组件应该继承React.Component父类,从而可以使用父类的方法和属性
-
类组件必须提供render()方法
-
render()方法必须有返回值,表示该组件的结构
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
/*
类组件
*/
class Hello extends React.Component {
render() {
return ( <div>这是我的第一个类组件</div> )
}
}
// 渲染类组件
ReactDOM.render(<Hello />, document.querySelector('#root'))
将组件抽离为独立的js文件
-
创建Hello.js
-
在Hello.js中导入React
-
创建组件
-
在Hello.js中导出该组件
-
在index.js中导入该组件
-
渲染组件
Hello.js内容如下:
import React from 'react'
class Hello extends React.Component {
render() {
return ( <div>这是抽离出来的类组件</div> )
}
}
// 导出Hello组件
export default Hello
index.js内容如下:
// 导入react-dom
import ReactDOM from 'react-dom'
/*
抽离组件到独立的JS文件中
*/
import Hello from './js/Hello.js'
// 渲染类组件
ReactDOM.render(<Hello />, document.querySelector('#root'))
React的事件处理
事件绑定
语法:
onClick = { () => {} }
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
render() {
// 绑定一个单击事件
return ( <button onClick = { function() {
console.log('我被点击了')
} }>点我</button> )
}
}
ReactDOM.render(<App />, document.querySelector('#root'))
事件对象
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
render() {
// 绑定一个单击事件,并获取事件对象
return ( <a href="http://itcast.cn/" onClick = { function(e) {
// 阻止默认行为
e.preventDefault()
console.log('我被点击了')
} }>点我</a> )
}
}
ReactDOM.render(<App />, document.querySelector('#root'))
有状态组件和无状态组件
函数组件又叫做无状态组件,类组件又叫做有状态组件
状态(state)即数据
函数组件没有自己的状态,只负责数据展示
类组件有自己的状态,负责更新UI
组件中的state和setState()
state的基本使用
-
状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
-
state的值是对象,表示一个组件中可以有多个数据
-
获取state使用this.state
import React from 'react'
import ReactDOM from 'react-dom'
/*
state的基本使用
*/
class App extends React.Component {
// constructor() {
// super()
// // 初始化
// this.state = {
// count: 0
// }
// }
state = {
count: 0
}
render() {
return (
<div>
<h1>计数器:{ this.state.count }</h1>
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector('#root'))
setState()修改状态
- 状态是可变的
- 语法:this.setState( {要修改的数据} )
- 不要直接改state中的值,这是不好的,并且报错
- setState()作用:1,修改state。2,更新UI
- 思想:数据驱动视图
import React from 'react'
import ReactDOM from 'react-dom'
/*
setState的基本使用
*/
class App extends React.Component {
// constructor() {
// super()
// // 初始化
// this.state = {
// count: 0
// }
// }
state = {
count: 0,
test: '测试数据'
}
render() {
return (
<div>
<h1>计数器:{ this.state.count }</h1>
<button onClick = { () => {
this.setState({
count: this.state.count + 1
})
} }>+1</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
从JSX中抽离事件处理程序
- JSX中掺杂过多JS逻辑代码,会显得非常混乱
- 将逻辑抽离到单独的方法中,保证JSX结构清晰
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
state = {
count: 0,
test: '测试数据'
}
//抽离事件处理程序
onIncrement() {
console.log(this) // undefined
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器: { this.state.count }</h1>
<button onClick = { this.onIncrement }>+1</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
事件绑定this指向问题
箭头函数改变this指向
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
state = {
count: 0,
test: '测试数据'
}
//抽离事件处理程序
onIncrement() {
console.log(this)
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{ this.state.count }</h1>
<button onClick = { () => this.onIncrement() }>+1</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
bind()方法改变this指向
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
state = {
count: 0,
test: '测试数据'
}
constructor() {
super()
this.onIncrement = this.onIncrement.bind(this)
}
//抽离事件处理程序
onIncrement() {
console.log(this)
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{ this.state.count }</h1>
<button onClick = { this.onIncrement }>+1</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
class的实例方法使用箭头函数改变this指向
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
state = {
count: 0,
test: '测试数据'
}
//抽离事件处理程序
onIncrement = () => {
console.log(this)
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{ this.state.count }</h1>
<button onClick = { this.onIncrement }>+1</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
表单处理
受控组件
- HTML的表单元素是可输入的,也就是有自己的可变状态
- 而React中可变状态通常保存在state中,并且只能通过setState()方法来修改
- React将state与表单元素值value绑定到一起,由state的值来控制表单元素的值
- 受控组件:其值受到React控制的表单元素
受控组件的使用步骤
- 在state中添加一个状态,作为表单元素的value
- 给表单元素绑定一个change事件,将表单元素的值设置为state的值
import React from 'react'
import ReactDOM from 'react-dom'
/*
受控组件:其值受到React控制的表单元素
操作文本框的值
*/
class App extends React.Component {
state = {
// 文本框的值
txt: ''
}
handleChange = (e) => {
console.log(this)
this.setState({
txt: e.target.value
})
}
render() {
return (
<input type="text" value={ this.state.txt } onChange={ this.handleChange } />
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
受控组件的示例
import React from 'react'
import ReactDOM from 'react-dom'
/*
受控组件示例
*/
class App extends React.Component {
state = {
// 文本框的值
txt: '',
// 富文本框的值
content: '',
// 下拉框
city: 'bj',
// 复选框
isChecked: false
}
// 监听文本框
handleChange = (e) => {
// console.log(this)
this.setState({
txt: e.target.value
})
}
// 监听富文本框
handleContent = (e) => {
// console.log(this)
this.setState({
content: e.target.value
})
}
// 监听下拉框
handleCity = (e) => {
this.setState({
city: e.target.value
})
}
// 监听复选框
handleCheck = (e) => {
this.setState({
isChecked: e.target.checked
})
}
render() {
return (
<div>
{ /* 文本框 */ }
<input type="text" value={ this.state.txt } onChange={ this.handleChange } /><br />
{ /* 富文本框 */ }
<textarea value={ this.state.content } onChange= { this.handleContent }></textarea><br />
{ /* 下拉框 */ }
<select value = {this.state.city} onChange = { this.handleCity }>
<option value="sh">上海</option>
<option value="bj">北京</option>
<option value="gz">广州</option>
</select><br />
{ /* 复选框 */ }
<input type="checkbox" checked={ this.state.isChecked } onChange={ this.handleCheck } /><br />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
多表单元素优化
-
问题:每个表单元素都有一个单独的事件处理程序处理太繁琐
-
优化:使用一个事件处理程序同时处理多个表单元素
多表单元素优化步骤:
- 给表单元素添加name属性,名称与state相同
- 根据表单元素类型获取对应值
- 在change事件处理程序当中通过{name}来修改对应的state
import React from 'react'
import ReactDOM from 'react-dom'
/*
受控组件示例
*/
class App extends React.Component {
state = {
// 文本框的值
txt: '',
// 富文本框的值
content: '',
// 下拉框
city: 'bj',
// 复选框
isChecked: false
}
// 监听state
handleChange = (e) => {
// console.log(this)
this.setState({
[e.target.name]: e.target.type === "checkbox" ? e.target.checked : e.target.value
})
}
render() {
return (
<div>
{ /* 文本框 */ }
<input type="text" value={ this.state.txt } onChange={ this.handleChange } name="txt" /><br />
{ /* 富文本框 */ }
<textarea value={ this.state.content } onChange= { this.handleChange } name="content"></textarea><br />
{ /* 下拉框 */ }
<select value = {this.state.city} onChange = { this.handleChange } name="city">
<option value="sh">上海</option>
<option value="bj">北京</option>
<option value="gz">广州</option>
</select><br />
{ /* 复选框 */ }
<input type="checkbox" checked={ this.state.isChecked } onChange={ this.handleChange } name="isChecked" /><br />
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
非受控组件
-
说明:借助于ref,使用原生DOM方式来获取表单元素值
-
ref作用:获取DOM或组件
非受控组件使用步骤
- 调用React.createRef()方法创建一个ref对象
- 将创建好的ref对象添加到文本框中
- 通过ref对象获取到文本框的值
import React from 'react'
import ReactDOM from 'react-dom'
/*
非受控组件
*/
class App extends React.Component {
constructor() {
super()
// 创建ref对象
this.txtRef = React.createRef()
}
getTxt = () => {
// 获取文本框的值
console.log("文本框对应的值为:", this.txtRef.current.value)
}
render() {
return (
<div>
{/* 将创建好的ref对象添加到文本框中 */}
<input type="text" ref={ this.txtRef } />
<button onClick= { this.getTxt }>获取文本框的值</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
综合案例
index.js
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入样式表
import './css/index.css'
/*
评论列表案例
*/
class App extends React.Component {
// 初始化状态
state = {
comments: [
{ id: 1, name: 'jack', content: '沙发!!!' },
{ id: 2, name: 'rose', content: '板凳~' },
{ id: 3, name: 'tom', content: '楼主好人' }
],
// 评论人
userName: '',
// 评论内容
userContent: '',
}
renderList() {
return this.state.comments.length === 0 ? (
// 渲染暂无评论
<div className="no-comment">暂无评论,快去评论吧~</div>
) : (
// 渲染评论列表
<ul>
{ this.state.comments.map(item => (
<li key= {item.id}>
<h3>评论人: { item.name }</h3>
<p>评论内容: { item.content}</p>
</li>
)) }
</ul>
)
}
// 处理表单元素值
handleChange = (e) => {
// 获取当前表单元素的name属性和value
const {name, value} = e.target
// 更新state
this.setState({
[name]: value
})
}
// 发表评论
addComment = () => {
const {comments, userContent, userName} = this.state
// const newComments = [{
// id: Math.random(), name: userName, content: userContent
// }, ...comments]
// 非空校验
if(userName.trim() === '' || userContent.trim() === '') {
alert('请输入评论人和评论内容')
return
}
// 复制comments
const newComments = comments.concat()
// 添加评论内容
newComments.push({id: Math.random(), name: userName, content: userContent})
// 更新state
this.setState({
comments: newComments,
userName: '',
userContent: '',
})
}
render() {
// 获取评论人和评论内容
const {userName, userContent} = this.state
return (
<div className="app">
<div>
<input className="user" type="text" placeholder="请输入评论人" value= { userName } name="userName"
onChange= { this.handleChange } />
<br />
<textarea
className="content"
cols="30"
rows="10"
placeholder="请输入评论内容"
value= { userContent }
name="userContent"
onChange= { this.handleChange }
/>
<br />
<button onClick= { this.addComment }>发表评论</button>
</div>
{ /* 通过条件渲染决定渲染什么内容 */ }
{ this.renderList() }
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
index.css
.app {
width: 300px;
padding: 10px;
border: 1px solid #999;
}
.user {
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
}
.content {
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
}
.no-comment {
text-align: center;
margin-top: 30px;
}
React组件进阶
组件通讯介绍
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据,在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能,而在这个过程中,多个组件不可避免的要共享某个数据,为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通,这个过程就是组件通讯
组件的props
- 组件是封闭的,要接收外部数据应该通过props来实现
- props的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数组:函数组件通过参数props接收数据,类组件通过this.props接收数据
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// props 函数组件接收数据通过props参数
const Hello = (props) => {
// props是一个对象
console.log(props)
return (
<div>
<h1>props: {props.name} </h1>
</div>
)
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Hello name="jack" />)
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// props 类组件接收数据通过this.props
class Hello extends React.Component {
render() {
console.log(this.props)
return (
<div>
<h1>props: {this.props.name}</h1>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Hello name="jack" />)
特点:
-
可以给组件传递任意类型的数据
-
props是只读的对象
-
使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取props
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
class App extends React.Component {
constructor(props) {
super(props)
console.log(props)
}
render() {
console.log(this.props)
return (
<div>
props: {this.props.name}
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App name="zs" />)
组件通讯的三种方式
父组件传递数据给子组件
- 父组件要提供传递给子组件的数据
- 给子组件标签添加属性,值为state中的数据
- 子组件通过props接收父组件中传递的数据
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'
// 父组件
class Father extends React.Component {
state = {
lastName: '老王'
}
render() {
return (
<div className="father">
父组件:
<Child name = { this.state.lastName } />
</div>
)
}
}
// 子组件
const Child = props => {
return (
<div className="child">
<p>子组件接收父组件的数据:{ props.name }</p>
</div>
)
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Father />)
my.css文件内容
.father {
height: 200px;
background-color: blue;
}
.child {
height: 100px;
background-color: yellowgreen;
}
子组件传递数据给父组件
思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
-
在父组件中提供一个回调函数,用于接收数据
-
将该函数作为属性的值,传递给子组件
-
子组件通过props调用回调函数
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'
// 父组件
class Father extends React.Component {
state = {
lastName: '老王'
}
// 提供回调函数用来接收数据
getChildMsg = (msg) => {
console.log("接收子组件传递过来的数据", msg)
}
render() {
return (
<div className="father">
父组件:
<Child getMsg = { this.getChildMsg } />
</div>
)
}
}
// 子组件
const Child = props => {
return (
<div className="child">
<p>子组件传递父组件数据:{ props.getMsg('张三') }</p>
</div>
)
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Father />)
兄弟组件
-
将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
-
思想:状态提升
-
公共父组件职责:1. 提供共享状态、2. 提供操作共享状态的方法
-
要通讯的子组件只需通过props接收状态或操作状态的方法
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 父组件
class Counter extends React.Component {
// 父组件提供共享状态
state = {
count: 0
}
// 父组件提供修改共享状态的方法
addCount = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
{ /* 传递数据 */ }
<Child1 count = { this.state.count } />
<Child2 addCount = { this.addCount } />
</div>
)
}
}
/*
兄弟关系
*/
// 子组件
const Child1 = props => {
return <h1>计数器: { props.count }</h1>
}
// 子组件
const Child2 = (props) => {
return <button onClick= { props.addCount }>+1</button>
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Counter />)
Context的基本使用
作用:跨组件传递数据
使用步骤:
- 调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
const {Provider, Consumer} = React.createContext()
- 使用Provider组件作为父节点
<Provider>
<div className="App">
<Child1 />
</div>
</Provider>
- 设置value属性,表示要传递的数据
<Provider value="pink"></Provider>
- 调用Consumer组件接收数据
<Consumer>
{ (data) => {
return <span>
data参数表示接收到的数据:{ data }
</span>
} }
</Consumer>
完成代码如下:
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'
// Context
// 创建Context
const {Provider, Consumer} = React.createContext()
class App extends React.Component {
render() {
return (
<Provider value="pink">
<div className="App">
<Node />
</div>
</Provider>
)
}
}
const Node = (props) => {
return (
<div className="Node">
<SubNode />
</div>
)
}
const SubNode = (props) => {
return (
<div className="SubNode">
<Child />
</div>
)
}
const Child = (props) => {
return (
<div className="Child">
我是子节点<br />
<Consumer>
{ (data) => {
return <span>
data参数表示接收到的数据:{ data }
</span>
} }
</Consumer>
</div>
)
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
my.css文件如下:
.App {
height: 400px;
background-color: blue;
padding-top: 10px;
}
.Node {
height: 200px;
background-color: yellowgreen;
padding-top: 10px;
}
.SubNode {
height: 100px;
background-color: red;
padding-top: 10px;
}
.Child {
height: 50px;
background-color: pink;
padding-top: 10px;
}
props深入
children属性
- children属性表示组件标签的子节点,当组件标签有子节点时,props就会有该属性
- children属性与普通的props一样,值可以是任意值
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// children属性
const App = (props) => {
console.log(props)
return (
<div>
<h1>组件标签的子节点:</ h1>
{ props.children }
</div>
)
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App>我是子节点</App>)
props校验
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
- props校验:允许在创建组件的时候,就指定props的类型,格式等
- 作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤:
-
安装包prop-types (npm i prop-types)
-
导入prop-types包
-
使用组件名.propTypes = {}来给组件的props添加校验规则
-
校验规则通过PropTypes对象来指定
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import PropTypes from 'prop-types'
// props校验
const App = (props) => {
const arr = props.colors
const list = arr.map((item, index) => <li key= { index }>{ item }</li>)
return <ul> {list} </ul>
}
// 添加props校验
App.propTypes = {
colors: PropTypes.array
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App colors={ ['red', 'blue'] } />)
props校验–约束规则
- 常见类型:array,bool,func,number,object,string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象
App.propTypes = {
// 常见类型
optionalFunc: PropTypes.func,
// 必填
requiredFunc: PropTypes.func.isRequired,
// 特定结构对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
}
props的默认值
场景:分页组件 —》每页显示条数
作用:给props设置默认值,在未传入props时生效
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// props的默认值
const App = (props) => {
console.log(props)
return (
<div>
<h1>此处显示props的默认值:{ props.pageSize }</h1>
</div>
)
}
// 添加props默认值
App.defaultProps = {
pageSize: 10
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 不传入数据
root.render(<App />)
组件的生命周期
组件的生命周期概述
- 意义:组件的生命周期有助于理解组件的运行方式,完成更复杂的组件功能,分析组件错误原因
- 组件的生命周期:组件从被创建到挂载到页面中运行,在到组件不用时卸载的过程
- 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
- 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机
- 只有类组件才有生命周期
生命周期的三个阶段
学习目标
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
创建时(挂载阶段)
-
执行时机:组件创建时,页面加载时
-
钩子函数执行顺序:constructor —> render —> componentDidMount
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
组件生命周期
*/
// 创建时
class App extends React.Component {
// 最先执行
constructor(props) {
super(props)
console.warn("生命周期钩子函数 constructor")
// let title = document.querySelector('#title')
// console.log(title)
}
// 其次执行
render() {
console.warn("生命周期钩子函数 render")
// let title = document.querySelector('#title')
// console.log(title)
return (
<div>
<h1 id="title">统计豆豆被打的次数:</h1>
<button id="btn">打豆豆</button>
</div>
);
}
// 最后执行
componentDidMount() {
console.warn("生命周期钩子函数 componentDidMount")
// let title = document.querySelector('#title')
// console.log(title)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
更新时(更新阶段)
执行时机:1. setState()、2. forceUpdate()、3. 组件接收到新的props
以上三者任意一种变化,组件就会重新渲染
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
组件生命周期
*/
// 更新时
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
handleClick = () => {
// this.setState({
// count: this.state.count + 1
// })
// 演示强制更新
this.forceUpdate()
}
render() {
console.log("生命周期钩子函数 render")
return (
<div>
<Counter count = { this.state.count } />
<button id="btn" onClick= { this.handleClick }>打豆豆</button>
</div>
);
}
}
class Counter extends React.Component {
render() {
console.log('子组件钩子函数 render')
return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
钩子函数执行顺序:render —》componentDidUpdate
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
组件生命周期
*/
// 更新时
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
// 演示强制更新
// this.forceUpdate()
}
render() {
return (
<div>
<Counter count = { this.state.count } />
<button id="btn" onClick= { this.handleClick }>打豆豆</button>
</div>
);
}
}
class Counter extends React.Component {
render() {
console.log('子组件钩子函数 render')
return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
}
componentDidUpdate(prevProps) {
console.log('子组件钩子函数 componentDidUpdate')
console.log('上一次的props', prevProps, '当前的props', this.props)
// 常用方法
if(prevProps.count !== this.props.count) {
// 如果要修改state
this.setState({})
}
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
卸载时(卸载阶段)
- 执行时机:组件从页面中消失
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
组件生命周期
*/
// 卸载时
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
{this.state.count > 3 ? <p>豆豆被打死了</p> : <Counter count = { this.state.count } />}
<button id="btn" onClick= { this.handleClick }>打豆豆</button>
</div>
);
}
}
class Counter extends React.Component {
componentDidMount() {
this.timerId = setInterval(() => {
console.log("定时器启动")
}, 1000)
}
render() {
return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
}
componentWillUnmount() {
clearInterval(this.timerId)
console.log('钩子函数 componentWillUnmount')
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
render-props和高阶组件
React组件复用概述
思考:如果两个组件的部分功能相似或相同,该如何处理
处理方式:复用相似的功能(联想函数封装)
复用什么?
- state
- 操作state的方法(组件状态逻辑)
实现复用的两种方式:
- render props模式
- 高阶组件模式(HOC)
这两种模式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式
render props模式
思路分析:将要复用的state和操作state的方法封装到一个组件中
问题1:如何拿到组件中复用的state
- 在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
问题2:如何渲染任意的UI
- 使用该函数的返回值作为要渲染的UI内容
使用步骤
- 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态、2. 操作状态的代码)
- 将要复用的状态作为props.render(state)方法的参数,暴露到组件外部
- 使用props.render()的返回值作为要渲染的内容
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
render props模式
*/
// 1.创建Mouse组件
class Mouse extends React.Component {
state = {
// 鼠标横坐标
x: 0,
// 鼠标纵坐标
y: 0
}
// 当鼠标移动改变state值使其等于鼠标坐标
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 当组件创建并渲染DOM后
componentDidMount() {
// 添加鼠标移动事件
window.addEventListener("mousemove", this.handleMouseMove)
}
// 暴露
render() {
// return null
return this.props.render(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
<Mouse render = { (mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p> } />
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
演示组件复用
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入图片
import image from './image/cat.png'
/*
render props模式
*/
// 1.创建Mouse组件
class Mouse extends React.Component {
state = {
// 鼠标横坐标
x: 0,
// 鼠标纵坐标
y: 0
}
// 当鼠标移动改变state值使其等于鼠标坐标
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 当组件创建并渲染DOM后
componentDidMount() {
// 添加鼠标移动事件
window.addEventListener("mousemove", this.handleMouseMove)
}
// 暴露
render() {
// return null
return this.props.render(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
<Mouse render = { (mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p> } />
<Mouse render = { (mouse) => {
return <img src={ image } alt="猫" style={{
position: 'absolute',
top: mouse.y - 64,
left: mouse.x - 64
}} />
}
} />
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
children代替render属性
- 注意:并不是该模式叫render props就必须使用名为render的prop,实际上可以使用任意名称的prop
- 把prop是一个函数并且告诉组件要渲染什么内容的技术叫做render props 模式
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入图片
import image from './image/cat.png'
/*
render props模式
*/
// 1.创建Mouse组件
class Mouse extends React.Component {
state = {
// 鼠标横坐标
x: 0,
// 鼠标纵坐标
y: 0
}
// 当鼠标移动改变state值使其等于鼠标坐标
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 当组件创建并渲染DOM后
componentDidMount() {
// 添加鼠标移动事件
window.addEventListener("mousemove", this.handleMouseMove)
}
// 暴露
render() {
// return null
return this.props.children(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
<Mouse>{(mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p>}</Mouse>
<Mouse>{(mouse) => {
return <img src={ image } alt="猫" style={{
position: 'absolute',
top: mouse.y - 64,
left: mouse.x - 64
}} />
}}</Mouse>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
代码优化
- 推荐:给render props添加props校验
- 应该在组件卸载时解除mousemove事件绑定
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入prop-types
import PropTypes from 'prop-types'
// 导入图片
import image from './image/cat.png'
/*
render props模式
*/
// 1.创建Mouse组件
class Mouse extends React.Component {
state = {
// 鼠标横坐标
x: 0,
// 鼠标纵坐标
y: 0
}
// 当鼠标移动改变state值使其等于鼠标坐标
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
// 当组件创建并渲染DOM后
componentDidMount() {
// 添加鼠标移动事件
window.addEventListener("mousemove", this.handleMouseMove)
}
// 当组件卸载时移除事件绑定
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
// 暴露
render() {
// return null
return this.props.children(this.state)
}
}
// 添加props校验
Mouse.propTypes = {
children: PropTypes.func.isRequired
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props 模式</h1>
<Mouse>{(mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p>}</Mouse>
<Mouse>{(mouse) => {
return <img src={ image } alt="猫" style={{
position: 'absolute',
top: mouse.y - 64,
left: mouse.x - 64
}} />
}}</Mouse>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
高阶组件
高阶组件概述
目的:实现状态逻辑复用
采用包装(装饰)模式,通过包装组件,增强组件功能
- 高阶组件是一个函数,接收要包装的组件,返回包装后的组件
- 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给被包装组件
使用步骤
- 创建一个函数,名称约定with开头
- 指定函数参数,参数应该以大写字母开头,作为要渲染的组件
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在该组件中渲染参数组件,同时将状态通过prop传递给参数组件
- 调用该高阶组件,传入要增强的组件,通过返回值,拿到增强后的组件,并将其渲染到页面中
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 提供复用的逻辑
class Mouse extends React.Component {
// 鼠标坐标
state = {
x: 0,
y: 0,
}
handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
render() {
return <WrappedComponent {...this.state} />
}
}
return Mouse
}
// 用来测试高阶组件
const Position = props => {
console.log(props)
return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}
const MousePosition = withMouse(Position)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件:<MousePosition /></h1>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
设置displayName
使用高阶组件存在的问题:得到的两个组件名称相同
原因:默认情况下,React使用组件名称作为displayName
解决方式:为高阶组件设置displayName便于调试时区分不同的组件
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import image from './image/cat.png'
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 提供复用的逻辑
class Mouse extends React.Component {
// 鼠标坐标
state = {
x: 0,
y: 0,
}
handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
render() {
return <WrappedComponent {...this.state} />
}
}
// 设置displayName
Mouse.displayName = `withMouse${getDisPlayName(WrappedComponent)}`
return Mouse
}
// 设置displayName
function getDisPlayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => {
console.log(props)
return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}
const Cat = props => {
return <img src={image} alt="猫" style = {
{
position: 'absolute',
top: props.y - 64,
left: props.x - 64
}
} />
}
// 增强组件(包装组件)
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件:<MousePosition /></h1>
<MouseCat />
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
高阶组件传递props
问题:有props丢失
原因:高阶组件没有向下传递props
解决方式:渲染WrappedComponent时,将state和props一起传递给组件
传递方式如下:
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import image from './image/cat.png'
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 提供复用的逻辑
class Mouse extends React.Component {
// 鼠标坐标
state = {
x: 0,
y: 0,
}
handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
render() {
// 传递props
return <WrappedComponent {...this.state} {...this.props} />
}
}
// 设置displayName
Mouse.displayName = `withMouse${getDisPlayName(WrappedComponent)}`
return Mouse
}
// 设置displayName
function getDisPlayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => {
console.log(props)
return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}
const Cat = props => {
return <img src={image} alt="猫" style = {
{
position: 'absolute',
top: props.y - 64,
left: props.x - 64
}
} />
}
// 增强组件(包装组件)
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件:<MousePosition a="1" /></h1>
<MouseCat />
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
React原理揭秘
setState()的说明
更新数据
-
setState是异步更新数据的
-
使用该语法时,后面的setState不要依赖于前面的setState
-
如果setState调用多次,只会触发一次render()
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// setState是异步更新数据的
class App extends React.Component {
state = {
count: 0
}
handleCount = () => {
// 异步更新
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 0
}
render() {
return (
<div>
<h1>计数器: {this.state.count}</h1>
<button onClick = {this.handleCount}>+1</button>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
推荐语法
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// setState是异步更新数据的
class App extends React.Component {
state = {
count: 0
}
handleCount = () => {
// 异步更新
// 推荐语法
// 第一个参数是最新的state
// 第二个参数是最新的props
this.setState((state, props) => {
return {
count: state.count + 1
}
})
this.setState((state, props) => {
return {
count: state.count + 1
}
})
console.log(this.state.count)
}
render() {
return (
<div>
<h1>计数器: {this.state.count}</h1>
<button onClick = {this.handleCount}>+1</button>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// setState是异步更新数据的
class App extends React.Component {
state = {
count: 0
}
handleCount = () => {
// 异步更新
// 推荐语法
this.setState((state, props)=> {
return {
count: state.count + 1
}
})
this.setState((state, props)=> {
return {
count: state.count + 1
}
}, () => {
console.log('状态更新完成', this.state.count)
console.log(document.querySelector('#title').innerText)
})
console.log(this.state.count)
}
render() {
return (
<div>
<h1 id="title">计数器: {this.state.count}</h1>
<button onClick = {this.handleCount}>+1</button>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
JSX语法转化过程
- JSX仅仅是createElement()方法的语法糖(简化语法)
- JSX语法被@babel/preset-react插件编译为createElement()方法
- React元素是一个对象,用来描述你希望在屏幕上看到的内容
组件更新机制
- 父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)
组件性能优化
减轻state
-
减轻state:只存储跟组件渲染相关的数据
-
不用做渲染的数据不要放在state中,比如定时器id等
-
对于这种需要在多个方法中用到的数据,应该放在this中
避免不必要的重新渲染
-
组件更新机制:父组件更新会引起子树更新,这种思路清晰
-
问题:子组件没有任何变化时也会重新渲染
-
如何避免不必要的重新渲染呢?
-
解决方式:使用钩子函数shouldComponentUpdate(nextProps, nextState)
-
作用:通过该钩子函数的返回值决定是否重新渲染组件,true表示需要重新渲染,反之则不需要重新渲染
-
触发时机:更新阶段的钩子函数,组件更新渲染前执行
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 避免不必要的重新渲染
class App extends React.Component {
state = {
count: 0
}
handleCount = () => {
this.setState((state, props)=> {
return {
count: state.count + 1
}
})
}
shouldComponentUpdate(nextProps, nextState) {
// 最新状态
console.log('最新的state:', nextState)
// 更新前的状态,props亦然
console.log(this.state)
// 避免组件重新渲染
return false;
}
render() {
return (
<div>
<h1 id="title">计数器: {this.state.count}</h1>
<button onClick = {this.handleCount}>+1</button>
</div>
)
}
}
// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
案例:随机数
import React from 'react'
import {createRoot} from 'react-dom/client'
/**
* 生成随机数
*/
class App extends React.Component {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
// 因为两次生成的随机数可能相同,如果相同则不重新渲染组件
shouldComponentUpdate(nextProps, nextState) {
console.log(nextState.number)
console.log(this.state.number)
// return this.state.number === nextState.number ? false : true
return !(this.state.number === nextState.number)
}
render() {
console.log('render')
return (
<div>
<h1>随机数:{ this.state.number }</h1>
<button onClick = { this.handleClick }>重新生成</button>
</div>
)
}
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
import React from 'react'
import {createRoot} from 'react-dom/client'
/**
* 生成随机数
*/
class App extends React.Component {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
// 因为两次生成的随机数可能相同,如果相同则不重新渲染组件
// shouldComponentUpdate(nextProps, nextState) {
// console.log(nextState.number)
// console.log(this.state.number)
// // return this.state.number === nextState.number ? false : true
// return !(this.state.number === nextState.number)
// }
render() {
// console.log('render')
return (
<div>
<NumberBox number = { this.state.number } />
<button onClick = { this.handleClick }>重新生成</button>
</div>
)
}
}
class NumberBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
console.log(nextProps.number)
console.log(this.props.number)
// return this.props.number === nextProps.number ? false : true
return !(this.props.number === nextProps.number)
}
render() {
console.log('render')
return <h1>随机数:{ this.props.number }</h1>
}
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
纯组件
- 纯组件:PureComponent与React.Component功能相似
- 区别:PureComponent内部自动实现了shouldComponentUpdate钩子,不需要手动比较
- 原理:纯组件内部通过分别对比前后两次props和state的值,来决定是否重新渲染组件
import React from 'react'
import {createRoot} from 'react-dom/client'
/**
* 生成随机数--纯组件
*/
class App extends React.PureComponent {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
render() {
console.log('render')
return (
<div>
<h1>随机数:{ this.state.number }</h1>
<button onClick = { this.handleClick }>重新生成</button>
</div>
)
}
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
- 说明:纯组件内部的对比是shallow compare(浅层对比)
import React from 'react'
import {createRoot} from 'react-dom/client'
/**
* 生成随机数
*/
class App extends React.PureComponent {
state = {
obj: {
number: 0
}
}
handleClick = () => {
// const newObj = this.state.obj
// newObj.number = Math.floor(Math.random() * 3)
// console.log(newObj)
// console.log(this.state.obj)
// this.setState(() => {
// return {
// obj: newObj
// }
// })
const newObj = {...this.state.obj, number: Math.floor(Math.random() * 3)}
this.setState(() => {
return {
obj: newObj
}
})
}
render() {
console.log('render')
return (
<div>
<h1>随机数:{ this.state.obj.number }</h1>
<button onClick = { this.handleClick }>重新生成</button>
</div>
)
}
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
虚拟DOM和Diff算法
- React更新视图的思想是:只要state变化就重新渲染视图
- 特点:思路清晰
- 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染到页面中?不是
- 理想状态:只更新变化的内容
- 问题:React是如何做到部分更新的?虚拟DOM和Diff算法配合
虚拟DOM
- 本质上就是一个js对象,用来描述你希望在屏幕上看到的内容
Diff算法
- 初次渲染时,React会根据初始的state,创建一个虚拟DOM对象
- 根据虚拟DOM生成真正的DOM树,渲染到页面中
- 当数据变化后,重新根据新的数据,创建新的虚拟DOM对象
- 与上一次得到的虚拟DOM对象使用Diff算法进行对比(找不同),得到需要更新的内容
- 最终,React只将变化的内容更新到DOM中,重新渲染到页面
React路由基础
React路由介绍
现在的前端应用大多都是SPA(单页应用程序),也就是只有一个HTML页面的应用程序,因为它的用户体验更好,对服务器的压力更小,所以更受欢迎,为了有效的使用单个页面来管理原来多页面的功能,前端路由应运而生
- 前端路由的功能:让用户从一个视图导航到另一个视图
- 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
- 使用React路由简单来说,就是配置路径和组件(配对)
路由的基本使用
使用步骤
- 安装:npm i react-router-dom
- 导入路由的三个核心组件:Router/Route/Link
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
-
使用Router组件包裹整个应用
-
使用Link组件作为导航菜单(路由入口)
-
使用Route组件配置路由规则和要展示的组件(路由出口)
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
const First = () => {
return (
<div>页面一的页面内容</div>
)
}
const App = () => {
return (
<Router>
<div>
<h1>React路由基础</h1>
<Link to="/first">页面一</Link>
<Route path="/first" component = { First }></Route>
</div>
</Router>
)
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
新版本(V6)有所变化:情况如下
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
// V6多了一个Routes
import {BrowserRouter as Router, Route, Routes, Link} from 'react-router-dom'
const First = () => {
return (
<div>页面一的页面内容</div>
)
}
const Second = () => {
return (
<div>页面二的页面内容</div>
)
}
const App = () => {
return (
<Router>
<div>
<h1>React路由基础</h1>
<Link to="/first">页面一</Link>
<Link to="/second" >页面二</Link>
{/* V6必须使用Routes包裹Route */}
<Routes>
<Route path="/first" element = { <First /> } ></Route>
<Route path="/second" element= {<Second />} ></Route>
</Routes>
</div>
</Router>
)
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
- 在react-router-dom的V6版本中必须使用Routes组件包裹Route组件
- Route组件的component属性替换为element属性,并使用标签的形式进行组件渲染
- 下面代码中index的作用是标记路由为索引路由,访问父组件时会同时展示该路由,如果我们想达到路由继承的目的,则在父组件中使用占位组件(Outlet组件),将子组件的内容渲染到父组件中
<Routes>
<Route path="/" element={ <AppLayout /> }>
<Route index element={ <Home /> }></Route>
<Route path="user" element={ <User /> }></Route>
<Route path="/user/detail/:id" element={ <Detail /> } />
</Route>
<Route path="/login" element={ <Login /> } />
</Routes>
- 在下面代码中可以实现路由跳转
import { Button } from 'antd'
import { useNavigate } from 'react-router-dom'
function Login() {
const navigate = useNavigate()
const login = () => {
navigate("/")
}
return (
<div>
<Button type="primary" onClick = { login }>登录</Button>
</div>
)
}
- 下面代码是将路由地址作为参数获取的方式
import { useParams } from 'react-router-dom'
function Detail() {
const params = useParams()
return (
<div>
user detail - { params.id }
</div>
)
}
<Route path="/user/detail/:id" element={ <Detail /> } />
<Link to="/user/detail/1">用户1</Link>
- 集中式路由的渲染方式
import { RouteObject } from 'react-router-dom'
const router: RouterObject[] = [
{
path: "/",
element: <AppLayout />,
children: [
{index: true, element: <Home />},
{path: "user", element: <User />},
{path: "/user/detail/:id", element: <Detail />}
]
},
{path: "/login", element: <Login />}
]
// 导入上面写的代码
import router from './router'
import { useRoutes } from 'react-router-dom'
function App() {
const element = useRoutes(router)
return element
}
- 页面懒加载
import { lazy } from 'react'
// 导入Home组件
import Home from './home'
// 让Home组件懒加载
const Home = lazy(() => import('./home'))
import router from './router'
import { useRoutes } from 'react-router-dom'
import {Suspense} from 'react'
function App() {
const element = (
<Suspense fallback={<>loading</>}>
useRoutes(router)
</Suspense>
)
return element
}
常用组件说明
- BrowserRouter组件:包裹整个应用,一个React应用只需要使用一次,使用H5的history API实现
- HashRouter组件:使用URL的哈希值实现,与BrowserRouter的作用一样
- Link组件:用于指定导航链接(a标签)
- NavLink组件:与Link组件功能一样,但方便修改样式
- Route组件:指定路由展示组件相关信息
路由执行过程
- 点击Link组件(a标签),使得浏览器地址栏的URL发生改变
- React路由监听到地址栏URL的变化
- React路由内部会遍历所有Route组件,使用路由规则(path)与pathname进行匹配
- 当路由规则path能够与地址栏中的pathname匹配时,就展示该Route组件的内容
编程式导航
-
场景:点击登录按钮,登录成功后,通过代码跳转到后台首页,如何实现?
-
编程式导航:通过JS代码来实现页面跳转
-
history是React路由提供的,用于获取浏览器历史记录的相关信息
-
push(path):跳转到某个页面,参数path表示要跳转的路径
-
go(n):前进或后退到某个页面,参数n表示前进或后退页面数量
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
class Login extends React.Component {
handleLogin = () => {
console.log(this)
// 使用编程式导航实现路由跳转
this.props.history.push("/home")
}
render() {
return (
<div>
<p>登录页面</p>
<button onClick = { this.handleLogin }>登录</button>
</div>
)
}
}
const Home = (props) =>
<div>
<h2>我是后台首页</h2>
<button onClick= {() => {
props.history.go(-1)
}}>返回登录页面</button>
</div>
const App = () => (
<Router>
<div>
<h1>编程式导航</h1>
<Link to="/login">去登录页面</Link>
<Route path="/login" component = { Login }></Route>
<Route path="/home" component = { Home }></Route>
</div>
</Router>
)
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
默认路由
-
问题:现在的路由都是点击导航菜单栏后展示的,如何在进入页面的时候就展示呢?
-
默认路由:表示进入页面是就会匹配的路由
-
默认路由path为"/"
<Route path="/" component= {Home}></Route>
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
const Home = () => <p>进入页面的时候你能看到我吗?</p>
const App = () => {
return (
<Router>
<div>
<h1>默认路由</h1>
<Route path="/" component= {Home}></Route>
</div>
</Router>
)
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
匹配模式
模糊匹配模式
-
问题:当Link组件的to属性值为"/login"时,为什么默认路由也被匹配成功
-
回答:默认情况下,React的路由是模糊匹配模式
-
模糊匹配规则:只要pathname(Link组件的to属性)以path(Route组件的path属性)开头就会匹配成功
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
const Home = () => <p>进入页面的时候你能看到我吗?</p>
const Login = () => <p>我是Login组件的内容</p>
const App = () => {
return (
<Router>
<div>
<h1>默认路由</h1>
<Link to="/login">登录页面</Link>
{/* 模糊匹配模式默认的 */}
{/*只要to以path开头就能匹配成功*/}
<Route path="/" component={Home}></Route>
<Route path="/login" component={Login}></Route>
</div>
</Router>
)
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
精确匹配模式
- 问题:默认路由任何情况下都会展示,如何避免这种问题
- 给Route组件添加exact属性,让其变为精确匹配模式
- 精确匹配规则:只有当pathname和path完全一致时才会展示该路由(v18默认该模式)
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
const Home = () => <p>进入页面的时候你能看到我吗?</p>
const Login = () => <p>我是Login组件的内容</p>
const App = () => {
return (
<Router>
<div>
<h1>默认路由</h1>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/login">登录页面</Link>
</li>
</ul>
{/* 精确匹配 */}
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
</div>
</Router>
)
}
const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
React路由基础总结
- React路由可以有效的管理多个视图(组件)实现SPA
- Router(BrowserRouter)组件包裹整个应用,只需要使用一次
- Link组件是入口,Route组件是出口
- 通过props.history实现编程式导航
- 默认模糊匹配,添加exact变精确匹配
- React路由的一切都是组件,可以像思考组件一样思考路由