# 【写在开头】
本笔记来自学习dselegent-blog 互联网小白 | dselegent-blog (opens new window),仅供本人学习使用。
# 【01React入门】
# 1.React简介
React 是一个用于构建用户界面的 JavaScript 库。
# 2.快速使用
首先需要引入几个 react 包(React 核心库、操作 DOM 的 react 扩展库、将 jsx 转为 js 的 babel 库)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
2
3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>hello_react</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>
<!-- 引入react核心库 -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
/* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = <h1>Hello</h1> /* 此处不要写引号,因为不是字符串 */
//2.渲染虚拟DOM到页面
const root = ReactDOM.createRoot(document.querySelector('#test'));
root.render(VDOM);
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3.JSX语法
JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。
# 3.1.注意点
1.只能有一个根标签
...
render () {
return (
<div>
<div>第一个</div>
<div>第二个</div>
</div>
)
}
...
2
3
4
5
6
7
8
9
10
2.自定义的组件都必须要用大写字母开头,普通的 HTML 标签都用小写字母开头
3.内敛样式要使用{{}}包裹
style={{color:'skyblue',fontSize:'37px'}}
4.class需要使用className代替
# 3.2.小练习
<body>
<div id="test"></div>
<script type="text/babel">
const arr = ["Vue","React","Angular"]
const VDOM = (
<div>
<ul>
{arr.map((item,index)=>
<li key={index}>{item}</li>
)}
</ul>
</div>
)
const root = ReactDOM.createRoot(document.querySelector('#test'));
root.render(VDOM);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 【02面向组件编程】
# 1.组件
# 1.1.只有两种方式的组件
- 函数组件
- 类式组件
函数式组件
//1.创建函数式组件
function MyComponent(props) {
console.log(this) //此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('test'))
2
3
4
5
6
7
8
9
类式组件
class MyComponent extends React.Component {
render() {
console.log('render中的this:', this)
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
2
3
4
5
6
7
8
9
# 1.2.组合使用
function Hello(props) {
return <h1>Hello, {props.name}</h1>;
}
function Hi(props) {
return <h1>Hi, {props.name}</h1>;
}
function App() {
return (
<div>
<Hello name="Ronin" />
<Hi name="Yuhao" />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.组件实例的三大属性之一state
# 2.1state
一个组件的显示形态是可以由它数据状态和配置参数决定的。一个组件可以拥有自己的状态,就像一个点赞按钮,可以有“已点赞”和“未点赞”状态,并且可以在这两种状态之间进行切换。React.js 的 state 就是用来存储这种可变化的状态的。
# 2.1setState
*setState 方法由父类 Component 所提供。当我们调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上。
注意:
1.当我们要改变组件的状态的时候,不能直接用
this.state = xxx这种方式来修改,如果这样做 React.js 就没办法知道你修改了组件的状态,它也就没有办法更新页面。所以,一定要使用 React.js 提供的setState方法,它接受一个对象或者函数作为参数。
2.当你调用
setState的时候,React.js 并不会马上修改 state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到state当中,然后再触发组件更新。所以如果你想在setState之后使用新的state来做后续运算就做不到了。
...
handleClickOnLikeButton () {
this.setState({ count: 0 }) // => this.state.count 还是 undefined
this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN
this.setState({ count: this.state.count + 2}) // => NaN + 2 = NaN
}
...
2
3
4
5
6
7
使用接受一个函数作为参数的方式可以解决这个问题。React.js 会把上一个
setState的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新state的对象
...
handleClickOnLikeButton () {
this.setState((prevState) => {
return { count: 0 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
})
this.setState((prevState) => {
return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
})
// 最后的结果是 this.state.count 为 3
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.组件实例的三大属性之一props
与state不同,state是组件自身的状态,而props则是外部传入的数据
# 3.1简单使用
1.单个传值
<body>
<div id = "div"></div>
</body>
<script type="text/babel">
class Person extends React.Component{
render(){
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age + 1}</li>
</ul>
)
}
}
//传递数据
ReactDOM.render(<Person name="Ronin" age = {23} sex="男"/>,document.getElementById("div"));
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2.传递的数据可以是一个对象
<script type="text/babel">
class Person extends React.Component{
render(){
return (
<ul>
<li>{this.props.name}</li>
<li>{this.props.age}</li>
<li>{this.props.sex}</li>
</ul>
)
}
}
const p = {name:"Daisy",age:"22",sex:"女"}
ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.2类型检查
要在组件的 props 上进行类型检查,需要配置特定的 propTypes 属性
写在组件外面:
//对标签属性进行类型、必要性的限制
Person.propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string, //限制sex为字符串
age:PropTypes.number, //限制age为数值
//speak:PropTypes.func //限制speak为函数
}
//指定默认标签属性值
Person.defaultProps = {
sex:'男',//sex默认值为男
age:23 //age默认值为23
}
2
3
4
5
6
7
8
9
10
11
12
写在组件里面:
//对标签属性进行类型、必要性的限制
static propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string, //限制sex为字符串
age:PropTypes.number, //限制age为数值
}
//指定默认标签属性值
static defaultProps = {
sex:'男',//sex默认值为男
age:23 //age默认值为23
}
2
3
4
5
6
7
8
9
10
11
12
# 3.3函数式组件使用props
函数在使用props的时候,是作为参数进行使用的(props)
<script type="text/babel">
//创建组件
function Person(props) {
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
Person.propTypes = {
name: PropTypes.string.isRequired, //限制name必传,且为字符串
sex: PropTypes.string, //限制sex为字符串
age: PropTypes.number, //限制age为数值
}
//指定默认标签属性值
Person.defaultProps = {
sex: '男', //sex默认值为男
age: 23, //age默认值为23
}
//渲染组件到页面
ReactDOM.render(<Person name="Daisy" sex="女" age=22/>, document.getElementById('test'))
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 4.组件实例的三大属性之一refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
# 4.1三种操作refs的方法
- 字符串形式
- 回调形式
createRef形式
# 4.2推荐使用createRef形式:
1.创建 Refs
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
2
3
4
5
6
7
8
9
2.访问 Refs
当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。
const node = this.myRef.current;
Tips:
1.不能在函数组件上使用 ref 属性,因为他们没有实例。但是能在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件
2.不要过度的使用 ref
3.React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新
# 【03事件处理】
# 1.React事件
React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- React 事件的命名采用小驼峰式(camelCase)
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
- 不能通过返回
false的方式阻止默认行为,必须显式地使用preventDefault
传统的 HTML
<button onclick="activateLasers()">
Activate Lasers
</button>
2
3
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
2
3
React
<button onClick={activateLasers}>
Activate Lasers
</button>
2
3
function Form() {
function handleSubmit(e) {
e.preventDefault(); //显式地使用preventDefault
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
2
3
4
5
6
7
8
9
10
11
12
# 2.绑定事件
必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定this。如果忘记绑定 this.handleClick 并把它传入了 onClick,当调用这个函数的时候 this 的值为 undefined
使用bind
...
handleUsername(e){
this.setState({
username:e.target.value
})
}
...
<input value={this.state.username} onChange={this.handleUsername.bind(this)} />
...
2
3
4
5
6
7
8
9
箭头函数
...
handleUsername = (e) => {
this.setState({
username:e.target.value
})
}
...
<input value={this.state.username} onChange={this.handleUsername} />
...
2
3
4
5
6
7
8
9
...
handleUsername(e){
this.setState({
username:e.target.value
})
}
...
<input value={this.state.username} onChange={(e) => this.handleUsername(e)} />
...
2
3
4
5
6
7
8
9
# 3.传递参数
常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
2
1.上述两种方式是等价的,分别通过箭头函数和Function.prototype.bind来实现
2.如果通过箭头函数的方式,事件对象必须显式的进行传递;而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递
# 4.受控和非受控组件
# 4.1受控组件
使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
saveName = (event) =>{
this.setState({name:event.target.value});
}
savePwd = (event) => {
this.setState({pwd:event.target.value});
}
render() {
return (
<form action="http://www.baidu.com" onSubmit={this.login}>
用户名:<input value={this.state.name} onChange={this.saveName} type = "text" />
密码<input value={this.state.pwd} onChange={this.savePwd} type = "password"/>
<button>登录</button>
</form>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 onchange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。
# 4.2非受控组件
非受控组件其实就是表单元素的值不会更新state。输入数据都是现用现取的。
class Login extends React.Component{
login = (e) =>{
e.preventDefault(); //阻止表单默认事件
console.log(this.name.value);
console.log(this.pwd.value);
}
render() {
return (
<form action="http://www.baidu.com" onSubmit={this.login}>
用户名:<input ref = {e => this.name = e } type = "text" name ="username"/>
密码: <input ref = {e => this.pwd = e } type = "password" name ="password"/>
<button>登录</button>
</form>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
没有使用state来控制属性,使用的是事件来控制表单的属性值
# 5.函数的柯里化
# 5.1高阶函数
高阶函数是一个接收函数作为参数或将函数作为输出返回的函数
# 5.2柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名的。
简单来说,柯里化是一项技术,它用来改造多参数的函数,比如:
// 这是一个接受3个参数的函数
const add = function(x, y, z) {
return x + y + z
}
2
3
4
变换一下,可以得到这样一个函数:
// 接收一个单一参数
const curryingAdd = function(x) {
// 并且返回接受余下的参数的函数
return function(y, z) {
return x + y + z
}
}
2
3
4
5
6
7
从调用上来对比:
// 调用add
add(1, 2, 3)
// 调用curryingAdd
curryingAdd(1)(2, 3)
// 看得更清楚一点,等价于下面
const fn = curryingAdd(1)
fn(2, 3)
2
3
4
5
6
7
8
可以看到,变换后的的函数可以分批次接受参数,先记住这一点,下面会讲用处。甚至fn(curryingAdd返回的函数)还可以继续变换:
const curryingAdd = function(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
// 调用
curryingAdd(1)(2)(3)
// 即
const fn = curryingAdd(1)
const fn1 = fn(2)
fn1(3)
2
3
4
5
6
7
8
9
10
11
12
13
上面的两次变换过程,就是函数柯里化。
简单讲就是把一个多参数的函数f,变换成接受部分参数的函数g,并且这个函数g会返回一个函数h,函数h用来接受其他参数。函数h可以继续柯里化。就是一个套娃的过程。
下面回到React:
使用柯里化
class Login extends React.Component{
state = {name:"",pwd:""};
//返回一个函数
saveType = (type) =>{
return (event) => {
this.setState({[type]:event.target.value});
}
}
//因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
render() {
return (
<form>
<input onChange = {this.saveType('name')} type = "text"/>
<button>登录</button>
</form>
)
}
}
ReactDOM.render(<Login />,document.getElementById("div"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
不使用函数柯里化
class Login extends React.Component{
state = {name:"",pwd:""};
//返回一个函数
saveType = (type,event) =>{
this.setState({[type]:event.target.value});
}
//因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
render() {
return (
<form>
<input onChange = {event => this.saveType('name',event)} type = "text"/>
<button>登录</button>
</form>
)
}
}
ReactDOM.render(<Login />,document.getElementById("div"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 【04生命周期】
# 1.react生命周期(新)全览
React组件中包含一系列钩子函数{生命周期回调函数},会在特定的时刻调用
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. getDerivedStateFromProps
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
1. getDerivedStateFromProps
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate
5. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.初始化阶段
# 2.1该阶段生命周期
- constructor(props)
static getDerivedStateFromProps(props,state)--替代了componentWillReceiveProps- render()
- componentDidMount()
# 2.2constructor
数据的初始化
接收props和context,当想在函数内使用这两个参数需要在super传入参数,当使用constructor时必须使用super,否则可能会有this的指向问题,如果不初始化state或者不进行方法绑定,则可以不为组件实现构造函数
注意:避免将 props 的值复制给 state,如此做毫无必要(可以直接使用 this.props.color)
# 2.3static getDerivedStateFromProps(新)
很少使用
从props获取state
- 首先,该函数会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用;
- 该函数必须是静态的;
- 给组件传递的数据(props)以及组件状态(state),会作为参数到这个函数中;
- 该函数也必须有返回值,返回一个Null或者state对象。因为初始化和后续更新都会执行这个方法,因此在这个方法返回state对象,就相当于将原来的state进行了覆盖,所以倒是修改状态不起作用。
注意:
state的值在任何时候都取决于传入的props,不会再改变
# 2.4render
class组件中唯一必须实现的方法
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。
注意:避免在
render中使用setState,否则会死循环
当render被调用时,他会检查this.props.和this.state的变化并返回以下类型之一:
- 通过jsx创建的react元素
- 数组或者fragments:使得render可以返回多个元素
- Portals:可以渲染子节点到不同的dom树上
- 字符串或数值类型:他们在dom中会被渲染为文本节点
- 布尔类型或者null:什么都不渲染
# 2.5componentDidMount
在组件挂在后(插入到dom树中)后立即调用
componentDidMount 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作
这个挂载完成指的是:组件插入 DOM tree
可以在这里调用Ajax请求,返回的数据可以通过setState使组件重新渲染,或者添加订阅,但是要在conponentWillUnmount中取消订阅
# 3.更新阶段
# 3.1该阶段生命周期
- static getDerivedStateFromProps(nextProps, prevState)
- shouldComponentUpdate(nextProps,nextState)
- render()
- getSnapshotBeforeUpdate(prevProps,prevState)
- componentDidUpdate(prevProps,precState,snapshot)
# 3.2shouldComponentUpdate
唯一用于控制组件重新渲染的钩子
在渲染之前被调用,默认返回为true;
返回值是判断组件的输出是否受当前state或props更改的影响,默认每次state发生变化都重新渲染,首次渲染或使用forceUpdate(使用this.forceUpdate())时不被调用
主要用于性能优化,会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。不建议深层比较,会影响性能。如果返回false,则不会调用componentWillUpdate、render和componentDidUpdate
使用场景:
react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断
# 3.3getSnapshotBeforeUpdate(新)
在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用,替换componetnWillUpdate
它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null
和componentWillUpdate的区别
- 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
- getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
# 3.4componentDidUpdate
组件在更新完毕后会立即被调用,首次渲染不会调用
可以在该方法调用setState,但是要包含在条件语句中,否则一直更新会造成死循环
当组件更新后,可以在此处对 DOM 进行操作。如果对更新前后的props进行了比较,可以进行网络请求。(当 props 未发生变化时,则不会执行网络请求)
componentDidUpdate(prevProps,prevState,snapshotValue) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
2
3
4
5
6
注意:如果组件实现了
getSnapshotBeforeUpdate()生命周期,则它的返回值将作为componentDidUpdate()的第三个参数 “snapshotValue” 参数传递。否则此参数将为 undefined。如果返回false就不会调用这个函数
# 4.卸载组件
# 4.1componentWillUnmount
在组件卸载和销毁之前调用
注意:在这之前必须执行一些清理操作,例如,清除timer(setTimeout,setInterval),取消网络请求,或者取消在componentDidMount的订阅,移除所有监听
使用一个flag可以有效防止销毁组件时,请求还未完成
componentDidMount() {
this.isMount === true
axios.post().then((res) => {
this.isMount && this.setState({ // 增加条件ismount为true时
rrr:res
})
})
}
componentWillUnmount() {
this.isMount === false
}
2
3
4
5
6
7
8
9
10
11
# 【05条件渲染】
# 1.条件判断语句
if--else if--else
# 2.三目运算符
condition ? a : b
# 3.与运算符&&
适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染
render() {
const { type } = this.state
return (
<div>
{type === 1 && <h2>第三种写法:type值等于1</h2>}
{type !== 1 && <h2 className="other">第三种写法:type值不等于1</h2>}
</div>
)
}
2
3
4
5
6
7
8
9
在 JavaScript 中,true && expression 总是会返回 expression,而false && expression 总是会返回 false。因此,如果条件是 true,&& 右侧的元素就会被渲染;如果是 false,React 会忽略并跳过它
注意:falsy 表达式 会使
&&后面的元素被跳过,但会返回 falsy 表达式的值。在下面示例中,render 方法的返回值是
<div>0</div>
render() {
const count = 0;
return (
<div>
{count && <h1>Messages: {count}</h1>}
</div>
);
}
2
3
4
5
6
7
8
# 4.元素变量
render() {
const { type } = this.state
//2. 第二种方法 声明变量 给变量赋值
let test = null
if (type === 1) {
test = <h2>第四种写法:type值等于1</h2>
} else {
test = <h2 className="other">第四种写法:type值不等于1</h2>
}
return <div>{test}</div>
}
2
3
4
5
6
7
8
9
10
11
# 5.阻止组件渲染
若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
2
3
4
5
6
7
8
9
10
11
# 【06列表 & Key】
# 1.列表
可以通过使用 {} 在 JSX 内构建一个元素集合
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
<li>{numbers}</li>
);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ul>{listItems}</ul>);
2
3
4
5
6
7
在一个组件中渲染列表
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);
2
3
4
5
6
7
8
9
10
11
12
13
上段代码会有
a key should be provided for list items,所以创建一个元素时,必须包括一个特殊的key属性
<li>{number}</li>
<li key={number.toString()}>{number}</li> //加上key
2
# 2.key
# 2.1意义&基本使用
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此应当给数组中的每一个元素赋予一个确定的标识。
1.一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key。
2.当元素没有确定 id 的时候,万不得已可以使用元素索引 index 作为 key。
# 2.2 用 key 提取组件
如果提取出一个 ListItem 组件,应该把 key 保留在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上
不正确的使用 key 的方式
function ListItem(props) {
const value = props.value;
return (
// 错误!你不需要在这里指定 key:
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 错误!元素的 key 应该在这里指定:
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
正确的使用 key 的方式
function ListItem(props) {
// 正确!这里不需要指定 key:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 key 值在兄弟节点之间必须唯一
数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当生成两个不同的数组时,可以使用相同的 key 值
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}> //这里的兄弟节点为<li>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}> //这里的兄弟节点为<div>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2.4 在 JSX 中嵌入 map()
在上面的例子中,我们声明了一个单独的 listItems 变量并将其包含在 JSX 中
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
12
嵌入 map()后
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
2
3
4
5
6
7
8
9
10
11
# 3.diff算法
# 3.1虚拟 DOM
在谈 diff 算法之前,我们需要先了解虚拟 DOM。它是一种编程概念,在这个概念里,以一种虚拟的表现形式被保存在内存中。在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是 JavaScript 对象。
虚拟 DOM 只保留了真实 DOM 节点的一些基本属性,和节点之间的层次关系,它相当于建立在 JavaScript 和 DOM 之间的一层“缓存”
每个 DOM 元素的结构都可以用 JavaScript 的对象来表示。你会发现一个 DOM 元素包含的信息其实只有三个:标签名,属性,子元素。所以HTML的信息可以用虚拟 DOM 结构来表示:
<div class='box' id='content'>
<div class='title'>Hello</div>
<button>Click</button>
</div>
2
3
4
{
tag: 'div',
props: { className: 'box', id: 'content'},
children: [
{
tag: 'div',
props: { className: 'title' },
children: ['Hello']
},
{
tag: 'button',
props: null,
children: ['Click']
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.2Diff算法
每个组件中的每个标签都会有一个key,不过有的必须显示的指定,有的可以隐藏。
如果标签是静态的(生成后不会改变里面的内容),那么不需要指定key(不指定key时,React也会生成一个默认的标识),或者将index作为key,只要key不重复即可
如果标签是动态的,是有可能刷新的,就必须显示的指定key
使用map进行遍历的时候就必须指定Key
不推荐使用Index作为Key去使用
最好使用每一条数据的唯一标识作为key 比如id,手机号,身份证号
Diff算法其实就是react生成的新虚拟DOM和以前的旧虚拟DOM的比较规则:
- 如果旧的虚拟DOM中找到了与新虚拟DOM相同的key
- 如果内容没有变化,就直接只用之前旧的真实DOM
- 如果内容发生了变化,就生成新的真实DOM
- 如果旧的虚拟DOM中没有找到与新虚拟DOM相同的key
- 根据数据创建新的真实的DOM,随后渲染到页面上
# 【07收集表单数据】
# 1.状态属性
表单元素有这么几种属于状态的属性:
value,对应<input>和<textarea>所有checked,对应类型为checkbox和radio的<input>所有selected,对应<option>所有
注意:在 HTML 中
<textarea>的值可以由子节点(文本)赋值,但是在 React 中,要用value来设置
# 2.表单元素的两种表现形式
# 2.1受控组件
在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()来更新。
可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作,被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange.bind(this)} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。
# 2.2非受控组件
使用非受控组件,这时表单数据将交由 DOM 节点来处理。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.input = React.createRef();
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。
# 2.2.1默认值
在非受控组件中,希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 可以指定一个 defaultValue 属性,而不是 value。
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
同样,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked,<select> 和 <textarea> 支持 defaultValue。
# 2.3总结
详情 选受控还是非受控 (opens new window)
| feature | uncontrolled | controlled |
|---|---|---|
| one-time value retrieval (e.g. on submit) | ✅ | ✅ |
| validating on submit | ✅ | ✅ |
| instant field validation | ❌ | ✅ |
| conditionally disabling submit button | ❌ | ✅ |
| enforcing input format | ❌ | ✅ |
| several inputs for one piece of data | ❌ | ✅ |
| dynamic inputs | ❌ | ✅ |
# 3.标签变化
# 3.1 textarea 标签
在 HTML 中, <textarea> 元素通过其子元素定义其文本
<textarea>
你好, 这是在 text area 里的文本
</textarea>
2
3
在 React 中,<textarea> 使用 value 属性代替
...
handleChange(event) {
this.setState({value: event.target.value});
}
...
<textarea value={this.state.value} onChange={this.handleChange.bind(this)} />
...
2
3
4
5
6
7
# 3.2 select 标签
在 HTML 中,<select> 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表
<select>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option selected value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
2
3
4
5
6
React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
注意:可以将数组传递到
value属性中,以支持在select标签中选择多个选项<select multiple={true} value={['B', 'C']}>1
# 3.3文件 input 标签
在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API (opens new window) 进行操作
<input type="file" />
在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。应该使用 File API 与文件进行交互。
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${this.fileInput.current.files[0].name}`
);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<FileInput />);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 【08状态提升】
# 1.介绍
使用 react 经常会遇到几个组件需要共用状态数据的情况,这就需要状态提升。所谓状态提升就是将各个子组件的共用state 提升到它们的父组件进行统一存储、处理。
# 2.子组件修改父组件的state
将父组件中负责setState的函数,以props的形式传给子组件,然后子组件在需要改变state时调用即可。
这是两个有关连的同级组件的传值,因为react的单项数据流,所以不在两个组件中进行传值,而是提升到 最近的共同的父级组件中,改变父级的state,进而影响了两个子级组件的render。
# 示例
让这两个温度保持一致(输入其中一个,另一个自动算出来)
子组件
class TemperatureInput extends React.Component {
handleChange = (e) => {
this.props.onTemperatureChange(e.target.value);
};
render() {
return (
<fieldset>
<legend>输入{scaleNames[this.props.scale]}:</legend>
<input type="number" value={this.props.temperature} onChange={this.handleChange}/>
</fieldset>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
父组件
class Calculator extends React.Component {
state = {
celsius: '',
fahrenheit: ''
};
onCelsiusChange = (value) => {
this.setState({
celsius: value,
fahrenheit: tryConvert(value, toFahrenheit)
});
};
onFahrenheitChange = (value) => {
this.setState({
celsius: tryConvert(value, toCelsius),
fahrenheit: value
});
};
render() {
return (
<div>
<TemperatureInput scale='c' temperature={this.state.celsius}
onTemperatureChange={this.onCelsiusChange}/>
<TemperatureInput scale='f' temperature={this.state.fahrenheit}
onTemperatureChange={this.onFahrenheitChange}/>
</div>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 【09组合组件】
# 1.包含关系
有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。
建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中
组件标签里面包含的子元素会通过
props.children传递进来常用于抽取组件(样式)
import './One.css'
function One(props) {
return (
<div className={`one ${props.className}`}>{props.children}</div>
//模板字符串(使样式叠加)
);
}
function Two(props) {
return (
//这使别的组件(Two)可以通过JSX嵌套,来将任意组件(div)作为子组件来传递给他们(One)
<One className='two'> //可以将类名绑定的css样式传过去
<div>Hello</div>
<div>World</div>
</One>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.特例关系
有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 AddDialog ,DeleteDialog可以说是 Dialog 的特殊实例
定制的Dialog可以给通用的Dialog传值,让它变成定制版的
function Dialog(props) {
return (
<h1>
{props.title}
</h1>
<p>
{props.message}
</p>
);
}
function AddDialog() {
return (
<Dialog
title="Add"
message="Do you wanna add this info?" />
);
function DeleteDialog() {
return (
<Dialog
title="Delete"
message="Do you wanna delete this info?" />
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 【10初始化脚手架】
React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我们提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用的最佳方式。
# 1.方式一:原生创建react项目
1.确保安装了 npm 和Node
npm i create-react-app -g
2.新建一个文件夹用于存放项目
在当前的文件夹下执行
create-react-app hello-react
3.在生成好的 hello-react 文件夹中执行如下命令启动项目
npm start
# 2.方式二:使用vite创建react项目
- 什么是vite?—— 新一代前端构建工具。
- 优势如下:
- 开发环境中,无需打包操作,可快速的冷启动。
- 轻量快速的热重载(HMR)。
- 真正的按需编译,不再等待整个应用编译完成。
## 创建工程
# npm 6.x
npm init vite@latest <project-name> --template react
## 如: npm init vite@latest react-app --template react
# npm 7+,需要加上额外的双短横线
npm init vite@latest <project-name> --template react
## 使用 PNPM:
pnpm create vite <project-name> --template react
# pnpm create vite react-app -- --template react
## 进入工程目录
cd <project-name>
## 安装依赖
pnpm install
## 运行
pnpm run dev
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.项目结构(方式一)
hello-react
├─ .gitignore // 自动创建本地仓库
├─ package.json // 相关配置文件
├─ public // 公共资源
│ ├─ favicon.ico // 浏览器顶部的icon图标
│ ├─ index.html // 应用的 index.html入口
│ ├─ logo192.png // 在 manifest 中使用的logo图
│ ├─ logo512.png // 同上
│ ├─ manifest.json // 应用加壳的配置文件
│ └─ robots.txt // 爬虫给协议文件
├─ src // 源码文件夹
│ ├─ App.css // App组件的样式
│ ├─ App.js // App组件
│ ├─ App.test.js // 用于给APP做测试
│ ├─ index.css // 样式
│ ├─ index.js // 入口文件
│ ├─ logo.svg // logo图
│ ├─ reportWebVitals.js // 页面性能分析文件
│ └─ setupTests.js // 组件单元测试文件
└─ package-lock.json
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
最主要的就是App.js(组件)以及index.js(将组件渲染到页面中)
解读public目录下的 index.html 文件中的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
//指定浏览器图标的路径,这里直接采用 %PUBLIC_URL% 原因是 webpack 配置好了,它代表的意思就是 public 文件夹
<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"
/>
//用于描述网站信息
<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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
推荐使用这种目录结构去使用组件

# 4.组件
在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种:基于函数的组件和基于类的组件。具体查看【02面向组件编程】。
# 4.1使用规范
为了使得项目结构更加的清晰,更易于维护,每个组件通常会存储到一个单独的文件中,并通过export导出。
//App.js
const App = () => {
return <h1>我是一个React的组件!</h1>;
};
export default App;
2
3
4
5
6
在其他文件中使用时,需要先通过import进行引入。
//index.js
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
2
3
4
5
6
# 4.2组件化编码流程
1.拆分组件:拆分界面,抽取组件
2.实现静态组件
3.实现动态组件
- 动态的显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件
- 交互
注意事项:
1.拆分组件、实现静态组件。注意className、style的写法
2.动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在自身的state中
- 某些组件使用:放在他们共同的父组件中【状态提升】
3.关于父子组件之间的通信
- 父组件给子组件传递数据:通过props传递
- 子组件给父组件传递数据:通过props传递,要求父组件提前给子组件传递一个函数
4.注意defaultChecked 和checked区别,defalutChecked只是在初始化的时候执行一次,checked没有这个限制,但是必须添加onChange方法类似的还有:defaultValue 和value
5.状态在哪里,操作状态的方法就在哪里
# 5.CSS样式
# 5.1内联样式
在React中可以直接通过标签的style属性来为元素设置样式。style属性需要的是一个对象作为值,来为元素设置样式。
<div style={{color:'skyblue'}}>
我是Div
</div>
2
3
注意:如果样式名不符合驼峰命名法,需要将其修改为符合驼峰命名法的名字。
如:background-color改为backgroundColor。
如果内联样式编写过多,会导致JSX变得异常混乱,此时也可以将样式对象定义到JSX外,然后通过变量引入。
样式过多,JSX会比较混乱:
const StyleDemo = () => {
return (
<div style={{color:'red', backgroundColor:'#bfa', fontSize:20, borderRadius:12}}>
我是Div
</div>
);
};
export default StyleDemo;
2
3
4
5
6
7
8
9
可以这样修改:
import React from 'react';
const StyleDemo = () => {
const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}
return (
<div style={divStyle}>
我是Div
</div>
);
};
export default StyleDemo;
2
3
4
5
6
7
8
9
10
11
12
13
# 5.2在内联样式中使用State
设置样式时,可以根据不同的state值应用不同的样式。
比如我们可以在组件中添加一个按钮,并希望通过点击按钮可以切换div的边框,代码可以这样写:
import React, {useState} from 'react';
const StyleDemo = () => {
const [showBorder, setShowBorder] = useState(false);
const divStyle = {
color: 'red',
backgroundColor: '#bfa',
fontSize: 20,
borderRadius: 12,
border: showBorder?'2px red solid':'none'
};
const toggleBorderHandler = ()=> {
setShowBorder(prevState => !prevState);
};
return (
<div style={divStyle}>
我是Div
<button onClick={toggleBorderHandler}>切换边框</button>
</div>
);
};
export default StyleDemo;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
上例中添加一个新的state,命名为showBorder,代码是这样的const [showBorder, setShowBorder] = useState(false);当该值为true时,我们希望div可以显示一条2像素的红色边框,当为false时,我们希望div没有边框。默认值为false。
divStyle的最后一个属性是这样设置的border: showBorder?'2px red solid':'none',这里我们根据showBorder的值来设置border样式的值,如果值为true,则设置边框,否则边框设置为none。
toggleBorderHandler 是负责修改showBorder的响应函数,当我们点击按钮后函数会对showBorder进行取反,这样我们的样式就可以根据state的不同值而呈现出不同的效果了。
# 5.3 外部样式表
直接在组件中import
//button.css
button{
background-color: #bfa;
}
2
3
4
//button.js
import './Button.css';
const Button = () => {
return <button>我是一个按钮</button>;
};
export default Button;
2
3
4
5
6
注意:
- 引入样式时直接import,无需指定名字,且引入样式必须以./或../开头
- 这种形式引入的样式是全局样式,有可能会被其他样式覆盖
# 5.4 css模块化
当组件逐渐增多起来的时候,就很有可能产生两个组件中样式名称有可能会冲突,这样会根据引入App这个组件的先后顺序,后面的会覆盖前面的。
使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以不用担心类名重复。
使用方式
CSS Module在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循如下几个步骤:
1.将css文件名修改: index.css --- >index.module.css
2.引入并使用的时候改变方式:
import React,{Component} from 'react'
import hello from './index.module.css' //引入的时候给一个名称
export default class Hello extends Component{
render() {
return (
<h1 className={hello.title}>Hello</h1> //通过大括号进行调用
)
}
}
2
3
4
5
6
7
8
9
10
# 6.配置代理
需要下载axios库
npm install axios
在使用的过程中很有可能会出现跨域的问题,这样就应该配置代理
所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port), 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域 。
React怎么通过代理解决跨域问题?
利用服务器之间访问不会有跨域,在中间开启一个服务器,端口号和项目端口号一样
# 6.1方法一
在package.json中追加如下配置
"proxy":"请求的地址" "proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
# 6.2方法二
1.创建代理配置文件
在src下创建配置文件:src/setupProxy.js
2.编写setupProxy.js配置具体代理规则
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但我们一般将changeOrigin值设为true
*/
pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api2': ''}
})
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
# 6.3方法三(vite配置proxy)
vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
// 是否自动打开浏览器
open: true,
// 代理
proxy: {
'/api': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 【11React路由5】
根据不同的路径,显示不同的组件(内容)
React使用的库react-router-dom
- 专门来实现 SPA 应用
# 1.安装
npm i react-router-dom@5
# 2.ReactRouter三大组件
Router:所有路由组件的根组件(底层组件),包裹路由规则的最外层容器
属性:basename->设置跟此路由根路径,router可以在1个组件中写多个
Route:路由规则匹配组件,显示当前规则对应的组件
Link:路由跳转的组件
注意:如果要精确匹配,那么可以在route上设置exact属性
# 示例
import React from 'react';
import Hello from "../Hello";
import Welcome from "../Welcome";
//hash模式
//import {HashRouter as Router,Link,Route} from 'react-router-dom'
//history模式/后端匹配使用
import {BrowserRouter as Router,Link,Route} from 'react-router-dom'
const RouterTest = () => {
return (
<div>
<Router>
<Link to="/hello">显示Hello页面</Link >
<br/>
<Link to="/welcome">显示Welcome页面</Link>
<Route path="/hello" component={Hello}></Route>
<Route path="/welcome" component={Welcome}></Route>
<Router/>
</div>)
}
export default RouterTest
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意点:
1.要在 Link 和 Route 标签的外层标签采用 BrowserRouter(或者HashRouter) 包裹,但是当路由过多时,要不停地更改标签包裹的位置,因此可以到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下
index.js
...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
...
2
3
4
5
6
7
8
2.Link组件可以设置to属性来进行页面的跳转,to属性可以直接写路径的字符串,也可以通过1个对象,进行路径的设置,如
...
let helloObj = {
pathname:"/hello",//跳转的路径
search:"?username=admin",//get请求参数
hash:"#abc",//设置的HASH值
state:{msg:'helloworld'}//传入组件的数据
};
...
<Router>
<Link to={helloObj}>显示Hello页面</Link >
<br/>
<Link to="/welcome">显示Welcome页面</Link>
<Route path="/hello" component={Hello}></Route>
<Route path="/welcome" component={Welcome}></Route>
<Router/>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.路由组件和一般组件
1.写法不同
一般组件:<Demo/>,路由组件:<Route path="/demo" component={Demo}/>
2.存放的位置不同
同时为了规范书写,将一般路由组件放在 pages/views 文件夹中,路由组件放在 components
而最重要的一点就是它们接收到的 props 不同,在一般组件中,如果我们不进行传递,就不会收到值。而对于路由组件而言,它会接收到 3 个固定属性 history 、location 以及 match

重要的属性
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4.二级路由样式丢失
解决这个问题,有三个方法:
1.样式加载使用绝对位置
<link href="/css/bootstrap.css" rel="stylesheet">
2.使用 %PUBLIC_URL%
<link href="%PUBLIC_URL%/css/bootstrap.css" rel="stylesheet">
3.使用HashRouter
因为HashRouter会添加#,默认不会处理#后面的路径,所以也是可以解决的
# 5.模糊匹配和精准匹配
路由的匹配有两种形式:一种是精准匹配,一种是模糊匹配
React 中默认开启的是模糊匹配
模糊匹配可以理解为,在匹配路由时,只要有匹配到的就好了
精准匹配就是,两者必须相同
比如:
<Link to = "/home/a/b" >Home</Link>
此时该标签匹配的路由,分为三个部分 home a b;将会根据这个先后顺序匹配路由。
如下就可以匹配到相应的路由:
<Route path="/home" component={Home}/>
但是如果是下面这个就会失败,也就是说他是根据路径一级一级查询的,可以包含前面那一部分,但并不是只包含部分就可以。
<Route path="/a" component={Home}/>
当然也可以使用这个精确的匹配exact={true}
如以下:这样就精确的匹配/home,则上面的/home/a/b就不行了
<Route exact={true} path="/home" component={Home}/>
//or
<Route exact path="/home" component={Home}/>
2
3
# 6.Switch解决相同路径问题
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
2
3

原因:Route 的机制,当匹配上了第一个 /about 组件后,它还会继续向下匹配,因此会出现两个 About 组件
办法:采用 Switch 组件进行包裹,让switch组件中的route只匹配1个,只要匹配到了,剩余的路由规则将不再匹配
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
</Switch>
2
3
4
5
# 7.路由重定向
如果访问某个组件时,如果有重定向组件,那么就会修改页面路径,使得页面内容显示为所定向路径的内容
function LoginInfo(props){
//props.loginState = 'success';
//props.loginState = "fail"
console.log(props)
if(props.location.state.loginState === 'success'){
return <Redirect to="/admin"></Redirect>
}else{
return <Redirect to="/login"></Redirect>
}
}
2
3
4
5
6
7
8
9
10
# 8.传递参数
# 8.1传递 params 参数
可以通过将数据拼接在路由地址末尾来实现数据的传递
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
注意:需要采用模板字符串以及
$符的方式来进行数据的获取
在注册路由时,可以通过 :参数名 来传递数据
<Route path="/home/message/detail/:id/:title" component={Detail} />
这样既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件
传递的数据被接收到了对象的 match 属性下的 params 中
因此可以在 Detail 组件中获取到由 Message 组件中传递来的 params 数据,并通过 params 数据中的 id 值,在详细内容的数据集中查找出指定 id 的详细内容
const { id, title } = this.props.match.params
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})
2
3
4
最后渲染数据即可
# 8.2传递 search 参数
首先在Link 中采用 ? 符号的方式来表示后面的为可用数据
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到
数据保存在了 location 对象下的 search 中,是一种字符串的形式保存的,可以引用一个库来进行转化 qs
采用 parse 方法,将字符串转化为键值对形式的对象
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1)) // 从?后面开始截取字符串
2
最后渲染数据即可
querystring
qs是一个npm仓库所管理的包,可通过npm install qs命令进行安装
qs.parse()将URL解析成对象的形式
qs.stringify()将对象序列化成URL的形式,以&进行拼接
//qs.parse()
const str = "username='admin'&password='123456'";
console.log(qs.parse(str));
// Object { username: "admin", password: "123456" }
//qs.stringify()
const a = qs.stringify({ username: 'admin', password: '123456' });
console.log(a);
// username=admin&password=123456
2
3
4
5
6
7
8
9
qs.stringify() 和JSON.stringify()有什么区别?
var a = {name:'hehe',age:10};
qs.stringify序列化结果如下
name=hehe&age=10
--------------------
而JSON.stringify序列化结果如下
"{"a":"hehe","age":10}"
2
3
4
5
6
7
8
# 8.3传递 state 参数
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
首先需要在 Link 中注册跳转时,传递一个路由对象,包括一个跳转地址名,一个 state 数据,这样就可以在 Detail 组件中获取到这个传递的 state 数据
注意:采用这种方式传递,无需声明接收
然后可以在 Detail 组件中的 location 对象下的 state 中取出我们所传递的数据
const { id, title } = this.props.location.state

为了解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如,
const { id, title } = this.props.location.state || {}
当获取不到 state 时,则用空对象代替
这里的 state 和状态里的 state 有所不同
# 8.4小结
1.params参数
路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
接收参数:this.props.match.params
2.search参数
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
3.state参数
路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.state
备注:刷新也可以保留住参数
2
3
4
5
6
7
8
9
10
11
12
13
14
接收参数
// 接收params参数
const {id,title} = this.props.match.params
// 接收search参数
const {search} = this.props.location
const {id,title} = qs.parse(search.slice(1))
// 接收state参数
const {id,title} = this.props.location.state || {}
2
3
4
5
6
7
8
9
# 9.路由跳转
# 9.1push 与 replace 模式
默认开启push 模式,即每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址
当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶
我们只需要在需要开启的链接上加上 replace 即可
<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msg
# 9.2编程式路由导航
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
-this.prosp.history.push()
-this.prosp.history.replace()
-this.prosp.history.goBack()
-this.prosp.history.goForward()
-this.prosp.history.go(1)
2
3
4
5
6
可以采用绑定事件的方式实现路由的跳转,在按钮上绑定一个 onClick 事件,当事件触发时,执行一个回调
//push跳转+携带params参数
// this.props.history.push(`/home/message/detail/${id}/${title}`)
//push跳转+携带search参数
// this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})
//replace跳转+携带params参数
//this.props.history.replace(`/home/message/detail/${id}/${title}`)
//replace跳转+携带search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 9.3 withRouter
当需要在页面内部添加回退前进等按钮时,由于这些组件一般通过一般组件的方式去编写,因此会遇到一个问题:无法获得 history 对象,这正是因为采用的是一般组件造成的。
只有路由组件才能获取到 history 对象
可以利用 react-router-dom 对象下的 withRouter 函数来对导出的 Header 组件进行包装,这样就能获得一个拥有 history 对象的一般组件
需要对哪个组件包装就在哪个组件下引入
// Header/index.jsx
import { withRouter } from 'react-router-dom'
// 在最后导出对象时,用 `withRouter` 函数对 index 进行包装
export default withRouter(index);
2
3
4
这样就能让一般组件获得路由组件所特有的 API
# 10.BrowserRouter 和 HashRouter 的区别
# 底层实现原理不一样
BrowserRouter 使用的是 React 为它封装的 history API ,这里的 history 和浏览器中的 history 有所不同。通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。
HashRouter是通过 URL 的哈希值
可以理解为是锚点跳转,因为锚点跳转会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,HashRouter 不会将
#符号后面的内容请求,兼容性更好
# 地址栏的表现形式不一样
- HashRouter 的路径中包含
#,例如localhost:3000/#/demo/test
# 刷新后路由 state 参数改变
- 在BrowserRouter 中,state 保存在 history 对象中,刷新不会丢失
- HashRouter 则刷新会丢失 state
# 【12React路由6】
# 1.安装
npm install react-router-dom@6
# 2.V6常用路由组件和hooks
| 组件名 | 作用 | 说明 |
|---|---|---|
<Routers> | 一组路由 | 代替原有<Switch>,所有子路由都用基础的Router children来表示 |
<Router> | 基础路由 | Route 用来定义一个访问路径与 React 组件之间的关系 |
<Link> | 导航组件 | 在实际页面中跳转使用 |
<NavLink> | 导航组件 | 作用和 Link 类似,多了一个导航高亮效果 |
<Outlet/> | 自适应渲染组件 | 根据实际路由url自动选择组件 |
| hooks名 | 作用 | 说明 |
|---|---|---|
useParams | 返回当前参数 | 根据路径读取参数 |
useNavigate | 返回当前路由 | 代替原有V5中的 useHistory |
useOutlet | 返回根据路由生成的element | |
useLocation | 返回当前的location 对象 | |
useRoutes | 同Routers组件一样,只不过是在js中使用 | |
useSearchParams | 用来匹配URL中?后面的搜索参数 |
# 3.Routes 与 Route
- v6版本中移出了先前的
<Switch>,引入了新的替代者:<Routes>。 <Routes>和<Route>要配合使用,且必须要用<Routes>包裹<Route>。<Route>相当于一个 if 语句,如果其路径与当前 URL 匹配,则呈现其对应的组件。<Route caseSensitive>属性用于指定:匹配时是否区分大小写(默认为 false)。- 当URL发生变化时,
<Routes>都会查看其所有子<Route>元素以找到最佳匹配并呈现组件 。 <Route>也可以嵌套使用,且可配合useRoutes()配置 “路由表” ,但需要通过<Outlet>组件来渲染其子路由
<Routes>
/*path属性用于定义路径,element属性用于定义当前路径所对应的组件*/
<Route path="/login" element={<Login />}></Route>
/*用于定义嵌套路由,home是一级路由,对应的路径/home*/
<Route path="home" element={<Home />}>
/*test1 和 test2 是二级路由,对应的路径是/home/test1 或 /home/test2*/
<Route path="test1" element={<Test/>}></Route>
<Route path="test2" element={<Test2/>}></Route>
</Route>
//Route也可以不写element属性, 这时就是用于展示嵌套的路由 .所对应的路径是/users/xxx
<Route path="users">
<Route path="xxx" element={<Demo />} />
</Route>
</Routes>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4.默认页路径实现(如 404 页)
在路由列表 Routes 中,可以加入一个 catch all 的默认页面,比如用来作 404 页面。
只要在最后加入 path 为 * 的一个路径,意为匹配所有路径
function App() {
return <BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
}
// 用来作为 404 页面的组件
const NotFound = () => {
return <div>你来到了没有知识的荒原</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.useRoutes注册路由实现(推荐)
src文件夹下新建子文件夹:routes,routes下新建文件:index.js 路由表独立成 js 文件:src/routes/index.js
//routes/index.js
import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>
},
{
path:"/",
element:<Navigate to="/about"/>
}
]
export default routes;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//App.js
import React from 'react'
import {NavLink,useRoutes} from "react-router-dom";
import routes from "./routes";
export default function App() {
const element = useRoutes(routes);
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<div className="content">
{element}
</div>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.嵌套路由的实现
路由结构如下:
- /about,About组件
- /home,Home组件
- /home/news,News组件
- /home/message,Message组件
用 children 来嵌套路由。
//routes/index.js
import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
import News from "../pages/News";
import Message from "../pages/Message";
const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>
},
]
},
{
path:"/",
element:<Navigate to="/about"/>
}
]
export default routes;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<Outlet>作用:当
<Route>产生嵌套时,渲染其对应的后续子路由。![]()
点击News下面就显示News组件的内容
点击Message下面就显示News组件的内容
如果不加
<Outlet>,就没有内容显示
//Home/index.jsx
import React from 'react';
import { NavLink,Outlet } from 'react-router-dom';
export default function Home() {
return (
<div>
<h2>Home组件内容</h2>
<div>
<ul className="nav nav-tabs">
<li>
<NavLink to="/home/news">News</NavLink>
{/* <NavLink to="./news">News</NavLink> */}
{/* <NavLink to="news">News</NavLink> */}
</li>
<li>
<NavLink to="/home/message">Message</NavLink>
</li>
</ul>
<Outlet/>
</div>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 路由链接中的
to属性值,可以是to="/home/news",即全路径(推荐这样写,不然直接看不知道是不是子路由)to="./news",即相对路径to="news"
# 7.路由传/接参
# 7.1 params
useParams()
需求:点击“消息x”,显示其id、title和content
//routes/index.js
//注意路径写法
import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
import News from "../pages/News";
import Message from "../pages/Message";
import Detail from "../pages/Detail";
const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>,
children:[
{
path:"detail/:id/:title/:content",
element:<Detail/>
}
]
},
]
},
{
path:"/",
element:<Navigate to="/about"/>
}
]
export default routes;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//Message/index.jsx
//传值到Detail组件
import React,{useState} from 'react'
import { NavLink,Outlet } from 'react-router-dom'
export default function Message() {
const [message] = useState([
{ id: "001", title: "消息1", content: "窗前明月光" },
{ id: "002", title: "消息2", content: "疑是地上霜" },
{ id: "003", title: "消息3", content: "举头望明月" },
{ id: "004", title: "消息4", content: "低头思故乡" }
])
return (
<div>
<hr />
<h3>Message组件内容</h3>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<NavLink to={`/home/message/detail/${msgObj.id}/${msgObj.title}/${msgObj.content}`}>{msgObj.title}</NavLink>
</li>
)
})
}
</ul>
<hr />
<Outlet />
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- 使用新Hook
useParams接 params 参数
//Detail/index.jsx
//接收Message组件传来的值
import React from 'react'
import { useParams } from 'react-router-dom'
// import { useMatch } from 'react-router-dom'
export default function Detail() {
const {id,title,content} = useParams();
// const {params:{id,title,content}}= useMatch("/home/message/detail/:id/:title/:content");
return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7.2 search
useSearchParams
需求:点击“消息x”,显示其id、title和content
//routes/index.js
...
{
path:"message",
element:<Message/>,
children:[
{
path:"detail",
element:<Detail/>
}
]
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
//Message/index.jsx
//传值到Detail组件
export default function Message() {
return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<Link to={`detail?id=${msgObj.id}&title=${msgObj.title}&content=${msgObj.content}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 使用新Hook
useSearchParams接 search参数
//Detail/index.jsx
//接收Message组件传来的值
import { useSearchParams } from 'react-router-dom'
export default function Detail() {
const [search,setSearch] = useSearchParams();
const id = search.get("id");
const title = search.get("title");
const content = search.get("content");
return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用setSearch可以设置参数的值
...
<button onClick={()=>setSearch('id=008&title=哈哈&content=嘻嘻')}>点我设置search参数</button>
...
2
3
也可以用 useLocation接 search 的值,但是结果如下
这时需要qs来parse
import { useLocation } from 'react-router-dom'
import qs from "qs";
export default function Detail() {
const {search} = useLocation();
const {id,title,content} = qs.parse(search.slice(1));
return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7.3 state(推荐)
//routes/index.js
...
{
path:"message",
element:<Message/>,
children:[
{
path:"detail",
element:<Detail/>
}
]
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
//Message/index.jsx
...
<NavLink to='detail' state={msgObj}>{msgObj.title}</NavLink>
...
2
3
4
5
- 使从新Hook
useLocation中获取 state 参数
//Detail/index.jsx
...
console.log(useLocation())
// 直接连续结构获取 location 中的 state 的属性
const { state: { id, title, info } } = useLocation()
...
2
3
4
5
6
7
# 8.编程式路由导航
使用新 Hook
useNavigate实现编程式路由导航useNavigate()获取的函数对象可以进行路由跳转,可以传入两个参数
- path
- 参数对象
- replace:跳转模式
- state:就是state参数
1.Detail组件:接收参数方式还是一样,具体看7.路由传/接参
2.routes/index.js里面的path方式也要对应
# 8.1路由传递params参数
//Message/index.jsx
import React,{useState} from 'react'
import { NavLink,Outlet,useNavigate } from 'react-router-dom'
export default function Message() {
const [message] = useState([
{id:"001",title:"消息1",content:"窗前明月光"},
{id:"002",title:"消息2",content:"疑是地上霜"},
{id:"003",title:"消息3",content:"举头望明月"},
{id:"004",title:"消息4",content:"低头思故乡"}
])
const navigate = useNavigate();
const handleClick = msgObj => {
const {id,title,content} = msgObj
navigate(`detail/${id}/${title}/${content}`,{replace:false})
}
return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<button onClick={() => handleClick(msgObj)}>查看消息</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 8.2路由传递search参数
//Message/index.jsx
export default function Message() {
const navigate = useNavigate();
const handleClick = msgObj => {
const {id,title,content} = msgObj
navigate(`detail?id=${id}&title=${title}&content=${content}`,{replace:false})
}
return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<button onClick={() => handleClick(msgObj)}>查看消息</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 8.3路由传递state参数(推荐)
//Message/index.jsx
export default function Message() {
const navigate = useNavigate();
const handleClick = msgObj => {
navigate("detail",{
replace: false,
state: msgObj
})
}
return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<button onClick={() => handleClick(msgObj)}>查看消息</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 8.4 useNavigate 实现前进后退操作
//Header.jsx
import React from 'react'
import { useNavigate } from 'react-router-dom'
export default function Header() {
const navgate = useNavigate()
const back = () => {
navgate(-1)
}
const forward = () => {
navgate(1)
}
return (
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header">
<h1>React Router Demo</h1>
<button onClick={back}>后退</button>
<button onClick={forward}>前进</button>
</div>
</div>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 9.汇总
# Components
BrovserRouter
HashRouter
Routes + Route
Link
NavLink
作用和 Link 类似,多了一个导航高亮效果(默认高亮样式类名是 active,可省略不写)
<NavLink end>, end 属性表示子路由高亮时此路由不再高亮V6移除了 activeClassName ,如果高亮样式类名是 active ,需要配置 className 为一个函数
函数接收两个参数
- isActive:是否激活
- isPending
const activeClassName = ({isActive}) => isActive ? 'list-group-item active' : 'list-group-item' <NavLink className={activeClassName} to="/about" >About</NavLink>1
2
3
4
Navigate
- V6中用
<Navigate>顶替了<Redirect>。用于路由重定向 - 只要
<Navigate>组件被渲染,就会修改路径,切换视图 replace属性用于控制跳转模式(push 或 replace,默认是 push)
- V6中用
Outlet
- 嵌套路由中,需要使用
<Outlet>设置子路由的路由出口,即在何处渲染子路由 - 设置二级路由链接时,可以是
to="news"、to="./news",但不能是to="/news"
- 嵌套路由中,需要使用
# Hooks
useRoutes()
useNavigate()
useParams()
useSearchParams()
useLocation()
useMatch()
useInRouterContext()
- 如果组件在
Router的上下文中呈现(处在路由组件内),则useInRouterContext()钩子返回 true,否则返回 false
- 如果组件在
useNavigationType()
- 返回当前的导航类型(用户是如何来到当前页面的)
- 返回值:
POP、PUSH、REPLACE - 备注:
POP是指在浏览器中直接打开了这个路由组件(刷新页面或者路由首页)
useOutlet()
用来呈现当前组件中要渲染的嵌套路由
const result = useOutlet() // 如果嵌套路由没挂载:null // 如果嵌套路由已挂载:嵌套路由对象1
2
3
useResolvedPath().
- 给定一个URL值,解析其中的 path、search、hash 值
# 【13Hook】
# 1.什么时候会用 Hook
如果在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在可以在现有的函数组件中使用 Hook。
# 2.State Hook
# 2.1声明 State 变量
在函数组件中,没有 this,所以不能分配或读取 this.state。可以直接在组件中调用 useState Hook:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
console.log(count, setCount)
}
2
3
4
5
6
7
它让函数式组件能够维护自己的
state,它接收一个参数,作为初始化state的值,赋值给count,因此useState的初始值只有第一次有效,它所映射出的两个变量count和setCount我们可以理解为setState来使用useState 能够返回一个数组,第一个元素是 state ,第二个是更新 state 的函数
# 2.2读取 State变量
<p>You clicked {count} times</p>
# 2.3 更新 State
<button onClick={() => setCount(count + 1)}>
Click me
</button>
2
3
# 2.4 使用多个 state 变量
将 state 变量声明为一对 [something, setSomething] 也很方便,因为如果想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
...
}
...
function handleOrangeClick() {
setFruit('orange');
}
2
3
4
5
6
7
8
9
10
11
# 3.Effect Hook
useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染
通过使用这个Hook,我们可以设置React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。
提示
如果熟悉 React class 的生命周期函数,可以把
useEffectHook 看做componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合。
# 3.1副作用操作
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的
副作用:React组件有部分逻辑可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。
# 3.2不需要清除的副作用
发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作
使用 Hook 的示例
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。
传递给 useEffect 的函数在每次渲染中都会有所不同,这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
为什么在组件内部调用 useEffect?
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
提示:与
componentDidMount或componentDidUpdate不同,使用useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffectHook 供你使用,其 API 与useEffect相同。
# 3.3需要清除的 effect
例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。
使用 Hook 的示例:
假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribe(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如果effect 返回一个函数,React 将会在下一次effect执行前调用它,我们可以在这个函数中清除掉前一次effect执行所带来的影响。
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
注意
并不是必须为 effect 中返回的函数命名。这里我们将其命名为
cleanup是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
# 3.4使用 Hook 的示例
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
2
3
4
5
这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
2
3
传入 [count] 作为第二个参数的作用:
如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribe(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
2
3
4
5
6
7
8
9
10
如果要用这种优化方式,确保使用了第二个参数里的变量的函数在useEffect里面
# 4.useRef
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
一个常见的用例便是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
useRef()返回的是一个普通的JS对象,JS对象中有一个current属性,它指向的便是原生的DOM对象。上例中,如果想访问div的原生DOM对象,只需通过inputEl.current即可访问,它可以调用DOM对象的各种方法和属性。
# 5.useReducer
# 5.1基本使用
为了解决复杂State带来的不便,React为我们提供了一个新的使用State的方式。reducer的作用就是将那些和同一个state相关的所有函数都整合到一起,方便在组件中进行调用。
工具都有其使用场景,Reducer也不例外,它只适用于那些比较复杂的state
const [state, dispatch] = useReducer(reducer, initialArg, init);
它的返回值和useState()类似
第一个参数是state用来读取state的值。
第二个参数dispatch是一个函数,不同于setState(),这个函数我们可以称它是一个“派发器”,通过它可以向reducer()发送不同的指令,控制reducer()做不同的操作。
它的参数有三个,第三个我们暂且忽略,只看前两个:
1.reducer()是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state值。当我们调用dispatch()时,dispatch()会将消息发送给reducer(),reducer()可以根据不同的消息对state进行不同的处理。
2.initialArg就是state的初始值,和useState()参数一样。
以下是用 reducer写的的计数器示例:
/*
* 参数:
* reducer : 整合函数
* 对于我们当前state的所有操作都应该在该函数中定义
* 该函数的返回值,会成为state的新值
* reducer在执行时,会收到两个参数:
* state 当前最新的state
* action 它需要一个对象
* 在对象中会存储dispatch所发送的指令
* initialArg : state的初始值,作用和useState()中的值是一样
* 返回值:
* 数组:
* 第一个参数,state 用来获取state的值
* 第二个参数,state 修改的派发器
* 通过派发器可以发送操作state的命令
* 具体的修改行为将会由另外一个函数(reducer)执行
* */
// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [count, countDispatch] = useReducer(countReducer, {count: 0});
// 这里本来初始值是直接给0的,但是为了countReducer函数中的state写成对象形式
return (
<>
Count: {count.count}
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 5.2state初始化的两种方式
指定初始 state
有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: 0} );
2
3
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
export default function App() {
return (
<div>
<Counter initialCount={0} />
</div>
)
}
function countInit(initialCount) {
return {count: initialCount};
}
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return countInit(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [count, countDispatch] = useReducer(countReducer, initialCount, countInit);
return (
<>
Count: {count.count}
<button
onClick={() => countDispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 5.3 跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 (opens new window) 来比较 state。)
这里的state如果是个对象,还是会渲染子组件,因为我们返回的是一个新对象,应该比较的是地址,如果直接将state返回,子组件是不会重新渲染的
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以不必担心。
# 6.React.memo(高阶组件)
# 6.1基本介绍
React.memo() 是一个高阶组件,如果组件在相同 props 的情况下渲染相同的结果,那么可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。
- 它接收另一个组件作为参数,并且会返回一个包装过的新组件
- 包装过的新组件就会具有缓存功能,这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
- 包装过后,只有组件的props发生变化,才会触发组件的重新的渲染,否则总是返回缓存中结果。如果函数组件被
React.memo包裹,且其实现中拥有useState,useReducer,useContext的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
# 6.2使用场景
当组件的父组件重新渲染,而自身的state没有改变时,没有必要重新渲染
//APP.jsx
import React, { useState } from 'react'
import A from './A'
export default function App() {
console.log('App渲染')
const [count, setCount] = useState(1)
const clickHandler = () => {
setCount(prevState => prevState + 1)
}
return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A />
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用React.memo包裹A组件
//A.jsx
const A = () => {
console.log('A渲染')
return <div>我是A组件</div>
}
export default React.memo(A)
2
3
4
5
6
7
没有直接使用A组件,而是在A组件外层套了一层函数React.memo(),这样一来,返回的A组件就增加了缓存功能,只有当A组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染
注意:
1.此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
2.与 class 组件中
shouldComponentUpdate()方法不同的是,如果 props 相等,areEqual会返回true;如果 props 不相等,则返回false。这与shouldComponentUpdate方法的返回值相反
# 6.3容易出错的情况
我们把App组件的clickHandler方法传递给A组件,让A组件也能够改变App组件的state
//APP.jsx
import React, { useState } from 'react'
import A from './A'
export default function App() {
console.log('App渲染')
const [count, setCount] = useState(1)
const clickHandler = () => {
setCount(prevState => prevState + 1)
}
return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A clickHandler={clickHandler} />
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//A.jsx
const A = props => {
console.log('A渲染')
return (
<div>
我是A组件
<button onClick={props.clickHandler}>A组件的增加</button>
</div>
)
}
export default React.memo(A)
2
3
4
5
6
7
8
9
10
11
12
点击A组件的增加,发现A组件也重新渲染了
这是因为App组件重新渲染的时候,clickHandler也重新创建了,这时传递给子组件的clickHandler和上一次不一样,所以react.memo失效了。
这个问题可以用useCallback解决。
# 7.useCallback
# 7.1基本介绍
/*
* useCallback()
* 这个hook会缓存方法的引用
* 参数:
* 1. 回调函数
* 2. 依赖数组
* - 当依赖数组中的变量发生变化时,回调函数才会重新<创建>
* - 如果不指定依赖数组,回调函数每次都会重新创建
* - 一定要将回调函数中使用到的所有变量都设置到依赖数组中
* 除了(setState)
* */
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
把内联回调函数及依赖数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
# 7.2使用场景
解决6.3的问题
将clickHandler方法改造一下
const clickHandler = useCallback(() => {
setCount(prevState => prevState + 1)
}, [])
2
3
第二个参数一定要加,不然和平常写没有区别
依赖项
[]的意思是只有第一次渲染时才会创建,之后都不会重新创建了
点击A组件的增加,发现只有App组件重新渲染了。因为clickHandler没有重新创建,传给子组件的没有变化,所以子组件这次没有重新渲染。
# 7.3测试第二个参数
改造代码:增加一个num,让每一次count的增加比上次多1
import React, { useState, useCallback } from 'react'
export default function App() {
console.log('App渲染')
const [count, setCount] = useState(1)
// 增加
const [num, setNum] = useState(1)
const clickHandler = useCallback(() => {
setCount(prevState => prevState + num)
// 增加
setNum(prevState => prevState + 1)
}, [])
return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A clickHandler={clickHandler} />
</div>
)
}
const A = React.memo(props => {
console.log('A渲染')
return (
<div>
我是A组件
<button onClick={props.clickHandler}>A组件的增加</button>
</div>
)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
现在这样写是有问题的
点击了两次增加后,预期值应该是4,但是显示的是3,是为什么呢?
因为clickHandler只在初次渲染的时候创建,当时num的值是1,这个函数一直没有重新创建,内部用的num一直是1
这时我们可以加一个依赖项
const clickHandler = useCallback(() => {
setCount(prevState => prevState + num)
setNum(prevState => prevState + 1)
}, [num])
2
3
4
这样num变化了,这个函数也会重新创建。
点击了两次增加后,count变成了预期值4
# 8.useMemo
# 8.1基本介绍
useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分复杂的逻辑,执行速度比较慢。避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,如:
const result = useMemo(()=>{
return 复杂逻辑函数();
},[依赖项])
2
3
useMemo中的函数会在依赖项发生变化时执行,执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。
注意:是执行,这点和useCallback不同,useCallback是创建。
# 8.2使用场景
import React, { useMemo, useState } from 'react'
const App = () => {
const [count, setCount] = useState(1)
let a = 123
let b = 456
function sum(a, b) {
console.log('sum执行了')
const begin = +new Date()
while (true) {
if (Date.now() - begin > 3000) break //这个函数起码3秒才能执行完
}
return a + b
}
return (
<div>
<h1>App</h1>
<p>sum的结果:{sum(a, b)}</p>
<h3>{count}</h3>
<button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
</div>
)
}
export default App
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这个时候因为sum函数要3秒才能执行完,导致下面数字显示也变慢了3秒
使用 useMemo 解决上面的问题
改写sum`方法的调用
<p>sum的结果:{useMemo(() => sum(a, b), [])}</p>
第一次加载慢是不可避免的,但是这个钩子函数将sum函数的返回值缓存起来,这样模板重新渲染时就没有再去执行sum函数,而是直接使用上一次的返回值
# 8.3测试第二个参数
改造上面的代码,把Sum单独抽离成一个组件
//Sum.jsx
import React from 'react'
export default function Sum(props) {
console.log('Sum执行了')
return <span>{props.a + props.b}</span>
}
App.jsx
2
3
4
5
6
7
8
添加一个功能可以变换a的值
import React, { useMemo, useState } from 'react'
import Sum from './Sum'
const App = () => {
const [count, setCount] = useState(1)
let a = 123
let b = 456
if (count % 2 === 0) a = a + 1
const result = useMemo(() => <Sum a={a} b={b} />, [])
return (
<div>
<h1>App</h1>
<p>sum的结果:{result}</p>
<h3>{count}</h3>
<button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
</div>
)
}
export default App
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
现在有一个问题,如果Sum组件接收的值变化了,网页上显示的还是原来的缓存值,这个时候就要利用第二个参数。

//App.jsx
const result = useMemo(() => <Sum a={a} b={b} />, [a])
2
如果
a的值变化了,将会重新计算

# 实际开发使用场景
1.用户停止输入一秒后向服务器发送请求
- 设置定时器
- 在useEffect中返回一个清除定时器的函数
