0 React简介
React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。React起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。
React 主要用于构建 UI,很多人认为 React 是 MVC 中的 V(视图)。
React 拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。
React 的生态体系比较庞大,它在web端,移动端,桌面端、服务器端,VR领域都有涉及。React可以说是目前为止最热⻔,生态最完善,应用范围最广的前端框架。React结合它的整个生态,它可以横跨web端,移动端,服务器端,乃至VR领域。可以毫不夸张地说,React已不单纯是一个框架,而是一个行业解决方案。
React特点:
- 声明式设计:React采用声明范式,可以轻松描述应用。
- 高效:React通过对DOM的模拟,最大限度地减少与DOM的交互。
- 灵活:React可以与已知的库或框架很好地配合。
- JSX:JSX 是JavaScript语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。
- 组件:通过 React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
- 单向响应的数据流:React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。
VS Code 编辑器安装插件:
1 React 入门
1 起步
javascript
创建项目: npx create-react-app my-project-name
打开项目: cd my-project-name
启动项目: npm start
暴露配置项: npm run eject
理解npx create-react-app my-project-name 指令,npx是高版本node所具有的(我的弄的版本18+)
npx是npm的附带产物
执行 npx create-react-app my-app 命令流程如下:
一旦使用npx去执行一段命令,那么npx会首先看第一个参数对应的工具是否被安装
如果没有被安装,npx就会告诉npm临时安装一下,临时安装进内存
当临时安装好了以后,npx会再次直接执行整段命令"create-react-app my-app"
create-react-app是react的官方脚手架
2 文件结构
javascript
README.md--文档(可用于说明项目)
public--存放静态资源(不被编译,原文)
favicon.ico--网站页面标签图
index.html--主页面
logo192.png--logo图
logo512.png--logo图
manifest.json--应用加壳的配置文件
robots.txt--爬虫协议文件
src--具体代码
assets--存放静态资源(被编译,原文)
App.css--根组件样式
App.js--根组件
App.test.js--用于给App做测试的(一般不用)
index.css--全局样式
index.js--入口文件
reportWebVitals.js--页面性能分析(需要web-vital库的支持)
setupTests.js--组件单元测试的文件(需要jest-dom库的支持)
package.json--npm 依赖
config--项目的具体配置
项目中config文件夹一般是隐藏掉的,如果想要出现可使用 npm run eject ,一旦执行,不可恢复
进入 webpack.config.js中,找到 alias 添加以下2行
javascript
// webpack.config.js
// 文件路径别名
'@': path.resolve(__dirname, '../src'),
'@pages': path.resolve(__dirname, '../src/pages'),
3 React和ReactDom
React元素:是构成 React 应用的最小单位,它用于描述屏幕上输出的内容。
javascript
// 创建虚拟dom
// JSX
const element = <h1>Hello, world!</h1>;
动态通过 React.createElement() 创建(语法糖:JSX)
与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的JS对象,React DOM 可以确保浏览器 DOM 的数据内容与 React 元素保持一致。
render():渲染虚拟DOM到真实DOM上,render(virtualDOM, containerDOM),即render(虚拟DOM, 真实DOM)
javascript
// 从 react 的包当中引入了 React。只要你要写 React.js 组件就必须引入React, 因为react里有一种语法叫JSX,稍后会讲到JSX,要写JSX,就必须引入React
// React 开发的核心模块
import React from 'react';
// ReactDOM 可以帮助我们把 React 组件渲染到页面上去,没有其它的作用了。它是从 react-dom 中引入的,而不是从 react 引入。
// ReactDOM 操作浏览器DOM的模块
import ReactDOM from 'react-dom';
// 1.创建虚拟DOM对象
const element = <h1>Hello, world!</h1>; // 不是字符串;JSX语法
// 2.将虚拟DOM渲染到真实DOM容器中
// ReactDOM里有一个render方法,功能就是把组件渲染并且构造 DOM 树,然后插入到页面上某个特定的元素上
ReactDom.render(
element,
document.getElementById('root')
)
要将React元素渲染到根DOM节点中,我们通过把它们都传递给 ReactDOM.render() 的方法来将其渲染到页面上;
ReactDom.render()
接受两个参数,第一个是要被插入的内容(JSX),第二个是插入到DOM或者说index.html
的位置
React负责逻辑控制,数据->VDOM,是react的核心库
ReactDom渲染实际Dom,VDOM->DOM
React节点:专门用于渲染到UI界面的对象,React会通过React元素,创建React节点,ReactDOM一定是通过React节点来进行渲染的
节点类型:
1、React DOM节点:创建该节点的React元素类型是一个字符串
javascript
import React from 'react';
import ReactDOM from 'react-dom';
const app = <div><h1>标题</h1></div>
ReactDOM.render(app, document.getElementById('root'));
2、React 组件节点:创建该节点的React元素类型是一个函数或是一个类
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App"
ReactDOM.render(<App />, document.getElementById('root'));
3、 React 文本节点:由字符串、数字创建的
javascript
import React from 'react';
import ReactDOM from 'react-dom';
const app = 'Hello World'
ReactDOM.render(app, document.getElementById('root'));
React可以使用jsx来描述ui
javascript
<script type="text/babel">
//script上有type="text/babel"属性, 将意味babel将接管所有的代码解析
const reactDivElement = (
<div>
hello jsx
<span>i am child span</span>
</div>
)
const root1 = ReactDOM.createRoot(document.getElementById("root1"))
root1.render(reactDivElement)
</script>
babel解析JSX原理
babel会监听全局的document.contentLoad,具体流程如下:
- 当前页面的所有的script标签全部生成完毕,babel会通过document.getElementByTagName,拿到所有的script标签
- 分别通过getAttributes函数,读取script上面的属性
- 如果type="text/babel",就把里面的代码全部拿过来,通过tansform方法转换一遍,通过新建一个script标签,将转换后的代码插入script中,将script插入到head标签
- 如果不写type,type默认位text/Javascript,此时以Javascript的形式解析
- 如果以上两种都不是,浏览器不会看script里面的代码
babel-loader把jsx编译成相应的js对象.
React.createElement再把这个JS对象构造成React需要的虚拟dom
2 JSX
JSX是一种javascript的语法扩展,其语言比较像模板语言,但实际上完全是javaScript内部实现的,jsx可以很好的描述UI,能够有效地提高开发效率。
JSX
:看起来像HTML的 Javascript
一种拓展语法,能够让你的 Javascript
中和正常描述 HTML一样编写 HTML。
使用 JSX 语法可读性更强,书写更方便
JSX语法中,使用 {} 代替 ""
在 JSX 中通过 { js语法 } 大括号使用 JavaScript
Babel 会把 JSX 在构建时调用 React.createElement() 函数创建VDOM。以下两个写法完全等价。
createElement():
createElement(标签名, { 属性: 属性名 }, 内容)
javascript
// 看下面的DOM结构
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 上面这个 HTML 所有的信息我们都可以用 JavaScript 对象来生成:
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
javascript
/*
jsx语法规则:
1.定义虚拟DOM时,不能写引号
2.标签中混入JS表达式时要用 {}
3.样式的类名指定不要用 class,要用 className
4.内联样式,要用 style={{ key:value,color: '#999', fontSize: '30px' }} 的形式去写,大驼峰
5.只有一个根标签
6.标签必须闭合
7.标签首字母
1)若小写字母开头,则将该标签为html中同名元素,若html中无该标签对应的同名元素,则报错
2)若大写字母开头,react就去渲染对应的组件,若组件未定义,则报错。
8.注释: {/* <input type="text" ref={c => this.input1 = c} /> */}
9.自定义属性,不需要驼峰 data- 前缀 <div data-props="自定义属性"></div>
10.JSX中默认对数组进行 join() 操作
React中使用:
1.只能返回一个根元素。如果你不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替
这个空标签被称作 Fragment。React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。
需要加key,就需要使用 Fragment
import { Fragment } from 'react'
<Fragment key={ }> ... </Fragment>
*/
1 基本使用
JSX变量渲染:
javascript
<!-- 获取msg变量进行渲染-->
<div>{msg}</div>
<!-- 函数组件获取父组件传递的数据进行渲染-->
<div>{props.msg}</div>
<!-- 类组件获取父组件传递的数据进行渲染-->
<div>{this.props.msg}</div>
<!-- 类组件获取当前组件内的state中的数据进行渲染,函数组件没有state-->
<div>{this.state.msg}</div>
javascript
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
//基本使用
const name = "react"
const jsx = <div>hello,{name}</div>
ReactDOM.render(
jsx,
document.getElementById('root')
);
2 函数(有返回值)
函数也是合法的表达式
javascript
import React from 'react';
import ReactDOM from 'react-dom';
//函数
const user = {
firstName:"Harry",
lastName:"Potter"
}
function formatName(obj){
return obj.firstName+" "+ obj.lastName
}
const jsx = <div>{formatName(user)}</div>
ReactDOM.render(
jsx,
document.getElementById('root')
);
3 对象
jsx也是js对象,也是合法表达式
javascript
import React from 'react';
import ReactDOM from 'react-dom';
const greet = <div>good</div>
const jsx = <div>{greet}</div>
ReactDOM.render(
jsx,
document.getElementById('root')
);
4 条件语句
javascript
import React from 'react';
import ReactDOM from 'react-dom';
//条件语句
const show = true;//false;
const greet = <div>good</div>;
const jsx = (
<div>
{
show ? greet:"登录"
}
{show && greet}
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
5 数组/列表渲染(选用有返回值的API)
JSX中默认对数组进行 join() 操作
数组作为一组子元素对待,数组中存放一组jsx用于显示列表数据
javascript
import React from 'react';
import ReactDOM from 'react-dom';
//数组
const list = [0,1,2]
//方式二
const listItems = list.map((item,index)=> <li key={index}>{item}</li>);
const jsx = (
<div>
<ul>
{
//diff时候,首先比较type,然后是key,所以同级同类型元素,key值必须唯一
list .map(item=>(
<li key={item}>{item}</li>
))
}
{listItems }
</ul>
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
6 属性的使用
属性:静态值用双引号,动态值用花括号,class、for要特殊处理
注意:class是保留字,如果要增加class,需要使用ClassName
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import logo from './logo.svg'
// src 动态属性
// style 行内样式
// className 外部样式表css类
const jsx = (
<div>
<img src={logo} style={{width:100}} className="img"/>
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
7 模块化
css模块化,创建index.css
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import logo from './logo.svg'
import './index.css'
const jsx = (
<div className = {styles.app}>
<img
src = {logo}
className = "logo"
style={{width:"50px",height:"30px"}}
alt="这个一个图片"
/>
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
或者npm install sass -D
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import logo from './logo.svg'
import styles from './index.module.scss'
const jsx = (
<div className = {styles.app}>
<img
src = {logo}
className={styles.logo}
style={{width:"50px",height:"30px"}}
alt="这个一个图片"
/>
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
3 基础组件
组件,从概念上类似javaScript函数,它可以接受任何形式的入参(props),并返回用于描述页面展示内容的React元素,组件有两种形式:类组件(class组件)和函数组件(function 组件)
1 class组件(有状态)
class组件(有状态组件)通常拥有状态和生命周期。(18以后很少使用,比较推崇无状态组件)
javascript
import React,{Component} from "react"
//React.createRef()创建一个ref对象,并将其分配给组件的某个实例属性。然后,你可以在组件的渲染方法中通过将ref属性添加到你想要获取DOM的元素上来引用DOM元素。
export default class ClassComponent extends Component{
constructor(props){
super(props); // 调用父类构造函数
// 创建一个ref对象
this.myDivRef = React.createRef();
//使用state属性维护状态,在构造函数中初始化状态
this.state = {
date:new Date(),
data: null, // 初始化数据为null
isLoading: false, // 初始化加载状态为false
error: null, // 初始化错误信息为null
}
}
fetchData = () => {
this.setState({ isLoading: true, error: null });
axios.get('https://api.example.com/data')
.then(response => {
this.setState({ data: response.data, isLoading: false });
})
.catch(error => {
this.setState({ error: error.message, isLoading: false });
});
};
componentDidMount(){
// 获取异步请求
this.fetchData();
//组件挂载之后启动定时器每秒更新状态
this.timerID = setInterval(()=>{
//使用setState方法更新状态
this.setState({
date:new Date()
})
},1000)
}
componentWillUnmount(){
//组件卸载前停止定时器
clearInterval(this.timerID)
}
// 组件更新
componentDidUpdate(){
console.log("compoentDidUpdate");
}
render(){
// 业务逻辑
// 接收父组件传递过来的参数
const someProp={someValue}
const { data, isLoading, error } = this.state;
return (
// 通过将ref属性添加到DOM元素上,将其与创建的ref对象关联
<div ref={this.myDivRef}>
{this.state.date.toLocaleTimeString()}
</div>
)
}
}
javascript
// 上述代码可简写为
import React,{Component} from "react"
/**
* Class 组件
*
* Class 组件是 ES6 语法,所以必须使用 class 关键字来定义
* Class 组件必须继承 Component 组件
* Class 组件必须实现 render 方法
* Class 组件必须使用 render 方法返回一个 React 元素
* Class 组件必须使用 export default 关键字导出
* Class 组件必须使用export 关键字导出
*/
export default class ClassComponent extends Component{
// 状态
state = {
date:new Date()
}
componentDidMount(){
//组件挂载之后启动定时器每秒更新状态
this.timerID = setInterval(()=>{
//使用setState方法更新状态
this.setState({
date:new Date()
})
},1000)
}
componentWillUnmount(){
//组件卸载前停止定时器
clearInterval(this.timerID)
}
componentDidUpdate(){
console.log("compoentDidUpdate");
}
render(){
// 接收父组件传递过来的参数
const someProp={someValue}
return (
<div>
{this.state.date.toLocaleTimeString()}
</div>
)
}
}
// 父组件,也可传递事件
<ChildComponent someProp={someValue} />
1.1 组件的组合、嵌套
将一个组件渲染到某一个节点里的时候,会将这个节点里原有内容覆盖
组件嵌套的方式就是将子组件写入到父组件的模板中去,且react没有Vue中的内容分发机制(slot),所以我们在一个组件的模板中只能看到父子关系
javascript
// 从 react 的包当中引入了 React 和 React.js 的组件父类 Component
// 还引入了一个React.js里的一种特殊的组件 Fragment
import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'
class Title extends Component {
render () {
return (
<h1>欢迎进入React的世界</h1>
)
}
}
class Content extends Component {
render () {
return (
<p>React.js是一个构建UI的库</p>
)
}
}
/** 由于每个React组件只能有一个根节点,所以要渲染多个组件的时候,需要在最外层包一个容器,如果使用div, 会生成多余的一层dom
class App extends Component {
render () {
return (
<div>
<Title />
<Content />
</div>
)
}
}
**/
// 如果不想生成多余的一层dom可以使用React提供的Fragment组件在最外层进行包裹
class App extends Component {
render () {
// 如果你的标签和 return 关键字不在同一行,则必须把它包裹在一对括号中
/*
只能返回一个根元素。如果你不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替
这个空标签被称作 Fragment。React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。
需要加key,就需要使用 Fragment
import { Fragment } from 'react'
<Fragment key={ }> ... </Fragment>
*/
return (
<Fragment>
<Title />
<Content />
</Fragment>
)
}
}
ReactDOM.render(
<App/>,
document.getElementById('root')
)
2 function组件(无状态)
函数组件(function组件)通常无状态和生命周期(react16.8开始引入hooks,函数组件也能拥有状态和相应的生命周期)
hook 允许开发者在不写类组件的情况下,生成状态(state以及一些其他曾经是类组件专属的东西
javascript
import React from "react";
// 由于元素没有办法传递参数,所以我们就需要把之前定义的变量改为一个方法,让这个方法去return一个元素
const ComponentA = (props) => { // 表达式写法
// 组件业务逻辑...
return (
// 特别注意这里的写法,如果要在JSX里写js表达式(只能是表达式,不能流程控制),就需要加 {},包括注释也是一样,并且可以多层嵌套
<div>组件</div>
);
};
export default ComponentA;
这样一个完整的函数式组件就定义好了。但要注意!注意!注意! 组件名必须大写,否则报错。
运行结果和之前完全一样,因为JS里没有真正的class,这个class只是一个语法糖, 但二者的运行机制底层运行机制不一样。
-
函数式组件是直接调用, 在前面的代码里已经有看到
-
es6 class
组件其实就是一个构造器,每次使用组件都相当于在实例化组件
2.1 useState 响应式数据
Hooks ------以
use
开头的函数------只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 "use" React 特性,类似于在文件顶部"导入"模块。Hooks其实就是一堆功能函数,一个组件想要实现哪些功能就可以引入对应的钩子函数,像插件一样非常的方便。
Hooks 分为:内置 Hooks ,自定义 Hooks ,第三方 Hooks
useState 通过在函数组件里调用它来满足给组件添加一些内部state(状态),调用useState会返回一个数组:当前状态和修改/更新状态的函数(异步),调用修改状态的函数来修改状态并触发视图的更新。
React 会等到事件处理函数中的所有代码都运行完毕再处理你的 state 更新。队列都执行完毕后,再进行 UI 更新,这种特性就是自动批处理
useState会返回一个数组,里面有两个成员:
- 以初始化为值的变量 。在调用的时候可以传递函数,也可以传递具体的值。如果传递的是函数,则会直接执行这个函数,将返回值作为初始化的状态。
注意: 虽然在初始化的时候允许传递函数(纯函数),我们也尽量不要传递函数,因为初始化只会执行一次。 - 修改该变量的异步函数 。这个函数的调用会导致函数组件的重新渲染。调用该函数的时候,可以直接传递一个值,也可以传递一个函数。如果你传递的是一个函数,则react会将上一次的状态传递给你帮助你,进行计算。(如果你传递的是一个函数,react会将这个函数放到一个队列里面等待执行,那如果我们想每次都时时拿到上一次的值,我们得写成一个函数 。此时状态得更新是批量进行的。
const [state, setState] = useState(initialValue);
- state: 用来存储状态的值;
- setState:修改状态的异步函数;
- initialValue:函数式组件第一次渲染时state的初始值。
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
javascript
import { useState } from "react";
// 只初始化执行一次,多用于复杂计算
function computed(n) {
return n + 1 + 2 + 3;
}
const Demo = () => {
const [num, setNum] = useState(0); // 可以有记忆功能
const handle = () => {
// num++; 视图不会改变,原因:组件的return没有重新渲染
setNum(num + 1); // 可以重新触发函数组件的执行;当修改状态的值没有发生改变的时候,函数组件并不会重新渲染
};
console.log(num) // 每次重新执行函数的num值都是不一样的,所以return的内容就会不一样
const [count, setCount] = useState(() => computed(0)); // 惰性初始化,简单性能优化
// 利用状态产生计算变量-类似Vue中的计算属性
const doubleCount = count * 2;
const handleCount = () => {
console.log(123); // 每次触发只会执行一次
setCount(count + 1);
};
/*
什么是受控组件与非受控组件
通过props 控制的组件称为受控组件;而通过 state 控制的组件称为非受控组件
React表单内置了受控组件的行为
1.value + onChange
2.checked + onChange
*/
// 受控组件
const [value,setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<>
<div>{num}</div>
<button onClick={handle}>新增</button>
<div>
<button onClick={handleCount}>惰性初始化{count}</button>
<p>{doubleCount}</p>
</div>
<div>
<input type="text" value={value} onChange={handleChange} />
</div>
</>
);
};
export default Demo;
简单分析Demo初始化到点击按钮修改num值后的重新渲染过程
- 进入页面会自动进行第一次渲染组件
- 首先,进入页面会自动进行第一次渲染组件,Demo函数执行,在Demo函数自身会产生一个私有上下文(这里我们假设名字为 Demo1),在它的内部私有变量如下:1. num = 0;2. setNum 修改状态的函数;3. handle 普通函数;
- 开始编译JSX视图,创建出virtualDOM(虚拟DOM),最后渲染为真实的DOM;
- 点击新增按钮,执行handle方法
- 执行handle方法,自身产生一个私有的上下文(它的上级上下文就是我们第一步中提到的Demo1),开始执行setNum(num + 1),setNum 和 num并不是自身的私有变量,则会去它的上级上下文也就是Demo1中找,即setNum 和 num访问的则是Demo1中的变量,执行完毕后,修改状态num的值,控制视图更新;
- 组件重新渲染
- num的值通过setNum更改后,触发函数的重新执行,这时和第一步一样,会自身产生一个私有的上下文(假设名为Demo2),在它的内部私有变量如下:1. num = 1(这里React内部处理,useState第二次及以后的执行,获取的状态值为新修改的状态值); 2. setNum 修改状态的函数(和第一步中的setNum并不是同一个,是一个新的引用);3. handle 普通函数(和第一步中的handle并不是同一个,是一个新的引用);
- 开始编译JSX视图,创建出virtualDOM(虚拟DOM),经过DOM-DIFF(diff算法进行虚拟DOM比较),最后渲染为真实的DOM。
函数组件的每一次渲染(或者是更新),都是把函数重新执行,产生一个全新的"私有上下文"!
- 内部的代码也需要重新执行
- 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行Demo函数产生的闭包}
- 每一次执行Demo函数,也会把useState重新执行,但是:
执行useState,只有第一次,设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值而不是初始值- 返回的修改状态的方法,每一次都是返回一个新的
useState异步更新
先来看一个例子:
javascript
import { useState } from "react";
const Demo = () => {
console.log('RENDER渲染');
const [x, setX] = useState(10);
const [y, setY] = useState(20);
const [z, setZ] = useState(30);
const handle = () => {
setX(x+ 1);
setY(y+ 1);
setZ(z+ 1);
};
return (
<>
<button onClick={handle}>新增</button>
</>
);
};
export default Demo;
在点击按钮后,'RENDER渲染'会输出几次?
答案是:1次。
执行handle函数时,会将所有的关于修改状态的函数放入更新队列最后,最后一起重新渲染视图。
useState自带性能优化机制
useState自带了性能优化的机制:
- 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较(基于Object.is做比较);
- 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新。
useState 函数状态的第二种写法
const [state, setState] = useState((prev) => prev + 1)
prev:存储上一次的状态值
return prev + 1:返回要修改为的状态值。
要严格遵守react状态不可变,只能通过修改状态函数进行修改
状态的重置处理问题:
- 当组件被销毁时,所对应的状态也会被重置
- 当组件位置没有发生改变时,状态是会被保留的
重置状态:1. 不同的结构体 2. 给组件添加 key 属性
React数据的操作推荐使用不可变状态:
- immer.js
- immutable.js
2.2 useEffect 处理副作用
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行(即:JSX渲染后触发)。默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有某些值改变的时候才执行
useEffect:处理副作用
副作用:完全不依赖React功能的外部操作【这些外部操作不经过react的手,但是却对react产生了一些影响】
例如:
1.http请求
2.dom操作(dom已经渲染完毕)
3.异步请求多数都是会产生副作用的
虽然我们不是所有的副作用操作都是在useEffect里进行,但是官方建议我们尽可以将副作用放在useEffect中运行。 因为副作用操作是会产生意外之外的结果,如果我们想要更好的追踪副作用的执行时机,就可以将副作用都归纳进useEffect里方便追踪
javascript
useEffect(effect,dependency)
effect:初始化的意思,是一个函数编写副作用
dependency : 这是一个可选参数,表示依赖项数组。如果省略,effect 将在每次渲染后运行。如果提供一个空数组,effect 将只在组件挂载和卸载时运行。如果数组包含变量,则每当这些变量改变时,effect 将重新运行。(初始时所有的useEffect都会执行一次)
useEffect的执行时机
- 当我们使用useEffect去注册effect以后,React会在该组件每次挂载完毕(渲染完毕)到页面时都会执行对应的effect函数,但是是异步执行的effect
- 当依赖项发生变更时,useEffect会重新执行对应的effect
**effect 函数会有一个返回值,这个返回值称为清理函数,清理函数会在组件卸载的时候执行 **
实际场景
1.http请求
2.访问真实dom
副作用的清除
1.dom事件的绑定清除
2.计时器绑定清除
下面是useState、useEffect实例:
javascript
import React,{useState,useEffect} from 'react'
export function FunctionComponent(props){
const [count,setCount] = useState(0)
const [date,setDate] = useState(new Date());
// 初始时,所有的 useEffect 都会触发
// 更新时,只有对应依赖项发生改变的才会触发(包括: props state ,计算变量等)
// 内部是通过 Object.is() 来判断是否改变
useEffect(()=>{
// 尽量在 useEffect 内部定义函数
document.title = `You clicked ${count} times`
},[count])
// 当空数组的时候,只有初始会触发,更新的时候是不会触发的
useEffect(()=>{
// 在组件挂载后运行的代码
const timer = setInterval(()=>{
setDate(new Date());
},1000)
// 清除effect
// 组件卸载时,需要清除effect创建的诸如订阅或者计时器等资源,要实现这一点,需要返回一个清楚函数,防止内存泄露,清除函数会在组件卸载前执行。
return () => clearInterval(timer);
},[]) // 空依赖数组意味着这个effect只会在组件挂载时执行一次
return(
<div>
<h3>FunctionComponent</h3>
<p>{date.toLocaleTimeString()}</p>
</div>
)
}
setState只有在React合成事件和生命周期函数 中是异步的,在原生事件和
setTimeout都是同步的,这里的异步其实是批量更新。
useEffect Hook可以看做componentDidMount、componentDidUpdate和componentwillUnmount这是三个组合
常见用途:
1.数据获取
javascript
useEffect(() => {
fetch('/data')
.then(response => response.json())
.then(data => setData(data));
}, []); // 组件挂载
2.响应更新
javascript
useEffect(() => {
console.log('props or state changed:', prop);
}, [prop]); // prop 初始化和数据更新执行
3.清理操作
javascript
useEffect(() => {
const subscription = someObservable.subscribe(msg => console.log(msg));
return () => {
subscription.unsubscribe();
};
}, []);
4.性能优化:通过限制 effect 的执行次数来减少不必要的重渲染。
注意事项:
- 避免在 effect 函数内部直接访问组件状态或 props,除非它们被添加到依赖数组中。
- 当依赖数组变化时,确保 effect 内部的逻辑能够正确处理这种变化,避免不必要的副作用。
- 使用
useEffect
来代替类组件中的componentDidMount
,componentDidUpdate
, 和componentWillUnmount
生命周期方法。
2.3 useContext 跨组件通信
上面介绍了 useState、useEffect 这两个最基本的 API,接下来介绍的 useContext 是 React 帮你封装好的,用来处理多层级传递数据的方式,在以前组件之间,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,我们还可以使用 React Context API 来帮我们做这件事,举个简单的例子:
javascript
// createContext 参数是默认值
// Provider 提供数据组件
const { Provider, Consumer } = React.createContext(null); // 创建一个上下文对象
// 声明式写法
function Bar() {
return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<Provider value={"grey"}>
<Foo />
</Provider>
);
}
通过 React createContext 的语法,在 APP 组件中可以跨过 Foo 组件给 Bar 传递数据。而在 React Hooks 中,我们可以使用 useContext 进行改造。
javascript
// createContext 参数是默认值
const colorContext = React.createContext("gray");
function Bar() {
// 接收数据
const color = useContext(colorContext);
return <div>{color}</div>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<colorContext.Provider value={"red"}>
<Foo />
</colorContext.Provider>
);
}
传递给 useContext 的是 context 而不是 consumer,返回值即是想要透传的数据了。用法很简单,使用 useContext 可以解决 Consumer 多状态嵌套的问题。
javascript
function HeaderBar() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
}
}
</CurrentUser.Consumer>
);
}
而使用 useContext 则变得十分简洁,可读性更强且不会增加组件树深度。
javascript
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);
return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
2.4 useReducer 统一的状态管理集合
useReducer 这个 Hooks 在使用上几乎跟 Redux/React-Redux 一模一样,唯一缺少的就是无法使用 redux 提供的中间件。
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种
情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
javascript
import React, { useReducer } from "react";
// import { useImmerReducer } from "use-immer";
const initialState = {
count: 0
};
// 由reducer函数完成外部逻辑的统一处理
function reducer(state, action) {
switch (action.type) {
case "increment":
// return 改变状态
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - action.payload };
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
<button onClick={() => dispatch({ type: "decrement", payload: 5 })}>
-
</button>
</>
);
}
用法跟 Redux 基本上是一致的,用法也很简单,算是提供一个 mini 的 Redux 版本。
2.5 memo 在 props 不变情况下跳过重新渲染
memo 允许您在 props 不变时跳过重新渲染组件,可以达到一些性能优化
javascript
import React, { useState, memo } from "react";
import { useImmer } from "use-immer";
// memo 允许您在 props 不变时跳过重新渲染组件,可以达到一些性能优化
const Head = memo(({ count }) => {
return <div>Hello Head,{Math.random()}</div>;
});
function Comp05() {
const [count, setCount] = useState(0);
const handleCount = () => {
setCount(count + 1);
};
return (
<div>
Comp05
<button onClick={handleCount}>点击</button>
<Head count={count}></Head>
</div>
);
}
export default Comp05;
4 表单
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:
javascript
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>
此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。如果你在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用"受控组件"。
1 受控组件
在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为"唯一数据源"。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做"受控组件"。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
javascript
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
valueTextarea: '请撰写一篇关于你喜欢的 DOM 元素的文章.',
valueSel:'coconut',
options: [
{ value: "grapefruit", label: "葡萄柚" },
{ value: "lime", label: "酸橙" },
{ value: "coconut", label: "椰子" },
{ value: "mango", label: "芒果" }
]
};
this.handleChange = this.handleChange.bind(this);
this.handleChangeTextarea = this.handleChangeTextarea.bind(this);
this.handleChangeSel = this.handleChangeSel.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleChangeTextarea(event) {
this.setState({valueTextarea: event.target.value});
}
handleChangeSel(event) {
this.setState({valueSel: event.target.value});
}
// 处理多个输入:当需要处理多个 `input` 元素时,我们可以给每个元素添加 `name` 属性,并让处理函数根据 `event.target.name` 的值选择要执行的操作。
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input name="value" type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<label>
文章:
<textarea value={this.state.valueTextarea} onChange={this.handleChangeTextarea} />
</label>
<label>
选择你喜欢的风味:
<select value={this.state.valueSel} onChange={this.handleChangeSel}>
{this.state.options.map((item,index) => {
return <option value={item.value} key={index}>{item.label}</option>;
})}
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。
注意
你可以将数组传递到
value
属性中,以支持在select
标签中选择多个选项:<select multiple={true} value={['B', 'C']}>
2 非受控组件
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。
例如,下面的代码使用非受控组件接受一个表单的值:
javascript
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
(1) 默认值
在 React 渲染生命周期时,表单元素上的 value
将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue
属性,而不是 value
。
javascript
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
同样,<input type="checkbox">
和 <input type="radio">
支持 defaultChecked
,<select>
和 <textarea>
支持 defaultValue
。
(2) 文件输入
在 HTML 中,<input type="file">
可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。因为它的 value 只读,所以它是 React 中的一个非受控组件。
在 React 中,<input type="file">
始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。
您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。
javascript
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>
);
}
}
ReactDOM.render(
<FileInput />,
document.getElementById('root')
);
5 生命周期
1 生命周期方法
生命周期方法,用于在组件不同阶段执行自定义功能 。在组件被创建并插入到DOM时(挂载中阶段)组件更新时,组件取消挂载或从DOM中删除时,都可以使用的生命周期方法
ReactV16.3之前的生命周期
ReactV16.4之后的生命周期
v17可能会废弃的三个生命周期函数用getDerivedStateFormProps替代,目前使用的话加上UNSAFE_:
javascript
componentWillMount
componentWillReceiveProps
componentWillUpdate
2 两个新的生命周期函数
javascript
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps会在render方法之前调用,并在初始化挂载及后续更新时都会被调用。它应返回一个对象来更新state,如果返回的为null,不更新任何内容
注意:不管什么原因,每次渲染前都会触发这个方法,相对于UNSAFE_componentWillReceiveProps而言,后者仅在父组件重新渲染时触发,而不在内部调用setState时触发
javascript
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate在render之后,在componentDidUpdate之前触发,会在最近一次渲染输出(提交到DOM节点)之前调用。它使得组件能够在发生更改之前从DOM中捕获一些信息。此生命周期的任何返回值都将作为参数传递给componentDidUpdate(prevProps,prevState,snapshot)
如果不想手动给将要废弃的生命周期添加UNSAFE_前缀,可以使用下面的命令
javascript
npx react-codemd react-unsafe-lifecycles
代码学习:
javascript
import React ,{Component,} from "react"
import PropTypes from 'prop-types'
export default class LifeCyclePage extends Component{
static defaultProps = {
msg:"omg"
}
static propTypes = {
msg:PropTypes.string.isRequired
}
constructor(props){
super(props)
this.state = {
count:0
}
console.log("constructor",this.state.count)
}
static getDerivedStateForProps(props,state){
const {count} = state
console.log("getDerivedStaticFormProps",count)
return count<5 ?null :{count:0}
}
getSnapshotBeforeUpdate(prevProps,prevState,snapshot){
const {count} = prevState
console.log("getSnapshotBeforeUpdate",count)
return null
}
componentDidMount(){
console.log("componentDidMount",this.state.count)
}
componentWillUnmount(){
console.log("componentWillUnmount",this.state.count)
}
componentDidUpdate(){
console.log("componentDidUpdate",this.state.count)
}
shouldComponentUpdate(nextProps,nextState){
const {count} = nextState;
console.log("shouldComponentUpdate",count,nextState.count)
return count !==3
}
setCount = ()=>{
this.setState({
count:this.state.count+1
})
}
render(){
const {count} = this.state;
console.log("render",this.state);
return (
<div>
<h1>LifeCyclePage</h1>
<p>{count}</p>
<button onClick={this.setCount}>改变count</button>
<Child count = {count}/>
</div>
)
}
}
class Child extends Component{
UNSAFE_componentWillReceiveProps(nextProps){
//在已挂载的组件接收新的 props 之前被调⽤
console.log("componentWillReceiveProps")
}
componentWillUnmount(){
//组件卸载之前
console.log("componentWillUnmount")
}
render(){
return(
<div
style={{border:"1px solid black",margin:"10px",padding:"10px" }}
>
我是child组件
<div>child count:{this.props.count}</div>
</div>
)
}
}
6 组件
注意:组件的首字母一定要大写!
1 函数组件
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 "props"(代表属性)对象与并返回一个 React 元素。这类组件被称为"函数组件",因为它本质上就是 JavaScript 函数。
相对比类组件,函数组件有以下特点:
- 没有组件实例
- 没有生命周期
- 没有 state 和 setState,只能接收 props
- 函数组件是一个纯函数,执行完即销毁,无法存储 state
javascript
/*
组件必须是一个纯函数
1.只负责自己的任务,它不会更改在该函数调用前就已存在的对象或变量
2.输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果
*/
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ReactDOM.render(<Welcome name="tom" />, document.getElementById('app'));
2 类组件
通过类来定义一个组件。类组件有以下特点:
- 有组件实例
- 有生命周期
- 有 state 和 setState({}),使用this.setState({name:'tom'})可以来修改state中的数据
javascript
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'briup'
};
render() {
return <h1>Hello, {this.props.name}, {this.state.name}</h1>;
}
}
ReactDOM.render(<Welcome name = "tom" />, document.getElementById('app'));
3 组件嵌套
在一个组件内使用另一个组件就形成组件嵌套,就有了父子组件的概念。
javascript
// 表格组件
class MyTable extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<table>
<tbody>
<tr>
<MyTd />
</tr>
</tbody>
</table>
);
}
}
// 表格的td组件
class MyTd extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<React.Fragment>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
</React.Fragment>
);
}
}
ReactDOM.render(<MyTable />, document.getElementById('app'));
React.Fragment作用:可以将内部内容当做一个整体,而不产生其他标签。
4 父子组件通信
父与子组件测试
javascript
const Parent = (props) => {
let myHandler = (message) => {
console.log('从子组件中接受的数据:', message);
}
return <Child name="我是父组件的数据" myA={myHandler}></Child>
}
const Child = ({ name, myA }) => {
// 调用myA方法执行,来将子组件内的数据传递给父组件的函数
myA('我是子组件的数据');
return <div>
父组件传递的数据:{name}
</div>
}
ReactDOM.render(<Parent />, document.getElementById('app'));
父给子:在父内使用子组件的时候,以属性的形式传递数据给子组件,子组件内使用props接收。
子给父:本质上是通过回调函数的方式来实现。在父内使用子组件的时候,给其传递函数,属性名称自定义。在子内部,通过props获取该属性,在子内部调用属性所对应的方法的执行,传递实参。
批量传参 {...} 和 通信限定数据类型 PropTypes
javascript
import React from "react";
// 子组件
// age = 20 默认值
function Comp01Child({ name, age = 20, sex, count = 123 }) {
return (
<div>
<h2>Comp01Child</h2>
<p>
个人信息{name}-{age}-{sex}
</p>
<p>数量{count}</p>
</div>
);
}
// React 添加默认值的方式,未来将不再使用
// Comp01Child.defaultProps = {
// count: 123,
// };
/*
通信限定数据类型 PropTypes
安装:npm install --save prop-types
*/
Comp01Child.propTypes = {
count: PropTypes.number.isRequired,
// 多类型
age: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// 具体值
sex: PropTypes.oneOf(["男", "女"]),
};
// 父组件
function Comp01() {
const obj = {
name: "muzidigbig",
age: 18,
sex: "男",
};
return (
<div>
<h1>Comp01</h1>
{/* 批量传参 */}
<Comp01Child count={456} {...obj} />
</div>
);
}
export default Comp01;
props的 children 属性,改变作用域指向
javascript
import React from "react";
function Comp02Sun({ count }) {
return (
<div>
<h2>Comp02Sun</h2>
{/* 123 */}
<p>数量{count}</p>
</div>
);
}
// 子组件
function Comp02Child({children}) {
const count = 456;
return (
<div>
<h2>Comp02Child</h2>
{/* props的 children 属性,将作用域指向父级(组件所在的作用域中) */}
{children}
</div>
);
}
// 父组件
function Comp02() {
const count = 123;
return (
<div>
<h1>Comp02</h1>
<Comp02Child>
<Comp02Sun count={count}></Comp02Sun>
</Comp02Child>
</div>
);
}
export default Comp02;
5 React类组件通信
在React类组件中,组件间通信可以通过以下方式实现:
-
父组件向子组件通信:通过props传递数据。
-
子组件向父组件通信:使用回调函数(callbacks)。
-
兄弟组件通信:使用共享的上下文(Context API)或状态管理库(如Redux)。
以下是使用React类组件实现兄弟组件通信的例子:
javascript
import React, { Component } from 'react';
import { render } from 'react-dom';
// 创建一个上下文对象
const MessageContext = React.createContext();
// 子组件
class Son extends Component {
render() {
return (
<MessageContext.Consumer>
{context => (
<button onClick={() => context.updateMessage('Hello from Son')}>
Update Message
</button>
)}
</MessageContext.Consumer>
);
}
}
// 兄弟组件
class Brother extends Component {
render() {
return (
<MessageContext.Consumer>
{context => (
<div>
<p>Message from Brother: {context.message}</p>
</div>
)}
</MessageContext.Consumer>
);
}
}
// 父组件
class Parent extends Component {
state = {
message: 'Initial message'
};
updateMessage = (newMessage) => {
this.setState({ message: newMessage });
};
render() {
return (
// 使用Provider提供共享的上下文
<MessageContext.Provider value={{ ...this.state, updateMessage: this.updateMessage }}>
<Son />
<Brother />
</MessageContext.Provider>
);
}
}
render(<Parent />, document.getElementById('root'));
7 绑定事件
采用on+事件名的方式来绑定一个事件,注意,这里和原生的事件是有区别的,原生的事件全是小写onclick
, React里的事件是驼峰onClick
,React的事件并不是原生事件,而是合成事件。
通过 React 元素处理事件跟在 DOM 元素上处理事件非常相似。但是有一些语法上的区别:
- React 事件使用驼峰命名,而不是全部小写
- 通过 JSX {}, 传递一个函数作为事件处理程序,而不是一个字符串
- 在React中不能通过返回
false
来阻止默认行为。必须明确的调用preventDefault
1 事件处理程序与事件绑定
在类组件内使用类内的方法来声明事件处理程序。
javascript
class MyCom extends React.Component {
// 事件处理程序
myHandler(event) {
console.log('事件处理程序');
console.log(this); //undefined,可将此函数换为箭头函数
event.preventDefault();
}
render() {
// 事件绑定
return <button onClick={this.myHandler}>按钮</button>
}
}
ReactDOM.render(<MyCom />, document.getElementById('app'));
事件处理程序内的this指向undefined ,如果想要指向该组件,有以下三个方案。
方案一:将事件处理程序声明为箭头函数
javascript
myHandler = (event) => {
console.log('事件处理程序');
console.log(this); //指向当前组件
}
方案二:在绑定的事件处理程序的时候使用箭头函数
javascript
render() {
return <button onClick={(e)=>{this.myHandler(e)}}>按钮</button>
}
方案三:需要在构造器中使用如下代码来修改函数内的this指向
javascript
constructor(props) {
super(props);
this.state = {};
// 修改事件处理程序内部this指向
this.myHandler = this.myHandler.bind(this);
}
方案四:在绑定的事件处理程序的时候指定this指向
javascript
render() {
return <button onClick={this.myHandler.bind(this)}>按钮</button>
}
javascript
class MyCom extends React.Component {
constructor(props) {
super(props);
// state状态
this.state = {
msg: 'hello',
name: 'briup',
};
// 修改事件处理程序内部this指向
this.myHandler = this.myHandler.bind(this);
}
myHandler(event) {
console.log('事件处理程序');
console.log(this); // 这里this指向组件实例本身
// 这里修改state中的数据
this.setState({
msg: "你好"
})
}
render() {
return <button onClick={this.myHandler}>{this.state.msg}, {this.state.name}
</button>
}
}
ReactDOM.render(<MyCom />, document.getElementById('app'));
2 事件传参
bind传参,最后一个参数是事件对象event。
javascript
// bind绑定事件并传参
onClick = { this.handle.bind(this, 1001, 1002) }
// 事件处理程序接收参数
handle = (a, b, e) => {
this
a 1001
b 1002
e event
}
handle(a, b, e){
this
a 1001
b 1002
e event
}
箭头函数传参,原则上没有固定顺序,建议与bind保持一致
javascript
// bind绑定事件并传参
onClick = {(e)=> { this.handle(1001, 1002, e) }}
// 事件处理程序接收参数
handle = (a, b, e) => {
this
a 1001
b 1002
e event
}
handle(a, b, e){
this
a 1001
b 1002
e event
}
3 函数式编程
如需添加一个事件处理函数,你需要先定义一个函数,然后将其作为props传入合适的 JSX 标签。例如,这里有一个没绑定任何事件的按钮:
- 在
Button
组件 内部 声明一个名为handleClick
的函数。 - 实现函数内部的逻辑(使用
alert
来显示消息)。 - 添加
onClick={handleClick}
到<button>
JSX 中。
javascript
export default function Button() {
function handleClick() {
alert('你点击了我!');
}
return (
<button onClick={handleClick}>
点我
</button>
);
}
// 方式二:
<button onClick={function handleClick() {
alert('你点击了我!');
}}>
// 方式三:
<button onClick={(e) => {
handleClick(e)
}}>
在事件处理函数中读取 props
由于事件处理函数声明于组件内部,因此它们可以直接访问组件的 props。示例中的按钮,当点击时会弹出带有 message
prop 的 alert:
javascript
function AlertButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div>
<AlertButton message="正在播放!">
播放电影
</AlertButton>
<AlertButton message="正在上传!">
上传图片
</AlertButton>
</div>
);
}
将事件处理函数作为 props 传递
通常,我们会在父组件中定义子组件的事件处理函数。比如:置于不同位置的 Button
组件,可能最终执行的功能也不同 ------ 也许是播放电影,也许是上传图片。
javascript
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function PlayButton({ movieName }) {
function handlePlayClick() {
alert(`正在播放 ${movieName}!`);
}
return (
<Button onClick={handlePlayClick}>
播放 "{movieName}"
</Button>
);
}
function UploadButton() {
return (
<Button onClick={() => alert('正在上传!')}>
上传图片
</Button>
);
}
export default function Toolbar() {
return (
<div>
<PlayButton movieName="魔女宅急便" />
<UploadButton />
</div>
);
}
命名事件处理函数 prop
内置组件(<button>
和 <div>
)仅支持浏览器事件名称,例如 onClick
。但是,当你构建自己的组件时,你可以按你个人喜好命名事件处理函数的 prop。
按照惯例,事件处理函数 props 应该以
on
开头,后跟一个大写字母。
javascript
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('正在播放!')}
onUploadImage={() => alert('正在上传!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<Button onClick={onPlayMovie}>
播放电影
</Button>
<Button onClick={onUploadImage}>
上传图片
</Button>
</div>
);
}
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
事件传播
事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e
,代表 "event"(事件)。你可以使用此对象来读取有关事件的信息。
这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件,你需要像下面 Button
组件那样调用 e.stopPropagation()
javascript
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}
阻止默认行为
可以调用事件对象中的 e.preventDefault()
来阻止
javascript
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('提交表单!');
}}>
<input />
<button>发送</button>
</form>
);
}
不要混淆 e.stopPropagation()
和 e.preventDefault()
。它们都很有用,但二者并不相关:
e.stopPropagation()
阻止触发绑定在外层标签上的事件处理函数。e.preventDefault()
阻止少数事件的默认浏览器行为。
8 自定义Hook与Hook使用规则
1 自定义Hook
有时候我们会想在组件之间重用一些状态逻辑,目前有两种主流方案来解决这个问题:高阶组件和render props 。自定义Hook可以让你在不增加组件的情况下达到同样的目的。
自定义Hook是一个函数,其名称"use"开头,函数内部可以调用其他的Hook
javascript
import React,{useState,useEffect,useDemo} from "react"
export default function CustomHookPage(props){
const [count,setCount] = useState(0);
useEffect(() => {
console.log("count effect");
document.title = `点击了${count}次`
}, [count])
return(
<div>
<h3>自定义Hook</h3>
<p>{count}</p>
<button onClick={()=>setCount(count+1)}>add</button>
<p>{useClock().toLocaleTimeString()}</p>
</div>
)
}
//自定义Hook
function useClock(){
const [date, setDate] = useState(new Date())
useEffect(() => {
console.log("date effect")
const timer = setInterval(()=>{
setDate(new Date())
},1000)
return () => clearInterval(timer);
}, [])
return date
}
2 Hook使用规则
Hook就是JavaScript函数,但是使用他们会有两个额外的规则:
- 只能在函数最外层调用Hook。不要在循环、条件判断或者子函数中调用。
- 只能在React的函数组件中调用Hook。不要在其他JavaScript函数中使用。
9 内置 Hook API
1 useDemo 对计算结果进行缓存
把"创建"函数和依赖项数组作为参数传入useDemo,它仅会在某个依赖项改变时才重新计算值。这种优化有助于避免在每次渲染时都进行高开销的计算。
javascript
import React,{useState,useMemo} from "react"
export default function UseMemoPage(props){
const [count,setCount] = useState(0);
// 计算结果缓存
const expensive = useMemo(()=>{
console.log("compute");
let sum = 0
for(let i=0;i<count;i++){
sum+=i
}
return sum
},[count])
const [value,setValue] = useState("")
return (
<div>
<h3>UseMemoPage</h3>
<p>expensive:{expensive}</p>
<button onClick = {()=>setCount(count+1)}>add</button>
<input value={value} onChange={event=>setValue(event.target.value)}/>
</div>
)
}
2 useCallback 对函数进行缓存
useCallback 是 useMemo 的一种特例写法而已。
javascript
const fn = (() => () => {console.log("123")},[count]);
把内联回调函数及其依赖项作为参数传入useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲(shouldComponentUpdate )的子组件时,它将非常有用。
javascript
import React,{useState,useCallback,PureComponent} from "react"
export default function useCallbackPage(props){
const [count,setCount] = useState(0);
// 函数缓存
const addClick = useCallback(() => {
let sum = 0;
for(let i=0;i<count;i++){
sum+=i
}
return sum
},[count])
const [value,setValue] = useState("")
return(
<div>
<h3>UseCallbackPage</h3>
<p>{count}</p>
<button onClick={()=>setCount(count+1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
<Child addClick={addClick} />
</div>
)
}
class Child extends PureComponent {
render(){
const {addClick} = this.props;
return (
<div>
<h3>Child</h3>
<button onClick={() => console.log(addClick())}>add</button>
</div>
)
}
}
3 useRef 保存引用值,DOM
useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:
javascript
import React, { useState, useRef } from "react";
function App() {
let [name, setName] = useState("Nate");
// 与DOM进行关联
let nameRef = useRef();
let myRef = useRef();
const submitButton = () => {
nameRef.current.value += "muzidigbig";
setName(nameRef.current.value);
// 更改样式
myRef.current.style.color = "red";
};
const [list, setList] = useState([
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
{ id: 3, name: "王五" },
]);
return (
<div className="App">
<p ref={myRef}>{name}</p>
<div>
<input ref={nameRef} type="text" />
<button type="button" onClick={submitButton}>
Submit
</button>
</div>
<ul>
{list.map((item) => (
// 在循环中操作ref 可以使用回调函数写法
<li key={item.id} ref={(myRef) => (myRef.style.color = "red")}>
{item.name}
</li>
))}
</ul>
</div>
);
}
useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。
当然 useRef 远比你想象中的功能更加强大,useRef 的功能有点像类属性,或者说您想要在组件中记录一些值,并且这些值在稍后可以更改。
利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
React Hooks 中存在 Capture Value 的特性:
javascript
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
alert("count: " + count);
}, 3000);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>增加 count</button>
<button onClick={() => setCount(count - 1)}>减少 count</button>
</div>
);
}
先点击增加button,后点击减少button,3秒后先alert 1,后alert 0,而不是alert两次0。这就是所谓的 capture value 的特性。而在类组件 中 3 秒后输出的就是修改后的值,因为这时候 message 是挂载在 this 变量上,它保留的是一个引用值,对 this 属性的访问都会获取到最新的值。讲到这里你应该就明白了,useRef 创建一个引用,就可以有效规避 React Hooks 中 Capture Value 特性。
javascript
function App() {
const count = useRef(0);
const showCount = () => {
alert("count: " + count.current);
};
const handleClick = number => {
count.current = count.current + number;
setTimeout(showCount, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={() => handleClick(1)}>增加 count</button>
<button onClick={() => handleClick(-1)}>减少 count</button>
</div>
);
}
只要将赋值与取值的对象变成 useRef,而不是 useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。
4 useImperativeHandle 透传 Ref
当组件添加ref 属性的时候,需要 forwardRef 进行转发(暴露), forwardRef 让您的组件通过 ref 向父组件公开DOM 节点。
useImperativeHandle 是 React 中的一个 Hook ,它能让你自定义由 ref 暴露出来的句柄。自己决定向外暴露什么
javascript
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(myCompRef, () => {
// 自定义暴露出一些方法、变量
return {
focus(){
inputRef.current.focus();
return 123
}
}
});
// useImperativeHandle 存在的时候可以不用写 ref
// <input type="text" name="child input" ref={ref} />;
return <input type="text" name="child input" ref={inputRef} />;
}
const ChildInput = forwardRef(ChildInputComponent);
function App() {
const ref = useRef(null);
useEffect(() => {
// ref.current.focus();
const result = ref.current.focus();
console.log('result',result)
// ref.current.style.background = "red";
}, []);
return (
<div>
<ChildInput ref={ref} />
</div>
);
}
通过这种方式,App 组件可以获得子组件的 input 的 DOM 节点。
5 useLayoutEffect 同步执行副作用
大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。
javascript
function App() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect");
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
useEffect(() => {
console.log("useEffect");
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
在上面的例子中,useLayoutEffect 会在 render,DOM 更新之后同步触发函数,会优于 useEffect 异步触发函数。
(1) useEffect和useLayoutEffect有什么区别?
- useEffect是在渲染被绘制到屏幕之后执行的,是异步的; useLayoutEffect 是在渲染之后但在屏幕更新之前,是同步的。
- 大部分情况下我们采用useEffect (),性能更好。但当你的 useEffect 里面的操作需要处理 DOM ,并且会改变页面的样式,就需要用 useLayoutEffect ,否则可能会出现闪屏问题。
- useLayoutEffect在useEffect之前触发。
官方建议优先使用useEffect
不过useLayoutEffect
在服务端渲染时会出现一个warning,要消除的话得用useEffect
代替或者推迟渲染时机。
6 startTransition 方法及并发模式
- React 18 之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断。
- React 18 引入并发模式,它允许你将标记更新作为一个 transitions ,这会告诉 React 它们可以被中断执行。这样可以把紧急的任务先更新,不紧急的任务后更新。
让我们来了解一下startTransition函数。startTransition函数可以用来告诉React,我们即将进行一次状态的变化,并且希望在这个过程中进行一些优化。通过使用startTransition函数,我们可以告诉React,这次状态的变化是一个次要的变化,不需要立即更新到用户界面上。这样一来,React就可以在适当的时机,选择最佳的时间点来进行状态的更新,以提供更好的用户体验。
Transition 是 react18 引入的新概念,用来区分紧急和非紧急的更新。
- 紧急的更新,指的是一些直接的用户交互,如输入、点击等;
- 非紧急的更新,指的是 UI 界面从一个样子过渡到另一个样子;
javascript
import React, { useState, startTransition } from "react";
function List({ query }) {
const items = [];
const word = "hello word";
if (query !== "" && word.includes(query)) {
const arr = word.split(query);
for (let index = 0; index < 1000; index++) {
items.push(
<li key={index}>
{arr[0]}
<span style={{ color: "red" }}>{query}</span>
{arr[arr.length - 1]}
</li>
);
}
} else {
for (let index = 0; index < 1000; index++) {
items.push(<li key={index}>{word}</li>);
}
}
return <ul>{items}</ul>;
}
function Comp06() {
// startTransition 不返回任何内容
const [search, setSearch] = useState("");
const [query, setQuery] = useState("");
const handleChange = (e) => {
// 1.紧急,数据太多不能看到一个一个的输入变化(后面的会影响前面的任务)
setSearch(e.target.value);
// 2.非紧急,不影响第一次任务的操作( UI 界面从一个样子过渡到另一个样子)
startTransition(() => {
setQuery(e.target.value);
});
};
return (
<div>
Comp06
<div>
<input type="text" value={search} onChange={handleChange} />
</div>
<List query={query} />
</div>
);
}
export default Comp06;
7 useTransition 与 useDeferredValue
useTransition 是一个让你在不阻塞 UI 的情况下来更新状态的 React Hook ,返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。
useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。
我们再来看看useTransition函数。useTransition函数可以帮助我们更好地管理组件状态的过渡效果。通过使用useTransition函数,我们可以告诉React,在状态变化的过程中,需要展示一个过渡效果给用户看。这样一来,用户就可以清晰地感知到组件状态的变化,并且可以更好地理解正在发生的事情。
javascript
import React, { useState, useTransition, useDeferredValue } from "react";
function List({ query }) {
const items = [];
const word = "hello word";
if (query !== "" && word.includes(query)) {
const arr = word.split(query);
for (let index = 0; index < 1000; index++) {
items.push(
<li key={index}>
{arr[0]}
<span style={{ color: "red" }}>{query}</span>
{arr[arr.length - 1]}
</li>
);
}
} else {
for (let index = 0; index < 1000; index++) {
items.push(<li key={index}>{word}</li>);
}
}
return <ul>{items}</ul>;
}
function Comp06() {
const [search, setSearch] = useState("");
const [query, setQuery] = useState("");
// useDeferredValue(state)将转态作为参数,得到对应search一样的值,只不过是一个延迟的副本
// const [query, setQuery] = useDeferredValue(search);
// 后面只留 setSearch(e.target.value); loading也不要
// [延迟状态(开始为false,变更后改为true),钩子]
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 1.紧急,数据太多不能看到一个一个的输入变化(后面的会影响前面的任务)
setSearch(e.target.value);
// 2.非紧急,不影响第一次任务的操作
startTransition(() => {
setQuery(e.target.value);
});
};
return (
<div>
Comp06
<div>
<input type="text" value={search} onChange={handleChange} />
</div>
{isPending&& <div>loading...</div>}
<List query={query} />
</div>
);
}
export default Comp06;
8 useId 生成唯一 ID 值
javascript
import React, { useId } from "react";
// 产生唯一id
const password = useId();
9 flushSync 获取立即更新后的DOM
类似于 Vue 中的 nextTick
javascript
import React, { useState, useRef } from "react";
// flushSync 让你强制 React 同步刷新提供的回调中的任何更新,这确保了 DOM 立即更新
// 类似于 Vue 中的 nextTick
import { flushSync } from "react-dom";
function Comp07() {
const [count, setCount] = useState(0);
const ref = useRef(null);
const handleClick = (e) => {
// setCount(count + 1);
// console.log(ref.current.innerHTML); // 拿到上一次值
//状态更新后拿到新数据
flushSync(() => {
setCount(count + 1);
});
console.log(ref.current.innerHTML); // 拿到最新的
};
return (
<div>
Comp07
<div>
<button onClick={(e) => handleClick(e)}>点击</button>
<p ref={ref}>{count}</p>
</div>
</div>
);
}
export default Comp07;
10 error boundary (错误边界)捕获渲染错误
默认情况下,如果您的应用程序在渲染期间抛出错误,React 将从屏幕上移除其 UI 。为防止这种情况,您可以将 UI 的一部分包装到错误边界中。错误边界是一种特殊组件,可让您显示一些后备 UI 而不是崩溃的部分,例如错误消息
第三方支持:
npm install react-error-boundary
javascript
import React, { useState, useRef } from "react";
import { classNames } from "classnames";
// 使用 error-boundary (错误边界)捕获渲染错误,不会影响界面渲染但控制台依然会报错
import { ErrorBoundary } from "react-error-boundary";
function Head(params) {
classNames(); // 错误的写法,为了演示 ErrorBoundary
return <div>My Head</div>;
}
function Comp07() {
return (
<div>
Comp07
<ErrorBoundary fallback={<div>出错了</div>}>
<Head></Head>
</ErrorBoundary>
</div>
);
}
export default Comp07;
11 createPortal 渲染到 DOM 的不同部分(将指定的内容传送到指定的位置)
javascript
import React, { useState, useRef } from "react";
// createPortal(传送的内容,传送的位置(选择器)); 将指定的内容传送到指定的位置
// createPortal 作用域还是当前的作用域
import { createPortal } from "react-dom";
function Comp07() {
const [isCP, setIsCP] = useState(false);
const handleCP = () => {
console.log("我是 portal");
setIsCP(!isCP);
};
return (
<div>
Comp07
<div>
<button onClick={handleCP}>点击显示弹窗内容</button>
{createPortal(<div>{isCP ? "我是 portal" : null}</div>, document.body)}
</div>
</div>
);
}
export default Comp07;
12 <Profiler>和 ReactDevTools 的性能测试
javascript
import React, { useState, useRef } from "react";
// 测试组件的性能
import { Profiler } from "react";
function Head(params) {
return <div>My Head</div>;
}
function Comp07() {
const onRender = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
// 对渲染时间进行汇总或记录... baseDuration - actualDuration = 最后执行的时间
console.log(id, phase, actualDuration, baseDuration, startTime, commitTime);
};
return (
<div>
Comp07
{/* id 保证唯一 */}
<Profiler id="head" onRender={onRender}>
<Head />
</Profiler>
</div>
);
}
export default Comp07;
10 react-router
React Router 是一个基于React 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
目前文档对应react-router版本:V6.X,V6.X 版本相对与V5.X做出很大改变。
react-router的6.0版本改变:
-
路由注册将<Switch />改成了<Routes />
-
路由引入将Component改成了element
-
新增多个hook新增组件....
路由相关插件:
react-router 插件是路由库的核心;
react-router-dom 插件是web程序的路由库,在react-router基础上添加了dom操作;
react-router-native 是app程序的路由库,在react-router基础上添加了本地操作;react-router-config 静态路由的配置
React Router基本原理:
React Router甚至大部分的前端路由都是依赖于history.js的,它是一个独立的第三方js库。可以用来兼容在不同浏览器、不同环境下对历史记录的管理,拥有统一的API。
-
老浏览器的history: 通过
hash
来存储在不同状态下的history
信息,对应createHashHistory
,通过检测location.hash
的值的变化,使用location.replace
方法来实现url跳转。通过注册监听window
对象上的hashChange
事件来监听路由的变化,实现历史记录的回退。 -
高版本浏览器: 利用HTML5里面的history,对应
createBrowserHistory
, 使用包括pushState
,replaceState
方法来进行跳转。通过注册监听window
对象上的popstate
事件来监听路由的变化,实现历史记录的回退。 -
node环境下: 在内存中进行历史记录的存储,对应
createMemoryHistory
。直接在内存里push
和pop
状态。
1 安装
javascript
npm install react-router-dom -S
// 指定版本
npm install react-router-dom@6.22.2 -S
2 基本使用
常用组件
注意:在跳转路由时,如果路径是/开头的则是绝对路由,否则为相对路由,即相对于"当前URL"进行改变。
react-router中奉行一切皆组件的思想,以下都是以组件形式存在:
BrowserRouter组件
路由的根组件。
Link组件
其中Link标签对A标签进行了封装,可以进行路由的跳转;Link组件只能在Router内部使用,因此使用到Link组件的组件一定要放在顶层的Router之内。
NavLink组件
- NavLink组件和Link组件的功能是一致的,区别在于可以判断其to属性是否是当前匹配到的路由
- NavLink组件的style或className可以接收一个函数,函数接收一个isActive参数,可根据该参数调整样式,带有激活状态
这里的Link或NavLink类似于Vue中的Router-link。
Routes组件
Routes功能上类似于Vue中的Router-view。将来匹配到的路由组件将会被加载到Routes所在的位置。
Routes组件内部存放Route组件。
Route组件
Route组件则是用来匹配路由对应的页面组件,在v5之前在使用Switch包裹的Route标签,在V6.X版本以后变成了Routes,并且Route标签中的component属性变成了element属性。
html
<Route path="/about" element={<About></About>}></Route>
path
:路径element
:要渲染的组件- 注意 :
BrowserRouter
组件最好放在最顶层所有组件之外,这样能确保内部组件使用 Link 做路由跳转时不出错
javascript
import React,{Component} from "react"
import {BrowserRouter as Router,Route,Link} from "react-router-dom"
export default class RouterPage extends Component{
render(){
return(
<div>
<h3>RouterPage</h3>
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
<Routes>
{/* 根据exact,实现精准匹配 */}
<Route
exact
path="/"
component ={HomePage}
// children ={()=><div>children</div>}
// render ={()=><div>render</div>}
/>
<Route path="/user" component = {UserPage}/>
</Routes>
</Router>
</div>
)
}
}
class HomePage extends Component{
render(){
return(
<div>
<h3>HomePage</h3>
</div>
)
}
}
class UserPage extends Component{
render(){
return(
<div>
<h3>UserPage</h3>
</div>
)
}
}
3 Route渲染内容的三种方式
Route渲染优先级:children>component>render
这三种方式互斥,你只能用其中一种
- children:func
有时候,不管location是否匹配,你都需要渲染一些内容,这个时候你可以用children,除了不管location是否匹配都会渲染之外,其他方法和render完全一样
- render:func
当你使用render时,你只是调用了个函数,只有当location匹配的时候渲染
- component:component
只有当location匹配的时候渲染
4 404页面
设定一个没有path的路由在路由列表最后面,表示一定匹配
javascript
import React,{Component} from "react"
import {BrowserRouter as Router,Route,Link,Switch} from "react-router-dom"
export default class RouterPage extends Component{
render(){
return(
<div>
<h3>RouterPage</h3>
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
{/* 根据exact,实现精准匹配 */}
<Switch>
<Route
exact
path="/"
component ={HomePage}
// children ={()=><div>children</div>}
// render ={()=><div>render</div>}
/>
<Route path="/user" component = {UserPage}/>
<Route component = {EmptyPage}/>
</Switch>
</Router>
</div>
)
}
}
class HomePage extends Component{
render(){
return(
<div>
<h3>HomePage</h3>
</div>
)
}
}
class UserPage extends Component{
render(){
return(
<div>
<h3>UserPage</h3>
</div>
)
}
}
class EmptyPage extends Component{
render(){
return(
<div>
<h3>EmptyPage-404</h3>
</div>
)
}
}
5 其他功能
路由种类
react提供了两种路由器,Browser、Hash,他们主要区别在于存储URL的⽅式以及与后端交互⽅式。
<BrowserRouter> 类似于history 模式,url格式看上去⾮常和谐,但是服务器需要进⾏配置 , 否则会与后端进⾏交互。
<HashRouter> 路径会保存在url后,通过#分割,这种⽅式不会与后端进⾏交互 。
可以将上方案例中的BrowserRouter组件更改为HashRouter组件对比查看效果。
重定向
React-route6.0版本已经移除了Redirect组件,实现重定向可以使用通配符配合Navigate组件的方式实现。
javascript
import { Navigate, Route } from "react-router-dom";
// ...
// 将/a路由重定向到/home路由
<Route path='/a' element={<Navigate to='/home'></Navigate>}></Route>
// 以下代码可实现其他未定义的路由路径重定向到/home路由
<Route path='*' element={<Navigate to='/home'></Navigate>}></Route>
路由高亮
路由高亮效果,activeClassName在6版本已经不再使用,如果需要实现高亮效果,将className写成函数的形式,函数接收一个参数,参数为一个对象名有isActive属性,该属性值为true,表示该路由被激活。
在App.css中编写两个类,分别代表激活的样式active-pages和未激活的样式pages
css
/* App.css */
a {
text-decoration: none;
}
.active-pages {
background-color: teal;
color: white;
}
.pages {
background-color: white;
color: teal;
}
在NavLink组价上使用className来进行高亮设置。
javascript
import { NavLink } from "react-router-dom";
import './App.css'
// ....
<NavLink className={(a)=>{return a.isActive ? 'acitve-pages': 'pages'}}
to="/home">Home</NavLink>
<br>
<NavLink className={(a)=>{return a.isActive ? 'acitve-pages': 'pages'}}
to="/about"
<Link> 最基本的路由
<NavLink> 带有激活状态的路由
6 嵌套路由
路由表文件
react-route6支持路由表配置了,新建一个文件夹routes,在新建一个路由表文件index.js,配置路由规则。
javascript
// routes/index.js
import Home from '../pages/Home';
import About from '../pages/About';
import PathQuery from '../pages/PathQuery';
import RouteParams from '../pages/RouteParams';
import { Navigate } from 'react-router-dom';
let routes = [
{
path: '/test',
// 重定向
element: <Navigate to='/home' />
},
{
path: '/home',
element: <Home />
},
{
path: '/about',
element: <About />
},
// 对其他路由的处理
{
path: '*',
element: <Navigate to='/home' />
},
]
export default routes
值得注意的是:路由表的使用方式的必须是Hooks的方式,即组件必须是函数组件,类组件无法实现。
使用路由表文件有如下三步骤:
-
导入路由表文件
-
注册路由表
-
使用路由表
javascript
// App.js
import { BrowserRouter, NavLink, Navigate, Route, Routes, useRoutes } from "reactrouter-
dom";
// 1.导入路由表
import indexRoutes from './routes'
import './App.css'
// 2.注册路由表进行使用
function RouteElement() {
return useRoutes(indexRoutes)
}
function App() {
return (
<div className="App">
<BrowserRouter>
<NavLink className={(a) => {
return a.isActive ? 'active-pages' : 'pages'
}} to="/test">redirect to home</NavLink>
<br />
<NavLink className={(a) => {
return a.isActive ? 'active-pages' : 'pages'
}} to="/home">home</NavLink>
<br />
<NavLink className={(a) => {
return a.isActive ? 'active-pages' : 'pages'
}} to="/about">about</NavLink>
<br />
{/* 3.使用路由表 */}
<RouteElement></RouteElement>
{/* 以下内容就可以不用了 */}
{/* <Routes>
<Route path="/" element={<Home></Home>}></Route>
<Route path="/about" element={<About></About>}></Route>
<Route path="/home" element={<Home></Home>}></Route>
<Route path='*' element={<Navigate to='/home'></Navigate>}></Route>
</Routes> */}
</BrowserRouter>
</div >
);
}
export default App;
嵌套路由
javascript
// routes/index.js
// ... 此处导入路由组件,User1和User2内部的内容简单提供即可。重点注意下方User组件内的内
容
export default [
{
path: '/home',
element: <Home />
},
// ...
{
path: '/user',
element: <User />,
children: [
{
path: 'user1',
element: <User1 />
},
{
path: 'user2',
element: <User2 />
},
]
}
]
嵌套路由使用路由出口组件Outlet(类似vue的routerView)
javascript
// pages/User.js
import { NavLink, Outlet } from "react-router-dom";
export default function User() {
return <div>
User页面
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="user1">用户一</NavLink>
<br />
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="user2">用户二</NavLink>
<br />
{/* 使用Outlet来加载路由子组件 */}
<Outlet></Outlet>
</div>
}
7 路由传参
组件路由传参和5版本的一样,只是由于是函数式组件接收参数需要借助hooks来实现。
查询字符串参数:
- 查询参数不需要在路由中定义
- 使用useSearchParams hook来访问查询参数。其用法和useState类似,会返回当前对象和更改它的方法
- 更改searchParams时,必须传入所有的查询参数,否则会覆盖已有参数。
路由跳转
javascript
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="/pathQuery?name=zhangsan&age=12">pathQuery</NavLink>
路由规则声明
javascript
// routes/index.js
import PathQuery from '../pages/PathQuery';
// ....
{
path: '/pathQuery',
element: <PathQuery />
},
// ....
路由组件内使用useSearchParams来接受参数
javascript
import { useSearchParams } from 'react-router-dom';
// 当前路径为 /pathQuery?name=zhagnsan&age=12
export default function PathQuery() {
const [searchParams, setSearchParams] = useSearchParams();
/*setSearchParams({
name: 'pathQuery'
}) */
// /pathQuery?name=pathQuery
return (
<div>路径查询字符串参数{searchParams.get('name')}</div>
)
}
动态路由参数:
- 在路由的path属性中定义路径参数
- 在组件内通过useParams hook访问路径参数
路由跳转
javascript
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="/routeParams/1">routeParams</NavLink>
动态路由声明
javascript
// routes/index.js
import RouteParams from '../pages/RouteParams';
// ....
{
path: '/routeParams/:id',
element: <RouteParams />
},
// ....
路由组件中接受参数
javascript
import { useParams } from 'react-router-dom'
export default function RouteParams() {
let params = useParams();
return (
<div>
动态路由参数:{params.id}
</div>
);
}
8 编程式路由导航
使用useNavigate(path,[obj])来进行编程式路由导航。
参数说明
- path:路由路径
- obj:配置对象(携带参数){state:{}}
路由跳转
javascript
// pages/TestApi.js
import { useNavigate } from "react-router-dom"
export default function TestApi() {
const navigate = useNavigate();
return <button onClick={
() => {
navigate('/home', {
state: { id: 1, name: 'tom' }
})
}
}>跳转到Home页面</button>
}
将TestApi当做路由组件来使用
javascript
//routes/index.js
// import TestApi from '../pages/TestApi';
// ...
{
path: '/testApi',
element: <TestApi />
},
{
path: '/home',
element: <Home />
},
{
path: '/about',
element: <About />
},
// ...
在App.js中编写路由入口NavLink
javascript
// app.js
// ...
<BrowserRouter>
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="/home">home</NavLink>
<br />
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="/about">about</NavLink>
<br />
<NavLink className={(a) => { return a.isActive ? 'active-pages' : 'pages' }}
to="/testApi">testApi</NavLink>
<br />
<RouteElement></RouteElement>
</BrowserRouter>
携带参数
在进行编程式导航的时候进行参数的携带
javascript
navigate('/home', {
// 此处必须为state,不能为其他属性
state: { id: 1, name: 'tom' }
})
参数获取
获取编程式导航携带的参数,使用state携带的参数可以在路由组件页面使用location获取
在Home.js中通过如下代码来获取编程式导航携带的参数
javascript
import { useLocation } from 'react-router-dom'
export default function Home(props) {
const location = useLocation()
console.log(location.state);
return <h1>Home页面{JSON.stringify(location)}</h1>
}
// 通过location.state就可以获取到参数
前进后退
使用useNavigate()实现路由的前进和后退
javascript
import { useNavigate } from 'react-router-dom'
// ...
// 在某个事件处理程序中
const navigate =useNavigate();
navigate(1); //前进一个路由
navigate(-1); //后退一个路由
修改TestApi.js中的内容,就可以实现前进和后退的效果
javascript
export default function TestApi() {
const navigate = useNavigate();
return <div>
<button onClick={
() => {
navigate(1)
}
}>前进</button>
<button onClick={
() => {
navigate(1)
}
}>后退</button>
</div>
}
使用路由表配置路由案例:
router/index.js (路由表及路由实例)
javascript
import React from "react";
import { createBrowserRouter, Navigate } from "react-router-dom";
import Main from "../pages/main";
import Home from "../pages/home/index";
import Mall from "../pages/mall/index";
import User from "../pages/user/index";
import PageOne from "../pages/other/pageOne";
import PageTwo from "../pages/other/pageTwo";
import Login from "../pages/login/index";
import { Component } from "react";
/*
路由懒加载
空档期加载 Suspense 中的内容
React.lazy() 实现路由懒加载
*/
let load = (Com) => <React.Suspense fallback={<div>Loading...</div>}><Com></Com></React.Suspense>
// 路由配置项
const routes = [
{
path: "/",
// component: () => import('../pages/main.js'),
// Component 这里首字母要大写
// Component: Main,
element: load(React.lazy(() => import('../pages/main'))),
// 子路由
children: [
// 重定向
{
path: "/",
element: <Navigate to="home" replace />
},
{
path: "home",
// element: <Home />
// Component: Home
element: load(React.lazy(() => import('../pages/home/index')))
},
{
path: "mall",
// Component: Mall
element: load(React.lazy(() => import('../pages/mall/index')))
},
{
path: "user",
// Component: User
element: load(React.lazy(() => import('../pages/user/index')))
},
{
path: "other",
children: [
{
path: "pageOne",
// Component: PageOne
element: load(React.lazy(() => import('../pages/other/pageOne'))),
},
{
path: "pageTwo",
// Component: PageTwo
element: load(React.lazy(() => import('../pages/other/pageTwo'))),
}
]
},
]
},
{
path: "/login",
Component: Login
}
]
export default createBrowserRouter(routes)
App.js 根组件
javascript
// 根组件
import logo from './logo.svg';
import './App.css';
// RouterProvider 挂载路由实例组件
import { RouterProvider } from "react-router-dom";
// 引入路由对象
import router from "@/router";
// React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头,否则它们将无法运行!
// 声明式写法
function App() {
// 如果你的标签和 return 关键字不在同一行,则必须把它包裹在一对括号中
/*
只能返回一个根元素。如果你不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替
这个空标签被称作 Fragment。React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。
需要加key,就需要使用 Fragment
import { Fragment } from 'react'
<Fragment key={ }> ... </Fragment>
*/
return (
<div className={myClass}>
{/* 路由出口(一级) */}
<RouterProvider router={router} />
</div>
);
}
export default App;
子路由出口
javascript
import { Outlet } from "react-router-dom";
{/* Outlet组件 子路由出口 */}
<Outlet />
11 Lazy 和 Suspense(路由懒加载)
1 React.lazy 定义
React.lazy()
函数能让你像渲染常规组件一样处理动态引入(的组件)。
什么意思呢?其实就是懒加载 。其原理就是利用es6 import()
函数。这个import
不是import命令
。同样是引入模块,import命令
是同步引入模块,而**import()
函数动态引入**。
当 Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。
(1) 为什么代码要分割
当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。
(2) import()函数 -- 异步
import静态
和export
命令只能在模块的顶层 ,不能在代码块之中 (比如,在if
代码块之中,或在函数之中)。
import()
函数与所加载的模块没有静态连接关系,这点也是与import
语句不相同。import()
类似于 Node.js 的require()
方法,区别主要是前者是异步加载,后者是同步加载。
javascript
//import命令 -- 同步,编译阶段
import { add } from './math.js';
console.log(add(16, 26));
//import函数 -- 异步,执行阶段
//由于import()返回 Promise 对象,所以需要使用then()方法指定处理函数。
import("./math.js").then(math => {
console.log(math.add(16, 26));
});
//考虑到代码的清晰,更推荐使用await命令。
async function addFun() {
const math = await import("./math");
math.add(16, 26);
}
addFun();
动态
import()
语法目前只是一个 ECMAScript (JavaScript) 提案, 而不是正式的语法标准。预计在不远的将来就会被正式接受。ES6 入门教程
(3) import函数示例
下面是import一个示例:
在test文件夹下新建两个文件
html
// test.html
<div id="root">
页面无内容
</div>
<button id="btn">加载js</button>
<script>
document.getElementById('btn').onclick=function(){
import('./test.js').then(d=>{
d.test()
})
}
</script>
javascript
function test(){
document.getElementById('root')
root.innerHTML='页面变的有内容了'
}
export {test}
这时候打开web服务让页面以http的方式访问, 我们在chrome的开发者工具下的Network可以看到只请求了一个页面。
但是当我们点击加载js,你会发现test.js会以动态的方式加入到代码中,同时执行了test函数,使页面的内容发生了变化。
在React.lazy
和常用的三方包react-loadable
,都是使用了这个原理,然后配合webpack进行代码打包拆分达到异步加载,这样首屏渲染的速度将大大的提高。
注意:由于React.lazy
不支持服务端渲染,所以这时候react-loadable
就是不错的选择。
2 如何使用React.lazy
下面示例代码使用create-react-app脚手架搭建:
javascript
//OtherComponent.js 文件内容
import React from 'react'
const OtherComponent = ()=>{
return (
<div>
我已加载
</div>
)
}
export default OtherComponent
// App.js 文件内容
import React from 'react';
import './App.css';
//使用React.lazy导入OtherComponent组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function App() {
return (
<div className="App">
<OtherComponent/>
</div>
);
}
export default App;
这是最简单的React.lazy
,但是这样页面会报错。这个报错提示我们,在React使用了lazy
之后,会存在一个加载中的空档期,React不知道在这个空档期中该显示什么内容,所以需要我们指定。接下来就要使用到Suspense
。
(1) Suspense 解决异步空挡显示问题
如果在 App
渲染完成后,包含 OtherComponent
的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense
组件来解决。
这里将App
组件改一改
javascript
import React, { Suspense, Component } from 'react';
import './App.css';
//使用React.lazy导入OtherComponent组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));
export default class App extends Component {
state = {
visible: false
}
render() {
return (
<div className="App">
<button onClick={() => {
this.setState({ visible: true })
}}>
加载OtherComponent组件
</button>
<Suspense fallback={<div>Loading...</div>}>
{
this.state.visible
?
<OtherComponent />
:
null
}
</Suspense>
</div>
)
}
}
我们指定了空档期使用Loading展示在界面上面,等OtherComponent
组件异步加载完毕,把OtherComponent
组件的内容替换掉Loading上。
注意:
Suspense
使用的时候,fallback
一定是存在且有内容的, 否则会报错。
12 redux
1 redux介绍
学习资源:学习资源 | Redux 中文官网
Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。可以开发出行为稳定可预测的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。Redux 除了和 React 一起用外,还支持其它界面库。它体小精悍(只有2kB,包括依赖),却有很强大的插件扩展生态。
Redux 是一个使用叫做"action"的事件来管理和更新应用状态的模式和工具库它以集中式Store(centralizedstore)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
Redux 提供的模式和工具使您更容易理解应用程序中的状态何时、何地、为什么以及如何更新,以及当这些更改发生时您的应用程序逻辑将如何表现. Redux 指导您编写可预测和可测试的代码,这有助于让您确信您的应用程序将按预期工作。
类似于vuex 但是不同于vuex,可以对状态进行管理。
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。这些 state可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
Redux 是⼀个有⽤的架构,但不是⾮⽤不可。事实上,⼤多数情况,你可以不⽤它,只⽤ React 就够了。
Redux 在以下情况下更有用:
- 在应用的大量地方,都存在大量的状态
- 应用状态会随着时间的推移而频繁更新
- 更新该状态的逻辑可能很复杂
- 中型和大型代码量的应用,很多人协同开发
- 不同身份的⽤户有不同的使⽤⽅式(⽐如普通⽤户和管理员)
- 与服务器⼤量交互,或者使⽤了WebSocket
- View要从多个来源获取数据
- 某个组件的状态,需要共享某个状态需要在任何地⽅都可以拿到
- ⼀个组件需要改变全局状态
- ⼀个组件需要改变另⼀个组件的状态
整个应用程序所需的全局状态应该放在 Redux store 中。而只在一个地方用到的状态应该放到组件的state。
在 React + Redux 应用中,你的全局状态应该放在 Redux store 中,你的本地状态应该保留在 React 组件中。
2 Redux 库和工具
- Redux:Redux 是一个小型的独立 JS 库
- React-Redux:Redux 可以集成到任何的 UI 框架中,其中最常见的是 React 。React-Redux 是官方包,它可以让 React 组件访问 state 和下发 action 更新 store,从而同 Redux 集成起来。
- Redux Toolkit:Redux Toolkit 是我们推荐的编写 Redux 逻辑的方法。它包含我们认为对于构建 Redux应用程序必不可少的包和函数。 Redux Toolkit 构建在我们建议的最佳实践中,简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。
- Redux DevTools 扩展:Redux DevTools 扩展可以显示 Redux 存储中状态随时间变化的历史记录。这允许您有效地调试应用程序,包括使用强大的技术,如"时间旅行调试"。
redux 其实是一个第三方数据状态管理的库,它不仅仅可以和react 结合使用,你也可以把它应用到 vue中, react-redux 其实是帮我们封装了 redux 连接 react 的一些操作,使用 react-redux 可以非常简单的在 react 中使用 redux 来管理我们应用的状态。
学习redux之前应该确保在浏览器中安装了 React 和 Redux DevTools 扩展:
npm install redux
npm install react-redux
npm install @reduxjs/toolkit
npm install --save-dev redux-devtools
3 单向数据流
javascript
import React, { useEffect, useState } from "react";
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: 当事件发生后,触发状态更新的代码
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: UI 定义
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
export default Counter
这是一个包含以下部分的自包含应用程序:
- state:驱动应用的真实数据源头
- view:基于当前状态的 UI 声明性描述
- actions:根据用户输入在应用程序中发生的事件,并触发状态更新
单向数据流(one-way data flow)特点:
- 用 state 来描述应用程序在特定时间点的状况
- 基于 state 来渲染出 View
- 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
- 基于新的 state 重新渲染 View
然而,当我们有多个组件需要共享和使用相同state 时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过"提升 state" 到父组件来解决,但这并不总是有效。
解决这个问题的一种方法是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大"view",任何组件都可以访问 state 或触发 action,无论它们在树中的哪个位置!
通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。
这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。
深入了解
具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:
- 初始启动:
- 使用最顶层的 root reducer 函数创建 Redux store
- store 调用一次 root reducer,并将返回值保存为它的初始state
- 当 UI 首次渲染时,UI 组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。
- 同时监听 store 的更新,以便他们可以知道 state 是否已更改。
- 更新环节:
- 应用程序中发生了某些事情,例如用户单击按钮
- dispatch 一个 action 到 Redux store,例如dispatch({type: 'counter/increment'})
- store 用之前的state 和当前的action 再次运行 reducer 函数,并将返回值保存为新的state
- store 通知所有订阅过的 UI,通知它们 store 发生更新
- 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
- 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页。
redux工作流
4 核心概念
应用的整体全局状态以对象树的方式存放于单个store。唯一改变状态树(state tree)的方法是创建action,一个描述发生了什么的对象,并将其dispatch 给 store。要指定状态树如何响应 action 来进行更新,你可以编写纯reducer 函数,这些函数根据旧 state 和 action 来计算新 state。
4.1 state
托管给redux管理的状态
javascript
let state = {
todos: [],
params: {}
}
4.2 action
Action 描述当前发⽣的事情。改变 State 的唯⼀办法,就是使⽤ Action。它会运送数据到 Store。 Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使⽤⼀个字符串类型的 type 字段来表示将要执⾏的动作。
type 字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"。我们通常把那个类型的字符串写成"域/事件名称",其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。
action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为**payload(参数)**的字段中,也可放放到其他属性中,不过最好放到payload中。
一个典型的 action 对象如下所示:
javascript
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
// action也可如下:
// { type: 'ADD_TODO', text: '去游泳馆' }
// { type: 'TOGGLE_TODO', index: 1 }
// { type: 'SET_VISIBILITY_FILTER', filter: 'completed' }
Action Creator
action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
javascript
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
4.3 reducer(重要)
reducer 是一个函数,接收当前的state 和一个action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState。可以将 reducer 视为一个事件监听器,它根据接收到的action(事件)类型处理事件。
"Reducer" 函数的名字来源是因为它和Array.reduce() 函数使用的回调函数很类似。
Reducer 必需符合以下规则:
- 仅使用 state 和 action 参数计算新的状态值
- 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新
- (immutable updates)。
- 禁止任何异步逻辑、依赖随机值或导致其他"副作用"的代码
reducer 函数内部的逻辑通常遵循以下步骤:
- 检查 reducer 是否关心这个 action
如果是,则复制 state,使用新值更新 state 副本,然后返回新 state - 否则,返回原来的 state 不变
声明reducer
javascript
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === 'counter/increment') {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1
}
}
// 返回原来的 state 不变
return state
}
Reducer 可以在内部使用任何类型的逻辑来决定新状态应该是什么,如if/else、switch、循环等等。
Reducers 指定了应⽤状态的变化如何响应 actions 并发送到 store 的。reducer 只是⼀个接收 state 和action,并返回新的 state 的纯函数。
对于⼤的应⽤来说,不⼤可能仅仅只写⼀个这样的函数,所以我们编写很多⼩函数来分别管理 state 的⼀部分:
javascript
// 1.reducer
function todos(state = { list: [], loading: false }, action) {
switch (action.type) {
case "ADD_TODO":
return {
...state,
list: state.list.concat(action.payload)
}
default:
return state
}
}
不可变性 Immutability
"Mutable" 意为 "可改变的",而 "immutable" 意为永不可改变。
JavaScript 的对象(object)和数组(array)默认都是 mutable 的。如果创建一个对象,可以更改其字段的内容。如果创建一个数组,可以更改其内部内容。即:内存中还是原来对象或数组的引用,但里面的内容变化了。
Redux 期望所有状态更新都是使用不可变的方式,如果想要不可变的方式来更新,代码必需先复制原来的object/array,然后更新它的复制体。JavaScript array/object 的展开运算符(spread operator)可以实现这个目的。
javascript
let obj = {};
let arr = [];
// 可变更新
obj.name = 'tom';
arr[0] = 'hello';
// 不可变更新
obj = {
...obj,
age: 12
}
arr = [
...arr,
'world'
]
应用Reducer
需要先创建⼀个store,然后将store注⼊给react组件,在组件中通过props来访问state以及通过props来dispatch action。
javascript
import { createStore } from 'redux'
// 一个reducer
export default function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text])
default:
return state
}
}
// 另一个reducer
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 如果该仓库只用一个reducer,则如下用法
const store = createStore(todos);
// 如果该仓库用多个reducer,则使用combineReducers函数来合并reducer,可以通过
combineReducers组合多个Reducer,然后通过createStore来创建状态机。
const store2 = createStore(combineReducers({
todos,
counter,
}));
你可以调用combineReducers({ todos: todos, counter: counter}) 将 state 结构变为{ todos,
counter }。
通常的做法是命名 reducer,然后 state 再去分割那些信息,这样你可以使用 ES6 的简写方法:
combineReducers({ counter, todos })。这与combineReducers({ counter: counter, todos:
todos }) 是等价的。
在 Redux 中,只有一个 store,但是combineReducers 让你拥有多个 reducer,同时保持各自负责逻辑块的独立性。
4.4 store
当前 Redux 应用的状态存在于一个名为store 的对象中。store 是通过传入一个 reducer 来创建的,并且有一个名为getState 的方法,它返回当前状态值。
Store 就是保存数据的地⽅,可以把它看成⼀个容器。整个应⽤只能有⼀个 Store。 Redux 提供createStore这个函数,⽤来⽣成 Store。
javascript
// 创建仓库 store并导出
import { createStore } from 'redux';
import reducer from './reducer';
// 将reducer与store绑定,创建store并返回
export default createStore(reducer);
// 如果该仓库用多个reducer,则使用combineReducers函数来合并reducer
const store2 = createStore(combineReducers({
todos,
counter,
}));
const store3 = createStore(reducer,
window.__REDUX_DEVTOOLS_EXTENSION__
&& window.__REDUX_DEVTOOLS_EXTENSION__());
上方代码中加入window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION())是为了能在谷歌浏览器中用redux devtools调试工具。
或者直接使用@reduxjs/toolkit包中的configureStore方法来生成store。
javascript
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
仓库创建好了之后,将store引入到需要使用仓库的组件中。
javascript
import store from './store/index.js'
// 分发动作
store.dispatch({type:'',payload:''})
// 获取仓库数据
store.getState();
4.5 Dispatch
Redux store 有一个方法叫dispatch。更新 state 的唯一方法是调用store.dispatch() 并传入一个 action对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用getState() 可以获取新 state。
javascript
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
dispatch 一个 action 可以形象的理解为 "触发一个事件" 。发生了一些事情,我们希望 store 知道这件事。
Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。
我们通常调用 action creator 来调用 action:
javascript
const increment = () => {
return {
type: 'counter/increment'
}
}
// 分发动作
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
4.6 Selector
Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑。
javascript
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
4.7 subscribe(listener)
添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用getState() 来拿到当前 state。你可以在变化监听器里面进行dispatch(),如果需要解绑这个变化监听器,执行subscribe 返回的函数即可。
参数listener (Function): 每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。你可以在回调函数里调用getState() 来拿到当前 state。store 的 reducer 应该是纯函数,因此你可能需要对state 树中的引用做深度比较来确定它的值是否有变化。
返回值(Function): 一个可以解绑变化监听器的函数。
javascript
store.subscribe(() => {
this.setState({
...store.getState()
});
}); //订阅者做的事情
函数式编程使用Redux案例:
1、src/store/reduces/tab.js 创建一个reducer子实例
javascript
import { createSlice } from "@reduxjs/toolkit";
// 创建一个 reducer实例
const tabSlice = createSlice({
// 模块名称
name: 'tab',
// 初始化数据
initialState: {
// 菜单折叠
isCollapse: false,
},
// 修改状态
reducers: {
setCollapse: (state, action) => {
state.isCollapse = action.payload
},
}
})
// 暴露出 tabSlice.actions 中的方法
export const { setCollapse} = tabSlice.actions
// 暴露出当前的reducer
export default tabSlice.reducer
2、src/store/index.js 创建store根实例(即 root reducer)
javascript
import { configureStore } from "@reduxjs/toolkit";
// tab 一个reducer子模块
import TabReducer from "./reduces/tab";
// 创建 root reducer实例
const store = configureStore({
// 组合redux子模块
reducer: {
tab: TabReducer,
// ...
}
})
// 导出store实例
export default store
3、src/index.js 应用入口文件全局注入 store
javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
// 根组件
import App from './App';
import reportWebVitals from './reportWebVitals'
// 注入 store
/*
ReactRedux库中的Provider组件,用于将Redux的store对象传递给组件树中的子组件,使子组件能够访问和使用store中的状态和dispatch方法。
它接受一个store作为props,然后将store通过React的Context API传递给子组件。
使用Provider可以方便地在组件中使用Redux的state和actions,而无需通过props逐层传递。
*/
import { Provider } from "react-redux";
import store from "./store/index";
// 引入mock
import '@/api/mock.js'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
/*
StrictMode 严格模式
1.检查组件是否是纯函数
2.及早的发现useEffect中的错误
3.警告过时的API
4.会将函数进行两次调用,用来判断是否为纯函数
*/
/*
组件必须是一个纯函数
1.只负责自己的任务,它不会更改在该函数调用前就已存在的对象或变量
2.输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果
*/
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
4、组件中使用
父组件:读取状态 useSelector
javascript
import HeaderC from "../components/common/header/index";
import React, { useState } from "react";
import { Outlet } from "react-router-dom";
// 引入获取状态 redux 中的state 的钩子 useSelector (注意要放到上面)
import { useSelector } from "react-redux";
import { Button, Layout, Menu, theme } from 'antd';
const { Header, Sider, Content } = Layout;
// hook 写法
const Main = () => {
// 组件定义状态
// const [collapsed, setCollapsed] = useState(false);
// 获取 redux 中的状态
const collapsed = useSelector(state => state.tab.isCollapse)
return (
<Layout>
<Aside collapsed={collapsed}></Aside>
<HeaderC collapsed={collapsed}></HeaderC>
</Layout>
);
}
export default Main;
子组件:调用方法 useDispatch
javascript
import React, { useState } from "react";
// 引入 redux 调用函数的钩子 useDispatch (注意要放到上面)
import { useDispatch } from "react-redux";
// 引入 store 对应子模块的方法
import { setCollapse } from "@/store/reduces/tab";
import { Button, Layout, Avatar, Space, Dropdown, Switch, Tooltip } from 'antd';
const { Header, } = Layout;
// {collapsed} 解构获取props
const HeaderC = ({ collapsed }) => {
// 实例化 dispatch (调用函数)
const dispatch = useDispatch();
// 点击展开收起方法
const setCollapsed = () => {
// 修改 store 中的状态
dispatch(setCollapse(!collapsed))
};
return (
<Header style={{ padding: 0, }} className="header-container">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={{
fontSize: '16px',
backgroundColor: '#fff'
}}
onClick={(e) => setCollapsed()}
/>
</Header>
)
}
export default HeaderC
13 immutable.js 不可变对象
immer 简化不可变对象操作
相对于深拷贝来说,它使拷贝相对便宜,不需要复制数据树的未更改部分,并且在内存中与相同状态的旧版本共享。复杂数据
javascript
npm install immer use-immer
javascript
import React from "react";
import { useImmer } from "use-immer";
function Comp04() {
const [list, setList] = useImmer([
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
{ id: 3, name: "王五" },
]);
const [info, setInfo] = useImmer({
username: {
first: "muzidigbig",
last: "Lee",
},
age: 18,
sex: "男",
});
const handleClick = () => {
setList((draft) => {
console.log(draft); // Proxy
// 修改数据
draft.push({ id: 4, name: "赵六" });
});
setInfo((draft) => {
draft.username.name = "muzidigbig.Lee"
});
};
return (
<div>
Comp04
<button onClick={handleClick}>点击</button>
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div>
{JSON.stringify(info)}
</div>
</div>
);
}
export default Comp04;
14 Mobx
Mobx是一个功能强大,上手非常容易的状态管理工具。redux的作者也曾经向大家推荐过它,在不少情况下可以使用Mobx来替代掉redux。
15 ahooks.js 好用Hooks库
ahooks是一款由阿里巴巴开发团队设计的React Hooks库,提供了一系列实用的React Hooks,以便开发者更好地使用React的功能。ahooks的设计原则是"最小API,最大自由",旨在提供最小的、最易于理解和使用的API,同时保留最大的使用自由度。
16 react函数中动态获取dom
在React函数组件中,你可以使用useRef
钩子来创建一个ref并将其附加到DOM元素上。然后,你可以使用这个ref来访问或者操作DOM元素。
以下是一个简单的例子:
javascript
import React, { useRef, useEffect } from 'react';
function MyComponent() {
const divRef = useRef(null);
useEffect(() => {
// 当组件挂载后,divRef.current会指向DOM元素
if (divRef.current) {
// 你可以在这里对divRef.current进行操作
console.log(divRef.current.textContent);
}
}, []); // 空依赖数组意味着这个effect只会在组件挂载时执行一次
return (
<div ref={divRef}>
这是一个DOM元素
</div>
);
}
export default MyComponent;
在这个例子中,一旦MyComponent
组件被渲染到DOM中,divRef.current
将指向对应的<div>
元素,你可以在useEffect
钩子中访问它并执行需要的操作。
17 Axios 封装
安装:
javascript
npm install axios
二次封装
api/axios.js
javascript
import axios from 'axios'
// url 的前缀
const baseUrl = '/api'
// axios二次封装请求
class HttpRequest {
constructor(baseUrl = baseUrl) {
this.baseUrl = baseUrl
}
getInsideConfig() {
const config = {
baseURL: this.baseUrl,
headers: {
// 'Content-Type': 'application/json;charset=utf-8'
}
}
return config
}
interceptors(instance) {
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
}
request(options) {
options = { ...this.getInsideConfig(), ...options }
// 创建 axios 的实例
const instance = axios.create()
// 实例拦截器的绑定
this.interceptors(instance)
return instance(options)
}
}
export default new HttpRequest(baseUrl)
api/index.js
javascript
import http from './axios'
export const getData = (params) => {
return http.request({
url: '/home/getData',
data: {},
header: { 'content-type': 'application/json' },
method: 'GET',
dataType: 'json',
params
});
}
// 登录
export const getMenu = (data) => {
return http.request({
url: '/permission/getMenu',
method: 'post',
// dataType: 'json',
data
});
}
组件中使用:
javascript
import { getMenu } from "@/api/index";
getMenu(values).then(res => {
console.log(res);
// ....
})
18 Mock 模拟数据
安装:
javascript
npm install mockjs
index.js 入口文件中引入 mock.js
javascript
// 引入mock
import '@/api/mock.js'
mock.js 拦截接口
javascript
// 需要在入口文件中引入 mock
import Mock from "mockjs";
import permissionApi from "./mockServeData/permission";
// 登录
Mock.mock(/permission\/getMenu/, 'post', permissionApi.getMenu)
发起请求参考 15
mockServeData/permission.js 模拟 mock 数据
javascript
import Mock from 'mockjs'
export default {
getMenu: config => {
const { username, password } = JSON.parse(config.body)
// 先判断用户是否存在
// 判断账号和密码是否对应
if (username === 'admin' && password === 'admin') {
return {
code: 20000,
data: {
menu: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 's-home',
url: 'home/index'
},
{
label: '其他',
icon: 'location',
children: [
{
path: '/page1',
name: 'page1',
label: '页面1',
icon: 'setting',
url: 'other/pageOne.vue'
},
]
}
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else if (username === 'xiaoxiao' && password === 'xiaoxiao') {
return {
code: 20000,
data: {
menu: [
{
path: '/',
name: 'home',
label: '首页',
icon: 's-home',
url: 'home/index'
},
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else {
return {
code: -999,
data: {
message: '密码错误'
}
}
}
}
}