React的学习笔记-(Bilibili天禹老师)
React的特点
- 采用
组件化
模式,声明式
编码,提高开发效率和组件复用率 - 在
React Native
中可以使用React
语法进行移动端开发(IOS和Android) - 使用
虚拟DOM
+优秀的Diffing
算法,尽量减少与真实DOM的交互
babel用处
- es6 => es5
- jsx => js
1.你好,react
- 注意引入顺序
1 |
|
二种创建虚拟DOM的方式
方式1-使用jsx的形式
1 |
|
效
方式2-使用javascript形式
- 主要是利用引入
React
库所当中的React.createElement
创建虚拟DOM React.createElement
语法参数- 第一个参数:为创建的标签名称
- 第二个参数:为标签的属性
- 第三个属性:为标签的内容
1 |
|
虚拟DOM和真实DOM
- 虚拟DOM输出查看
- 真实DOM输出查看
- 总结
- 虚拟DOM的本质其实就是Object类型的一般对象
- 虚拟DOM比较’轻’,真实DOM比较’重’
- 轻,重体现在这个对象的所具有的属性和方法上
jsx的语法规则
- 若第一层存在多个标签,则必须要有一个统一的标签,否者就报错(也就是根标签只有一个)
- 书写js表达式代码,需要以
{ }
开头,用于区分普通的字符代码 - 如果需要书写内联样式,需要以
{ {...} }
,第一个花括号是用来区分普通的字符代码的,代表要书写js代码,其中,{...}
是对象,并且要以小驼峰的形式命名key
- 书写类名要以 className 书写
- 标签必须要闭合,否者会报错
- 创建虚拟DOM的时候,不需要引号
- 在jsx中,标签名是可以随意书写的
- 如果是[小写]的,react则会将当前的jsx标签对应为一个html标签,若对应成了,直接渲染展示,否则的话就报错
- 如果是[大写]的,react则会将当前的jsx标签对应为一个组件,去查找组件的定义位置,若找到了,直接渲染展示,否者就报错
1 |
|
表达式和语句(代码)的区别
- 一句话概括就是
表达式
左侧是可以有值返回的,而语句
是没有的 - 所以不管是在React还是Vue当中,插值语法都是只能书写表达式的
表达式
- 表达式就是会产生结果的值,可以放在React任何一个需要值的地方
- 下面这些都是表达式
a
a+b
demo(1)
(函数返回值)function test() { }
语句(代码)
- 语句是不会产生结果的
- 下面这些都是语句(代码)
if( ){ }
for( ) { }
switch( ) {case: xxx}
JSX小练习
1 | <body> |
React函数式组件
- Reace有函数式组件,也有类似组件,这里做下函数式组件的笔记
1 |
|
- 执行
ReactDOM.render
发生了什么?- 1.React发现
<MyComponent/>
标签,去寻找MyComponent
组件定义的位置,发现是函数式组件 - 2.是函数式组件,则react调用该函数
- 3.react调用该函数,函数会返回一个虚拟的DOM,并由react转换为真实的DOM,并显示在页面上
- 1.React发现
React类式组件
- 什么是类式组件,就是通过类来创建组件
1 | <div id="test"></div> |
render
方法,是给react渲染时候调用的- 执行
ReactDOM.render
发生了什么?- 1.React发现
<MyComponent/>
标签,去寻找MyComponent
组件定义的位置,发现是类式组件 - 2.是类式组件,则react创建该类的实例对象
- 3.react调用该类的实例对象身上的
render
方法,render
方法返回一个虚拟的DOM,并由react转换为真实的DOM,并显示在页面上
- 1.React发现
组件的三大属性之state
在学之前,先来复习下this的指向
- 严格模式,是禁止
this
指向window的
1 | function demo1(){ |
- 在严格模式下,
this
禁止指向window
,所以下面这个例子中,this
为undefined
1 | <script> |
- 在普通模式下(除类外,并且非箭头),
this
指向执行者(刽子手~)
1 | <script> |
- 在普通模式下(除类外,箭头函数下),
this
指向上层作用域下的this
1 | var obj = { |
在类下
先来想一下这个
this
指向哪里1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<script type="text/javascript">
class Demo {
show(){
console.log("看看this在哪里",this);
}
}
const example = new Demo();
example.show(); //输出结果为?
const x = example.show;
x(); //输出结果为?
</script>答案依次为
example
实例对象- 因为此时的
show
方法是由example
实例化对象所调用的
- 因为此时的
undefined
- 此时
show
方法是由window
调用的,如果没有开启严格模式,那么应该是输出window
,但是类中的方法局部默认开启了严格模式,所以禁止指向window
,所以就输出了undefined
- 此时
state
- 未改变state当中的状态
1 | <body> |
- 改变state状态的写法
- 通过继承的
React.Component
身上的方法setState
来完成响应式操作(数据变了,视图也变)- setState是一个同步的方法,但是后续更新状态的动作是异步的
- 通过继承的
1 |
|
- 调用次数
- 构造器调用次数: 使用组件多少次,就调用多少次
state的简写形式-解决this指向问题
- 使用到了箭头函数
- 使用到了class在原型链上定义属性
1 | <body> |
- 原理
1 | class Weather extends React.Component{ |
- 复习
1 | <html lang="en"> |
组件的三大属性之props
- 注意在组件当中的扩展运算符的使用
- 这里的扩展运算符并不是js原生的
{...info}
,而是babel+react环境就可以让展开运算符展开一个对象,但是仅仅适用于传递标签属性
- 这里的扩展运算符并不是js原生的
1 | <body> |
对props进行约束-类式组件
- 引入约束文件
1 | <script src="../js/prop-types.js"></script> |
- 使用约束
- 通过在类身上添加属性
propTypes
,结合约束文件当中的PropTypes
对象当中的类型来使用
1 | <body> |
- 当接收到一个不符合的值的时候,控制台会报警告,但是不会影响视图的渲染
Warning: Failed prop type: Invalid prop 'age' of type 'string' supplied to 'Person', expected 'number'.
对props进行约束-函数式组件
- 函数式组件当中,props是唯一可以使用的三大属性
- 添加约束的方法和类差不多,也是向函数添加属性
propTypes
即可
1 | <body> |
ref
- 通过React当中的
ref
,可以获取指定节点,获取的节点为真实DOM
字符串形式的ref
17.x版本已经不推荐使用了,这里做一个学习记录
原理就是通过为元素添加
ref
属性后,会在实例对象身上的ref
属性上添加对应的节点,如下图,就是添加了二个ref
在input
身上,输出this
就可以看到ref属性下的二个节点
- 具体例子
1 | <body> |
回调形式的ref
格式
<input ref={currentNode = this.input1 = currentNode}/>
- 其中,
ref
里面的为一个函数,React调用后会传入一个参数(这个参数就是这个ref
所指向的节点),由currentNode接收后,通过this.input1
挂载到了当前实例对象身上
不过这种方式的ref也不推荐了….
示例
1 | <body> |
createRef的使用
- React为了避免
this
身上太多的属性,就推出了createRef - createRef的使用如下
- 实例对象添加
container1
属性(只可存储一个节点数据),值设置为React.createRef()
- 在需要获取节点的身上添加
ref
属性,值为{this.container1}
- 输出查看获取的节点的值
this.container1.current.value
- 实例对象添加
- 输出查看
container1
- 示例
1 | <body> |
React当中为什么要建立一个onClick,onXxx的事件
我们知道,在原生js当中,是有事件
onclick
的,那么为什么React需要创建一个onClick
来绑定事件呢?原因
- React使用的自定义事件(
onClick
,onBlur
等),而不是使用原生DOM事件,是为了更好的兼容性 - React的事件是通过事件委派的方式处理的,委托给组件最外层的元素,是为了提高效率
- 所以你不必担心在
li
等元素身上绑定自定义事件太多而会出现执行效率问题,React会自动将他们处理为事件委派的形式
- 所以你不必担心在
- React使用的自定义事件(
其他
在自定义事件当中,可以通过
event.target
得到发生事件的DOM元素对象示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<body>
<div id="app"></div>
<script src="../js/react.development.js"></script>
<script src="../js/react-dom.development.js"></script>
<script src="../js/babel.min.js"></script>
<script type="text/babel">
class Demo extends React.Component {
sayHello = (event) => {
console.log(event.target);
}
render(){
return (
<div>
<button onClick={this.sayHello}>点我输出发生事件的DOM元素</button>
</div>
)
}
}
ReactDOM.render(<Demo/>,document.getElementById('app'));
</script>
</body>
React之受控组件和非受控组件
非受控组件
- 概念:现用现取(你不受我控制,我需要你的时候你再告诉我)
示例
1 | <body> |
受控组件
- 概念:组件中输入类的DOM,随着用户的输入,将输入的值维护到
state
当中- 组件受到我控制,你随时都需要向我报告
1 | <body> |
- 效果
区别
- 非受控组件就是现用现取,想获取什么值就去元素中获取
- 而受控组件就是值统一存储在
state
的当中,需要的时候就去取 - 说通俗点就是非受控组件组件不囤积’粮食’,没有食物了就去超时买而受控组件会**囤积’粮食’,**需要的时候就去仓库取
高阶函数和函数的柯里化
**高阶函数:**符合下面任意一个条件即为高阶函数
- 本身是一个函数,又返回了一个函数
- 函数当中的参数接受到了一个函数
高阶函数常见的有
Promise
,setTimeout
,arr.map()
**函数的柯里化:**通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式,比如下面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<script type="text/javascript">
//普通的函数
function sum1(a,b,c){
return a+b+c;
}
const result1 = sum1(1,2,3);
console.log(result1);
//函数的柯里化
function sum2(a){
return (b) => {
return (c) => {
return a+b+c;
}
}
}
const result2 = sum2(1)(2)(3);
console.log(result2);
</script>函数的柯里化在React的应用
- 如下代码所示,通过一个函数就完成了二个参数的传入,达到了函数复用的效果
1 | <body> |
- 上面例子当中,不使用函数的柯里化在
collectInfo
当中的实现
1 | <body> |
关于类式组件当中的构造器
类式组件中的构造器,完全可以省略掉
1
2
3
4
5
6
7
8
9
10
11
12
13<script type="text/babel">
class Demo extends React.Component {
//可以被省略
//constructor(props){
//必须要调用
//super();
//}
render(){
return ''
}
}
ReactDOM.render(<Demo name='李白'/>,document.getElementById('app'));
</script>若在类式组件当中写了构造器,那么就必须要调用
super
1
2
3
4
5
6
7
8
9
10
11
12<script type="text/babel">
class Demo extends React.Component {
constructor(props){
//必须要调用
super();
}
render(){
return ''
}
}
ReactDOM.render(<Demo name='李白'/>,document.getElementById('app'));
</script>若调用
super
时,不传props
,那么在构造器当中,通过this.props
是不可以访问props
的,反之就可以访问1
2
3
4
5
6
7
8
9
10
11
12<script type="text/babel">
class Demo extends React.Component {
constructor(props){
super();
console.log(this.props); //输出undefined
}
render(){
return ''
}
}
ReactDOM.render(<Demo name='李白'/>,document.getElementById('app'));
</script>
React生命周期
- 生命周期是啥子就是一个组件从创建到销毁过程当中React会在特定阶段执行特定函数
- 在类式组件当中,
render
方法会被调用1+n次,1次创建的时候被执行,n次则是数据被更新,DOM被更新的次数
生命周期16.x
生命周期
constructor
:构造器componentWillMount
:组件将要被挂载render
:组件初次渲染和后期更新(调1+n次,n为更新的次数)componentDidMount
:组件挂载完成后执行(仅在一次渲染时被调用一次,如果开启了严格模式,可能会调用多次)- 一般可以在这个钩子当中做一些初始的事情,如果说开启定时器,发送网络请求,订阅消息
componentWillUnmount
:组件将要被卸载的时候执行(调1次)- 一般在这个钩子中做一些收尾的事情,比如关闭定时器,取消订阅消息
componentWillReceiveProps
:props发生变化时候的回调- 初次渲染的时候不会执行此钩子
shouldComponentUpdate
:组件是否可以更新,当返回值为true
的时候允许更新,false
禁止更新componentWillUpdate
:组件将要更新(调n次,n为更新的次数)componentDidUpdate
:组件更新完成(调n次,n为更新的次数)
生命周期常用函数
fourceUpdate()
:不经过shouldComponentUpdate
,直接更新组件
卸载组件
ReactDOM.unmountComponentAtNode(要卸载的节点DOM)
- 比如
ReactDOM.unmountComponentAtNode(document.getElementById('app'));
示例1-透明度改变
- 组件挂载完成后执行定时器,之后通过按钮执行卸载,卸载的时候执行组件卸载前生命周期函数
1 | <body> |
示例2-更新与强制更新
- 每次单击按钮数字就会加一,同时React自动执行生命周期,通过另外一个按钮控制是否允许DOM更新
1 | <body> |
示例3-props的接收监听
- 父组件传递props给子组件,子组件接收,后期父组件更新了props值,子组件就会调用
componentWillReceiveProps
1 | <body> |
生命周期17.x
17.x开始,React就不推荐使用下列三种生命周期钩子了,如需使用,需要在前添加前缀
UNSAFE_
componentWillMount
= >UNSAFE_componentWillMount
componentWillReceiveProps
=>UNSAFE_componentWillReceiveProps
componentWillUpdate
=>UNSAFE_componentWillUpdate
与此同时也添加了几个新的生命周期钩子
getDerivedStateFromProps
:如果该组件所有的state都取决于父组件传递过来的(也就是props),可以使用这个,这个函数必须要返回一个对象或者是null
- 使用
static getDerivedStateFromProps(props,state)
- 使用
getDerivedStateFromError
:一旦后台组件报错,就会触发,这个函数必须要返回一个对象或者是null
- 使用
static getDerivedStateFromError(error)
- 使用
getSnapshotBeforUpdate
:这个函数必须要配合componentDidUpdate
使用且getSnapshotBeforeUpdate
必须要有返回值,返回值为componentDidUpdate
当中第三个形参- 使用
getSnapshotBeforeUpdate(preProps,preState)
componentDidUpdate(prevProps, prevState, snapshot)
- 使用
componentDidCatch
:统计页面的错误,可以发送请求到后台去- 使用
componentDidCatch(error,info)
- 使用
新生命周期钩子图示
示例1-使用不安全的生命周期钩子
1 | <body> |
如果不添加UNSAFE_
前缀,后有如下图警告
示例2-使用新钩子,体会聊天窗口有新消息出现时候的做法
注意点如下
getSnapshotBeforeUpdate(preProps, preState)
:返回更新视图前的props,和statecomponentDidUpdate(preProps,preState,fromsnapshot)
:第三个参数名称随意,接收的是来自getSnapshotBeforeUpdate
返回的值getSnapshotBeforeUpdate
和componentDidUpdate
必须要连起来用,并且getSnapshotBeforeUpdate
必须要返回一个值
1 | <body> |
没有使用getSnapshotBeforeUpdate
的时候,效果如下,可以看到,每次有新消息出来,位置就会被改变,不会停留在一处,之前的为什么会被改变,那是因为浏览器会自动生成计算scrollTop
现在使用了getSnapshotBeforeUpdate
,每一次更新前返回更新前的滚动条位置信息,通过componentDidUpdate
手动设置scrollTop
,从而每一次都在原处(为什么要减去的,因为每一次都增加了一部分,我想留在原地,当然要减去了)
那么原理是什么呢? 首先知道,scrollTop是设置(获取)这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离,所以如果我们不去改变滚动条,那么scrollTop的值是永远不会改变的(不相信你可以输出看看),scrollTop值不变,内容逐渐增多,就会导致内部容器滚动高度增长,所以就会看到位置逐渐后移的效果(再说通俗点就是修路,你站在原地,别人一直修路,随着路增长,你距离也会离他们越来越远)
1 | <body> |
React的Differ算法
React/vue中的key有什么作用(key的内部原理是什么?)
作用
- key用于标识「虚拟DOM」,当状态当中的数据发生改变的时候,React会根据新数据生成「新的虚拟DOM」,之后React会将「新虚拟DOM」和「旧虚拟DOM」进行differ比较**(比较的最小力度是节点(标签))**,规则如下
- 旧虚拟DOM中找到了和新虚拟DOM相同的key
- 若内容没有发生变化,就直接使用之前的真实DOM
- 若内容发生了变化,则生成新的真实DOM,随后替换掉页面中与之对应的真实DOM
- 旧虚拟DOM中未找到与虚拟DOM相同的key
- 根据数据创建新的真实DOM,随后渲染到页面
大概流程
为什么遍历列表的时候,key最好不要用index
- 用index可能引发的问题
- 若对数据进行:「逆序添加」,「逆序删除」等破坏顺序的操作
- 会产生没有必须要的真实DOM更新 => 界面没有问题,但是效率低
- 如果结构中包含输入类的DOM
- 会产生错误的DOM更新 => 界面有问题
- 如果不存在对数据的「逆序添加」,「逆序删除」等破坏顺序的操作,仅用于渲染列表用于展示,使用index作为key是没有问题的
- 若对数据进行:「逆序添加」,「逆序删除」等破坏顺序的操作
- 开发如何选择key
- 最好使用每条数据的唯一标识作为key,比如说id,手机号,身份证号,学号等唯一值
- 如果确定只是简单的展示数据,用index也是可以的
React脚手架
- 安装
1 | npm install create-react-app -g |
- 创建一个名为
react_01
的react项目
1 | create-react-app react_01 |
React脚手架创建的项目目录和文件分析
- public:
- favicon.ico => 网站图标
- index.html => 主页面
- logo192.png / logo512.png => logo图,不同分辨率的
- manifest.json => 应用加壳的配置文件
- robots.txt => 爬虫协议文件
- src:
- App.css => App组件的样式
- App.js => App组件
- App.test.js => 用于给App做测试
- index.css => 样式
- index.js => 入口文件
- logo.svg => logo图,矢量图
- reportWebVital.js => 页面性能分析文件(需要
web-vitals
库的支持) - setupTests.js => 组件单元测试文件(需要
jest-dom
库的支持)
- public:
我们看下一个没见过的短命令在
package.json
当中react-scripts eject
:可以输出React当中的所有配置文件,过程不可逆
一些小技巧
- react脚手架当中**,可以省略.js ,.jsx后缀**,并且如果文件夹当中有index.js ,那么引入的时候就可以省略index.js
- 比如import a from “/component/Person/index” => 可以省为 import a fro “/component/Person”
- 引入css文件,只需要
import "./XXXX.css"
即可 - 如果组件需要传入全部的props值,可以使用
<Item {...todos}/>
,使用...
进行展开
React小案例之todoList
重要
操作
state
数据的时候,只能通过setState
去操作,不可以通过其他方式去更改比如不推荐如下
1
2
3let {todos} = this.state;
todos.unshift(todoObj);
this.setState({todos});推荐下面
1
2
3
4const {todos} = this.state;
this.setState({
todos:[todoObj,...todos],
})
知识点
- 记住,状态驱动数据,数据驱动视图
- 状态在哪里,操作状态的方法就在哪里
arr.reduce
方法当中,如果没有传入initialValue
,则第一次的preValue值就是数组中第一个元素的值
技巧
- 结构赋值重命名
const {a:newName} = obj;
- 结构赋值重命名
工具库
nanoid
:一个小巧、安全、URL友好、唯一的 JavaScript 字符串ID生成器。
React脚手架配置代理
方式1-在package.json当中配置
- 步骤
package.json
当中添加"proxy": "http://localhost:5000/students"
即可,其中http://localhost:5000
代表的是服务器的地址- 然后我们发送ajax请求的时候
- 之前会产生跨域问题
axios.get('http://localhost:5000/students')
, - 在package配置好后我们更改为
axios.get('http://localhost:3000/students')
即可
- 之前会产生跨域问题
- 总结
- package是实际的服务器ip地址,axios是本地的地址
- 缺点就是只能配置一台代理服务器~
- 其他
- 不知道为什么,我添加proxy,关闭服务重新启动就启动不了,提示
Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
,删除后就可以启动了,所以这里就做一个学习吧,反正这方法也不太可能用到
- 不知道为什么,我添加proxy,关闭服务重新启动就启动不了,提示
方式1-src创建配置文件xxxx当中配置(推荐)
1.在
src
目录下建立setupProxy.js
(规范为cjs,也就是nodejs使用的规范)2.写如下配置
1 | const proxy = require("http-proxy-middleware") |
- 关于
changeOrigin
React小案例之github搜索
fetch和axios
fetch:为原生函数,不用安装也可以使用,不使用XMLHttpRequest,但是老版本可能不支持fetch
想获取服务器返回的信息,就不需要像axios一样多访问一层data了,直接就返回的是原始数据
想获取错误信息,依旧是要再访问一层
message
1
2
3
4
5
6
7
8
9
10
11
12
13try {
let response = await fetch(`https://api.github.com/search/users?q=${value}`)
let result = await response.json();
PubSub.publish('updateList', {
userList:result?.items ?? [],
isLoading:false,
})
}catch (e) {
PubSub.publish('updateList',{
isLoading: false,
errMsg:e.message,
})
}Promise之链式调用复习
如果需要链式调用,需要可能返回一个promise,也可以返回一个非promise
- 如果是返回promise,那么下一步的then的成功还是失败就取决于返回的promise
- 如果是返回非promise,那么下一步的then状态一定是成功的
链式调用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41const bbb = new Promise((resolve,reject)=>{
let a = Math.random() * 100;
if(a>50){
resolve('第一次返回的resolve' + a);//标记为解决,并返回a - 第一次返回
}else{
reject('第一次返回的reject' + a);//标记为未解决,并返回a
}
});
bbb.then(res=>{
console.log(res);
return new Promise((resolve, reject)=>{
reject('第二次返回的reject')
})
})
.then(res=>{
console.log(res);//输出 第二次返回的reject
})
.catch(error=>{
console.log(error);//统一处理错误,不在.then当中处理
})
//当然,你也可以写的更加简单点
new Promise((resolve,reject)=>{
let a = Math.random() * 100;
if(a>50){
resolve('第一次返回的resolve' + a);//标记为解决,并返回a - 第一次返回
}else{
reject('第一次返回的reject' + a);//标记为未解决,并返回a
}
}).then(res=>{
console.log(res);
return new Promise((resolve, reject)=>{
reject('第二次返回的reject')
})
})
.then(res=>{
console.log(res);//输出 第二次返回的reject
})
.catch(error=>{
console.log(error);//统一处理错误,不在.then当中处理
})- 调用关系如下 相同颜色的代表为实际调用.then的promise
React之路由前置
预备,知道history
- 知识点
- 现实中的路由器-管理多个上网设备
- 前端的路由器-管理多个页面
- 现在主流的为SPA(也就是单页面,多组件)
- 什么是哈希路由
- 通俗点就是定位 #后面的都算的前端的东东,后端接收不到也不会将这些传递给后端
- 比如 前端请求
/abc#/a/b/c
,那么向服务器请求的时候,后端收到的是/abc
,而不会收到/a/b/c
- replace操作
- 原来从上到下是
/test1
,/test2
后面replace(‘/test3’) 那么从上到下就依次变成了/ test1
,/test3
原来的/test2
被替换为了/test3
- 原来从上到下是
- 示例
1 | <!DOCTYPE html> |
React路由@5版本
基本使用
安装react-router-dom库,这里以5.2.0版本为例子,6.x开始写法就和5.2.0不一样了
- 如果出现
A <Route> is only ever to be used as the child of <Routes> element, never rendered directl
就说明用5.2.0的版本的写法在6.x版本了,所以要么更改6.x的写法,要么更换react-router-dom为5.2.0版本
- 如果出现
index.js
主入口文件最外层包裹一个<BrowserRouter></BrowserRouter>
或者<HashRouter></HashRouter>
,因为如果不这样子做,每次使用路由,都需要在使用的地点添加此标签,所以可以直接在主入口统一添加1
2
3
4
5
6
7
8import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import {BrowserRouter} from "react-router-dom";
ReactDOM.render(
(<BrowserRouter>
<App/>
</BrowserRouter>),document.getElementById('root'))导航区的标签更换为
Link
标签或NavLink
标签- Link标签:点完要去一个地方,使用
to
来指明<Link to="/about">前往About页面</Link>
- NavLink标签:点完要去一个地方,使用
to
来指明,和Link不同的是,多了activeClassName
属性,可以指明选中路由的样式,如果不使用activeClassName
指明活动类样式,那么会自动添加一个类名为active,使用和Link是一样的使用<NavLink activeClassName="activeStyle" to="/about">前往About页面</NavLink>
- Link标签:点完要去一个地方,使用
知道什么是一般组件(非路由组件)和路由组件
- 程序员没有直接写组件方式去使用组件,而是通过路由标签使用的形式,那么就是路由组件,否则的话就是非路由组件 还有就是路由组件不可以传递props属性.会收到固定的属性,而非路由组件(一般组件),可以接收到props
如果在路由组件输出
this.props
,会输出如下图内容
路由组件,接收到的三个固定的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//操作历史记录的的方法
history:
go: f go(n)
goBack: f goBack()
goForward: f goForward()
push: f push(path,state)
replace: f replace(path,state)
location:
pathname: "/about"
//接收传递过来的search参数(也就是query参数)
search: ""
//接收传递过来的state参数
state: undefined
match:
//接收传递过来的params参数
params:{}
path: "/about"
url: "/about"
基本使用示例代码
index.js
1 | import React from "react" |
App.jsx
1 | import React, { Component } from 'react' |
pages/About.jsx
1 | import React, {Component} from 'react'; |
pages/Home.jsx
1 | import React, {Component} from 'react'; |
二次封装NavLink
- 现在的组件基本上都是这样子
<标签 属性A = "属性值" 属性B = "属性值">标签体</标签>
,在react当中,标签体会放置在children
属性当中,也就是可以通过this.props.children
来读取标签体内容 - 这样子就可以不用每次书写
className
了~,简化了我们操作 - MyNavLink示例如下代码
1 | import React, {Component} from 'react'; |
Switch的在路由的使用-遇见就返回
正常情况下,react通过匹配路径的方式去寻找对应路径,并渲染,但是找到后不会停止寻找,而是会继续寻找
1
2
3
4
5
6
7
8
9//当我们切换到/about路径的时候
//本意是想渲染 About组件,通过匹配路径的方式,然后匹配后react依旧会接着匹配
//匹配成功,显示
<Route path="/about" component={About}/>
//匹配失败,不显示
<Route path="/home" component={Home}/>
//匹配成功,显示
<Route path="/about" component={Other}/>这种找到后还接着寻找,很会影响效率,所以我们可以在外面嵌套一层
Switch
,(在vue当中就不用)1
2
3
4
5
6
7
8import {Switch} from "react-router-dom";
//当我们切换到/about路径的时候
//匹配成功,显示,不会接着往下匹配
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/about" component={Other}/>
解决路径丢失的问题
- 当我们路径从
/about
=> 改为/gugu/about
的时候
1 | <NavLink className="list-group-item" to="/gugu/about">About</NavLink> |
- 切换路由后刷新页面,就会发现页面css丢失了
- 可以看到,路由切换到
/gugu/about
后刷新页面,发现样式丢失了,原因如下- 在引入样式的时候,使用了
href="./css/bootstrap.css"
,也就是去寻找当前目录下的bootstrap,所以我们切换到/gugu/about
,这个是一个多级路径,所以react会认为gugu
是300端口下的一个路径,所以就造成浏览器去访问http://localhost:3000/gugu/css/bootstrap.css
了,正常的应该是寻找http://localhost:3000/css/bootstrap.css
- 在引入样式的时候,使用了
- 解决办法
- 引入样式的时候,去掉
.
,原来的href="./css/bootstrap.css"
=> 改为href="/css/bootstrap.css"
,这样子浏览器就会永远去3000端口下寻找东西
- 引入样式的时候, 原来的
href="./css/bootstrap.css"
=> 改为href="%PUBLIC_URL%/css/bootstrap.css"
,让react永远将这个资源定位到public目录下的css目录 - 使用HashRouter(也就是哈希模式)
- 因为之前我们说过,哈希模式,是不会将
#
后面的视为浏览器请求内容的,也就是说,即使你路径切换到了http://localhost:3000/#/gugu/home
,#
后面的/gugu/home
浏览器都不会去使用的,查找资源的时候,只会使用#
左侧指明的路径- 所以请求css的时候的请求地址始终为
http://localhost:3000/css/xxxx
- 所以请求css的时候的请求地址始终为
- 考考你,当浏览器
http://localhost:3000/abc/efg#/gugu/home
请求一个css,那么url的请求地址会是什么?- 答案是
http://localhost:3000/abc/efg/css
- 答案是
- 因为之前我们说过,哈希模式,是不会将
- 引入样式的时候,去掉
BrowserRouter和HashRouter的区别
- 底层原理不一样
BrowserRouter
使用的是H5的history API,不兼容IE9及以下版HashRouter
使用的是URL的哈希值
- path表现形式不一样
BrowserRouter
路径中没有**#**,例如:localhost:3000/demo/test
HashRouter
的路径中包含**#,例如:localhost:3000/#/demo/test
,并且#**后面的内容服务端是不会接收到的
- 刷新后对路由state参数的影响
BrowserRouter
没有任何影响(但是清空缓存后会有),因为state保存在history对象中HashRouter
老师我看是可以使用的,但是我使用就报Warning: Hash history cannot replace state; it is ignored
,并且state输出也是undefined
,所以估计HashRouter不支持传递state了
- 备注
HashRouter
可以解决一些路径错误相关的问题,因为并且**#**后面的内容服务端是不会接收到的,所以不管怎么样请求资源,都是会以localhost:3000
开头的,不会出现localhost:3000/about
这种开头
路由的匹配之模糊路由和精准(严格)匹配
- 知道react的路由匹配是怎么匹配的 - 模糊匹配
path
要的东西,to
里面一个都不能少,其他你随意.并且path当中的顺序不能乱- 比如
to = "/a/home/c"
path = "/home/a/c"
那么react会将to拆分为 a 和 home 和 c 将 path拆分为 home 和 a 和 c ,path当中和to去匹配,发现第一个home 和 a 不对应,就不匹配下去了,就匹配失败了 - 比如
to = "/home/c"
path = "/home"
那么react会将to拆分为 home和c 将 path拆分为 home path当中和to去匹配,path当中的都与to都的匹配,所以就匹配成功了
- 比如
- 精准(严格)匹配开启
<Route exact={true}></Route>
或者<Route exact ></Route>
即为开启严格匹配
- 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
Redirect标签 - 重定向
- 路由的兜底
- 当路由都匹配不到的时候,就会使用
Redirect
当中的to值,放的顺序我试了,放在最前面也没有事
1 | <Switch> |
嵌套路由
- 什么是嵌套呢,就是一个展示区里面又出现了另外一个展示区
- 必须要知道的知识点-路由匹配
- React的路由匹配的从最开始注册的路由开始匹配的,一直匹配到最后注册的路由(也就是从一级路由开始,二级路由继续后,三级路由继续,…..一直继续下去)
- 所以当我们路径切换到
/home/news
的时候,React会从一级路由开始(如果有)- 一级路由有
/home 和 /about
那么一级路由将会匹配/home
(因为是模糊匹配),所以会展示/home
下的所有组件 - 当我们来到
/home的时候
,就会开始二级路由匹配(如果有) - 二级路由有
/home/news 和 /home/message
,那么二级路由就会匹配/home/news
,所以会展示/home/news
下的组件 - ….如果存在三级,四级等,就会按照这个规律匹配下去
- 一级路由有
- 所以为什么要慎重开始严格(精准)匹配,因为如果开启了严格
- 如果路径变为了
/home/news
,React会从一级路由开始,那么/home/news
,不会和/home
进行匹配,因为开启了严格(精准)匹配
- 如果路径变为了
- 示例(伪代码)
1 | <div> |
路由传参之params
params传参,在vue,axios,react之中,都是这种形式
/a/b/c/?/?/?/....
的react添加params
- 路由导航(路由链接):
<Link to="/home/message/1">
- 注册路由(路由当中需要声明):
<Route path = "/home/message/:id" component = "ABC"/>
- 如果存在多个也是这种写法
<Route path = "/home/message/:id/:name" component = "ABC"/>
- 路由导航(路由链接):
接收参数: 子组件可以通过
this.props.match.params
来接收传递的params参数,如果没有传params参数,则会返回空对象重新来看下路由接收到的三个固定的属性
1 | //操作历史记录的的方法 |
- 示例(伪代码)
1 | //Message组件 |
1 | //Detail组件 |
路由传参之search
search传参和vue,axios的query参数一样,也是通过?来区分,比如
/a?name=abc&id=1
这种形式React添加search参数
- **路由导航(路由链接):**当中
<Link to="/demo/test?name=tom&age=18">信息</Link
- 注册路由(search参数不需要声明):
<Route path = "/demo/test" component = {Demo} />
- **路由导航(路由链接):**当中
接收参数:
- 子组件通过
this.props.location.search
进行获取,如果是没有传search参数,则会返回空字符串,否则就会返回字符串,比如?name=tom&age=18
- 子组件通过
特别注意
获取到的search是字符串(?name=tom&age=18) 所以需要通过
querystring
进行解析querystring解析
1
2
3
4
5import qs from "querystring";
const demo = '?name=tom&age=18';
const result = qs.parse(demo.slice(1));
//输出对象 注意,value均为字符串!
// { name:'tom',age:'18' }querystring编码
1
2
3
4
5import qs from "querystring";
const demo = { name:'tom',age:'18' }
const result = qs.stringify(demo)
//输出字符串
// "name=tom&age=18"
伪代码示例
1 | //search传参 |
- 重新来看下路由接收到的三个固定的属性
1 | //操作历史记录的的方法 |
路由传参之state
React添加state参数
- **路由导航(路由链接):**当中
<Link to={{pathname:'/home/message',state:{id:666}}}>信息</Link
- 注册路由(search参数不需要声明):
<Route path = "/home/message" component = {Demo} />
- **路由导航(路由链接):**当中
接收参数:
- 子组件通过
this.props.location.state
进行获取,如果是没有传state参数,则会返回undefined
,否则就会返回对象,比如{ id:666}
注意,这个666是number类型,而不是string类型,因为你在路由导航中传入的是number
- 子组件通过
特别注意
- state传参只适用于
BrowserRouter
使用在HashRouter
会出现react_devtools_backend.js:4026 Warning: Hash history cannot PUSH the same path; a new entry will not be added to the history stack
,并且没有效果在哈希模式下 - 刷新可以保留住参数,但是清空缓存会没有
- state传参只适用于
示例(伪代码)
1 | //state传参 |
- 重新来看下路由接收到的三个固定的属性
1 | //操作历史记录的的方法 |
路由的push 和 replace
- 默认情况下是push
- 如果需要开启replace,只需要在路由声明当中添加
replace
即可 <Link replace to="/demo">跳转到Demo</Link>
或<Link replace={true} to="/demo">跳转到Demo</Link>
- 当然,你也可以直接就写一个单词
<Link replace to="/demo">跳转到Demo</Link>
路由编程式导航
编程式导航
- 通过借助
this.props.history
对象上面的方法实现跳转,前进,后退,比如this.props.history.push(path,state)
this.props.history.replace(path,state)
this.props.history.goBack()
this.props.history.goForward()
this.props.history.go(n)
- push方式(默认)
- 调用
this.props.history.push(path,state)
,第一个path为路径,第二个state为state传参
- 调用
- replace
- 调用
this.props.history.replace(path,state)
,第一个path为路径,第二个state为state传参
- 调用
- 通过借助
声明式导航
- 在react当中就是通过
Link
或者是NavLink
就是声明式导航 - 在vue当中就是
<router-link></router-link>
- 在react当中就是通过
示例(伪代码)
1 | //编程式导航传递 - push |
- 重新来看下路由接收到的三个固定的属性
1 | //操作历史记录的的方法 |
路由的withRouter
作用
withRouter
可以让一般组件(非路由组件),可以让一般组件使用路由组件的相关APIwithRouter
调用后返回是一个新组件
使用
- 返回的时候用这个包裹一下
示例(伪代码-在一般组件当中使用)
1 | import {withRouter} from "react-router-dom"; |
React路由@6版本
概述
React Router 以三个不同的包发布到 npm 上,它们分别为:
- react-router: 路由的核心库,提供了很多的:组件、钩子。
- react-router-dom: 包含react-router所有内容,并添加一些专门用于 DOM 的组件,例如
<BrowserRouter>
等 。 - react-router-native: 包括react-router所有内容,并添加一些专门用于ReactNative的API,例如:
<NativeRouter>
等。
与React Router 5.x 版本相比,改变了什么?
内置组件的变化:移除
<Switch/>
,新增<Routes/>
等。语法的变化:
component={About}
变为element={<About/>}
等。新增多个hook:
useParams
、useNavigate
、useMatch
等。官方明确推荐函数式组件了!!!
……
一级路由
- 在react路由@6版本当中,我们必须要在所有的
Route
外面包一层Routes
,作用和Switch
类型,Routes
中的Route
只有一个会被匹配 Route
不再使用component
属性,而是element
属性<Route caseSensitive>
属性用于指定:匹配时是否区分大小写(默认为 false)。- 伪代码示例
1 | import React from 'react' |
Navigate组件-路由重定向
- react路由@6当中,由于删除了
Redirect
,所以我们需要使用Navigate
来达到重定向 - Navigate只要被渲染出来,就会引起视图的切换
replace
属性用于控制跳转模式(push 或 replace,默认是push)。
1 | <Routes> |
NavLink组件
在router@5版本当中,我们可以为
NavLink
添加activeClassName
来指定激活状态下的样式但是在router@6版本当中,就没有这个了,取而代之的是我们需要通过函数来传递类名
语法
- className的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//css内容
.active_style{
background-color: red;
}
//jsx内容
import React from 'react';
import {Link,NavLink} from "react-router-dom";
import "./test.css";
const Menu = () => {
const activeStyle = ({isActive}) => {
return isActive ? 'active_style' : null;
}
return (
<div>
<Link to={'/home'}>Home页面</Link>
{/*<NavLink to='/home/page' className={activeStyle}>Home页面下的page</NavLink>*/}
{/*或者*/}
<NavLink to={'/home/page'} className={
({isActive}) => {
return isActive ? 'active_style' : null;
}
}>Home页面下的page</NavLink>
<Link to={'/about'}>关于页面</Link>
</div>
);
};
export default Menu;
//激活的时候HTML状态
<a href="/home/page" aria-current="page" class="active_style">Home页面下的page</a>
//未激活的时候HTML状态
<a href="/home/page">Home页面下的page</a>- 或者styleName的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import React from 'react';
import {Link,NavLink} from "react-router-dom";
const Menu = () => {
const activeStyle = ({isActive}) => {
return isActive ? {backgroundColor:'red'} : null;
}
return (
<div>
<Link to={'/home'}>Home页面</Link>
{/*<NavLink to='/home/page' style={*/}
{/* ({isActive}) => {*/}
{/* return isActive ? {backgroundColor:'red'} : null*/}
{/* }*/}
{/*}>Home页面下的page</NavLink>*/}
<NavLink to='/home/page' style={activeStyle}>Home页面下的page</NavLink>
<Link to={'/about'}>关于页面</Link>
</div>
);
};
export default Menu;
//激活的时候HTML状态
<a aria-current="page" class="active" href="/home/page" style="background-color: red;">Home页面下的page</a>
//未激活的时候HTML状态
<a class="" href="/home/page" style="">Home页面下的page</a>isActive
代表该路由标签是否是活动状态
使用
1 | <div className="list-group"> |
useRoutes(类似于vue的路由表)
- 在之前,我们是通过下面这样子书写注册路由的
1 | <Routes> |
有了
useRoutes
后,我们就可以通过此来构成这种结构语法
const element = useRoutes([ {path:匹配的路径,element:要渲染的组件,children:子路由结构相同} ])
示例
1 | import {NavLink,useRoutes} from "react-router-dom"; |
不过routes都是统一管理
- 所以在src下建立
router/index.js
- index.js内容如下
1 | import About from "../views/About"; |
- 我们在使用的时候就需要引入并生成路由表
1 | import {useRoutes} from "react-router-dom"; |
嵌套路由和Outlet
嵌套路由,就是本身自己是一个路由组件,里面又嵌套了另外一个路由组件
路由表和路由的书写技巧
- 不可以写斜杆,写斜杆代表不管你从哪里开始,我都是从
/
开始的,
所以我们需要在不破坏当前路由的基础上把路由加进去,所以不能有/
/
带了斜杆就是根了(也就url的路径从我开始算起)./
在不破坏当前路径下载后面添加内容,也可以不写./
- 于是乎下面三个效果都是一样的,最终匹配的都是/home/message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//路由表
{
path:'/home',
element:<Home/>
children:[
path:"./message",
path:'/home/message',
path:'message',
]
}
//路由链接的to的书写也和这个相同,
//下面二个均是在对应url后面添加相应的内容
//比如之前url为/home,那么点击`message组件显示`,
//那么url就会变为/home/message
<NavLink to="message">Message组件显示</NavLink>
<NavLink to="news">News组件显示</NavLink>- 不可以写斜杆,写斜杆代表不管你从哪里开始,我都是从
示例需求,在Home组件下建立二个子路由组件,我们先建立好如下的路由表
1 | import About from "../views/About"; |
- 然后我们在Home组件当中书写路由链接,并使用
Outlet
进行占位显示(表示组件要显示的位置)
1 | import {Outlet,NavLink} from "react-router-dom"; |
路由params传参
传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
281.在路由表当中使用占位
import Detail from "../views/Detail"
{
path:"detail/:id/:title/:content",
/*params传参*/
element:<Detail/>
}
2.路由传入参数
const [list] = useState([
{id:'001',title:'消息1',content:'动感超人1'},
{id:'002',title:'消息2',content:'动感超人2'},
{id:'003',title:'消息3',content:'动感超人3'},
{id:'004',title:'消息4',content:'动感超人4'},
{id:'005',title:'消息5',content:'动感超人5'},
])
<ul>
{list.map(item => (
<li key={item.id}>
<Link to={`detail/${item.id}/${item.title}/${item.content}`}>
{item.title}
</Link>
</li>
))}
</ul>接收参数
- 使用
useParams
获取params- 直接获取传入占位符所组成的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import React from 'react';
import {useParams} from "react-router-dom";
const Detail = () => {
const data = useParams();
//{id: '003', title: '消息3', content: '动感超人3'}
return (
<div>
<p>{data.id}</p>
<p>{data.title}</p>
<p>{data.content}</p>
</div>
)
};
export default Detail;- 使用
useMatch
获取params参数- 调用此函数需要传入完整的,包含占位的url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import React from 'react';
import {useParams,useMatch} from "react-router-dom";
const Detail = () => {
const data2 = useMatch('/home/message/detail/:id/:title/:content')
const {params} = data2;
return (
<div>
<p>{params.id}</p>
<p>{params.title}</p>
<p>{params.content}</p>
</div>
)
};
export default Detail;- 使用
路由search传参
传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
271.在路由表当中书写路由,search参数不需要任何占位
import Detail from "../views/Detail"
{
path:"detail",
element:<Detail/>
}
2.路由传入参数
const [list] = useState([
{id:'001',title:'消息1',content:'动感超人1'},
{id:'002',title:'消息2',content:'动感超人2'},
{id:'003',title:'消息3',content:'动感超人3'},
{id:'004',title:'消息4',content:'动感超人4'},
{id:'005',title:'消息5',content:'动感超人5'},
])
<ul>
{list.map(item => (
<li key={item.id}>
<Link to={`detail?id=${item.id}&title=${item.title}&content=${item.content}`}>
{item.title}
</Link>
</li>
))}
</ul>接收参数
- 使用
useSearchParams
,用法有点类似useStateconst [xxx,setXXX] = useSearchParams( )
xxx
通过调用get方法,并传入要获取的key值,就可以得到对于search参数的值setXXX
传入search参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import React from 'react';
import {useSearchParams} from "react-router-dom";
const Detail = () => {
const [search,setSearch] = useSearchParams();
const id = search.get('id');
const title = search.get('title');
const content = search.get('content');
return (
<div>
<p>{id}</p>
<p>{title}</p>
<p>{content}</p>
<button onClick={() => setSearch('id=888&title=设置的标题&content=设置的内容')}>点击我调用setSearch方法</button>
</div>
)
};
export default Detail;当然了,
xxx
不光只有search,还有其他方法,类型推断出来的- 使用
- 当然,你也可以使用
useLocation
来获取参数
1 | import {useLocation} from "react-router-dom" |
路由state传参
- 传参
1 | 1.在路由表当中书写路由,search参数不需要任何占位 |
接收参数
- 使用
useLocation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import React from 'react';
import {useLocation} from "react-router-dom";
const Detail = () => {
const { state:{id,title,content} } = useLocation()
return (
<div>
<p>{id}</p>
<p>{title}</p>
<p>{content}</p>
</div>
)
};
export default Detail;- 使用
编程式路由导航
- 使用
useNavigate
即可 - 示例
1 | import {useState} from "react"; |
useNavigate
支持的参数- 官方地址说明@地址
1 | declare function useNavigate(): NavigateFunction; |
使用useNavigate进行页面后退,前进
- 说明-摘自@官网
1 | Pass the delta you want to go in the history stack. For example, navigate(-1) is equivalent to hitting the back button. |
- 示例
1 | import {useNavigate} from "react-router-dom"; |
useInRouterContext()
- 作用:如果组件在
<Router>
的上下文中呈现,则useInRouterContext
钩子返回 true,否则返回 false。 - useInRouterContext 什么时候不为true,当组件脱离了路由器的管理
- 有时候一些封装的组件可能需要判断是否处于路由环境
useNavigationType()
作用:返回当前的导航类型(用户是如何来到当前页面的)。
返回值:
POP
、PUSH
、REPLACE
。备注:
POP
是指在浏览器中直接打开了这个路由组件(刷新页面)。
useOutlet()
作用:用来呈现当前组件中渲染的嵌套路由。
示例代码:
1 | const result = useOutlet() |
useResolvedPath()
- 作用:给定一个 URL值,解析其中的:path、search、hash值。
redux
redux的核心概念
action
- 和reducer打交道,通过action来告诉reducer应该怎么去操作中心仓库的值
store
- 中心仓库,用于存储数据和接收reducer的初始化数据和改变后的数据
reducer
- 和中心仓库直接打交道的值,返回的状态会影响到store
redux核心API
getState()
:获取状态dispatch({type:xxxx,data:xxxx})
操作reducer当中的方法,找到对应的casestore更新后视图更新:
- 由于react中状态变了视图不一定会发生变化,也就是状态变了一定要重新调用render后视图才会变化
- 所以这里需要监听store变化后改变,需要在index.js主入口文件填下如下内容
1
2
3
4
5
6
7
8
9import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App/>,document.getElementById('root'));
//监听store的变化,进而重新渲染
store.subscribe( () => {
ReactDOM.render(<App/>,document.getElementById('root'));
} )
redux编写案例
案例略,案例不是精髓,里面的笔记才是精髓~
redux简版的笔记
1 | 1.安装redux |
redux完整版的笔记
新增:action对象由专门的文件进行管理,比如
count_action.js
,就是为count所创建的action,当然也可以别的文件夹- 这样子我们在调用
store.dispatch(action)
就不需要手动写action了,而是通过一个变量或者函数来输入action值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//比如原来让数字+1,我们在Count.jsx是这样子做的
store.dispatch({type:'increment',data:1});
//我们对action对象进行专门文件管理后就可以这样子
1.创建一个action.js文件
//当然,你也可以对字符串'increment'单独建立一个文件夹
//便于统一管理action的type类型
export function createIncrement(number){
return {type:'increment',data:number};
}
2.调用的时候只需要引入action.js当中对应的函数
import {createIncrement} from "action.js"
store.dispatch(createIncrement(1));- 这样子我们在调用
新增:action对象当中的type也建立专门的文件进行管理,比如
constance.js
,就可以放置action的type,避免编码疏忽
redux异步版的笔记
什么是异步的呢?有点像nodejs的一些操作,既有同步操作,也有异步操作,比如nodejs的readFile操作
在之前的时候,我们把等待的操作放在了React组件当中,现在我们讲这个等待过程放在action里面去等待,由action定时等待后再去执行reducer(也就是把延迟的动作转交给action)
1 | //之前的做法 |
什么时候需要异步action: 想要对状态进行操作,但是具体的数据靠异步任务返回(非必须)
具体编码(由于redux不支持改写法,所以需要借助中间件来处理)
1.安装
redux-thunk
,在store添加如下配置1
2
3
4
5import thunk from "redux-thunk";
import {createStore,applyMiddleware} from "redux";
import countReducer from "./countReducer";//自己的reducer
export default createStore(countReducer,applyMiddlerWare(thunk));//使用中间件创建action函数不再返回一般对象,而是一个函数,在此函数中书写异步任务
异步任务有结果后,分发一个同步的action去真正操作数据
1
2
3
4
5
6
7
8export const demo = (data,time) => {
return (dispatch) => {
setTimeout(() => {
//有结果了,分发一个同步的action
dispatch({type:'demo',data:data});
},time)
}
}异步action不是必须要写的,完全可以自己等待异步任务的结果后再去分发同步action
react-redux基本概念
- 为什么需要使用react-redux
- 在平时中,我们需要react-redux结合redux来使用
- 单单使用redux操作过程事情太多了,不容易理解,操作和管理(面对大一点项目的时候就非常容易出现这种情况)
- 先来学习下模型图吧,我感觉学习这个模型图,就是在学习设计思想,多学学总是没有错的
- react-redux模型要求如下
- 所有的UI组件都应该包裹一个容器组件,他们是父子关系(也就是
src/component
都是UI组件,src/container
都是容器组件) - **容器组件是真正和redux打交道的,**容器组件可以随意的使用redux的api
- UI组件不能使用任何redux的api(都需要借助于父亲(容器组件))
- 容器组件会传给UI组件如下东东
- redux中保存的状态
- 用于操作状态的方法
- 注意:容器给UI传递的状态 丶方法都是通过props传递的
- 所有的UI组件都应该包裹一个容器组件,他们是父子关系(也就是
react-redux操作代码
store不变,依旧是使用redux
安装react-redux
明确二个概念
- UI组件:不能使用任何redux的api,只负责页面的呈现,交互等
- 容器组件:负责和redux通信,将结果交给UI组件
如果创建一个容器组件–通过react-redux 的 connect函数
connect
函数调用:connect(mapStateToProps,mapDispatchToProps)(UI组件)mapStateToProps
映射状态,返回值是一个对象mapDispatchToProps
映射操作状态的方法,返回值是一个对象
备注1:容器组件中的store的靠props传进去的,而不是在容器组件中直接引入的
备注2:
apDispatchToProps
也可以是一个对象
简化mapDispatch
有几个需要注意的点在简写上,老师视频没有说,导致饶了很大弯路
- 就是想简写
mapDispatchToProps
操作的时候,一定要直接传递对象,而不是通过一个函数返回对象
- 就是想简写
之前的
1 | import {connect} from "react-redux"; |
- 简化后的
1 | import {connect} from "react-redux"; |
- 千万要注意的!千万要注意的!千万要注意的!千万要注意的!千万要注意的!千万要注意的!千万要注意的!千万要注意的!,如果想像上面这种简化形式的写法(不需要dispatch的那种),就一定要直接传递对象,而不是通过一个函数返回对象
1 | //错误的,会导致state状态得不到更新,也不会自动调用dispatch,所以不可以这样子写 |
- 还有值得一提的是,如果你是异步增加的时候,明明配置了中间件thunk,依旧提示报错
Actions must be plain objects. Instead, the actual type was:
,那么可能是你action写法的问题
1 | import {INCREMENT,REDUCE} from "./CountConstance"; |
Provider
- 为了避免出现下面这种多次传入store的情况
1 | import React, {Component} from 'react'; |
- 我们就可以使用react-redux当中的Provider组件,只需要在外面传入一次,App当中所有的后代容器组件都能接收到store
1 | import React from "react"; |
整合UI组件和容器组件
说通俗点就是将UI组件和容器组件写在一个文件里面,因为容器组件最终是暴露一个通过
connect
加工处理后的UI组件,所以写在一起也可以的伪代码示例
1 | import React, {Component} from 'react'; |
数据共享(combineReducers)
- 之前的store我们只在一个Count组件使用,这个时候我们可能不会这么简单,我们需要让这个
redux
可以被所有的组件所使用,并且可以区分数据是谁的
- 目录分级
- 建立
action
文件夹:用于放置每一个数据所需的action - 建立
reducer
文件夹,用于放置每一个数据所需的reducer
- 建立
使用combineReducers合并,合并后是一个总的状态对象
1
2
3
4
5
6
7
8
9
10
11
12import {createStore,applyMiddleware,combineReducers} from "redux";
import thunk from "redux-thunk"
import CountReducer from "./reducer/count";
import PeopleReducer from "./reducer/people";
const allReducer = combineReducers({
count:CountReducer,
people:PeopleReducer,
})
export default createStore(allReducer,applyMiddleware(thunk));- 使用
combineReducers
和没有使用的对比
- 使用
- 我们取值的时候就可以像下面这样子
1 | import Count from "./Count"; |
- 如果出现
Error: Objects are not valid as a React child (found: object with keys {count, people}). If you meant to render a collection of children, use an array instead.
,因为react不能直接展示对象,所以你错误原因就是展示了一个对象数据
下面这个代码用
unshift
为什么有问题,因为unshift
没有返回值刚开始执行default,返回空数组,所以初始化的时候没有什么问题
当type有值的时候,就会执行
case ADDPEOPLE
,然后return preInfo.unshift(data)
运行后的结果,也就是一个null
,和我们想象的返回添加后的数据是不一样的,或者你自己写一个代码块去处理也可以~
1 | import {ADDPEOPLE} from "../const"; |
- 再来看看这个为什么使用
unshift
有问题- 原理上:因为redux发现返回的地址值和之前是一样的,就不更新了
- 其他:因为这个不是纯函数(这里导致传入的preInfo参数被修改了)
1 | import {ADDPEOPLE} from "../const"; |
纯函数和高阶函数
纯函数
- 只要是输入同样的实参,就必定会得到相同的数据输出(返回)结果
1
2
3
4
5
6
7
8
9
10//无论今天,明天还是将来某个时间调用 Math.cos(0) 都没关系,输出始终为1,这就是一个纯函数
//我们来看一个新的例子,下面函数totalApples就是一个纯函数
const numberOfApples = 5;
const applesBought = 5;
const add = (num1, num2) => num1 + num2;
const totalApples = add(numberOfApples, applesBought) // 10
const totalApples = add(numberOfApples, applesBought) // 10
// ... 过了很久很久
const totalApples = add(numberOfApples, applesBought) //10- 不得改写参数数据(也就是你传入的参数,在函数内部不可以去修改)
- 不会产生副作用
- 函数执行的过程中对外部产生了可观察的变化,我们就说函数产生了副作用。
- 例如修改外部的变量、调用DOM API修改页面,发送Ajax请求、调用window.reload刷新浏览器甚至是console.log打印数据,都是副作用
- 说通俗点就是:你这个函数不可以访问外部的作用域和不可以让调试工具知道你这个函数干了什么(当然,debugger肯定不算),因为你发送ajax浏览器会捕捉,window.reload会进行刷新,console.log会进行控制台输出
高阶函数
- 参数是函数或者返回值是函数
- 常见的高阶函数,
forEach/map/filter/reducer/find/bind/promise
等
redux开发者工具的使用
安装
- 谷歌浏览器插件市场搜索
Redux DevTools
即可
- 谷歌浏览器插件市场搜索
使用
- 项目安装依赖(这个是已被废弃的插件),推荐用下一个
1
2
3npm install --save-dev redux-devtools-extension
或者简写
npm install -D redux-devtools-extension- 项目安装依赖(推荐)
1
2yarn add @redux-devtools/extension -D
npm install @redux-devtools/extension -D- 项目使用
1
2
3
4
5
6
7
8
9
10import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/extension';
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(...middleware)
// other store enhancers if any
)
);
打包运行
- 打包
1 | yarn build |
- 打包后在build文件(注意,打包后的文件需要以服务端的形式运行)
- 所以为了简单以服务端的形式运行,我们可以安装下面的插件
1 | yarn add serve -g |
- 安装后运行即可
1 | serve 需要运行的东东 |
React扩展
setState更新状态的2种写法
一
setState(stateChange, [callback])
对象式的setStatestateChange
为状态改变对象(该对象可以体现出状态的更改)callback
是可选的回调函数,它在状态更新完毕,界面也更新后(render被调用)后才执行- 很有用其实,比如在状态更新后对DOM进行一些操作
二.
setState(updater, [callback])
函数式的setStateupdater
为返回stateChange对象的函数updater
可以接收到state和propscallback
是可选的回调函数,它在状态更新完毕,界面也更新后(render被调用)后才执行- 很有用其实,比如在状态更新后对DOM进行一些操作
setState是一个同步的方法,但是后续更新状态的动作是异步的
示例
1 | import React, {Component} from 'react'; |
- 总结
- 对象式的setState是函数式的setState的简写方式(语法糖)
- 使用原则(可以忽略,哪一个喜欢就用哪一个)
- 如果新状态不依赖于原状态 => 使用对象方式
- 如果新状态依赖原状态 => 使用函数方式
- 如果需要在setState() 执行后获取最新的状态数据,要在第二个callback函数中读取
lazy之路由的懒加载
一句话,没有使用懒加载之前,你用到了几个路由组件,那么就会将这些路由组件全部加载过来,而现在我们做的就是使用到了的时候再去加载,加载的时候需要写一个加载等待的页面
基本操作
- 引入
lazy,Suspense
from “react” - 路由使用
Suspense
包括,并添加fallback
属性
- 引入
使用示例如下
1 | import React, {Component,lazy,Suspense} from 'react'; |
- 报错
Error: A React component suspended while rendering, but no fallback UI was specified. Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.
没有使用Suspense
包裹或者没有书写fallback
1 | //错误的 |
Hooks
- Hook是React 16.8.0版本增加的新特性/新语法
- 可以让我们在函数组件中使用state以及其他React特性
useState
- 在之前,我们使用函数组件只能定义一些简单的组件(也就是没有state的组件),但是有了setStateHook,我们就可以使用state了
- 基本语法
1 | const [xxx,setXxx] = React.useState(initValue); |
- 示例
1 | /*函数式组件*/ |
- 报错
React Hook "React.useState" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
1 | //报错的写法,把这个React.useState写在了顶部 |
- 疑问,不是说每一次渲染页面都会重新执行render吗?在函数式组件就是重新执行函数,那为什么状态值还是会更新,按照之前的理解不是应该都是0吗?
- 因为react会在初次调用的时候保存这个状态值
- 当state的值是一个对象时候,修改时是使用新的对象去替换原来的值
1 | const [user,setUser] = useState({ |
- 为什么使用新对象去替换呢?因为旧值和新值指向是同一个地址,react会默认为同一个,就不去更新了,所以set方法就需要整体替换才可以更新视图
1 | const [user,setUser] = useState({ |
- setXxx改变的是下一次渲染时state的值
- setState()会触发组件的重新渲染,它是异步的(不是立刻更新的),相当于有一个队列,剩下代码执行完成后才去执行队列当中的更新操作
1 | const [user,setUser] = useState(0); |
- 使用当setState需要使用旧值state,一定要注意有可能出现计算错误的情况,为了避免这种情况,可以通过为setState设置回调函数的形式来避免
1 | const addHandler = () => { |
useEffect
- Effect Hooks:可以让我们在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
- React中的副作用操作
- 发ajax请求获取数据
- 设置订阅 / 启动定时器
- 手动更改真实DOM
- 语法和说明
1 | import React from "react"; |
我理解为:监听传入参数属性的变化,在组件加载后就立马执行一次回调,如果传入的参数为空数组,那么就不监听值的变化,只会在初次调用,如果不传入,那么会监听所有值的变化,发生变化就执行回调
错误
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
,你useEffect
返回的是一个非函数了
1
2
3
4
5
6
7
8
9
10import React from "react";
React.useEffect(() => {
//do something
// ...
//返回的函数将在组件卸载前执行
return '我返回非函数';//报错,报上面的错误
},[stateValue]) //如果指定的是 [], 那么回调函数只会在第一次render后执行
//否者就会里面的值发生变化,就执行一次回调(因为render被重新执行了)
useRef
- 可以在函数组件中存储/查找组件内的标签或任意其他数据
- 语法
1 | import React from "react"; |
作用:保存标签
对象,功能与
React.createRef()
一样示例
1 | import React from "react"; |
- 其实,直接创建一个对象,对象当中有
current
也可以和useRef一样的效果
1 | const h1Ref = {current:null} |
- useRef()则可以确保每一次渲染获取的是同一个对象,这就是和自己直接创建的区别
- 当需要一个对象不会因为组件的重新渲染而改变的时候,就可以使用useRef
Fragment
- 使用
1 | import {Fragment} from "react" |
- 作用
- 可以不用必须有一个真实的DOM根标签了
- 要参与遍历就需要使用Fragment,这样子就可以设置key值,不参与直接空标签
- 或者使用空标签,空标签不能设置key值,
1 | <> |
- 不管是fragment还是空标签,都不可以设置css属性
Context和useContext
一种组件通信方式,常用于
[祖组件]
和[后代组件]
间的通信一般用于插件
遵循就近原则
- 当我们通过context访问数据时候,他会读取离他最近的provider中的数据,比如下面,,B组件读取的是沙和尚类似于作用域,如果没有provider,则读取context中创建的时候提供的默认值
- 使用
1 | 0.引入React |
示例-类式组件接收
1 | import React, {Component} from 'react'; |
示例-函数式组件接收
1 | import React, {Component} from 'react'; |
有多个数据需要传递也可以,传入一个对象就可以
1 | const MyContext = React.createContext({}); |
useContext(函数式组件)
1 | 除了上面集中使用context,还可以通过context来 |
组件优化
缺点
- 目前我们使用继承
Component
来完成类式组件的书写,但是存在下面二个问题- 只要执行setState(),即使不改变状态数据,组件也会重新render()
- 只要当前组件render()被执行了,就会自动重新调用子组件的render()
1 | import React, {Component} from 'react'; |
可以看到,不管有没有数据,只要调用了setState,数据就会被更新
效率高的做法
- 只有当组件的state或props数据发生改变的时候才重新执行render()
原因
Component
中的shouldComponentUpdate()
总是返回true
解决
方法1
- 重写
shouldComponentUpdate()方法
, - 通过比较新旧state或props数据,如果有变化就返回true,没有就返回false
- 重写
方法2
使用
PureComponent
PureComponent
重写了shouldComponentUpdate(),只有state或props数据发生变化了才返回true注意:
只是进行
state
和props
数据的浅比较,如果知识数据对象内部数据变了,shouldComponentUpdate依旧会返回false,如下代码就是修改对象内部数据,就不会更新,也就是shouldComponentUpdate返回false了1
2
3
4
5const changeCart = () => {
const obj = this.state;
obj.carName = '动感超人',
this.setState(obj);
}所以需要产生新数据,不要直接修改state数据
项目一般使用
PureComponent
来优化
PureComponent的例子
1 | import React, {PureComponent} from 'react'; |
render props(标签体)
- 平时我们写的
<div>中间内容</div>
,这个”中间内容”我们叫他标签体内容 - 标签体内容如果在react当中使用,比如
<A>传入的值</A>
,那么标签体内容再A组件当中就会出现在this.props.children
当中
那么如果我们需要像vue插槽一样可以实现动态传入带内容的结构,在react当中怎么实现呢
- 使用
children
的props属性,通过组件标签体传入结果 - 使用
render props
:通过组件标签属性传入结构,一般用render函数属性
children props(通过组件标签体传入)
1 | <A> |
- 示例
1 | import React, {PureComponent} from 'react'; |
render props(通过组件标签属性传入)
- 这样子我们就可以在A组件当中随意控制一个东西放置在里面了
1 | <A render={ (data) => <C data={data}/> }></A> |
- 示例
1 | import React, {PureComponent} from 'react'; |
错误边界
- 错误边界(error boundary),用来捕获后代组件错误,渲染出备用页面
- 特点
- 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件,定时器中产生的错误
- 使用方式
getDerivedStateFromError
配合componentDidCatch
- 在生产环境下才可以看到效果,开发模式只会一闪而过
1 | //生命周期函数,一旦后台组件报错,就会触发 |
- 示例
1 | import React, {Component} from 'react'; |
随记
简称要全大写
reduce条件统计
开启了严格模式,禁止函数的this指向window
类
如果继承后想要有自己的属性,那么就需要在构造器当中去调用
super
,super
必须要在this之前被调用如果继承后没有自己的属性,可以省略
super
,甚至构造器都可以省略不写如果继承后和父类有相同的名字函数,不是被覆盖了,而是在这一个函数之前,有同名的函数了,所以当依据原型链去寻找的时候,找到了一个,就不会接着找下去了,(就近原则),达到了重写的目的
类中书写的属性
在state
静态属性
- 只可以类名.属性名调用
static
关键字修饰
实例属性
1
2
3
4
5
6
7
8class Person {
static num = 20;
address = '深圳'
constructor(n1,a1){
this.name = n1;//实例属性
this.age = a1;//实例属性
}
}
如果有
null
,可能是暗示你书写一个对象axios当中的then返回的参数是axios封装的一层对象,想要获取真正结果,需要多一层data才可以,也就是res.data才是获取真实结果
- 同理,axios的catch也是,需要通过error.message才是获取错误信息
src目录为程序写的东东
对象不能直接展示在react当中
kebab-case
烤肉串形式命名,全部都是小写的,通过’-‘穿在一起