初识 React
是什么
用于构建用户界面的js库,是一个将数据渲染为html视图的开源js库,由facebook开发的
为什么要学习react
原生js操作dom繁琐,效率低,
使用js直接操作dom,浏览器会进行大量的重绘重排
原生js没有组件化编码方案,代码复用率低
特点
- 采用
组件化
模式、声明式编码
,提高开发效率及组件复用率。 - 在
React Native
中可以使用React语法进行移动端开发 - 使用
虚拟DOM
+优秀的Diffing
算法,实现dom的复用,尽量减少与真实DOM的交互。

hello_react
1 |
|
- 上述三个js文件一定要按顺序引入
- 我们引入
react.development.js
后,在全局就会出现React
对象,引入react-dom.development.js
后,在全局就会出现ReactDOM
对象 - script标签的类型一定要是是
text/babel
,因为我们写的是jsx代码,然后借助**浏览器的babel
**进行代码转换
jsx
是什么
jsx是javascript
和xml
的缩写,xml早期用于存储和传输数据。现在已经被json替代
1 | <student> |
为什么在react中使用jsx不使用js?
创建虚拟dom
两种语言创建虚拟dom的语法不同
jsx
1
const VDOM = <h1 id="title"><span>Hello,React</span></h1>
js
1
const VDOM = React.createElement('h1',{id:'title'},React.createElement('span',{},'Hello,React'))
显然,使用jsx创建虚拟dom更简单。
虚拟dom是什么
我们知道使用jsx创建虚拟dom更简单,那虚拟dom是什么呢?
虚拟dom本质就是一个js对象,是对真实dom的高度抽象,我们可以通过输出虚拟dom和真实dom来比较分析它们的区别
1 | <script type="text/babel"> |
我们可以观察到,虚拟dom和真实dom都是对象,但是真实dom身上的属性,比虚拟dom上的属性多得多。
jsx语法规则
- 定义虚拟dom的时候,不要写引号
- 在标签中混入js表达式的时候要用
{}
,一定注意区分:js语句
与js表达式
- 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,下面这些都是表达式:
- a
- a+b
- demo(1),函数调用,值是函数的返回值
- arr.map(),函数调用,值是函数的返回值
- function test(){},函数定义
- 语句(代码):下面这些都是语句(代码):
- if(){}
- for(){}
- switch(){case:xxxx}
- 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,下面这些都是表达式:
- 样式的类名不要用
class
,而要用className
,为了避开es6的class关键字 - 内联样式要用
style={{key:value}}
的形式书写 - 标签必须闭合
- 只有一个根标签,类似vue2
- 标签首字母如果是小写的,则将该标签转换为html中的同名标签,如果html没有对应的标签则报错
- 标签首字母如果是大写的,react就去渲染对应的组件,若该组件没有定义则报错。
1 | <!DOCTYPE html> |
组件与模块化
模块化
将复杂的js文件拆分成一个一个js文件,每个js文件就是一个模块
组件化
组件是能实现局部功能的代码和资源的集合(html,css,js,images…)
组件化
函数式组件
1 | <script type="text/babel"> |
要注意的点包括:
- 函数名必须大写,函数名就是组件名,要符合组件名的规范,首字符必须大写
- 函数必须有返回值,返回一个
虚拟dom
ReactDOM.render
的第一个参数必须是组件标签,而不是组件名。- 函数式组件可以接收传入的props,因为函数可以传参
类式组件
1 | <script type="text/babel"> |
要注意的点包括:
- 如果一个类想要成为
类式组件
,必须继承React.Component
类 - 这个类必须实现
render
方法,且这个方法必须有返回值
组件实例的3大属性

组件实例上的所有属性,都是React.Component
类的构造函数
初始化的,因为我们定义的类并没有构造器。然而即便我们不书写构造器,也会默认添加一个构造器:
1 | class Child extends React.Component {} |
1 | class Parent { |
要注意的是,使用super关键字调用父类构造函数,和直接调用父类构造函数的很重要的区别在于,使用super关键字调用父类构造函数,构造函数的this指向子类实例。
state
可以看到上面实例的state属性是null,如果我们想要修改这个属性,就必须在自定义的组件
(类)中添加构造函数。
初始案例
1 | <script type="text/babel"> |
要注意的包括以下几点:
- 类中的方法都在局部开启了严格模式
- render方法是通过组件实例调用的,不过这个组件实例不是我们手动创建的,这个方法也不是我们手动调用的。
- 在react中
onClick
不能写成onclick,虽然在js原生语法中就是写做onclick
state的精简
在上述代码中我们为了初始化state等操作,引入了构造函数,其实我们可以直接省略构造函数
1 | <script type="text/babel"> |
setState
在react中,我们不能直接修改state,否则虽然数据会改变,但是react无法监听到,视图也不会更新,我们必须使用this.setState
方法,修改数据并通知视图更新。
props
1 | <script type="text/babel"> |
可以看出在react中,也是通过给组件标签
添加属性
来实现给组件传值的,然后这些属性会被收集到组件实例的props属性中,值为一个对象。而下面的代码则展示了如何快速的给组件传值
1 | let obj = {name:"jerry",age:19,sex:"男"} |
限制传入组件值的类型
1 | ReactDOM.render(<Person name="jerry" age="19" sex="男"/>, document.getElementById('test1')); |
通过上述方式传入的name,age等属性值,它们的类型都是字符串,后续如果需要在模板中实现:
1 | <li>年龄:{age+1}</li> |
的效果,得到的就是191
,即字符串拼接。
想要对传入组件的值的类型进行限制,并添加默认值,需要添加如下代码:
1 | //对标签属性进行类型、必要性的限制 |
其中的PropTypes
对象是通过,引入react/prop-types.min.js
文件后,出现的全局的对象。
可以看出在react中限制给组件传入的值的类型是非常麻烦的。
其实我们可以把 Person.propTypes = {...}
,和Person.defaultProps = {...}
的操作写在类的内部,从而简化代码。本质都是在类的构造函数上加属性(typeof Class A = 'function'
)
1 | <script type="text/babel"> |
我们之前在学习react组件的构造器的时候也发现了props的身影:
1 | constructor(props) { |
其中的props
,就是传递给组件的所有参数组成的对象,如果不写super(props)
,就无法在构造函数中通过this.props
访问到传递给组件的参数对象,但是最终this.props
还是会被正确初始化。
我们可能认为,因为函数式组件内部没有this,所以不存在组件实例的三大属性。但是因为函数可以传参,我们可以在函数式组件中拿到props:
1 | function Person(props){ |
同时我们也观察到到,类式组件的构造函数中(如果书写的话),传入的参数也包含props。
refs
this.refs
1 | class Demo extends React.Component { |
在react中,我们没有必要直接通过document.querySelector
来获得dom对象,我们直接给组件标签添加ref
属性,并传入一个唯一的值key,然后这个标签对应的dom元素,就可以通过this.refs.key
访问到,这一点和vue2中的语法是很像的(在vue2中是通过this.$refs.key
拿到)。这种写法因为需要借助this,所以不能在函数式组件中使用。
回调函数
然而这种给ref属性赋值字符串的语法,是不被推荐的,因为被认为是效率低的,推荐的写法是传入回调函数
,在传入的回调函数的参数中能拿到对应的dom。
1 | render() { |
不过要注意的是,如果 ref 回调函数是以内联函数的方式定义的(比如上面的例子),在更新过程中,它会被执行两次,第一次传入参数 null
,然后第二次会传入 DOM 元素。这是因为在每次渲染时,会创建一个新的函数实例,所以 React 清空旧的 ref(传入null), 并且设置新的,不过这是无关紧要的,开发过程中仍然可以使用。
1 | <script src="./react/react.development.js"></script> |
为了避免这种问题,我们可以传入一个在类中已经定义好的,挂载到组件实例上的函数。
React.createRef()
然而,react最推荐的方式是使用React.createRef()
先定义一个容器,然后再把dom放入容器中:
1 | myRef = React.createRef() |
最后通过this.myRef.current
就能访问到dom,要注意的是每个容器只能放一个dom,感觉不如document
….
事件处理
- 通过
onXxx
属性指定事件处理函数(注意大小写) - React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件
- React中的事件是通过
事件委托
方式处理的(委托给组件最外层的元素) - 通过 event.target 得到发生事件的 DOM 元素对象
受控组件和非受控组件
- 受控组件就是表单组件值改变的时候就更新值到state
- 而非受控组件就是通过ref获取表单组件dom,然后需要的时候通过dom.value来获得值
生命周期(旧)
1 | <script type="text/babel"> |
不能在render函数中开启定时器,通过this.setState
来修改状态,因为这个操作会触发render,从而导致无限调用render,开启多个定时器,所以我们把开启定时器的代码写在生命周期函数中。简单的来说,不能在render函数中书写可能触发render的操作。

shouldComponentUpdate
:这个钩子的作用就类似一个阀门,如果我们在组件中不写这个钩子,这个钩子默认存在且返回值为true
。如果这个钩子的返回值为false,那么数据更新了也不会调用render方法更新视图forceUpdate
方法的效果是强制组件更新,即便数据没有改变,也不会经过shouldComponentUpdate
钩子的判断具有父子组件的页面初次加载的时候,先按顺序执行父组件的
constructor
,componentWillMount
,render
钩子,再按顺序执行子组件的constructor
,componentWillMount
,render
,componentDidMount
钩子,再执行父组件的componentDidMount
钩子。也就是说,页面初次加载的时候,和update
有关的钩子都不会被执行父组件执行到
render
函数的时候,开始解析子组件,直到子组件的dom创建好了(componentDidMount),父组件再执行后续代码,挂载dom(componentDidMount
)。当父组件间中的数据更新(调用
setState
),父组件会依次调用shouldComponentUpdate
,componentWillUpdate
,render
钩子,调用到render
钩子的时候,因为render
中包含了子组件,所以开始触发子组件更新,然后子组件依次调用:componentWillReceiveProps
,shouldComponentUpdate
,componentWillUpdate
,render
,componentDidUpdate
钩子,再执行父组件的componentDidUpdate
的钩子。也就是说,父组件如果不是某个组件的子组件,就不会触发
componentWillReceiveProps
钩子。其实如果父组件并没有给子组件传值,父组件重新render,也会导致子组件重新render。但是子组件重新render,只会调用子组件
shouldComponentUpdate
,componentWillUpdate
,render
,componentDidUpdate
钩子,父组件不会重新渲染。我们可以看到,react的所有生命周期钩子都包含
component
,且几乎都以它开头,这是否有点繁琐呢?相比于vue;去除掉component
,我们可以观察到,willMount
就是vue中的beforeMount
,DidMount
就是vue中的mounted
;对于更新部分,在vue中不存在
shouldComponentUpdate
这样控制是否更新的钩子,而willUpdate
就是vue中的beforeUpdate
,而DidUpdate
就是vue中的updated
。对于render,在vue中,虽然
render
函数出现频率没有react中的那么高,但是它们的作用都是一致的,就是创建虚拟dom,而且它们被调用的时机都是相似的,在WillMount
和DidMount
之间,或者在WillUpdate
和DidUpdate
之间。

案例代码:
1 |
|
生命周期(新)
在react的17.x版本后,componentWillMount
,componentWillUpdate
,componentWillReceiveProps
这三个钩子(3个will)已经不推荐使用,因为它们是不重要的,而且经常被错误的使用,并且还可能在未来的异步渲染中引发更多问题,因此17.x版本后这个三个钩子必须加上UNSAFE_
前缀,这并不意味这这三个钩子是不安全的,只是为了加长单词长度让人们尽可能的少用它们,并且这三个钩子在未来很可能被删除。

除此以外,还添加了2个新的生命周期钩子:getDerivedStateFromProps
和getSnapshotBeforeUpdate
,虽然这2个钩子在开发过程这几乎没有用武之地,但是还是需要了解的。

getDerivedStateFromProps
1 | static getDerivedStateFromProps(props,state){ |
这个钩子的中文意思就是,从props获取派生的状态,当你的state在任何的时候都取决于props,那么这个钩子才有作用。
而且这个钩子前必须使用static
修饰,说明它其实会挂载到类上面,也就是构造函数上去。
这个静态方法会在组件实例化,以及每次组件更新之前被调用,它接收两个参数:props
(新的属性)和state
(当前的状态)。该方法的目的是根据传入的新属性,来决定是否需要更新组件的状态。
getSnapshotBeforeUpdate
这个钩子的中文意思是,在更新之前获取快照
1 | //在更新之前获取快照 |
这钩子的返回值,会被传递给componentDidUpdate
钩子,也就是snapshotValue
。
1 | <script type="text/babel"> |
diff算法
diff算法的最小比较单位是结点
react/vue中的key有什么作用?(key的内部原理是什么?)
简单的说:key是虚拟DOM对象的唯一标识,在更新显示时key起着极其重要的作用。
详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后进行新旧虚拟dom的比较,比较规则如下:
存在与新虚拟dom的key值相等的旧虚拟dom
- 若虚拟DOM中内容没变,直接使用之前的真实DOM
- 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中旧的真实DOM
不存在与新虚拟dom的key值相等的旧虚拟dom,根据新的虚拟dom,创建新的真实DOM,随后渲染到到页面
使用每条数据的唯一标识作为key,有利于diff算法,有利于提高dom的复用率。
为什么遍历列表时,key最好不要用index?
如果逆序添加数据,会造成没有必要的真实dom更新。
脚手架
介绍
- xxx脚手架:用来帮助程序员快速创建一个基于xxx库的模板项目
- 包含了所有需要的配置(语法检查、jsx编译、devServer)
- 下载好了所有相关的依赖
- 可以直接运行一个简单效果
- react提供了一个用于创建react项目的脚手架库:
create-react-app
- 项目的整体技术架构为:
react + webpack + es6 + eslint
- 使用脚手架开发的项目的特点:模块化,组件化,工程化;工程化的意思是构建工具(webpack)自动帮我们进行
语法检查,代码压缩,兼容性处理
等等一系列的功能
创建项目并启动
全局安装
1
npm install -g create-react-app
切换到想创项目的目录,使用:
1
create-react-app hello-react
进入项目文件夹:
1
cd hello-react
启动项目:
1
npm start
项目结构分析
public
public目录下存放的都是静态文件
html文件
1 |
|
robots.txt
爬虫规则文件
src
项目的源代码
App.js
App组件,也叫根组件,类似vue中的App.vue
1 | import logo from './logo.svg'; |
看以看出,App.js使用的是函数式组件,而且对于使用react脚手架开发的项目,在js文件中也可以直接使用jsx语法,也不会报错。
App.css
App组件的样式文件,在App.js文件中被导入
App.test.js
App组件的测试文件,不过使用的频率并不高
index.css
项目的全局样式文件
index.js
项目的入口文件
1 | import React from 'react'; |
在 <App />
标签外包裹<React.StrictMode>
的作用是,帮助我们自动检查代码书写不合理的地方。
而reportWebVitals
是一个函数,是用来记录页面性能的,用到了web-vitals
库
setup.Tests.js
是用来测试整个项目的
样式的模块化
React本身并没有提供像Vue那样的scoped
属性,来直接实现样式的局部作用域,那再react中如何实现组件样式的隔离呢?
如果我们在不同的组件中定义了相同的样式比如:
在Hello/Hello.css
文件中
1 | .title{ |
在Welcome/Welcome.css
文件中
1 | .title{ |
这样当我们在App.js中同时引入Hello组件和Welcome组件,就会产生样式冲突 ,那如何避免样式冲突呢?
一种解决办法就是修改Hello.css
文件名为Hello.module.css
,同时还要修改引入css文件的方式
1 | import hello from './Hello.module.css |
vscode的react插件安装

安装这个插件后,书写react代码就有对应的代码提示了
rcc:快速创建一个类式组件
1
2
3
4
5
6
7
8
9import React, { Component } from 'react'
export default class index extends Component {
render() {
return (
<div>index</div>
)
}
}rfc:快速创建一个函数式组件
功能界面的组件化编码
- 拆分组件:拆分界面,抽取组件
- 实现静态组件:使用组件实现静态页面效果
- 实现动态组件
- 动态显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件?,即数据存放的位置
- 交互(从绑定事件监听开始)
- 动态显示初始化数据
案例todolist
body的宽度默认等于视口宽度,等于html的宽度,没有必要使用
body{width:100%}
在react组件名必须大写,否则会被认为是html标签
搭建静态结构
确定状态由谁来维护比较合适?如何把header(子组件)的数据传递给app(父组件),父组件给子组件传入一个方法,
子组件调用这个方法的时候传入值,就能修改父组件中的数据。
如何监听表单输入?添加onKeyUp属性
1
<input type="text" placeholder='请输入你的任务名称,回车键确认' onKeyUp={this.keyUp}/>
如何确定按下了enter键?(
event.keyCode
每个按键都又对应的keyCode)添加的内容不能为空,enter后要清空输入框如何拿到
<input type="checkbox"/>
的值?通过e.target.checked
item组件如何修改父组件的父组件(app组件)中的数据?先将在app组件中定义修改todos的方法,在传递给List组件,List组件直接传递给item组件。
鼠标悬浮到指定事项上,应该改变样式,可以使用
:hover
伪类实现1
2
3
4
5
6.item button{
visibility: hidden;
}
.item:hover button{
visibility: visible;
}实现删除,如何实现一个提示框提示是否删除?window.confirm,为什么不能直接写confirm
在react中如何实现vue中的计算属性?
defaultChecked属性只能生效一次,之后被删除
总数为0的时候,不能显示全选
所学知识点:
- 拆分组件、实现静态组件,注意:className、style的写法
- 动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在其自身的state中
- 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
- 关于父子之间通信:
- 【父组件】给【子组件】传递数据:通过props传递
- 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
- 注意defaultChecked和checked的区别,类似的还有:defaultValue和value
- 状态在哪里,操作状态的方法就在哪里
在react中配置代理
方法一 :在package.json
中添加proxy属性
这种配置方法的缺点很明显,就是不能配置多个代理(多个目标服务器)
1 | "proxy":"http://localhost:5000" |
方法二:在src目录下新建setupProxy.js
文件,该文件中只能使用CJS语法,因为会被合并到webpack配置文件中。
这种方法的好处是能配置多个代理,并且能控制哪个请求要代理到哪个服务器;缺点是配置较为复杂,而且需要修改请求的url
1 | const proxy = require('http-proxy-middleware') |
其实前端中代理的配置,本质都差不多,都是基于http-proxy-middleware
这个库。关于代理的更多介绍,参考前端面试vue一文。
在兄弟组件间传递数据
使用消息订阅发布机制,下载pubsub库。
react路由
路由的理解
什么是路由
- 一个路由就是一个映射关系(key:value)
- key为路径, value可能是
function或component
路由分类
后端路由:
- 理解: value是function, 用来处理客户端提交的请求。
- 注册路由:
router.get(path, function(req, res))
- 工作过程: 当node接收到一个请求时, 根据请求路径,找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据。
前端路由:
- 浏览器端路由, value是component, 用于展示页面内容。
- 注册路由:
<Route path="/test" component={Test}>
- 工作过程: 当浏览器的path变为
/test
时, 当前路由组件就会变为Test组件。
react-router
- react的一个插件库
- 专门用来实现一个SPA应用
- 基于react的项目基本都会用到此库。
- 这个库其实有三个版本,分别是
web,native和anywhere
,我们学的其实是react-router-dom
,即web端的路由库。
推荐一个网站:印记中文 - 深入挖掘国外前端新领域,为国内 Web 前端开发人员提供优质文档!
单页面程序的原理就是,点击修改路由,然后路由器
监听到路由改变,替换组件。
原生html中通过a
标签来跳转页面,在react中通过Link
标签来实现,而且这个链接要使用特定的Router
标签(BrowserRouter或者HashRouter)包裹。
其实link标签最后还是会被转化成a标签,不过在此基础上添加了监听,阻止了页面跳转
再react中通过Route
标签,在组件中注册路由(注册组件的子路由),当这个组件被渲染的时候,对应的路由被注册,且启动一次路由匹配,可能触发重定向。
而在vue中,在router/index.js
文件中注册路由
1 | //BrowserRouter使用的是history路由,HashRouter代表使用的是哈希路由 |
要注意的是Link
标签,和对应的Route
标签必须在同一个BrowserRouter
标签下,为了方便起见,我们通常使用BrowserRouter
包裹整个App组件,毕竟这个应用应该只有一个路由器。
1 | //index.jsx |
如果想要点击标签有对应的高亮样式,那么就不能使用Link标签
而是NavLink标签
1 | <NavLink activeClassName="atguigu"> </NavLink> |
activeClassName
的值默认是active,但是如果你想要自定义高亮样式,那么就自定义一个类,然后传入。
封装NavLink:如果每个NavLink标签都加上activeClassName,那么这个标签的长度就变得非常长了,不方便阅读,也不够简洁,其实我们可以自定义一个MyNavLink组件,实现对NavLink的封装。本质就是返回一个添加了activeClassName
的NavLink
。
1 | import React, { Component } from 'react' |
然后复用,这样在MyNavLink标签上就不需要写activeClassName
1 | import './App.css'; |
值得注意的是,组件标签(比如NavLink,MyNavLink)
的标签体,也算是一个标签属性(children
属性),同样的,在组件标签中给children属性赋值,其实就是在书写标签体。
1 | <MyNavLink to='/about'> About </MyNavLink> |
路由组件和一般组件
根据组件的用途不同
,可分为一般组件
和路由组件
写法不同
路由组件是指切换路由展示的组件,不通过直接书的方式写来渲染;而一般组件则是直接拿来渲染的组件;比如我们有个组件About,通过上述方式使用的就叫做路由组件,如果通过直接书写即<About/>
方式展示的,就叫做一般组件。
接收到的参数不同
一般组件如果不在标签上传值,那么这个组件内部就接收不到任何值(this.props
是空对象);但是路由组件即便没有显式给它传参,也会接收到参数。
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: “”
- state:undefined
match:
- params: {}
- path: “/about”
- url: “/about”
存放位置不同
一般组件通常放到components目录下,而路由组件通常放在pages或者views目录下。
Switch组件
1 | <Switch> |
如果不使用Switch组件,如果路由匹配到Test组件,还会继续匹配,最终同时会展示Home组件和Test组件;但是如果使用了Switch组件,匹配成功后,就不会继匹配了。简单的说,给Route标签包裹Switch组件的作用就是,确保只展示第一个匹配到的组件。
模糊匹配
react路由默认使用的是模糊匹配,也就是说,如果当前路由是/home/a/b
,某个组件的path是/home
,那么这个组件将会被展示,但是如果给route
标签添加exact
属性,就开启了严格匹配。
我们一般情况是不开启严格匹配的,举个例子,如果我们想要展示二级路由组件,就必须先展示一级路由组件,但是开启了严格匹配,就匹配不到一级路由组件了。
redirect
1 | //App.js |
当页面url等于localhost:3000/
,渲染的其实是App组件
,也就是根组件(因为访问localhost:3000/
会返回index.html
文件,然后解析这个html文件构建dom树,异步加载引入的js文件,当dom树构建好便执行js文件,将App组件挂载到这个html文件上),然后上述注册路由的代码就会被触发,并开始与当前页面url匹配,然后发现前2个都匹配不了,于是重定向到/about
。
嵌套路由
开启二级路由,只需要在一级路由组件中(比如About组件),继续书写NavLink
和Route
标签即可
1 | export default class About extends Component { |
必须注意的是,二级组件的匹配路径,必须以父组件(一级组件)的匹配路径开头,的比如/about
,为什么要这样写呢?因为我们想要展示出二级组件,必须先展示出一级组件,而只有按照这种方式书写二级组件匹配路径,才能做到同时匹配一级组件和二级组件。当我们点击跳转到/about
只会展示一级组件about,同时注册二级路由(因为相关注册代码就再about组件里)。如果我们再添加Redirect
,就会重定向到message组件。
1 | <div className='content'> |
携带路由参数
动态路由传参
完成动态路由传参需要三步,分别是传参,声明,和取值
1 | export default class News extends Component { |
取值:可以在Detail组件中,通过this.props.match.params
获取到我们传入的参数。
查询参数传参
完成查询参数传参需要2步,分别是传参和取值
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
按收参数:this.props.location.search
备注:获取到的search是urlencoded
编码字符串,需要借助querystring
解析
通过state传参
通过state传参只需要2步,第一步是传参,第二部步是接收。
这种方式区别于前2种方式,路径里没有任何提示,或者说路径中不包含任何传递给路由组件的数据,
路由链接(携带参数):<Link to = {{pathname:'/demo/test', state:{name:'tom',age:18} }}>详情</Link>
感觉这个state好像history API中的history.pushState(title, state, url)
中的state。
注册路由(无需声明,正常注册即可):<Routepath="/demo/test" component={Test}/>
接收参数:this.props.location.state
备注:刷新也可以保留住参数
路由跳转模式
默认情况下,我们的路由跳转模式是push
,也就是说每次路由跳转都会往历史记录栈里push一条历史记录,其实我们可以修改路由模式为replace
,只需要在Link标签上添加replace属性。
其实在vue中也是一样的Router.push()
对应的就是push模式,Router.replace
对应的就是replace模式
编程式路由导航
this.props.hsitory
属性下有2个方法:push和replace,分别对应2中路由跳转模式。
1 | history: |
1 | <ul> |
withRouter
一般组件并不会被传递路由组件的那些api(比如go,goBack,goForward),不显式地给一般组件传参,一般组件就接收不到任何参数,为了能让一般组件也能使用路由组件的那些api,我们就需要借助withRouter
这个函数
1 | import { withRouter } from 'react-router-dom' |
在新版本的react-router中,这个api已经被移除了。
BrowserRouter和HashRouter的区别
BrowserRouter使用的是History API,借助这些api来形成历史记录,而HashRouter单纯是通过修改URL的哈希部分,来形成历史记录的
state传参中的state参数,是history api特有的,使用BrowserRouter并刷新页面,state不会丢失,而使用HashRouter并刷新页面,state会丢失
1
history.pushState(state, title, url)
UI组件库Ant Design
- 使用ui组件库不需要去背,用熟练了就好
- 难点在于分析那些代码是属于你想要的组件的
- 按需引入样式,减少最终文件的体积,查看文档(具体位置是《在create-react-app中使用》)按照文档的指示一步一步操作即可。
- 修改主题颜色
redux
学习文档
英文文档:https://redux.js.org/
中文文档:http://www.redux.org.cn/
Github:https://github.com/reactjs/redux
redux是什么
redux是一个专门用于做
状态管理
的JS库(不是react插件库)。它可以用在react,angular, vue等项目中,但基本与 react 配合使用。
作用:集中式管理react应用中多个组件共享的状态
什么情况下需要使用redux
- 某个组件的状态,需要让其他组件可以随时拿到(共享)。
- 一个组件需要改变另一个组件的状态(通信)。
- 总体原则:能不用就不用,如果不用比较吃力才考虑使用。

reducer
的作用不仅仅包括更新数据,还有初始化数据的作用,因为刚开始是没有previewState
的,所以它的值是undefined
redux的三个核心概念
react-redux

react扩展
setState
setState(stateChange, [callback])
—— 对象式的 setState
stateChange
为状态改变对象(该对象可以体现出状态的更改)callback
是可选的回调函数,它在数据和视图都更新完毕后 (render
调用后) 才被调用
setState(updater, [callback])
—— 函数式的 setState
updater
为返回stateChange
对象的函数。updater
可以接收到state
和props
(第一个参数是state,第二个参数是props)callback
是可选的回调函数,它在数据和视图都更新完毕后 (render
调用后) 才被调用- 这种情形,传入的2个参数都是函数
总结:
- 对象式的
setState
是函数式的setState
的简写方式(语法糖)。 - 使用原则:
- 如果新状态不依赖于原状态,比如
this.setState({count:90})
使用对象方式 - 如果新状态依赖于原状态,使用函数方式
- 如果需要在
setState()
执行后获取最新的状态数据, 要在第二个callback
函数中读取
- 如果新状态不依赖于原状态,比如
懒加载
- 从react中引入lazy函数,修改导入路由组件的方式(竟然不是从
react-router
中导入的吗) - 导入Suspense组件包括Route,传入组件尚未被加载的时候,展示的结构或者组件
1 | import React, { Component,lazy,Suspense } from 'react' |
1 | <Suspense fallback={<h3>loading</h3>}> |
Hooks
React.useState
这个钩子的作用就是,让函数式组件也可以有自己的状态,并且可以对状态进行读写。
每次修改状态,都会重新调用函数式组件,但是由于对useState
特殊处理,并不会重新初始化变量。
1 | import React from "react" |
React.useEffect
Effect Hook 可以让你在函数组件中执行副作用操作,用于模拟类式组件中的生命周期钩子
React 中的副作用操作:
- 发 ajax 请求数据获取
- 设置订阅 / 启动定时器
- 手动更改真实 DOM
语法和说明:
1 | useEffect(() => { |
1 | React.useEffect(()=>{ |
可以把 useEffect Hook
看做如下三个函数的组合:
componentDidMount()
:如果第二个参数穿入的是空数组,表示不监听任何状态改变,则效果就相当于这个生命周期钩子componentDidUpdate()
:如果第二个参数穿入的不是空数组,监听的状态改变后,也会触发传入的回调函数。componentWillUnmount()
:传入的回调函数返回的函数,其作用就相当于这个钩子
可以看出在函数式组件中使用useEffect来模仿类式组件中的生命周期钩子,还是比较麻烦的。
React.useRef
RefHook可以在函数组件中存储/查找组件内的标签或任意其它数据
语法:const refContainer = React.useRef()
作用:保存标签对象,功能与React.createRef()
一样
1 | import React from "react" |
React.Fragments
1 | import { Fragment } from 'react' |
用来解决react组件中只能有一个根标签的问题,Fragment标签不会被渲染,就类似vue3中的fragment或者template
React.createContext
这个api主要用来解决祖先组件给后代组件传值,就类似vue中的provide和inject
1 | import React, { Component } from 'react' |
组件优化
Component的2个问题
只要执行setState()
,即使不改变状态数据,组件也会重新render()
,效率低
只要当前组件重新render()
,就会自动重新render子组件,纵使子组件没有用到父组件的任何数据,效率低
1 | //父组件 |
1 | //子组件 |
效率高的做法:只有当组件的state或者props数据发生改变时,才重新render()
原因:Component中的shouldComponentUpdate()
总是返回true
解决:
重写
shouldComponentUpdate()
方法:比较新旧state或props数据,如果有变化才返回true
,如果没有返回false
shouldComponentUpdate()
接收两个参数:nextProps
: 下一次的props
。nextState
: 下一次的state
。
需要在这个方法中比较当前的
props
和state
(通过this.props
和this.state
)与下一次的props
和state
。如果返回true
,组件会重新渲染;如果返回false
,组件不会重新渲染。问题是如何进行对象之间的比较呢?直接使用this.props === nextProps
这种方式吗?我测试过,即便父组件给子组件传入的只是一个常量,父组件触发render之后,子组件的nextProps
也部等于this.props
,虽然这2个是内容完全相同的对象,但是地址不同。使用
PureComponent
:PureComponent
重写了shouldComponentUpdate()
,只有state或props数据有变化才返回true
注意:
- PureComponent只是进行state和props数据的浅比较,本质上就是只比较对象的第一层级属性值。对于基本数据类型(如字符串、数字、布尔值等),它直接比较这些值是否相等;而对于引用数据类型(如对象和数组),则比较它们的引用地址是否相同。
- 不要直接修改state数据,而是要产生新数据,否则视图不会更新。
项目中一般使用PureComponent来优化上述问题
render Props
1 | import React, { Component } from 'react' |
上述例子中,A和B的父子组件关系是显而易见的,因为我们直接把<B></B>
写在了A组件中,此时如果A组件想要给B组件传值,直接在B组件标签上添加属性即可。
但是除此以外,让A,B组件形成父子关系的方式还有如下方式:
1 | import React, { Component } from 'react' |
此时想要在A组件中展示B组件,还需要添加代码:
1 | class A extends Component { |
其中this.props.children
的值是一个对象,结构较为复杂。
此时想要实现A组件向B组件传值,貌似就难以实现了,反而,实现Parent组件向B组件传值变得简单了。
其实,要实现A,B组件的父子关系,还可以通过另一中方法实现:
1 | import React, { Component } from 'react' |
然后只需要修改A组件代码:
1 | class A extends Component { |
就能实现A,B组件的父子关系,这种方法还有一个好处就是,能实现A组件给B组件传递参数
1 | import React, { Component } from 'react' |
我们可以发现,这一点其实很像vue中的插槽,A,B组件,在我们未使用它们之前,它们并没有明确的父子组件关系,但是我们可以借助render props
实现给组件动态传递结构。
error boundary
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面
特点:只能捕获后代组件生命周期中(比如render中)产生的错误,不能捕获组件自己产生的错误,和其他组件在合成事件、定时器中产生的错误。
使用方式:getDerivedStateFromError
配合componentDidCatch
1 | import React, { Component } from 'react' |
组件通信总结
组件间的关系
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
几种通信方式
- props
- children props
- render props
- 消息订阅-发布
pubs-sub、event等等 - 集中式管理:
redux、dva等等 - ConText:生产者-消费者模式
比较好的搭配方式
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
React Router6
概述
- 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 等。
- 官方明确推荐函数式组件了!!!
- 内置组件的变化:移除
变化
Routes与Route
移除
<Switch/>
,新增<Routes/>
,而且是必须使用<Routes/>
包括<Route/>
,默认匹配到了组件就不会继续匹配。使用Route注册组件的语法也改变了:
1
2<Route path='/about' component={About}></Route> //旧版
<Route path='/about' element={<About/>}></Route> //新版而且后面使用Route来注册子路由的方式也不推荐使用了,转为使用路由表。
Navigate
删除了Redirect,添加了Navigate,只要 <Navigate>
组件被渲染,就会修改路径,切换视图。
replace
属性用于控制跳转模式(push 或 replace,默认是 push)。
1 | import React, { useState } from 'react'; |
<Navigate>
组件的作用是只要被渲染就会修改路径,切换视图。replace
属性用于控制跳转模式,默认是push
。示例代码展示了如何根据
sum
的值来决定是否使用<Navigate>
组件进行路径切换。如果
<Navigate to='/about' />
始终存在于组件中(即没有通过条件判断来控制它的渲染),那么无论用户点击哪个<NavLink>
,页面都会被强制跳转回/about
,最好的做法应该是写在路由表中:1
2
3
4{
path:'/',
element: <Navigate to='/about'></Navigate>
}
NavLink
在react router6中,activeClassName已经被废弃,下面是新的语法介绍:
1 | <NavLink className={computedClassName} to="/about">About</NavLink> |
1 | function computedClassName({isActive}){ |
useRoutes
在React Router6中,貌似想要使用嵌套路由,就必须使用路由表,即借助useRoutes
1 | // routes/index.jsx |
1 | //App.js |
1 | // pages/about/index.jsx |
useParams
被用来在函数式组件中,获取传递过来的动态路由参数。
调用这个函数,不需要传入参数,会返回一个动态路由参数对象
1 | //about/news/detail/index.jsx |
useSearchParams
被用来在函数式组件中,获取传递过来的查询参数。
调用这个函数,也不需要传入任何参数,返一个数组;第一个参数是一个URLSearchParams
对象,只能调用这个对象的get方法取得对应的值;第二个参数是一个函数,用来修改当前页面的查询参数,用的不多,会触发组件的重新渲染。
1 | import React from 'react' |
useMatch
useLocation
调用这个函数,不需要传入值,返回一个location对象,这个对象的结构和类式路由组件中的this.props.location
的结构是相同的。
我们就可以通过这个函数,来接受传入函数式路由组件中的state。
1 | <div className='header'> |
1 | import React from 'react' |
useNavigate
无论是普通组件还是路由组件,都可以使用这个函数实现编程式导航。
直接调用useNavigate,返回一个navigate函数。
navigate(to, options)
接受两个参数:
- **
to
**:- 类型:
string
或object
- 表示要导航的目标路径。
- 如果是字符串,则表示路径(如
/about
)。 - 如果是对象,可以包含以下属性:
pathname
: 目标路径(如/about
)。search
: 查询参数(如?id=123
)。hash
: 锚点(如#section1
)。state
: 传递的状态数据。
- 类型:
- **
options
**:- 类型:
object
- 可选配置项:
replace: true
:替换当前的历史记录条目,而不是添加一个新的条目。state
:传递状态数据(与to
对象中的state
等效)。
- 类型:
useNavigate
还支持基于相对路径的导航。例如,你可以通过传递一个负数来返回上一页。
1 | const navigate = useNavigate(); |
其他
useInRouterContext:用来判断一个组件是否在路由环境中,简单的来说,是否被
BrowserRouter
或者HashRouter
包裹。useNavigationType:返回当前的导航类型(用户是如何来到当前页面的)。返回值:POP、PUSH、REPLACE。POP是指在浏览器中直接打开了这个路由组件(刷新页面)。
useOutLet:用来呈现当前组件中渲染的嵌套路由
1
2
3
4const result = useOutlet()
console.log(result)
//如果嵌套的路由组件没有挂载,则result为null
//如果嵌套路由已经挂载,则展示嵌套的路由对象useResolvedPath:用来解析路径,传入任意一个路径,返回一个解析后的对象。