React
React简介
react是什么?
React用于构建用户界面的JS库。是一个将数据渲染为HTML视图的开源JS库。
为什么学?
- 原生JS操作DOM繁琐,效率低
- 使用JS直接操作DOM,浏览器会进行大量的重绘重排
- 原生JS没有组件化编码方案,代码复用低
React入门
React 基础案例
先倒入三个包
【先引入react.development.js,后引入react-dom.development.js
cmdreact.development.js react-dom.development.js babel.min.js
创建一个容器
创建虚拟DOM,渲染到容器中
html<body> <!-- 准备好容器 --> <div id="test"> </div> </body> <!-- 引入依赖 ,引入的时候,必须就按照这个步骤--> <script src="../js/react.development.js" type="text/javascript"></script> <script src="../js/react-dom.development.js" type="text/javascript"></script> <script src="../js/babel.min.js" type="text/javascript"></script> <!--这里使用了babel用来解析jsx语法--> <script type="text/babel"> // 1.创建虚拟DOM const VDOM = <h1>Hello</h1> //这个地方使用的是JSX语法,不需要加"" // 2.渲染,如果有多个渲染同一个容器,后面的会将前面的覆盖掉 ReactDOM.render(VDOM,document.getElementById("test")); </script> </html>
这样,就会在页面中的这个div容器上添加这个h1.
JSX基础语法
- 定义虚拟DOM,不能使用“”
- 标签中混入JS表达式的时候使用{}
- 样式的类名指定不要使用class,使用className
- 内敛样式要使用双大括号包裹
- 不能有多个根标签,只能有一个跟标签
- 标签必须闭合
- 如果小写字母开头,就将标签转化为html同名元素,如果html中无该标签对应的元素,就报错;如果是大写字母开头,react就去渲染对应的组件,如果没有就报错
关于JS表达式和JS语句:
JS表达式:返回一个值,可以放在任何一个需要值的地方 a a+b demo(a) arr.map() function text(){} JS语句:if(){} for(){} while(){} swith(){} 不会返回一个值
实例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.sss{
color: red;
}
</style>
</head>
<body>
<!-- 准备好容器 -->
<div id="test">
</div>
</body>
<!-- 引入依赖 ,引入的时候,必须就按照这个步骤-->
<script src="../js/react.development.js" type="text/javascript"></script>
<script src="../js/react-dom.development.js" type="text/javascript"></script>
<script src="../js/babel.min.js"></script>
<!--这里使用了js来创建虚拟DOM-->
<script type="text/babel">
const MyId = "title";
const MyData = "Cyk";
// 1.创建虚拟DOM
const VDOM = (
<h1 id = {MyId.toLocaleUpperCase()}>
span className="sss" style={{fontSize:'50px'}}>sss</span>
</h1>
)
// 2.渲染,如果有多个渲染同一个容器,后面的会将前面的覆盖掉
ReactDOM.render(VDOM,document.getElementById("test"));
</script>
</html>
两种创建虚拟DOM的方式
1.使用JSX创建虚拟DOM
const VDOM = (
<h1 id = {MyId.toLocaleUpperCase()}>
<span className="sss" style={{fontSize:'50px'}}>sss</span>
</h1>
)
这个在上面的案例中已经演示过了 ,下面看看另外一种创建虚拟DOM的方式
2.使用JS创建虚拟DOM
// 1.创建虚拟DOM[在这使用了js的语法]React.createElement(标签,标签属性,内容)
const VDOM = React.createElement('h1',{id:"title"},"nihao")
使用JS和JSX都可以创建虚拟DOM,但是可以看出JS创建虚拟DOM比较繁琐,尤其是标签如果很多的情况下,所以还是比较推荐使用JSX来创建。
组件
当应用是以多组件的方式实现,这个应用就是一个组件化的应用
注意: 组件名称必须以大写字母开头。
React 会将以小写字母开头的组件视为原生 DOM 标签。例如,< div />
代表 HTML 的 div 标签,而
< Weclome /> 则代表一个组件,并且需在作用域内使用Welcome
传递的参数,不能在组件中改动
函数式组件
//1.先创建函数,函数可以有参数,也可以没有,但是必须要有返回值 返回一个虚拟DOM
function Welcome(props) {
console.log(this) // 此处的this是undefined,因为babel开启了严格模式
return <h1>Hello, {props.name}</h1>;
}
//2.进行渲染
ReactDOM.Render(<Welcom name="ss" />,document.getElementById("div"));
// 执行了ReactDOM.Render(<Welcom />...),发生了什么
// 1. React解析组件标签,找到了Welcome组件。
// 2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM
让我们来回顾一下这个例子中发生了什么:
- 我们调用
ReactDOM.render()
函数,并传入<Welcome name="Sara" />
作为参数。 - React 调用
Welcome
组件,并将{name: 'Sara'}
作为 props 传入。 Welcome
组件将Hello, Sara
元素作为返回值。- React DOM 将 DOM 高效地更新为
Hello, Sara
。
Class组件
//必须继承React.Component
//然后重写Render()方法,该方法一定要有返回值,返回一个虚拟DOM
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
//渲染 【这个跟之前也是一样的】
ReactDOM.Render(<Welcom name="ss" />,document.getElementById("div"));
// 执行了ReactDOM.Render(<Welcom />...),发生了什么
// 1. React解析组件标签,找到了Welcome组件。
// 2. 发现组件是使用类定义的,随后new出来该类实例,并通过该实例调用原型链上的render方法
// 3. 将render返回的虚拟DOM转为真实DOM
执行过程:
1.React解析组件标签,找到相应的组件
2.发现组件是类定义的,随后new出来的类的实例,并通过该实例调用到原型上的render方法
3.将render返回的虚拟DOM转化为真实的DOM,随后呈现在页面中
组件案例
下面,我们通过一个案例更好的理解组件:【只关注与核心代码】
我们发现组件是可以包含中使用的, 而且如果创建的数组,必须要代一个key。数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值
<script type="text/babel">
//创建一个组件<li>
function GetLi(props){
return <li>{props.value}</li>
};
// 1.创建类式组件<ul>
class MyComponent extends React.Component{
render(){
console.log(this.props.arr);
let com = this.props.arr.map((item,index)=>
//在这个地方包含了GetLi这个组件,【注意不能用{}】
//因为这个是一个列表,所以必须传递一个key【独一无二的Key】
//key 帮助 React 识别哪些元素改变了,比如被添加或删除。
<GetLi value={item} key={index} />
);
console.log(com);
return <ul>{com}</ul>
}
}
let num = [1,2,3,4]
//2.渲染组件
ReactDOM.render(<MyComponent arr={num}/>,document.getElementById("test");
</script>
组件实例的三大属性
state
我们都说React是一个状态机,体现是什么地方呢,就是体现在state上,通过与用户的交互,实现不同的状态,然后去渲染UI,这样就让用户的数据和界面保持一致了。state是组件的私有属性。
在React中,更新组件的state,结果就会重新渲染用户界面(不需要操作DOM),一句话就是说,用户的界面会随着状态的改变而改变。
state是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)
案例:
1.需求:页面显示【今天天气很炎热】,鼠标点击文字的时候,页面更改为【今天天气很凉爽】
核心代码如下:
<body>
<!-- 准备好容器 -->
<div id="test">
</div>
</body>
<!-- 引入依赖 ,引入的时候,必须就按照这个步骤-->
<script src="../js/react.development.js" type="text/javascript"></script>
<script src="../js/react-dom.development.js" type="text/javascript"></script>
<script src="../js/babel.min.js"></script>
<!--这里使用了js来创建虚拟DOM-->
<script type="text/babel">
//1.创建组件
class St extends React.Component{
// 构造器调用1次
constructor(props) {
super(props);
// 先给state赋值
this.state = {isHot: true, win: "ss"};
// 找到原型的dem,根据dem函数创建了一个dem1的函数,并且将实例对象的this赋值过去
this.dem1 = this.dem.bind(this);
}
// render会调用1+n次【1就是初始化的时候调用的,n就是每一次修改state的时候调用的】
render() { //这个This也是实例对象
// 如果加dem(),就是将函数的回调值放入这个地方
// this.dem这里面加入this,并不是调用,只不过是找到了dem这个函数,在调用的时候相当于直接调用,并不是实例对象的调用
return <h1 onClick={this.dem1}>今天天气很{this.state.isHot? "炎热" : "凉爽"}</h1>
}
// 通过state的实例调用dem的时候,this就是实例对象
dem() {
const state = this.state.isHot;
// 状态中的属性不能直接进行更改,需要借助API
// this.state.isHot = !isHot; 错误
// 必须使用setState对其进行修改,并且这是一个合并
this.setState({isHot: !state});
}
}
// 2.渲染,如果有多个渲染同一个容器,后面的会将前面的覆盖掉
ReactDOM.render(<St />,document.getElementById("test"));
</script>
需要注意的是:
- 组件的构造函数,必须要传递一个props参数
- 特别关注this【重点】,类中所有的方法局部都开启了严格模式,如果直接进行调用,this就是undefined
- 想要改变state,需要使用setState进行修改,如果只是修改state的部分属性,则不会影响其他的属性,这个只是合并并不是覆盖。
this.setState(),该方法接收两种参数:对象或函数。
- 对象:即想要修改的state
- 函数:接收两个函数,第一个函数接受两个参数,第一个是当前state,第二个是当前props,该函数返回一个对象,和直接传递对象参数是一样的,就是要修改的state;第二个函数参数是state改变后触发的回调
在此还需要注意的是,setState有异步更新和同步更新两种形式,那么什么时候会同步更新,什么时候会异步更新呢?
React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval等。
大部分开发中用到的都是React封装的事件,比如onChange、onClick、onTouchMove等,这些事件处理程序中的setState都是异步处理的。
// 1.创建组件
class St extends React.Component{
// 可以直接对其进行赋值
state = {isHot: 10};
render() {
// 这个This也是实例对象
return <h1 onClick={this.dem}>点击事件</h1>
}
// 箭头函数 [自定义方法--->要用赋值语句的形式+箭头函数]
dem = () => {
//修改isHot
this.setState({isHot: this.state.isHot + 1})
console.log(this.state.isHot);
}
}
上面的案例中预期setState使得isHot变成了11,输出也应该是11。然而在控制台打印的却是10,也就是并没有对其进行更新。这是因为异步的进行了处理,在输出的时候还没有对其进行处理。
componentDidMount() {
document.getElementById("test").addEventListener("click",()=>{
this.setState({isHot: this.state.isHot + 1});
console.log(this.state.isHot);
})
}
但是通过这个原生JS的,可以发现,控制台打印的就是11,也就是已经对其进行了处理。也就是进行了同步的更新。
React怎么调用同步或者异步的呢?
在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
如果是同步更新,每一个setState对调用一个render,并且如果多次调用setState会以最后调用的为准,前面的将会作废;如果是异步更新,多个setSate会统一调用一次render
dem = () => {
this.setState({
isHot: 1,
cont: 444
})
this.setState({
isHot: this.state.isHot + 1
})
this.setState({
isHot: 888,
cont: 888
})
}
上面的最后会输出:isHot是888,cont是888
dem = () => {
this.setState({
isHot: this.state.isHot + 1,
})
this.setState({
isHot: this.state.isHot + 1,
})
this.setState({
isHot: this.state.isHot + 888
})
}
初始isHot为10,最后isHot输出为898,也就是前面两个都没有执行。
注意!!这是异步更新才有的,如果同步更新,每一次都会调用render,这样每一次更新都会。
React 18 无论是同步调用还是异步调用setSta,进行异步更新。 多次调用setState会以最后调用的为准,前面的将会作废。
// 由于是异步执行,打印的结果仍然是更新前的状态
dem = () => {
this.setState({
isHot: this.state.isHot + 1,
})
console.log(this.state.isHot)
}
// 如果要在setState 后同步执行
dem = () => {
this.setState({
isHot: this.state.isHot + 1,
}, () => {
console.log(this.state.isHot)
})
}
简化版本:
1.state的赋值可以不再构造函数中进行
2.使用了箭头函数,将this进行了改变
<body>
<!-- 准备好容器 -->
<div id="test">
</div>
</body>
<!-- 引入依赖 ,引入的时候,必须就按照这个步骤-->
<script src="../js/react.development.js" type="text/javascript"></script>
<script src="../js/react-dom.development.js" type="text/javascript"></script>
<script src="../js/babel.min.js"></script>
<script type="text/babel">
class St extends React.Component{
// 可以直接对其进行赋值
state = {isHot: true};
render(){ // 这个This也是实例对象
return <h1 onClick={this.dem}>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h1>
// 或者使用{() => this.dem()也是可以的}
}
// 箭头函数 [自定义方法--->要用赋值语句的形式+箭头函数]
dem = () => {
console.log(this);
const state = this.state.isHot;
this.setState({isHot: !state});
}
}
ReactDOM.render(<St />,document.getElementById("test"));
</script>
如果想要在调用方法的时候传递参数,有两个方法:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind
来实现。
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
Props
Props主要用来传递数据,比如组件之间进行传值
基本使用:
<body>
<div id = "div"></div>
</body>
<script type="text/babel">
class Person extends React.Component{
render(){
return (
<ul>
// 接受数据并显示
<li>{this.props.age}</li>
<li>{this.props.sex}</li>
</ul>
)
}
}
// 传递数据
ReactDOM.render(<Person name="tom" age="41" sex="男"/>,document.getElementById("div"));
</script>
如果传递的数据是一个对象,可以更加简便的使用
<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: "张三", age: "18", sex: "女"}
ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
</script>
... 这个符号恐怕都不陌生,这个是一个展开运算符,主要用来展开数组,如下面这个例子:
arr = [1,2,3];
arr1 = [4,5,6];
arr2 = [...arr,...arr1]; //arr2 = [1,2,,3,4,5,6]
但是他还有其他的用法:
1.复制一个对象给另一个对象{...对象名}。此时这两个对象并没有什么联系了
const p1 = {name: "张三", age: "18", sex: "女"}
const p2 = {...p1};
p1.name = "sss";
console.log(p2) //{name: "张三", age: "18", sex: "女"}
2.在复制的时候,合并其中的属性
const p1 = {name: "张三", age: "18", sex: "女"}
const p2 = {...p1, name: "111", hua: "ss"};
p1.name = "sss";
console.log(p2) //{name: "111", age: "18", sex: "女",hua:"ss"}
注意!! {...P}并不能展开一个对象
props传递一个对象,是因为babel+react使得{..p}可以展开对象,但是只有在标签中才能使用
对于props限制
很多时候都想要传递的参数进行相应的限制,比如:限制传递参数的类型,参数的默认值等等
react对此提供了相应的解决方法:
- propTypes:类型检查,还可以限制不能为空
- defaultProps:默认值
<script type="text/babel">
class Person extends React.Component{
render(){
//props是只读的
return (
<ul>
<li>{this.props.name}</li>
<li>{this.props.age}</li>
<li>{this.props.sex}</li>
</ul>
)
}
//对组件的属性对其进行限制
static propTypes = {
name: PropTypes.string.isRequired, //限定name是string类型,并且必须要传递
sex: PropTypes.string, //限定sex是string类型
speak: PropTypes.func //限定speak是function类型
}
//指定默认的标签属性
static defaultProps = {
sex: "不男不女",
age: 18
}
}
//在js中可以使用{...p}来复制一个对象,但是这个地方并不是复制对象,而是babel+react通过展开运算符,展开了一个对象
//但是只能在标签中进行使用
//const p = {name: "张三", age: "18", sex: "女"} {14}就代表的是数值
//ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
ReactDOM.render(<Person name="sss" age={14} speak="8"/>,document.getElementById("div"));
function speak(){
console.log("这个是一个函数")
}
</script>
</html>
函数式组件的使用:
函数在使用props的时候,是作为参数进行使用的(props);
function Person(props){
return (
<ul>
<li>{props.name}</li>
<li>{props.age}</li>
<li>{props.sex}</li>
</ul>
)
}
Person.defaultProps = {
name: 'ReoNa',
age: 22
}
Person.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
Refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
Refs主要提供了三种方式:
1. 字符串形式
在想要获取到一个DOM节点,可以直接在这个节点上添加ref属性。利用该属性进行获取该节点的值。
案例:给需要的节点添加ref属性,此时该实例对象的refs上就会有这个值。就可以利用实例对象的refs获取已经添加节点的值
<input ref="dian" type="text" placeholder="点击弹出" />
inputBlur = () => {
alert(this.refs.shiqu.value);
}
2. 回调形式
回调形式会在ref属性中添加一个回调函数。将该DOM作为参数传递过去。
如:ref里面就是一个回调函数,self就是该input标签。然后在将该DOM元素赋值给实例对象中的一个属性
注意:在组件更新时ref会首先清空,后重新收集ref,因此当输出ref时,会首先输出null,第二次输出真正的DOM。使用class事件可以避免该情况。
<input ref={self => { this.dian=self;console.log(self)}} placeholder="点击弹出" />
也可以将函数提取出来,在ref中进行调用
isRef = (self) => {
this.dian = self;
console.log(self)
}
<input ref={this.isRef} type="text" placeholder="点击弹出" />
3. API形式
React其实已经给我们提供了一个相应的API,他会自动的将该DOM元素放入实例对象中
如下:依旧先在DOM元素中添加一个ref元素
{/*<input ref={this.容器名称} type="text" placeholder="点击弹出" />*/}
<input ref={this.MyRef} type="text" placeholder="点击弹出" />
<input ref={this.MyRef1} type="text" placeholder="点击弹出" />
通过API,创建React的容器,相当于省略了回调的中间环节。但是这个容器是专门专用的,所以每一个ref都需要创建这个。该API会将DOM元素赋值给实例对象的名称为容器名称的属性的current【这个current是固定的】
{/*容器名称 = React.createRef()*/}
MyRef = React.createRef();
MyRef1 = React.createRef();
然后就可以使用了
btnOnClick = () => {
//创建之后,将自身节点,传入current中
console.log(this.MyRef.current.value);
}
官方提示我们不要过度的使用ref,如果发生时间的元素刚好是需要操作的元素,就可以使用事件去替代。
React(类似vue中的常见指令)
条件渲染(v-if v-show)
{
condition ? null : <div>display something</div>
}
{
condition && <div>display something</div>
}
{
<div className="{condition ? 'display' : 'hidden'}">display something</div>
}
html渲染(v-html)
__html为固定写法
{
<span dangerouslySetInnerHTML={
{
__html: text
}
}></span>
}
React事件
React的事件
- 通过onXxx属性指定事件处理函数
- React使用的都是自定义的事件,而不是原生的事件
- React中的事件是通过事件委托方式处理的,事件中必须返回的是函数
- 通过event.target得到发生事件的Dom元素对象
比如:
先声明一个事件,然后在根据事件创建相应的函数,根据事件的event参数,将DOM元素获取到。
<input onChange={this.saveName} type="text" name ="username"/>
saveName = (event) =>{
this.setState({name: event.target.value});
}
受控和非受控组件
先来说说受控组件:
使 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>
)
}
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 onchange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。
非受控组件:
非受控组件其实就是表单元素的值不会更新state。输入数据都是现用现取的。
如下:下面并没有使用state来控制属性,使用的是事件来控制表单的属性值。
class Login extends React.Component{
login = (event) =>{
event.preventDefault(); //阻止表单提交
console.log(this.name.value);
console.log(this.pwd.value);
}
render() {
return (
<form action="http://www.baidu.com" onSubmit={this.login}>
用户名:<input ref={self => this.name =self } type="text" name ="username"/>
密码:<input ref={self => this.pwd =self } type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
高级函数
1.如果函数的参数是函数
2.如果函数返回一个函数
函数的珂里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
如下,我们将上面的案例简化,创建高级函数:
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"));
不使用柯里化实现
class Login extends React.Component {
// 初始化状态
state = {
name: '',
pwd: ''
}
saveFormData = (type, event) => {
this.setState({[type]:event.target.value })
}
login = (event) => {
event.preventDefault()
}
render() {
return (
<form action="https://www.baidu.com" onSubmit={this.login}>
用户名:<input value={this.state.name} onChange={event => this.saveFormData('name', event)} type="text" />
密码<input value={this.state.pwd} onChange={event => this.saveFormData('pwd', event)} type="password"/>
<button>登录</button>
</form>
)
}
}
生命周期
(旧)
组件从创建到死亡,会经过一些特定的阶段
React组件中包含一系列钩子函数{生命周期回调函数},会在特定的时刻调用
我们在定义组件的时候,会在特定的声明周期回调函数中,做特定的工作
如下图是旧生命周期的结构图:
我们通过一个案例更详细的了解这个生命周期:
class A extends React.Component{
constructor(props){
console.log("A --- constructor")
super(props);
this.state = {num:1}
}
add = () => {
let {num} = this.state;
this.setState({num: num + 1});
// 强制更新
// this.forceUpdate();
}
render(){
console.log("A --- render");
return (
<div>
<h1>这个是第{this.state.num}个</h1>
<B name={this.state.num}/>
<button onClick={this.add}>点击加一</button>
</div>
)
}
// 在render之前执行
componentWillMount(){
console.log("A --- componentWillMount");
}
// 在render之后执行
componentDidMount(){
console.log("A --- componenetDidMount");
}
// 更新操作 setState之后执行,判断是否可以更新(true可以,false不可以)
shouldComponentUpdate(){
console.log("A --- shouldComponentUpdate");
return true;
}
// 组件将要更新之前
componentWillUpdate(){
console.log("A --- componentWillUpdate");
}
// 组件更新之后,该函数可以接受相应的参数
componentDidUpdate(){
console.log("A --- componentDidUpdate");
}
// 卸载组件之后
componentWillUnmonut(){
console.log("A --- componentWillUnmonut");
}
}
class B extends React.Component{
render(){
return(
<div>
<h1>这个是B组件,传递过来的是:{this.props.name}</h1>
</div>
)
}
// 父组件进行了更新,子组件先执行这个【注意,第一次传递数据的时候,并不执行】
componentWillReceiveProps(){
console.log("A --- componentWillReceiveProps");
}
}
ReactDOM.render(<A />,document.getElementById("div"));
(新)
在最新的react版本中,有些生命周期钩子被抛弃了,在官网中是这样说的:
我们得到最重要的经验是,过时的组件生命周期往往会带来不安全的编码实践,具体函数如下:
componentWillMount
componentWillReceiveProps
componentWillUpdate
这些生命周期方法经常被误解和滥用;此外,我们预计,在异步渲染中,它们潜在的误用问题可能更大。我们将在即将发布的版本中为这些生命周期添加 “UNSAFE_” 前缀。(这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后。)
由此可见,新版本中并不推荐持有这三个函数,取而代之的是带有UNSAFE_ 前缀的三个函数,比如: UNSAFE_ componentWillMount。即便如此,其实React官方还是不推荐大家去使用,在以后版本中有可能会去除这几个函数。
如下图是新的生命周期:
从图上可以看出,新生命周期和旧生命周期的区别主要有:
- 抛弃了上面所说的三个钩子函数【其实还可以使用】
- 新添加了两个钩子函数
class Count extends React.Component {
// constructor
constructor(props) {
super(props)
console.log('Count-constructor')
this.state = {num: 0}
}
// 被废除
// componentWillMount() {
// console.log('Count-componentWillMount')
// }
// render 方法之前调用,并且在初始挂载及后续更新时都会被调用
// 用处: 若 state 的值永远都取决与 props
static getDerivedStateFromProps(nextProps, state) {
console.log('getDerivedStateFromProps', nextProps, state)
return null
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate', prevProps, prevState)
return null
}
// 组件挂载完毕后调用
componentDidMount() {
console.log('Count-componentDidMount')
}
// 组件销毁时调用
componentWillUnmount() {
console.log('Count-componentWillUnmount')
}
// 组件更新前判断
shouldComponentUpdate() {
console.log('Count-shouldComponentUpdate')
return true
}
// 被废除
// componentWillUpdate() {
// console.log('Count-componentWillUpdate')
// }
// 组件更新后调用
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('Count-componentDidUpdate', prevProps, prevState, snapshot)
}
add = () => {
this.setState({num: this.state.num + 1})
}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
force = () => {
this.forceUpdate()
}
render() {
console.log('render')
return (
<div>
<h2>当前求和为{this.state.num}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>强制更新</button>
</div>
)
}
}
ReactDOM.render(<Count />, document.getElementById('test'));
现在重点说一下,新添加的钩子函数
static getDerivedStateFromProps(props, state)
首先,该函数会调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用;该函数必须是静态的;给组件传递的数据(props)以及组件状态(state),会作为参数到这个函数中;该函数也必须有返回值,返回一个Null或者state对象。因为初始化和后续更新都会执行这个方法,因此在这个方法返回state对象,就相当于将原来的state进行了覆盖,所以倒是修改状态不起作用。
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递componentDidUpdate()
。
补充一下:componentDidUpdate(prevProps, prevState, snapshot)
该生命周期函数,可以有三个参数:原始传过来的参数,最开始的状态,getSnapshotBeforeUpdate传递的值
关于更多关于生命周期的介绍,可以参考官方文档:
https://zh-hans.reactjs.org/docs/react-component.html#render
以上就是两个新添加的钩子函数,但是在现实开发中可能并不常用这两个。
**案例:在一个区域内,定时的输出以行话,如果内容大小超过了区域大小,就出现滚动条,但是内容不进行移动 **
如上面的动图:区域内部的内容展现没有变化,但是可以看见滚动条在变化,也就是说上面依旧有内容在输出,只不过不在这个区域内部展现。
实现:
【一些css样式,就不在这展示了】
首先我们先实现定时输出内容
我们可以使用state状态,改变新闻后面的值,但是为了同时显示这些内容,我们应该为state的属性定义一个数组。并在创建组件之后开启一个定时器,不断的进行更新state。更新渲染组件
class New extends React.Component {
state = {num: []};
//在组件创建之后,开启一个定时任务
componentDidMount() {
setInterval(() => {
let {num} = this.state;
const news = (num.length + 1);
this.setState({num: [news,...num]});
},2000);
}
render(){
return (
<div ref="list" className="list">{
this.state.num.map((n,index) => {
return <div className="news" key={index} >新闻{n}</div>
})
}
</div>
)
}
}
ReactDOM.render(<New />,document.getElementById("div"));
接下来就是控制滚动条了
我们在组件渲染到DOM之前获取组件的高度,然后用组件渲染之后的高度减去之前的高度就是一条新的内容的高度,这样在不断的累加到滚动条位置上。
getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight;
}
componentDidUpdate(preProps,preState,height) {
this.refs.list.scrollTop += (this.refs.list.scrollHeight - height);
}
这样就实现了这个功能。
Diff算法
提到这个算法,就必须说一下关于Key的事情了。
其实每个组件中的每个标签都会有一个key,只不过有的必须显示的指定,有的可以隐藏。
如果生成的render出来后就不会改变里面的内容,那么你不需要指定key(不指定key时,React也会生成一个默认的标识),或者将index作为key,只要key不重复即可。
但是如果你的标签是动态的,是有可能刷新的,就必须显示的指定key。必须上面案使用map进行便利的时候就必须指定Key:
this.state.num.map((n,index) => {
return <div className="news" key={index}>新闻{n}</div>
})
这个地方虽然显示的指定了key,但是官网并不推荐使用Index作为Key去使用;
这样会很有可能会有效率上的问题
举个例子:
在一个组件中,我们先创建了两个对象,通过循环的方式放入< li>标签中,此时key使用的是index。
person: [
{id: 1, name: "张三", age: 18},
{id: 2, name: "李四", age: 19}
]
this.state.person.map((preson,index) => {
return <li key={index}>{preson.name}</li>
})
此时,我们想在点击按钮之后动态的添加一个对象,并且放入到li标签中,在重新渲染到页面中。
我们通过修改State来控制对象的添加。
<button onClick={this.addObject}>点击增加对象</button>
addObject = () => {
let {person} = this.state;
const p = {id: (person.length + 1),name: "王五",age: 20};
this.setState({person: [p,...person]});
}
这样看,虽然完成了功能。但是其实存在效率上的问题,我们先来看一下两个前后组件状态的变化:
我们发现,组件第一个变成了王五,张三和李四都移下去了。因为我们使用Index作为Key,这三个标签的key也就发生了改变【张三原本的key是0,现在变成了1,李四的key原本是1,现在变成了2,王五变成了0】在组件更新状态重新渲染的时候,就出现了问题:
因为react是通过key来比较组件标签是否一致的,拿这个案例来说:
首先,状态更新导致组件标签更新,react根据Key,判断旧的虚拟DOM和新的虚拟DOM是否一致
key = 0 的时候 旧的虚拟DOM 内容是张三 新的虚拟DOM为王五 ,react认为内容改变,从而重新创建新的真实DOM.
key = 1 的时候 旧的虚拟DOM 内容是李四,新的虚拟DOM为张三,react认为内容改变,从而重新创建新的真实DOM
key = 2 的时候 旧的虚拟DOM没有,创建新的真实DOM
这样原本有两个虚拟DOM可以复用,但都没有进行复用,完完全全的都是新创建的;这就导致效率极大的降低。
其实这是因为我们将新创建的对象放在了首位,如果放在最后其实是没有问题的,但是因为官方并不推荐使用Index作为key值,我们推荐使用id作为key值。从而完全避免这样的情况。
用index作为key可能会引发的问题:
若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 界面效果没问题,但效率低。
如果结构中还包含输入类的DOM:
会产生错误DOM更新 界面有问题。
注意! 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
开发如何选择key?
最好使用每一条数据的唯一标识作为key 比如id,手机号,身份证号
如果确定只是简单的展示数据,用Index也是可以的
而这个判断key的比较规则就是Diff算法
Diff算法其实就是react生成的新虚拟DOM和以前的旧虚拟DOM的比较规则:
如果旧的虚拟DOM中找到了与新虚拟DOM相同的key:
- 如果内容没有变化,就直接只用之前旧的真实DOM
- 如果内容发生了变化,就生成新的真实DOM
如果旧的虚拟DOM中没有找到了与新虚拟DOM相同的key:
- 根据数据创建新的真实的DOM,随后渲染到页面上
React脚手架
react提供了一个用于创建react项目的脚手架库:create-react-app
创建项目并启动
1.全局安装:npm i -g create-react-app
2.创建项目:create-react-app 项目名
在这一步,有可能会出现:
这样可以直接使用:npx create-react-app 项目名
3.等待下载完成,进入项目文件夹,运行一下
比如,我这的项目名称是hello,就先进入hello文件夹
cd hello
npm start //启动这个项目
这个时会自动的打开浏览器,展现这个项目:
项目的目录结构
这里面最主要的还是这个Index.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!--%PUBLIC_URL%表示public文件夹的路径-->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!--用于开启理想视口,用于移动端页面的适配-->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--用于配置浏览器地址栏的颜色(仅支持安卓手机浏览器)-->
<meta name="theme-color" content="#000000" />
<!--描述网页信息的-->
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--用于指定网页添加到手机主屏幕后的图标(仅仅支持ios)-->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--应用加壳时候的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 浏览器不支持JS的运行的时候展现 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
src文件:
这里面其实最主要的就是App.js以及index.js,一个是组件,一个是将组件渲染到页面中的。
第一个脚手架应用
1.我们保持public中的Index.html不变
2.修改src下面的APP.js以及index.js文件
App.js: 【注意:创建好的组件一定要暴露出去】
//创建外壳组件APP
import React from 'react'
class App extends React.Component{
render(){
return (
<div>Hello word</div>
)
}
}
export default App
index.js: 【主要的作用其实就是将App这个组件渲染到页面上】
//引入核心库
import React from 'react'
import ReactDOM from 'react-dom'
//引入组件
import App from './App'
ReactDOM.render(<App />,document.getElementById("root"))
这样在重新启动应用,就成功了。
我们也不建议这样直接将内容放入App组件中,尽量还是用内部组件。
我们在顶一个Hello组件:
import React,{Componet} from 'react'
export default class Hello extends Componet{
render() {
return (
<h1>Hello</h1>
)
}
}
在App组件中,进行使用
class App extends Component{
render(){
return (
<div>
<Hello />
</div>
)
}
}
这样的结果和前面是一样的。
但是由于普通的Js和组件都是js,所一最好组件使用jsx去展示。
样式冲突
当组件逐渐增多起来的时候,我们发现,组件的样式也是越来越丰富,这样就很有可能产生两个组件中样式名称有可能会冲突,这样会根据引入App这个组件的先后顺序,后面的会覆盖前面的,
为了避免这样的样式冲突,我们采用下面的形式:
1.将css文件名修改: hello.css --- >hello.module.css
2.引入并使用的时候改变方式:
import React,{Component} from 'react'
import hello from './hello.module.css' //引入的时候给一个名称
export default class Hello extends Component{
render() {
return (
<h1 className={hello.title}>Hello</h1> //通过大括号进行调用
)
}
}
功能界面的组件化编码流程
1.拆分组件:拆分界面,抽取组件
2.实现静态组件
3.实现动态组件
- 动态的显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件
- 交互
注意事项:
1.拆分组件、实现静态组件。注意className、style的写法
2.动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在自身的state中
- 某些组件使用:放在他们共同的父组件中【状态提升】
3.关于父子组件之间的通信
- 父组件给子组件传递数据:通过props传递
- 子组件给父组件传递数据:通过props传递,要求父组件提前给子组件传递一个函数
4.注意defaultChecked 和checked区别,defalutChecked只是在初始化的时候执行一次,checked没有这个限制,但是必须添加onChange方法类似的还有:defaultValue 和value
5.状态在哪里,操作状态的方法就在哪里
react ajax
React本身只关注与页面,并不包含发送ajax请求的代码,所以一般都是集成第三方的一些库,或者自己进行封装。
推荐使用axios。
在使用的过程中很有可能会出现跨域的问题,这样就应该配置代理。
所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port), 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域 。
那么react通过代理解决跨域问题呢
方法一
在package.json中追加如下配置
"proxy":"请求的地址" "proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
方法二
第一步:创建代理配置文件
在src下创建配置文件:src/setupProxy.js
编写setupProxy.js配置具体代理规则:
jsconst 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': ''} }) ) }
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
兄弟之间进行通信
这就要借助消息订阅和发布机制。
举个例子来说就是张三想要跟李四进行通信,张三就需要订阅一个消息【比如A消息】,李四想要给张三数据,就必须发布一个A消息,在发布的同时将数据放入消息中,因为张三订阅了名称为A的消息,此时就能接受到李四发布的消息,从而获取到数据。
这就有点类似于看报纸,甲想要知道每天都发生什么事情,于是订阅了每天日报,乙每天都会发布这个每天日报,因为甲订阅了,于是乙就会每天就给甲方推送,甲方从而获取数据。
在消息订阅和发布中,我们可以使用PubSubJs进行通信:
引入PubSubJs:
import PubSub from 'pubsub-js'
订阅消息:
PubSub.subscribe("getSate", (_, data) => {
console.log(data)
})
PubSub.subscribe("订阅的消息名称",回调函数,第一个参数是消息名称,可以使用_来占位,第二个是传递的数据)
发布消息:
PubSub.publish("getSate", {isFrist: false, isLoad: true})
PubSub.publish("订阅的消息名称",传递的数据)
async和await
async:
该关键字是放在函数之前的,使得函数成为一个异步函数,他最大的特点就是将函数回封装成Promise,也就是被他修饰的函数的返回值都是Promise对象。而这个Promise对象的状态则是由函数执行的返回值决定的。
如果返回的是一个非promise对象,该函数将返回一个成功的Promise,成功的值则是返回的值;
如果返回的是一个promise对象,则该函数返回的就是该promise对应的状态。
await
await右边是一个表达式,如果该表达式返回的是一个Promise对象,则左边接收的结果就是该Promise对象成功的结果,如果该Promise对象失败了,就必须使用try..catch来捕获。如果该表达式返回的是是一个不是promise对象,则左边接受的就是该表达式的返回值。
当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。
举个例子:
f1 = () => {
return new Promise((resolve,reject) => {
// resolve(1);
reject("错误")
})
}
async function test() {
try {
const p = await f1();
console.log(p)
} catch(error) {
console.error(error)
}
}
test();
fetch
以前发送请求,使用ajax或者axios,现在还可以使用fetch。这个是window自带的,和xhr是一个级别的。
可以查看这个文章,写的真的不错:fetch
React路由
SPA
单页Web应用(single page web application,SPA)。整个应用只有一个完整的页面。
点击页面中的链接不会刷新页面,只会做页面的局部更新。
数据都需要通过ajax请求获取,并在前端异步展现
什么是路由
一个路由其实就是一个映射关系(k:v)
key为路径,value可能是function 或者是 component
后端路由:
value是function,用来处理客户端提交的请求
注册路由:router.get(path,function(req,res))
工作过程:当node接收一个请求的时候,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应的数据
前端路由:
浏览器端路由,value是Component,用于展示页面内容
注册路由:< Route path="/test" component={Test}>
工作过程:当浏览器的path变为/test的时候,当前路由组件就会变成Test组件
前端路由的原理
这个主要是依靠于history,也就是浏览器的历史记录。
浏览器上的记录其实就是一个栈,前进一次就是入栈,后退一次就是出栈。
并且历史记录上有一个监听的方法,可以时时刻刻监听记录的变化。从而判断是否改变路径。History
react-router-dom
react的路由有三类:
web【主要适用于前端】,native【主要适用于本地】,anywhere【任何地方】
在这主要使用web也就是这个标题 react-router-dom
基本的使用:
导航中的a标签改写成Link标签
< Link to="/路径" >xxx< /Link>
展示区写Route标签进行路径的匹配
< Route path = '/路径' component={组件名称}>
< App>最外侧包裹了一个< BrowserRouter>或者< HashRouter>
<div className="list-group">
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
<div className="panel-body">
{/* 注册路由,也就是写对应的关系 */}
<Route path="/about"component={About}/>
<Route path="/home"component={Home}/>
</div>
// index.js:
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
,document.getElementById("root"))
那么使用Link代替a标签之后,在页面上会是什么呢,我们发现其实页面上也是把link转化为了a标签
路由组件以及一般组件
写法不一样
一般组件:< Demo>
路由组件:< Route path="/demo" component ={Demo}/>
存放的位置一般不同
一般组件:components
路由组件:pages
接收的内容【props】
一般组件:写组件标签的时候传递什么,就能收到什么
路由组件:接收到三个固定的属性【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"
NavLink
因为Link不能够改变标签体,因此只适合用于一些写死的标签。而如果想要有一些点击的效果,使用NavLink.
如下代码,就写了ctiveClassName,当点击的时候就会触发这个class的样式
{/*NavLink在点击的时候就会去找activeClassName="ss"所指定的class的值,如果不添加默认是active
这是因为Link相当于是把标签写死了,不能去改变什么。*/}
<NavLink ctiveClassName="ss" className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/home">Home</NavLink>
但是可能一个导航又很多标签,如果这样重复的写NavLink也会造成很多的重复性的代码问题。
因此可以自定义一个NavLink:
// 通过{...对象}的形式解析对象,相当于将对象中的属性全部展开
// <NavLink to = {this.props.to} children = {this.props.children}/>
<NavLink className="list-group-item" {...this.props}/>
在使用的时候:直接写每个标签中不一样的部分就行,比如路径和名称
{/*将NavLink进行封装,成为MyNavLink,通过props进行传参数,标签体内容props是特殊的一个属性,叫做children */}
<MyNavLink to="/about">About</MyNavLink>
样式错误
拿上面的案例来说:
这里面会有一个样式:
此时,加载该样式的路径为:
但是在写路由的时候,有的时候就会出现多级目录,
<MyNavLink to="/cyk/about" >About</MyNavLink>
<Route path="/cyk/about"component={About}/>
这个时候就在刷新页面,就会出现问题:
样式因为路径问题加载失败,此时页面返回public下面的Index.html
解决这个问题,有三个方法:
样式加载使用绝对位置
jsx<link href="/css/bootstrap.css" rel="stylesheet">
使用 %PUBLIC_URL%
jsx<link href="%PUBLIC_URL%/css/bootstrap.css" rel="stylesheet"
使用HashRouter
因为HashRouter会添加#,默认不会处理#后面的路径,所以也是可以解决的
模糊匹配和精准匹配
react默认是开启模糊匹配的。
比如:
<MyNavLink to="/home/a/b" >Home</MyNavLink>
此时该标签匹配的路由,分为三个部分 home a b;将会根据这个先后顺序匹配路由。
如下就可以匹配到相应的路由:
<Route path="/home"component={Home}/>
但是如果是下面这个就会失败,也就是说他是根据路径一级一级查询的,可以包含前面那一部分,但并不是只包含部分就可以。
<Route path="/a" component={Home}/>
当然也可以使用这个精确的匹配 exact=
如以下:这样就精确的匹配/home,则上面的/home/a/b就不行了
<Route exact={true} path="/home" component={Home}/>
// 或者
<Route exact path="/home" component={Home}/>
初始化路由
在配置好路由,最开始打开页面的时候,应该是不会匹配到任意一个组件。这个时候页面就显得极其不合适,此时应该默认的匹配到一个组件。
此时就需要使用Redirect进行默认匹配了。如下的代码就是默认匹配/home路径所到的组件
<Switch>
<Route path="/about"component={About}/>
{/* exact={true}:开启严格匹配的模式,路径必须一致 */}
<Route path="/home" component={Home}/>
{/* Redirect:如果上面的都没有匹配到,就匹配到这个路径下面 */}
<Redirect to="/home"/>
</Switch>
就可以做到如下的效果:
嵌套路由
简单来说就是在一个路由组件中又使用了一个路由,就形成了嵌套路由。
举个例子来说:
我们在home这个路由组件中又添加两个组件:
// APP.jsx:
<Route path="/home" component={Home}/>
// Home.jsx:
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
<Redirect to="/home/message"/>
</Switch>
</div>
react中路由的注册是有顺序的,因此在匹配的时候也是按照这个顺序进行的,因此会先匹配父组件中的路由
比如上面的 /home/news的路由处理过程:
1.因为父组件home的路由是先注册的,因此在匹配的时候先去找home的路由,也就是根据/home/news先模糊匹配到/home
2.在去Home组件里面去匹配相应的路由,从而找到了/home/news进行匹配,因此找到了News组件。
但是如果开启精确匹配,就会在第一步的时候卡住,这个时候就走不下去了。因此不要轻易的使用精确匹配
路由传参
params参数
// 路由链接传递参数
<Link to="/home/message/1">News</Link>
// 注册路由声明接收参数
<Route path="/home/message/:id" component={News} />
// 在 News路由组件中接收
const {id} = this.props.match.params
search参数
// 路由链接传递参数
<Link to="/home/message/?id=1&title=news">News</Link>
// 注册路由声明接收参数
<Route path="/home/message" component={News} />
// 在 News路由组件中接收
// 接收到的是字符串形式 '?id=1&title=news' 需要自己整理数据
this.props.location.search
state 参数
// 路由链接传递参数
<Link to={{"/home/message", state:{id:1, title: 'news'}}}>News</Link>
// 注册路由声明接收参数
<Route path="/home/message" component={News} />
// 在 News路由组件中接收
const {id, title} = this.props.location.state
push 和 replace
// 默认为 push 可以手动设置为replace
<Link replace={true} to="/home/message/1">News</Link>
编程时路由导航
routerJump = () => {
// 携带 params
this.props.history.push(`/About/News/${id}`)
// 携带 search
this.props.history.push(`/About/News/?id=${id}`)
// 携带 state
this.props.history.push('/About/News', state:{id:1, title: 'news'})
}
// push 跳转
this.props.history.push(path, state)
// replace 跳转
this.props.history.replace(path, state)
// goForward 跳转 前进一步
this.props.history.goForward()
// goBack 跳转 后退一步
this.props.history.goBack()
// go 跳转
this.props.history.go(n)
withRoute
只有路由组件
中才有props.history之类的方法。而一般组件
跳转需要使用到withRoute。
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
class News extends Component {
render() {
return (
<div>
news
</div>
)
}
}
// 使用withRouter(News)暴露,才能得到路由组件中专属的props
export default withRouter(News)
BrowserRouter 和 HashRouter
底层原理不一样:
BrowserRouter
使用的是H5的history API 不兼容IE9及以下版本。HashRouter
使用的是URL的哈希值。url表现形式不一样
BrowserRouter
的路径中没有#HashRouter
的路径中包含#刷新后对路由state参数的影响
BrowserRouter
没有任何影响,因为state保存在history对象中HashRouter
刷新后会导致路由state参数的丢失备注:
HashRouter
可以用于解决一些路径错误相关的问题
Redux
Redux理解
redux是什么
- redux 是一个专门用于状态管理的JS库(不是react插件库)
- 它可以在react,angular,vue等项目中使用,但基本与react配合使用。
- 作用:集中式管理react应用中多组件共享的状态
什么情况使用redux
- 某个组件的状态,需要让其他组件可以随时共享
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用,如果不用比较吃力才考虑使用
工作流程
三个核心概念
action
动作的对象。
包含2个属性。
type
:标识属性, 值为字符串, 唯一, 必要属性data
:数据属性, 值类型任意, 可选属性例子:
{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }
reducer
- 用于初始化状态、加工状态。
- 加工时,根据旧的state和action, 产生新的state的
纯函数
。 - 有几个组件就有几个reducer。
store
- 将state、action、reducer联系在一起的对象。
- 如何得到此对象
import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
- 此对象的功能
getState()
: 得到statedispatch(action)
: 分发action, 触发reducer调用, 产生新的statesubscribe(listener)
: 注册监听, 当产生了新的state时, 自动调用
中间件
redux-thunk
用于处理 action 中的异步操作
求和案例
-src
-redux
-constant.js
-count_action.js
-count_reducer.js
-store.js
// src/Count/index.jsx
import React, { Component } from 'react'
import store from '../../redux/store'
import { createAddAction, createSubtractAction, createAddAsyncAction } from '../../redux/count_action'
class Count extends Component {
// componentDidMount() {
// // 检测redux状态变化
// store.subscribe(() => {
// this.setState({})
// })
// }
state = { count: 0 }
add = () => {
const { value } = this.selectNum
store.dispatch(createAddAction(Number(value)))
}
subtract = () => {
const { value } = this.selectNum
store.dispatch(createSubtractAction(Number(value)))
}
addOdd = () => {
const { value } = this.selectNum
const count = store.getState()
if (count % 2 !== 0) {
store.dispatch(createAddAction(Number(value)))
}
}
addAsync = () => {
const { value } = this.selectNum
store.dispatch(createAddAsyncAction(Number(value), 1000))
}
render() {
return (
<div>
<h1>当前求和为:{ store.getState() }</h1>
<select ref={ c => this.selectNum = c }>
<option value="1" defaultValue>1</option>
<option value="2" defaultValue>2</option>
<option value="3" defaultValue>3</option>
</select>
<button onClick={ this.add }>+</button>
<button onClick={ this.subtract }>-</button>
<button onClick={ this.addOdd }>奇数加</button>
<button onClick={ this.addAsync }>异步加</button>
</div>
)
}
}
export default Count
// src/redux/constant.js
/*
* 该文件用于定义action对象中type类型
* */
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
// src/redux/count_action.js
/*
* 该文件专门为Count组件生成action对象
* */
import { INCREMENT, DECREMENT } from './constant'
export const createAddAction = value => ({
type: INCREMENT,
value
})
export const createAddAsyncAction = (value, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createAddAction(Number(value)))
}, time)
}
}
export const createSubtractAction = value => ({
type: DECREMENT,
value
})
// src/redux/count_reducer.js
/*
* 1. 该文件是用于创建一个为Count组件服务的reducer,reducer本质是一个函数
* 2. reducer 函数会接收到两个参数 per action
* */
import { INCREMENT, DECREMENT } from './constant'
export default function countReducer(per = 0, action) {
const {type, value} = action
switch (true) {
case type === INCREMENT:
return per + value
case type === DECREMENT:
return per - value
default:
return per
}
}
// src/redux/store.js
import { legacy_createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk'
// applyMiddleware(thunk)
// 当action的回调是一个异步函数时,需要使用中间件处理
export default legacy_createStore(countReducer, applyMiddleware(thunk))
// index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import store from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
// 监听store数据变化,自动render
store.subscribe(() => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})
react-redux
简介
- 明确两个概念
- UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
- 容器组件:负责和redux通信,将结果交给UI组件。
- 如何创建一个容器组件—靠react-redux 的
connect
函数:mapStateToProps
:映射状态,返回值是一个对象。返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value。mapDispatchToProps
:映射操作状态的方法,返回值是一个对象。返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value。
- 备注:容器组件中的store是靠
props
传进去的,而不是在容器组件中直接引入
案例
redux文件夹保存不变
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'
import store from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={ store }>
<App/>
</Provider>
</React.StrictMode>
)
// App.jsx
import React, {Component} from 'react'
import Count from './containers/Count'
class App extends Component {
render() {
return (
<div>
<Count />
</div>
);
}
}
export default App
// src/containers/Count/index.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createAddAction, createSubtractAction, createAddAsyncAction } from '../../redux/count_action'
// 映射状态
// const mapStateToProps = (state) => ({ n: state })
//
// 映射操作状态的方法
// const mapDispatchToProps = (dispatch) => ({
// add: number => dispatch(createAddAction(number)),
// subtract: number => dispatch(createSubtractAction(number)),
// addAsync: (number, time) => dispatch(createAddAsyncAction(number, time)),
// })
//
// export default connect(mapStateToProps, mapDispatchToProps)(CountUI)
// 定义UI组件
class Count extends Component {
state = { count: 0 }
add = () => {
const { value } = this.selectNum
this.props.add(value * 1)
}
subtract = () => {
const { value } = this.selectNum
this.props.subtract(value * 1)
}
addOdd = () => {
const { value } = this.selectNum
if (this.props.n % 2 !== 0) {
this.props.add(value * 1)
}
}
addAsync = () => {
const { value } = this.selectNum
this.props.addAsync(value * 1, 500)
}
render() {
return (
<div>
<h1>当前求和为:{ this.props.n }</h1>
<select ref={ c => this.selectNum = c }>
<option value="1" defaultValue>1</option>
<option value="2" defaultValue>2</option>
<option value="3" defaultValue>3</option>
</select>
<button onClick={ this.add }>+</button>
<button onClick={ this.subtract }>-</button>
<button onClick={ this.addOdd }>奇数加</button>
<button onClick={ this.addAsync }>异步加</button>
</div>
)
}
}
// 简写形式
export default connect(
state => ({ n: state }),
{
add: createAddAction,
subtract: createSubtractAction,
addAsync: createAddAsyncAction
}
)(Count)
数据共享
- 定义一个Person组件,和Count组件通过redux共享数据。
- 为Person组件编写:reducer、action,配置constant常量。
- 重点:Person的reducer和Count的Reducer要使用combineReducers进行合并。合并后的总状态是一个对象!!!
- 交给store的是总reducer,最后注意在组件中取出状态的时候,记得“取到位”。
- reducers进行浅比较,数组、对象只比较地址,所以地址不变就不会重新渲染。
案例
目录结构
src
-containers
-Count
-Person
-redux
-action
-count.js
-person.js
-reducers
-count.js
-person.js
-constant.js
-store.js
-App.jsx
-index.js
// App.jsx
import React, {Component} from 'react'
import Count from './containers/Count'
import Person from './containers/Person'
class App extends Component {
render() {
return (
<div>
<Count />
<Person />
</div>
);
}
}
export default App
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'
import store from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={ store }>
<App/>
</Provider>
</React.StrictMode>
)
// src/containers/Count
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createAddAction, createSubtractAction, createAddAsyncAction } from '../../redux/action/count'
// 定义UI组件
class Count extends Component {
state = { count: 0 }
add = () => {
const { value } = this.selectNum
this.props.add(value * 1)
}
subtract = () => {
const { value } = this.selectNum
this.props.subtract(value * 1)
}
addOdd = () => {
const { value } = this.selectNum
if (this.props.n % 2 !== 0) {
this.props.add(value * 1)
}
}
addAsync = () => {
const { value } = this.selectNum
this.props.addAsync(value * 1, 500)
}
render() {
return (
<div>
<h1>当前求和为:{ this.props.n } {this.props.obj.length}</h1>
<select ref={ c => this.selectNum = c }>
<option value="1" defaultValue>1</option>
<option value="2" defaultValue>2</option>
<option value="3" defaultValue>3</option>
</select>
<button onClick={ this.add }>+</button>
<button onClick={ this.subtract }>-</button>
<button onClick={ this.addOdd }>奇数加</button>
<button onClick={ this.addAsync }>异步加</button>
</div>
)
}
}
// 简写形式
export default connect(
// 数据共享
state => ({ n: state.count, obj: state.person }),
{
add: createAddAction,
subtract: createSubtractAction,
addAsync: createAddAsyncAction
}
)(Count)
// src/containers/Person
import React, { Component } from 'react'
import { nanoid } from 'nanoid'
import { connect } from 'react-redux'
import { createAddPersonAction } from '../../redux/action/person'
class Person extends Component {
addPerson = () => {
const name = this.nameNode.value
const age = this.ageNode.value
const personObj = { id: nanoid(), name, age }
this.props.addPerson(personObj)
}
render() {
return (
<div>
<h1>我是person {this.props.n}</h1>
<input ref={ c => this.nameNode = c } type="text" placeholder="输入名字"/>
<input ref={ c => this.ageNode = c } type="text" placeholder="输入年龄"/>
<button onClick={ this.addPerson }>点击</button>
<ul>
{
this.props.obj.map(item => {
return <li key={ item.id }>{ item.name }--{ item.age }</li>
})
}
</ul>
</div>
)
}
}
export default connect(
state => ({ obj: state.person, n: state.count }),
{
addPerson: createAddPersonAction
}
)(Person)
// src/redux/action/count.js
/*
* 该文件专门为Count组件生成action对象
* */
import { INCREMENT, DECREMENT } from '../constant'
export const createAddAction = value => ({
type: INCREMENT,
value
})
export const createAddAsyncAction = (value, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createAddAction(Number(value)))
}, time)
}
}
export const createSubtractAction = value => ({
type: DECREMENT,
value
})
// src/redux/action/person.js
import { ADD_PERSON } from '../constant'
export const createAddPersonAction = personObj => ({
type: ADD_PERSON,
value: personObj
})
// src/redux/reducers/count.js
/*
* 1. 该文件是用于创建一个为Count组件服务的reducer,reducer本质是一个函数
* 2. reducer 函数会接收到两个参数 per action
* */
import { INCREMENT, DECREMENT } from '../constant'
export default function countReducer(per = 0, action) {
const {type, value} = action
switch (true) {
case type === INCREMENT:
return per + value
case type === DECREMENT:
return per - value
default:
return per
}
}
// src/redux/reducers/person.js
import {ADD_PERSON} from '../constant'
export default function personReducers(per = [], action) {
const {type, value} = action
switch (type) {
case ADD_PERSON:
return [...per, value]
default:
return per
}
}
// src/redux/store.js
import { legacy_createStore, applyMiddleware, combineReducers } from 'redux'
import countReducers from './reducers/count'
import personReducers from './reducers/person'
import thunk from 'redux-thunk'
export default legacy_createStore(combineReducers({
count: countReducers,
person: personReducers
}), applyMiddleware(thunk))
// src/redux/constant.js
/*
* 该文件用于定义action对象中type类型
* */
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'addPerson'
持久化
import { legacy_createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk'
import {persistStore, persistReducer} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'root',
storage,
// 白名单,持久化
whitelist: ['count']
// 黑名单,不持久化
blacklist: ['xxx']
}
const persistedReducer = persistReducer(persistConfig, countReducer)
const store= legacy_createStore(persistedReducer, applyMiddleware(thunk))
const persistor = persistStore(store)
export {
store,
persistor
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'
import { store, persistor } from './redux/store'
import {PersistGate} from 'redux-persist/integration/react'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={ store }>
<PersistGate loading={null} persistor={persistor}>
<App/>
</PersistGate>
<App/>
</Provider>
</React.StrictMode>
)
Immutable
不可变数据 (Immutable Data )就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是持久化数据结构( Persistent Data Structure),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的s性能损耗,Immutable 使用了 结构共享(Structural Sharing),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
import { Map, List, is, fromJS } from 'immutable'
const obj = {
name: 'John',
age: 30
}
const oldObj = Map(obj)
const newObj = oldObj.set('age', 31)
console.log(oldObj.get('age'), newObj.get('age'))
const list = List([1, 2, 3, 4])
const newList = list.push(5)
console.log(list, newList)
console.log(list.get(4), newList.get(4)) // undefined 5
console.log(list.size, newList.size) // 4 5
const arr = [1, 2, 3, 4]
console.log(List.isList(list), List.isList(arr)) // true false
const map = Map({
a: 1,
b: 2,
c: 3
})
const anotherMap = Map({
a: 1,
b: 2,
c: 3
})
console.log(map == anotherMap) // false
console.log(map === anotherMap) // false
console.log(map.equals(anotherMap)) // true
console.log(is(map, anotherMap)) // true
const objJs = {
a: [1, 2, 3],
b: 1,
c: {
d: 4
}
}
const newObjJs = fromJS(objJs)
console.log(newObjJs)
redux 中使用
import { Map } from 'immutable'
import ActionTypes from '../actions'
const initialState = Map({
count: 0
})
export default (state = initialState, action) => {
switch (action.type) {
case ActionTypes.INCREAMENT:
return state.set('count', state.get('count') + 1) // 使用set或setIn来更改值, get或者getIn来取值
case ActionTypes.DECREAMENT:
return state.set('count', state.get('count') - 1)
default:
return state
}
}
扩展
1. setState
setState更新状态的2种写法
- setState(stateChange, [callback])-----------对象式的setState
- stateChange为状态改变对象(该对象可以体现出状态的更改)
- callback是可选的回调函数,它在更新状态完毕,界面也更新后(render调用后)才被调用
- setState(updater, [callback])-------------函数式的setState
- updater为返回stateChange对象的函数
- unpater可以接收到state和props
- callback是可选的回调函数,它在状态更新,界面也更新后(render调用后)才被调用
总结:
- 对象式的setState是函数式的setState的简写方式
- 使用原则:
- 如果新状态不依赖原状态 ===> 使用对象方式
- 如果新状态依赖原状态 ===> 使用函数方式
- 如果需要在setState()执行后获取最新的状态数据,要在第二个callback函数中读取
// 对象式
state = { count: 0 }
add = () => {
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
}
// 函数式
state = { count: 0 }
add = () => {
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log(this.state.count)
})
}
2. lazyLoad
路由组件的lazyLoad
// 1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
import Loading from './pages/Loading'
const Home = lazy(() => import('./pages/Home'))
// 2. 通过<Suspense></Suspense>指定在加载得到路由打包文件前的自定义组件
<Suspense fallback={<Loading />}>
<Switch>
<Route path="/Home" component={ Home }/>
<Route path="/About" component={ About }/>
<Redirect to="/About" component={ About }/>
</Switch>
</Suspense>
3. Hooks
1. React Hooks是什么
(1). Hooks是react 16.8.0版本新增的新特性/新语法
(2). 可以让你的函数组件使用 state 以及其他的 react 特性
2. 三个常用的React Hooks
(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()
3. useState
(1). State Hook 让函数组件也可以用state状态,并进行状态数据的读写操作。
(2). 语法:const [xxx, setXxxx] = React.useState()
(3). useState() 说明:
参数:第一次初始化指定的值在内部作为缓存
返回值:包含两个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数
(4). setXxx() 2种写法
setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setXxx(value => newValue):参数作为函数,接收原本的状态值,内部用其覆盖原来的状态值
import React from 'react'
function Demo() {
const [count, setCount] = React.useState(0)
const add = () => {
// 第一种写法
// setCount(count + 1)
// 第二种写法
setCount(count => count + 1)
}
return (
<div>
<h1>当前求和为:{ count }</h1>
<button onClick={ add }>click</button>
</div>
);
}
export default Demo
3. useEffect
(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期)
(2). React中的副作用函数操作:
发 AJAX 请求、 设置订阅/启动定时器、手动修改dom
(3). 语法:
useEffect(() => {
// 此时可以执行任何副作用函数操作
return () => {
// 在组件卸载前执行
// 在此做一些收尾工作
}
},[stateValue]) // 如果指定[],回调函数只会在第一次render后执行
(4). 可以把 useEffect Hook 看成三个函数组合
componentDidMount()
componentDidUpdate()
componentWillUnmount()
(5). useLayoutEffect 与 useEffect 简单来说只是调用时机不同,在实际使用中如果想避免页面抖动(在 useEffect 中修改 dom 可能出现,因为 useEffect dom 树已经构建完成,重新改修dom会造成页面重绘。因此在需要修改 dom 可以使用 useLayoutEffect,其他情况优先使用 useEffect。)
React.useEffect(() => {
console.log(123)
return () => {
console.log('@@@')
}
},[])
4. useRef
(1). Ref Hook 可以在函数组件中存储/查找组件内的标签或者任意其他数据
(2). 语法:const ref = React.useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
5. useContext
(1). 一种组件间通信方式,常用于【祖组件】与【后代组件】间通信
(2). 创建createContext
(3). Provider 指定使用的范围
(4). 最后使用useContext
// 创造一个上下文
const C = createContext(null);
function App() {
const [n, setN] = useState(0)
return(
// 指定上下文使用范围,使用provider,并传入读数据和写入据
<C.Provider value={{n,setN}}>
这是爷爷
<Baba></Baba>
</C.Provider>
)
}
function Baba(){
return(
<div>
这是爸爸
<Child></Child>
</div>
)
}
function Child() {
// 使用上下文,因为传入的是对象,则接受也应该是对象
const {n, setN} = useContext(C)
const add = () => {
setN(n => n+1)
};
return(
<div>
这是儿子:n:{n}
<button onClick={add}>+1</button>
</div>
)
}
6. useCallBack (记忆函数)
(1). 防止因为组件重新渲染,导致方法被重新创建,起到缓存作用,只有第二个参数变化,才重新声明一次。
(2). useCallBack不要每个函数都包一下,否则就会变成反向优化,useCallBack本身就是需要一定性能的。
(3). useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变。
(4). useCallBack需要配合React.memo使用。
const handleClick = useCallback(() => {
console.log(count)
}, [count])
// 只有count 改变后,这个函数才会重新声明一次
// 如果传入空数组,那么就是第一次创建后会就会被缓存,如果后期改变了拿到的还是旧数据
// 如果不传入第二个参数,每次都会被重新声明一次,拿到的都是新数据
7. useMemo (记忆组件)
(1). useMemo 是一个 React Hook,它用于优化渲染性能。会将结果进行缓存,等到参数变化时,才会更新数据。类似于 vue 计算属性。
(2). useMemo 会接收一个箭头函数包裹的回调函数和依赖项数组,然后返回回调函数的计算结果。当依赖项数组中的某个值发生变化时,useMemo 会重新计算回调函数。如果依赖项没有发生变化,useMemo 会返回上一次计算的结果,这样可以避免不必要的计算。
(3). 优化性能:当组件重新渲染时,useMemo 可以避免重复执行开销较大的计算。
(4). 尽量保持引用不变:当使用引用类型(如对象或数组)作为依赖项时,useMemo 可以确保引用在依赖项未发生变化时保持不变。这有助于避免不必要的组件重新渲染。
// 语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
8.useReducer
(1). useReducer 是 useState的代替方案,用于 state 复杂变化
(2). useReducer 是单个组件状态管理,组件通讯还需要 props
(3). useReducer 通常配合 useContext 使用
import React, { createContext, useContext, useReducer } from 'react';
const C = createContext(null);
function Index(props) {
const init = {
a: 1,
b: 2
}
const reducer = (pre, action) => {
let newState = {...pre}
switch(action.type) {
case 'change-a':
newState.a = action.value
return newState
default:
return pre
}
}
const [state, dispatch] = useReducer(reducer, init)
return (
<div>
<C.Provider value={ {
state,
dispatch
} }>
<Child1/>
<Child2/>
<Child3/>
</C.Provider>
</div>
);
}
function Child1() {
const { dispatch } = useContext(C)
return (
<div onClick={() => {
dispatch({
type: 'change-a',
value: 3
})
}}>change</div>
)
}
function Child2() {
const { state } = useContext(C)
return (
<div>{ state.a }</div>
)
}
function Child3() {
const { state } = useContext(C)
return (
<div>{ state.b }</div>
)
}
export default Index;
9.自定义 hooks
当我们想在两个函数之间共享逻辑时,我们会把他提取到第三个函数中。
必须以"use"开头吗?必须如此,不遵循约定的化,无法判断某个函数是否包含对其内部 Hooks 的调用,React 将无法自动检测你的 Hooks 是否违法了 Hooks 规则。
4. Fragment
使用
<Fragment></Fragment>
<></>
作用
可以不用必须有一个真实的DOM标签,Fragment 可以用于遍历,可以有个key属性
5. Context
理解
一种组件间通信方式,常用于【祖组件】与【后代组件】间通信
使用
(1). 创建Context容器对象:
const UserNameContext = React.createContext(undefined)
(2). 渲染子组件时,外部包裹UserContext.Provider,用过value属性给后代组件传值:
class Demo extends Component {
state = { name: 'tom' }
render() {
return (
<div>
<h3>我是A组件: {this.state.name}</h3>
<UserNameContext.Provider value={ this.state.name }>
<B/>
</UserNameContext.Provider>
</div>
);
}
}
(3). 后代组件读取数据
// 第一种方式用于类组件
class C extends Component {
static contextType = UserNameContext
render() {
console.log(this)
return (
<div>
<h3>我是C组件</h3>
<div>接收A: {this.context}</div>
</div>
)
}
}
// 第二种方式用于类组件和函数组件
class C extends Component {
static contextType = UserNameContext
render() {
console.log(this)
return (
<div>
<h3>我是C组件</h3>
<div>接收A:
<UserNameContext.Consumer>
{
value => `${value}`
}
</UserNameContext.Consumer>
</div>
</div>
)
}
}
6. PureComponent
比较新旧 props 和 新旧 state 的值,决定 shouldComponentUpdate 返回 true 或 false ,从而决定是否重新 render。
注:如果你的 props 和 state 永远都会变化, 那 PureComponent 并不会比较快,因为 shallowEqual 也需要花时间。
import React, { PureComponent } from 'react';
class Demo extends PureComponent {
render() {
return (
<div>
<Child/>
</div>
);
}
}
7. render props
如何向组件内部动态传入带内容的结构(标签)
vue中:
使用slot技术,也就是通过组件标签传入结构 <A><B /></A>
react中:
使用children prop:通过组件标签传入结构
使用render prop:通过组件标签属性传入结构,一般用render函数属性
children props
class Demo extends Component {
state = { name: 'tom' }
render() {
return (
<div>
<h3>我是A组件: { this.state.name }</h3>
<B>
<C/>
</B>
</div>
);
}
}
class B extends Component {
render() {
return (
<div>
<h3>我是B组件</h3>
{this.props.children}
</div>
)
}
}
class C extends Component {
render() {
console.log(this)
return (
<div>
<h3>我是C组件</h3>
</div>
)
}
}
render props
class Demo extends Component {
render() {
return (
<div>
<h3>我是A组件</h3>
<B render={(name) => <C name={name}/>} />
</div>
);
}
}
class B extends Component {
state = {name: 'tom'}
render() {
return (
<div>
<h3>我是B组件</h3>
{this.props.render(this.state.name)}
</div>
)
}
}
class C extends Component {
render() {
console.log(this)
return (
<div>
<h3>我是C组件 {this.props.name}</h3>
</div>
)
}
}
8.Portal
提供了一个在父组件内包含DOM 结构,但在最外层渲染的方法。类似于 vue 中的 Teleport
import React from 'react';
import { createPortal } from 'react-dom'
function index(props) {
return createPortal(
<div>Dialog</div>, document.body
);
}
export default index;
9.forwardRef
forwardRef 接受一个函数作为参数,该函数有两个参数:props 和 ref。你可以在这个函数中定义组件的 JSX 渲染部分,并使用 ref 参数来传递给子组件中需要暴露的元素或组件实例。
import React, { forwardRef, useRef } from 'react';
function Index(props) {
const myRef = useRef()
return (
<div>
<Child ref={myRef} />
<button onClick={() => {
console.log(myRef.current)
}}>click</button>
</div>
);
}
const Child = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 使用 ref 回调将子组件的 inputRef 传递给父组件的 myRef
React.useImperativeHandle(ref, () => ({
getInputValue: () => {
return inputRef.current.value;
}
}));
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
});
export default Index;
10.memo
是 React 中的一个高阶函数(higher-order function),用于性能优化。它用于包装函数式组件,以便在组件的输入(props)没有发生变化时,避免重新渲染该组件,从而提高应用的性能。
import React, { memo, useState } from 'react';
function Index(props) {
const [num, setNum] = useState(0)
return (
<div>
<Child></Child>
<button onClick={() => {
setNum(num + 1)
}}>click--{num}</button>
</div>
);
}
const Child = memo(() => {
console.log('Child')
return (
<div>
<h1>Child</h1>
</div>
)
})
export default Index;
React Router 6
1. 概述
- React Router 以三个不同的包发布到了 npm 上,他们分别是
- react-router:路由核心库,提供了很多组件、钩子
- react-router-don:包含所有内容,并添加一些专门用于 DOM 的组件
- react-router-native:包含react-router所有内容,并添加一些专门用于 RN 的API
- 与React Router 5.x相比,改变了什么
- 内置组件变化:移除 Switch,新增 Routers
- 语法变化:component={About}变为element={}
- 新增多个Hooks
- 官方明确推荐函数式组件
2. Component
1. <BrowserRouter>
说明:
<BrowserRouter>
用于包裹整个应用。示例代码:
jsximport React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; ReactDOM.render( <BrowserRouter> {/* 整体结构(通常为App组件) */} </BrowserRouter>,root );
2. <HashRouter>
- 说明:作用与
<BrowserRouter>
一样,但<HashRouter>
修改的是地址栏的hash值。 - 备注:6.x版本中
<HashRouter>
、<BrowserRouter>
的用法与 5.x 相同。
3. <Routes/> 与 <Route/>
v6版本中移出了先前的
<Switch>
,引入了新的替代者:<Routes>
。<Routes>
和<Route>
要配合使用,且必须要用<Routes>
包裹<Route>
。<Route>
相当于一个 if 语句,如果其路径与当前 URL 匹配,则呈现其对应的组件。<Route caseSensitive>
属性用于指定:匹配时是否区分大小写(默认为 false)。当URL发生变化时,
<Routes>
都会查看其所有子<Route>
元素以找到最佳匹配并呈现组件 。<Route>
也可以嵌套使用,且可配合useRoutes()
配置 “路由表” ,但需要通过<Outlet>
组件来渲染其子路由。示例代码:
jsx<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>
4. <Link>
作用: 修改URL,且不发送网络请求(路由链接)。
注意: 外侧需要用
<BrowserRouter>
或<HashRouter>
包裹。示例代码:
jsximport { Link } from "react-router-dom"; function Test() { return ( <div> <Link to="/路径">按钮</Link> </div> ); }
5. <NavLink>
作用: 与
<Link>
组件类似,且可实现导航的“高亮”效果。示例代码:
jsx// 注意: NavLink默认类名是active,下面是指定自定义的class //自定义样式 <NavLink to="login" className={({ isActive }) => { console.log('home', isActive) return isActive ? 'base one' : 'base' }} >login</NavLink> /* 默认情况下,当Home的子组件匹配成功,Home的导航也会高亮, 当NavLink上添加了end属性后,若Home的子组件匹配成功,则Home的导航没有高亮效果。 */ <NavLink to="home" end >home</NavLink>
6. <Navigate>
作用:只要
<Navigate>
组件被渲染,就会修改路径,切换视图。replace
属性用于控制跳转模式(push 或 replace,默认是push)。示例代码:
jsximport React,{useState} from 'react' import {Navigate} from 'react-router-dom' export default function Home() { const [sum,setSum] = useState(1) return ( <div> <h3>我是Home的内容</h3> {/* 根据sum的值决定是否切换视图 */} {sum === 1 ? <h4>sum的值为{sum}</h4> : <Navigate to="/about" replace={true}/>} <button onClick={()=>setSum(2)}>点我将sum变为2</button> </div> ) }
7. <Outlet>
当
<Route>
产生嵌套时,渲染其对应的后续子路由。示例代码:
jsx//根据路由表生成对应的路由规则 const element = useRoutes([ { path:'/about', element:<About/> }, { path:'/home', element:<Home/>, children:[ { path:'news', element:<News/> }, { path:'message', element:<Message/>, } ] } ]) //Home.js 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 className="list-group-item" to="news">News</NavLink> </li> <li> <NavLink className="list-group-item" to="message">Message</NavLink> </li> </ul> {/* 指定路由组件呈现的位置 */} <Outlet /> </div> </div> ) }
3. Hooks
1. useRoutes()
作用:根据路由表,动态创建
<Routes>
和<Route>
。示例代码:
jsx//路由表配置:src/routes/index.js import About from '../pages/About' import Home from '../pages/Home' import {Navigate} from 'react-router-dom' export default [ { path:'/about', element:<About/> }, { path:'/home', element:<Home/> }, { path:'/', element:<Navigate to="/about"/> } ] //App.jsx 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> ...... {/* 注册路由 */} {element} ...... </div> ) }
2. useNavigate()
作用:返回一个函数用来实现编程式导航。
示例代码:
jsximport React from 'react' import {useNavigate} from 'react-router-dom' export default function Demo() { const navigate = useNavigate() const handle = () => { //第一种使用方式:指定具体的路径 navigate('/login', { replace: false, state: {a:1, b:2} }) //第二种使用方式:传入数值进行前进或后退,类似于5.x中的 history.go()方法 navigate(-1) } return ( <div> <button onClick={handle}>按钮</button> </div> ) }
3. useParams()
作用:回当前匹配路由的
params
参数,类似于5.x中的match.params
。示例代码:
jsximport React from 'react'; import { Routes, Route, useParams } from 'react-router-dom'; import User from './pages/User.jsx' function ProfilePage() { // 获取URL中携带过来的params参数 let { id } = useParams(); } function App() { return ( <Routes> <Route path="users/:id" element={<User />}/> </Routes> ); }
4. useSearchParams()
作用:用于读取和修改当前位置的 URL 中的查询字符串。
返回一个包含两个值的数组,内容分别为:当前的seaech参数、更新search的函数。
示例代码:
jsximport React from 'react' 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> <button onClick={()=>setSearch('id=008&title=哈哈&content=嘻嘻')}>点我更新一下收到的search参数</button> </li> <li>消息编号:{id}</li> <li>消息标题:{title}</li> <li>消息内容:{content}</li> </ul> ) }
5. useLocation()
作用:获取当前 location 信息,对标5.x中的路由组件的
location
属性。示例代码:
jsximport React from 'react' import {useLocation} from 'react-router-dom' export default function Detail() { const x = useLocation() console.log('@',x) // x就是location对象: /* { hash: "", key: "ah9nv6sz", pathname: "/login", search: "?name=zs&age=18", state: {a: 1, b: 2} } */ return ( <ul> <li>消息编号:{id}</li> <li>消息标题:{title}</li> <li>消息内容:{content}</li> </ul> ) }
6. useMatch()
作用:返回当前匹配信息,对标5.x中的路由组件的
match
属性。示例代码:
jsx<Route path="/login/:page/:pageSize" element={<Login />}/> <NavLink to="/login/1/10">登录</NavLink> export default function Login() { const match = useMatch('/login/:x/:y') console.log(match) //输出match对象 //match对象内容如下: /* { params: {x: '1', y: '10'} pathname: "/LoGin/1/10" pathnameBase: "/LoGin/1/10" pattern: { path: '/login/:x/:y', caseSensitive: false, end: false } } */ return ( <div> <h1>Login</h1> </div> ) }
7. useInRouterContext()
作用:如果组件在 <Router>
的上下文中呈现,则 useInRouterContext
钩子返回 true,否则返回 false。
8. useNavigationType()
- 作用:返回当前的导航类型(用户是如何来到当前页面的)。
- 返回值:
POP
、PUSH
、REPLACE
。 - 备注:
POP
是指在浏览器中直接打开了这个路由组件(刷新页面)。
9. useOutlet()
作用:用来呈现当前组件中渲染的嵌套路由。
示例代码:
jsxconst result = useOutlet() console.log(result) // 如果嵌套路由没有挂载,则result为null // 如果嵌套路由已经挂载,则展示嵌套的路由对象
10.useResolvedPath()
- 作用:给定一个 URL值,解析其中的:path、search、hash值。
MobX
1. MobX介绍
- MobX是一个功能强大,上手非常容易的状态管理工具。
- MobX背后的哲学:任何源自应用的东西都应该自动获得。
- MobX利用 getter 和 setter 来收集组件的数据依赖关系,从而在数据变化时候精确知道哪些组件需要重绘,在界面的规模变化很大的时候,往往会有很多细粒度更新。
2. MobX 与 Redux 的区别
- MobX 写法上更偏向OOP
- 对一份数据可以直接进行修改操作,不需要始终返回一个新的数据
- 并非单一 store 可以多 store
- Redux 默认以 JavaScript 原生对象形式存储数据,而 MobX使用可观察对象。
优点:
a. 学习成本小
b. 面向对象编程,而且对 TS 友好
缺点:
a. 过于自由:MobX提供的约定及模板代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一
b. 相关的中间件很少,逻辑层业务整合是问题
3. 基本使用
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'mobx-react'
import appStore from './mobx/store'
import { BrowserRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
{/* Provider 向后代传递 store 实例 */}
<Provider appStore={appStore}>
<App/>
</Provider>
</BrowserRouter>
</React.StrictMode>
);
store.js
import { action, computed, makeObservable, observable, runInAction } from "mobx"
class Store {
// state
number = 0
constructor() {
makeObservable(this, {
number: observable,
addNum: action,
asyncAdd: action,
computedNum: computed
})
}
// action
addNum() {
this.number += 1
}
// 异步 action
asyncAdd() {
runInAction(() => {
setTimeout(() => {
this.addNum()
}, 2000)
})
}
// computed 计算属性
get computedNum() {
return this.number * 2
}
}
const appStore = new Store()
export default appStore
Count.jsx
import {observer, inject} from 'mobx-react'
function Count({appStore}) {
return (
<div>
{appStore.number}
<button onClick={() => {
appStore.addNum();
}}>add</button>
<button onClick={() => {
appStore.asyncAdd();
}}>addAsync</button>
{appStore.computedNum}
</div>
);
}
// observer 监听 store state 数据变化,进行自动更新
// inject 接收 Provider 传递的 store 实例
export default inject('appStore')(observer(Count));
TS
安装
create-react-app my_react_ts --template typescript
类组件
import React, {Component} from 'react';
interface IState {
name: string
}
interface IProps {
age?: number
}
class Index extends Component<IProps, IState> {
state = {
name: 'Bin'
}
myRef = React.createRef<HTMLInputElement>()
render() {
return (
<div>
{this.state.name}
<button onClick={() => {
this.setState({
name: 'Bin07'
})
}}>click</button>
<input type="text" ref={this.myRef}/>
<button onClick={() => {
console.log(this.myRef.current?.value)
}}>show</button>
</div>
);
}
}
export default Index;
函数组件
import React, {useRef, useState} from 'react';
interface IProps {
name?: string
}
const Index = (props: IProps) => {
const [num, setNum] = useState(0);
const myRef = useRef<HTMLInputElement>(null);
return (
<div>
<button onClick={() => {
setNum(num + 1);
console.log(num);
}}></button>
<input type="text" ref={myRef}/>
<button onClick={() => {
console.log(myRef.current?.value)
}}></button>
</div>
);
};
export default Index;
Redux-sage
什么是sage
redux-saga
和 redux-thunk
一样,是一个Redux
中获取存储异步数据的中间件。 redux-saga
可以直接拦截dispatch派发的action,从而实现在执行reducer之前执行一些其它操作。
使用
安装
npm install redux-sage
在 store 创建 sage
import { legacy_createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
import createSagaMiddleware from 'redux-saga'
import watchSage from './saga'
//导入的redux-saga并不是像redux-thunk一样是一个对象,而是一个方法
const sagaMiddleware = createSagaMiddleware();
//使用该方法创建一个saga对象
const storeEnhancer = applyMiddleware(sagaMiddleware);
//告诉系统使用中间件
const store = legacy_createStore(countReducer, storeEnhancer);
// 将saga和reducer联系起来
sagaMiddleware.run(watchSage);
export default store
在生成器中发布异步请求
import { INCREMENT_ASYNC, HANDLE_ASYNC } from './constant'
import { take, fork, call, put } from 'redux-saga/effects'
function* watchSage() {
while (true) {
// take 监听组件发来的 action
yield take(INCREMENT_ASYNC)
// fork 同步执行异步函数
yield fork(getList)
}
// 或使用 takeEvery
// yield takeEvery(INCREMENT_ASYNC, getList)
}
function* getList() {
// 处理异步
// call 函数发出异步请求
let res = yield call(getAsyncData)
// put 发出新的 action
yield put({ type: HANDLE_ASYNC, value: res })
}
function getAsyncData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
}
export default watchSage
调用 sage 发出的新 action
import { INCREMENT, DECREMENT, HANDLE_ASYNC } from './constant'
export default function countReducer(per = 0, action) {
const {type, value} = action
switch (true) {
case type === INCREMENT:
return per + value
case type === DECREMENT:
return per - value
case type === HANDLE_ASYNC:
return per + value
default:
return per
}
}
多个 action 异步请求 借助 all()
yield all([
yield put(changeUserAction(data1)),
yield put(changeInfoAction(data2)),
yield put({type:'CHANGE_USER_NAME', name: data1.name})
yield put({type:'CHANGE_USER_Age', name: data1.age}),
])
yield all([
yield takeEvery(GET_USER_INFO, myHandler),
yield takeEvery(ADD_COUNT, myHandler),
yield takeEvery(SUB_COUNT, myHandler),
])