视频地址
https://www.bilibili.com/video/BV1wy4y1D7JT
入门
React是什么
使用 React 框架的程序,展现页面需要分三步:
- 发送请求获取数据
- 处理数据(过滤、整理格式等)
- 操作DOM呈现页面
React 只负责第三步!
React:是一个将数据渲染为 HTML 视图的开源 JavaScript 库
发展史
- 起初由 Facebook 的软件工程师 Jordan Walke 创建
- 于 2011 年部署于 Facebook 的 newsfeed 程序
- 随后在 2012 年部署于 Instagram
- 2013 年 5 月宣布开源
- 近 10 年陈酿,React 正在被腾讯、阿里等一线大厂广泛使用
为什么要学React
因为原生 JS 面临很多痛点:
- 原生 JS 操作 DOM 时很繁琐,效率低,需要使用 DOM 的 API 去操作界面 UI
- 使用 JS 直接操作 DOM,浏览器会进行大量的重绘重排
- 原生 JS 没有 组件化 编码方案,代码复用率低
React的特点
- 采用组件化模式、声明式编码,提高开发效率及组件复用率
- 在 React Native 中可以使用 React 语法进行移动端开发
- 使用虚拟 DOM 和优秀的 Diffing 算法,尽量减少与真实 DOM 的交互
虚拟DOM的优势
假设先将两个人的数据渲染到页面上,然后想要渲染第三个人:
真实 DOM:
虚拟 DOM:
数组中添加 ‘肖战’ 后,新的虚拟 DOM 会和旧的虚拟 DOM 进行比较,发现之前的 ‘鹿晗’ 和 ‘李现’ 没有变化,只是多了个 ‘肖战’,所以页面真实 DOM 中的 ‘鹿晗’ 和 ‘李现’ 也没有重新渲染,只是新增了一个 ‘肖战’
DOM的diffing算法
在上一节中,新旧虚拟 DOM 的比较使用的是 diffing 算法
diffing 算法的最小比较单位是一个标签
class Diffing extends React.Component {
state = {
now: new Date()
}
componentDidMount() {
setInterval(() => {
this.setState({ now: new Date() })
}, 1000);
}
render() {
return (
<div>
{/* 此处的input不会被更新 */}
<input type="text" />
<p>
{/* 虽然此处input处于p标签内,且发生变化的now字段也处于p标签内,
但是由于diffing的最小比较单位是一个标签,所以此处的input并不会被更新;
即使input里面的value值变了,但是虚拟DOM是拿不到真实DOM的value值的*/}
<input type="text" />
当前时间为:{this.state.now.toTimeString()}
</p>
</div>
)
}
}
ReactDOM.render(<Diffing />, document.getElementById('test'))
JS前置知识
学习 React 之前必须掌握以下 JS 知识:
- 判断 this 的指向
- class 类
- ES6 语法规范
- npm 包管理器
- 原型、原型链
- 数组常用方法
- 模块化
React基本使用
<body>
<div id="test"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
const VDOM = <h1>Hello,React</h1>
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
虚拟DOM的两种创建方式
-
使用 JSX 语法创建虚拟 DOM <!-- 此处一定要写babel -->
<script type="text/babel">
// 此处可以用小括号括起来,表示是一个整体
const VDOM = (
// 可以按照html代码的格式编写
<h1>
<span>
Hello,React
</span>
</h1>
)
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
-
使用原生 js 创建虚拟 DOM
React.createElement(type, [props], [...children]) :创建一个虚拟 DOM,第一个参数传标签名,必填;第一个参数传属性,非必填;第三个参数传子节点内容,非必填 <!-- 此处指定为原生js代码 -->
<script type="text/javascript">
// 1. 创建单层虚拟DOM
const VDOM1 = React.createElement('h1', { id: 'title' }, 'Hello,React')
// 2. 创建多层虚拟DOM,写起来及其繁琐
// babel其实就是将我们写的jsx创建虚拟DOM的代码解析为下面这样
const VDOM2 = React.createElement('h1', { id: 'title' }, React.createElement('span', {}, 'Hello,React'))
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
使用 JSX 的唯一好处就是可以让我们按照 html 代码的格式去编写虚拟 DOM
虚拟DOM和真实DOM
<script type="text/babel">
// 创建虚拟DOM
const VDOM = (<h1>Hello,React</h1>)
// 创建真实DOM
const TDOM = document.createElement('h1')
console.log(typeof VDOM); // object
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
虚拟 DOM:
真实 DOM:
关于虚拟 DOM:
- 本质是 Object 类型的对象(一般对象)
- 虚拟 DOM 比较 “轻”,真实 DOM 比较 “重“,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性
- 虚拟 DOM 最终会被 React 转化为真实 DOM,呈现在页面上
JSX
定义
- 全称:JavaScript XML
- react 定义的一种类似于 XML 的 JS 扩展语法:JS + XML
- 本质是
React.createElement(component, props, ...children) 方法的语法糖 - 作用:用来简化创建虚拟 DOM
- 写法:
var ele = <h1>Hello JSX!</h1> - 注意1:它不是字符串,也不是 HTML/XML 标签
- 注意2:它最终产生的就是一个 JS 对象
- 标签名任意:HTML 标签或其它标签
语法规则
- 定义虚拟 DOM 时,不要写引号
- 标签中混入 JS 表达式(注意区分 ‘表达式’ 和 ‘语句’)时要用
{} - 指定样式的类名时不要用 class,要用 className
- 内联样式,要用
style={{key:'value', key:'value'}} 的形式去写,如果 key 为多个单词组成,则转成小驼峰形式 - 只能有一个根标签
- 标签必须闭合
- 标签首字母:
- 若小写字母开头,则将该标签转为 html 中同名元素。若 html 中无该标签对应的同名元素,则报错
- 若大写字母开头,react 就去渲染对应的组件(定义 React 组件见后续章节),若组件没有定义,则报错
示例:
<script type="text/babel">
let txt = 'Hello React!'
const VDOM = (
<div>
<h1>{txt}</h1>
<h2 className="pink">我是粉色</h2>
<h3 style={{ color: 'red', fontSize: '33px' }}>我是红色</h3>
</div>
)
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
区分 js 表达式和 js 语句:
表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,如下所示
- a
- a + b
- demo(1)
- arr.map()
- function test() {}
语句:
- if() {}
- for() {}
- switch() {case:xxxx}
动态渲染数据
模拟从后端获取到数据,并通过 React 渲染的场景:
<script type="text/babel">
// 假设从后端获取到数据
let data = ['张三', '李四', '王五']
const VDOM = (
<div>
<ul>
{data.map((item, index) => {
// 每个li需要一个唯一的key,这里使用遍历下标index作key
return <li key={index}>{item}</li>
})}
</ul>
</div>
)
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
组件与模块
模块
- 理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
- 为什么要拆成模块:随着业务逻辑增加,代码越来越多且复杂
- 作用:复用 js,简化 js 的编写,提高 js 运行效率
- 模块化:当应用的 js 都以模块来编写的,这个应用就是一个模块化应用
组件
- 理解:用来实现局部功能效果的代码和资源的集合(html / css / js / image 等)
- 为什么:一个界面的功能更复杂
- 作用:复用编码,简化项目编码,提高运行效率
- 组件化:当应用是以多组件的方式实现,这个应用就是一个组件化的应用
class类的基本使用
基础用法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
const p1 = new Person('张三', 18);
const p2 = new Person('李四', 20);
p1.speak();
p2.speak();
继承
-
无构造器
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
class Student extends Person {
}
const s1 = new Student('张三', 18)
console.log(s1);
s1.speak()
-
有构造器
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log(`我叫${this.name},我今年${this.age}岁`)
}
}
class Student extends Person {
constructor(name, age, sno) {
super(name, age);
this.sno = sno;
}
speak() {
console.log(`我叫${this.name},我今年${this.age}岁,我的学号是${this.sno}`);
}
study() {
console.log('学习?学个屁!');
}
}
const s1 = new Student('张三', 18, 2017211257)
console.log(s1);
s1.speak()
s1.study()
共同属性定义
某个类的属性可能是所有实例都拥有且相同的,比如汽车都是 4 个轮子,人都是 1 个脑袋,那么这样的属性我们可以为其设置默认值:
class Student {
constructor(name, age, sno) {
this.name = name;
this.age = age;
this.sno = sno;
this.head = 1
}
foot = 2
}
const s1 = new Student('张三', 18, 2017211257)
const s2 = new Student('李四', 20, 2018211257)
console.log(s1);
console.log(s2);
React面向组件编程
函数式组件
<script type="text/babel">
// 1. 创建函数式组件(函数名首字母要大写)
function MyComponent() {
// 此处的this为undefined,因为babel编译后开启了严格模式
console.log(this)
return <h2>我是用函数定义的组件(适用于简单组件的定义)</h2>
}
// 2. 渲染组件到页面(第一个参数要传组件名的标签格式)
ReactDOM.render(<MyComponent/>, document.getElementById('test'))
</script>
执行 ReactDOM.render() 方法之后,发生了什么?
- React 解析组件标签,找到了 MyComponent 组件
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
类式组件
类式组件三要素:
- 创建类,并继承
React.Component - 添加
render() 方法 - 返回 DOM
<script type="text/babel">
// 1. 创建类,并继承React.Component
class MyComponent extends React.Component {
// 2. 添加render()函数
render() {
// 3. 返回DOM
return <h2>我是用类定义的组件(适用于复杂组件的定义)</h2>
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('test'))
</script>
思考:
render() 方法会被放到哪里?
render() 中的 this 是谁?
- 执行
ReactDOM.render(...) 之后,发生了什么?
- React 解析组件标签,找到了 MyComponent 组件
- React 发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到其原型上的
render() 方法 - 将
render() 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
组件实例的三大核心属性
state
state(状态)是放在组件实例上的,所以只有类式组件有 state(其实函数式组件可以通过 hooks 实现 state,但此处不过多介绍)
数据存放在状态上,状态驱动着页面的显示;状态中的数据改变,页面会随之改变(重新调用 render 方法)
state 的值必须是一个对象
下面我们用一个小案例来讲解 state 的基本使用方式
案例:页面上有 “今天天气很炎热” 这句话,当点击这句话时,“炎热” 和 “凉爽” 会互相切换
初始化、取值、注册事件
<body>
<div id="test"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
class WeatherCpn extends React.Component {
constructor(props) {
super(props)
// 1. 初始化state
this.state = {
isHot: true
};
}
render() {
// 2. 解构赋值,拿到state中的isHot值,根据isHot的值来决定展示什么内容
const { isHot } = this.state
// 3. 注册点击事件,onclick的C要大写,值用{}包裹,里面写函数名,不要忘了加this
// 函数后不要加括号,不然会在渲染时立刻调用;而且相当于把函数的返回值交给onClick去回调,而不是把函数交给onClick
// 此处虽然是this.函数,但是并不是通过实例对象调用了此函数,而是通过this在其原型对象上找到这个函数,并给到onClick作为回调函数使用
return <h1 onClick={this.changeWeather}>今天天气真{isHot ? '炎热' : '凉爽'}</h1>
}
// 点击时回调的函数
changeWeather() {
// changeWeather通过实例对象调用时,this才会指向实例对象
// 这里changeWeather是作为onClick的回调,所以不是通过实例对象调用的,而是直接调用
// 并且由于React类中的方法默认开启严格模式,所以此处的this值为undefined
this.state.isHot = !this.state.isHot // Cannot read property 'state' of undefined
}
}
ReactDOM.render(<WeatherCpn />, document.getElementById("test"))
</script>
</body>
解决函数中this指向问题
在上一节中,我们触发点击事件时回调函数报了 Cannot read property 'state' of undefined 错误,因为回调函数中的 this 值为 undefined
方式一:利用bind()
我们先来熟悉一下 js 中的 bind() 方法:
function fn() {
console.log(this);
}
fn()
let fn2 = fn.bind({ name: '张三' })
fn2()
那么我们可以利用 bind() 方法,来解决上一节中遇到的 this 指向问题:
class WeatherCpn extends React.Component {
constructor(props) {
super(props)
this.state = {
isHot: true
};
// 此处调用bind()方法来修改changeWeather函数中的this指向
// 等号右边的changeWeather是原型对象上的changeWeather
// 等号左边的changeWeather则是组件实例上的changeWeather(本来组件实例自己是没有这个函数的,实例调用的是原型对象上的)
// bind()方法会返回一个新函数,并修改新函数this为当前this,也就是组件实例对象
// 将新函数添加到组件实例对象上
this.changeWeather = this.changeWeather.bind(this)
}
render() {
const { isHot } = this.state
// 因为组件实例自身已被添加了changeWeather函数,所以此处的this.changeWeather不会再去原型对象上找
// 又因为组件实例自身的changeWeather函数中的this已被bind()函数设置为组件实例对象,所以回调函数时其this指向组件实例对象
return <h1 onClick={this.changeWeather}>今天天气真{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// 虽然isHot值确实被改变了,但是页面并没有动态渲染,为什么?请见下一节
this.state.isHot = !this.state.isHot
}
}
ReactDOM.render(<WeatherCpn />, document.getElementById("test"))
方式二:利用箭头函数
利用箭头函数的特性可以很好的解决 this 指向问题:
- 箭头函数向外层作用域一层层查找
this ,将找到的第一个 this 作为它的 this
class WeatherCpn extends React.Component {
constructor(props) {
super(props)
this.state = {
isHot: true
};
}
render() {
const { isHot } = this.state
return <h1 onClick={this.changeWeather}>今天天气真{isHot ? '炎热' : '凉爽'}</h1>
}
// 这里使用箭头函数的方式来定义changeWeather,并且这种等号的函数定义方式,不会将函数定义到原型对象上,而是组件实例对象上
changeWeather = () => {
// 箭头函数中的this会指向外层第一个this,也就是组件实例对象
this.setState({ isHot: !this.state.isHot })
}
}
ReactDOM.render(<WeatherCpn />, document.getElementById("test"))
setState方法
在上一节中,我们给元素添加了点击事件,通过点击来修改 this.state.isHot 的值,但是发现页面并没有实时渲染
之前不是说状态驱动着页面的显示吗?这里状态都变了,可以页面为什么没变呢
原因是因为 state 里的值不能直接修改,这样的操作 React 认为是不合法的,所以并没有去重新渲染页面
修改组件状态需要使用 setState 方法:
class WeatherCpn extends React.Component {
constructor(props) {
super(props)
this.state = {
// 记得先初始化isHot,然后再去修改值,这样最符合规范
isHot: true
};
this.changeWeather = this.changeWeather.bind(this)
}
render() {
const { isHot } = this.state
return <h1 onClick={this.changeWeather}>今天天气真{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// (×)直接修改状态:this.state.isHot = !this.state.isHot // Cannot read property 'state' of undefined
// (√)使用setState方法修改组件状态:
// 参数传了哪个属性就去state中修改哪个属性,不会影响其他属性
this.setState({ isHot: !this.state.isHot })
}
}
ReactDOM.render(<WeatherCpn />, document.getElementById("test"))
思考:
- render 方法调用了几次?
- 1 + n 次,1 是初始化的那次,n 是状态更新的次数
- changeWeather 方法调用了几次?
- 构造器调用了几次?
简写初始化state
既然每个组件实例都要初始化 state,那我们可以直接把它放到构造器外面赋默认值
class WeatherCpn extends React.Component {
constructor(props) {
super(props)
this.changeWeather = this.changeWeather.bind(this)
}
// 简写初始化state
state = { isHot: true }
render() {
const { isHot } = this.state
return <h1 onClick={this.changeWeather}>今天天气真{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
this.setState({ isHot: !this.state.isHot })
}
}
ReactDOM.render(<WeatherCpn />, document.getElementById("test"))
props
案例:假如有一个 Person 组件,我们想在页面上输出张三和李四的信息
class Person extends React.Component {
state = {
name: '',
age: '',
}
render() {
const { name, age } = this.state
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person />, document.getElementById("test1"))
ReactDOM.render(<Person />, document.getElementById("test2"))
我们会发现张三和李四的信息不知道如何分别渲染,因为如果 state 中是张三的信息,那么会渲染出两个张三出来,李四同理
所以我们需要从外部分别传入张三和李四的信息,并通过一个属性来接收,这个属性就是 props
props基本使用
props 不需要初始化,因为其默认是一个空对象
class Person extends React.Component {
render() {
// 从props属性中取值
const { name, age } = this.props
return (
<ul>
// 渲染:
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 传入组件的时候,可以在标签属性中添加键值对,其会被赋值到组件的props属性中
ReactDOM.render(<Person name="张三" age={18} />, document.getElementById("test1"))
// 注意,此处age传的是字符串格式,那么props取到的也是字符串,无法作为数字进行运算
ReactDOM.render(<Person name="李四" age="20" />, document.getElementById("test2"))
批量传递props
在上一节中,我们通过在标签属性中添加键值对来传递 props,如果要传的内容比较少还好,假如有几十个属性呢,这样就不太合适了
React 为我们提供了批量传递 props 的功能:
class Person extends React.Component {
render() {
const { name, age } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const p1 = { name: '张三', age: 18 }
const p2 = { name: '李四', age: "22" } // 注意,此处age传的是字符串格式,那么props取到的也是字符串,无法作为数字进行运算
// 批量传入props
// 注意:此处的大括号不是ES6中复制对象时用的大括号,而是React中的大括号,用于写表达式的
// ...运算符在ES6中只允许展开数组,是不允许展开对象的,但此处在babel+React的加持下,允许展开对象,并作为标签属性
// 虽然在babel和React的加持下可以通过...展开对象,但不能随意使用,只有在特定的语境下才会生效
ReactDOM.render(<Person {...p1} />, document.getElementById("test1"))
ReactDOM.render(<Person {...p2} />, document.getElementById("test2"))
props传值限制
props 可以对外部传入的值进行限制
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel">
class Person extends React.Component {
render() {
const { name, age } = this.props
// 报错,props的内容不允许更改
// this.props.name = 'abc'
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 对标签属性进行类型、必要性的限制;这里小写的propTypes是React的语法规则
Person.propTypes = {
// name必须传String类型,且为必传;这里大写的PropTypes是引入prop-type.js后读取的限制类型
name: PropTypes.string.isRequired,
// age必须是Number类型,但不是必传
age: PropTypes.number,
// speak必须是函数
speak: PropTypes.func
}
// 指定标签属性默认值
Person.defaultProps = {
// 若不传age,则age默认为999
age: 999
}
const p1 = { name: '张三', age: 18 }
const p2 = { name: '李四', age: 22 }
ReactDOM.render(<Person {...p1} />, document.getElementById("test1"))
ReactDOM.render(<Person {...p2} />, document.getElementById("test2"))
</script>
props简写
在上一节中,propTypes 和 defaultProps 被写到了类的外面,可是它们也是关于组件本身的内容,我们希望将其写到类中:
class Person extends React.Component {
// 这里要用static关键字,将propTypes添加到Person组件本身;如果不加的话,则是加到组件实例上
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
speak: PropTypes.func
}
static defaultProps = {
age: 999
}
render() {
const { name, age } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const p1 = { name: '张三' }
const p2 = { name: '李四', age: 22 }
ReactDOM.render(<Person {...p1} />, document.getElementById("test1"))
ReactDOM.render(<Person {...p2} />, document.getElementById("test2"))
构造器接收props
构造器中是否要接收 props,是否要传递给 super(),取决于是否希望在构造器中通过 this 访问 props(一般没这需求,而且一般构造器都不写)
class Person extends React.Component {
// 构造器接收props
constructor(props) {
// 传递super()
super(props)
console.log(this.props) // { name: '李四', age: 22 }
}
// 构造器不接收props
constructor() {
// 不传递super()
super()
console.log(this.props) // 此处构造器中打印undefined,但是可以通过别的地方的this.props获取值,因为React帮我们把值赋到了this.props上
}
render() {
console.log(this.props);
const { name, age } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const p2 = { name: '李四', age: 22 }
ReactDOM.render(<Person {...p2} />, document.getElementById("test2"))
函数式组件使用props
类式组件使用 props 时是通过 this.props 调用的,所以是由组件实例调用的,因此如果没有组件实例,就无法使用 props
但是函数式组件即使没有组件实例,也可以使用 props,因为函数式组件可以接收参数
// 函数参数接收props,会自动将传入的各项属性封装为对象
function Person(props) {
const { name, age } = props
console.log(props) // {name: "张三", age: 18}
return (
<ul>
<li>姓名:{name}</li> // 张三
<li>年龄:{age}</li> // 999
</ul>
)
}
// 函数中无法使用static关键字,所以props参数限制和默认值要写到外面
Person.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
}
Person.defaultProps = {
age: 999
}
ReactDOM.render(<Person name='张三' />, document.getElementById('test1'))
children属性
在调用组件时,标签体内容会被作为 children 属性传递给组件的 props 中
如:
// 调用自己封装的MyNavLink组件,标签体内容为about
<MyNavLink to="/about">about</MyNavLink>
// 在MyNavLink中打印props:
{to: '/home', children: 'about'}
refs
React 建议尽量减少 ref 的使用
案例:有一个输入框和一个按钮,点击按钮,弹出输入框中的内容
分别使用字符串形式、回调函数形式和 createRef 形式来实现
字符串形式ref
在虚拟 DOM 上添加 ref 属性,此虚拟 DOM 转为真实 DOM 后的节点就会被放到组件实例的 refs 对象中
class MyComponent extends React.Component {
render() {
return (
<div>
// 给元素添加ref属性,该元素在转为真实DOM节点后会被添加至组件实例的refs对象中
<input ref="ipt" type="text" />
<button onClick={this.show}>点我</button>
</div>
)
}
show = () => {
// 直接从refs属性中获取节点即可;不需要通过给元素添加id,然后再getElementById这种方式获取节点
const { ipt } = this.refs
alert(ipt.value);
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
由于字符串形式 ref 的效率问题,React 官方并不推荐使用,并且将在后续版本中删除
回调函数形式ref
内联函数形式
内联函数,可以理解为直接在表达式里面定义了一个函数,或者直接把一个函数丢在那
class MyComponent extends React.Component {
render() {
return (
<div>
// 此处ref的值是一个回调函数,函数的参数就是该节点,我们可以把该节点放到组件实例上
<input ref={(c) => { this.ipt = c }} type="text" />
<button onClick={this.show}>点我</button>
</div>
)
}
show = () => {
// 从组件示例上获取该节点
const { ipt } = this
alert(ipt.value);
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
内联函数形式扩展:
- 内联函数在更新时会被执行两次,第一次传入 null,第二次才是 DOM 节点
- 这是因为每次渲染时会创建一个新的函数实例,所以 React 要先清空旧的 ref 再设置新的,所以第一次传入的参数为 null
- 这个问题其实是无关紧要的,如果想避免此问题,可以将回调函数绑定到类上
测试:我们为标签添加内联形式回调函数,然后再添加一个修改 state 的方法,通过修改 state 触发组件的重新 render,看看内联函数打印的参数是什么
class MyComponent extends React.Component {
state = {
num: 0
}
render() {
return (
<div>
// 1. 此处内联函数在第一次渲染页面的时候执行了一次,并成功打印节点
// 2. 更新页面时,内联函数执行了两次,第一次为null,第二次成功打印节点
<input ref={(c) => { this.ipt = c; console.log(c); }} type="text" />
<button onClick={this.show}>点我展示输入框内容</button>
<button onClick={this.add}>点我修改state</button>
</div>
)
}
show = () => {
const { ipt } = this
alert(ipt.value);
}
add = () => {
this.setState({ num: ++this.state.num })
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
类绑定函数形式
类绑定函数,就是在类中定义的函数
class MyComponent extends React.Component {
state = {
num: 0
}
render() {
return (
<div>
// 1. 此处ref值为类绑定函数,只在第一次渲染页面时执行一次
// 2. 更新页面时,不会再次执行类绑定函数,因为该函数已经放在类自身了,就算重新调用render,也知道它并不是一个新的函数
<input ref={this.getIpt} type="text" />
<button onClick={this.show}>点我展示输入框内容</button>
<button onClick={this.add}>点我修改state</button>
</div>
)
}
show = () => {
const { ipt } = this
alert(ipt.value);
}
add = () => {
this.setState({ num: ++this.state.num })
}
// 类绑定函数
getIpt = (c) => {
this.ipt = c;
console.log(c);
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
createRef形式
class MyComponent extends React.Component {
// createRef()函数会返回一个容器,这个容器存储被ref所标识的节点
// 一个容器只能存放一个节点,后存的节点会把先存的顶掉
// 如果想同时保存多个节点,则需要创建多个容器
myRef = React.createRef()
render() {
return (
<div>
// 此处ref值为createRef()返回的容器名
<input ref={this.myRef} type="text" />
<button onClick={this.show}>点我展示输入框内容</button>
</div>
)
}
show = () => {
console.log(this.myRef); // {current: input}
alert(this.myRef.current.value)
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
事件处理
-
通过 onXxx 属性指定事件处理函数
- React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件(为了更好的兼容性)
- React 中的事件是通过事件委托方式(冒泡)来处理的,也就是委托给组件最外层的元素(为了更高的效率)
-
通过 event.target 可以得到触发事件的 DOM 元素对象(可以减少 ref 的使用) class MyComponent extends React.Component {
render() {
return (
<div>
// 注册鼠标失焦事件
<input onBlur={this.show} type="text" />
</div>
)
}
// 事件处理函数的参数就是触发事件的DOM元素对象
show = (event) => {
console.log(event.target); // <input type="text">
alert(event.target.value)
}
}
ReactDOM.render(<MyComponent />, document.getElementById("test"))
非受控组件
当需要使用表单数据时才去取,就是非受控
class Login extends React.Component {
render() {
return (
<div>
<form action="#">
用户名:<input ref={c => this.username = c} type="text" />
密码:<input ref={c => this.password = c} type="password" />
<button onClick={this.submit}>提交</button>
</form>
</div>
)
}
submit = () => {
// 当需要使用表单数据时才去取
const { username, password } = this
console.log("准备提交用户名和密码...");
console.log(`用户名为${username.value},密码为${password.value}`);
}
}
ReactDOM.render(<Login />, document.getElementById("test"))
受控组件
表单数据随着输入被维护到状态中,就是受控
推荐使用受控组件,因为可以减少 ref 的使用
class Login extends React.Component {
state = {
username: "",
password: ""
}
render() {
return (
<div>
<form action="#">
// 表单数据随着输入被维护到状态中
用户名:<input onChange={this.saveUsername} type="text" />
密码:<input onChange={this.savePassword} type="password" />
<button onClick={this.submit}>提交</button>
</form>
</div>
)
}
saveUsername = (event) => {
this.setState({ username: event.target.value })
}
savePassword = (event) => {
this.setState({ password: event.target.value })
}
submit = () => {
const { username, password } = this.state
console.log("准备提交用户名和密码...");
console.log(`用户名为${username},密码为${password}`);
}
}
ReactDOM.render(<Login />, document.getElementById("test"))
函数柯里化
函数柯里化:通过调用函数继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
高阶函数:如果一个函数符合下面两个规范中的任意一个,那该函数就是高阶函数(常见高阶函数:Promise、setTimeout、arr.map)
- 若 A 函数接收的参数是一个函数,那么 A 就可以称之为高阶函数
- 若 A 函数的返回值依然是一个函数,那么 A 就可以称之为高阶函数
案例:在上一节中,我们分别写了两个方法用于保存用户名和密码,可如果有很多个表单时就很麻烦,所以需要抽象为一个方法来保存
class Login extends React.Component {
state = {
username: "",
password: ""
}
render() {
return (
<div>
<form action="#">
// 既然onChange需要接收一个函数,那就让saveProp()方法返回一个函数
// 这里函数名后有括号,会直接进行调用,并传入参数,然后将saveProp()方法返回的函数注册到onChange事件
用户名:<input onChange={this.saveProp('username')} type="text" />
密码:<input onChange={this.saveProp('password')} type="password" />
</form>
</div>
)
}
// key:传入的参数,用于接收表单的name
saveProp = (key) => {
// 最终将下面这个函数注册到onChange事件,所以这个函数可以拿到event对象
return (event) => {
// 设置状态:注意这里[key]要用中括号包起来,才可以读取到变量key的值,不然会原封不动的将'key'添加到状态里
this.setState({ [key]: event.target.value })
}
}
}
ReactDOM.render(<Login />, document.getElementById("test"))
上面是使用函数柯里化的实现方式,我们不使用柯里化也可以实现:
class Login extends React.Component {
state = {
username: "",
password: ""
}
render() {
return (
<div>
<form action="#">
// 既然onChange需要接收一个函数,那我们直接传一个匿名函数,函数内部调用saveProp()方法,传递name值和event
用户名:<input onChange={(event) => { this.saveProp('username', event) }} type="text" />
密码:<input onChange={(event) => { this.saveProp('password', event) }} type="password" />
</form>
</div>
)
}
saveProp = (key, event) => {
this.setState({ [key]: event.target.value })
}
}
ReactDOM.render(<Login />, document.getElementById("test"))
纯函数
纯函数定义:
- 无论何时传入同样的参数,都会得到同样的输出
- 不得对参数进行修改
- 不做不稳定的事情,如发起网络请求,连接输入设备
- 不能调用 Date.now() 或 Math.random() 等不纯的方法
代码演示:
function pure(n){
n = 1;
axios.get();
const now = Date.now();
}
pure(1);
pure(1);
生命周期
组件从创建到死亡会经历一些特定的阶段
React 组件中包含一系列勾子函数(生命周期回调函数),会在特定的时刻调用
我们在定义组件时,会在特定的生命周期回调函数中做特定的工作
生命周期流程(旧)
- constructor:构造函数
- render:渲染组件
- componentWillMount:组件将要挂载(挂载:组件第一次渲染时,其实就是将组件挂载到页面上)
- componentDidMount:组件挂载完毕
- componentWillUnmount:组件将要卸载(卸载:将组件从页面上删除)
- shouldComponentUpdate:控制组件更新的阀门,执行 setState() 方法时,需要
- componentWillUpdate:组件将要更新
- componentDidUpdate:组件更新完毕
class LiftCycle extends React.Component {
// 构造函数
constructor() {
console.log('constructor');
super()
this.state = {
count: 0
}
}
// 渲染组件
render() {
console.log('render');
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.add}>点我+1</button>
<button onClick={this.unmount}>卸载组件</button>
</div>
)
}
// 组件将要挂载
componentWillMount() {
console.log('componentWillMount');
}
// 组件挂载完毕
// 只会在第一次渲染时执行一次
componentDidMount() {
console.log('componentDidMount');
}
// 组件将要卸载
componentWillUnmount() {
console.log('componentWillUnmount');
}
// 控制组件更新的阀门,也就是调用setState()后会先来判断一下这个,决定流程是否继续往下走
// 如果不写该函数,React默认其返回值为true;如果写了,则必须返回一个boolean值
// 返回true时,允许组件更新;否则禁止更新
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
return true;
}
// 组件将要更新
componentWillUpdate() {
console.log('componentWillUpdate');
}
// 组件更新完毕
componentDidUpdate() {
console.log('componentDidUpdate');
}
add = () => {
let { count } = this.state
this.setState({ count: ++count })
}
unmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
}
ReactDOM.render(<LiftCycle />, document.getElementById('test'))
forceUpdate()
forceUpdate() 方法会强制更新组件,且不受 shouldComponentUpdate 阀门的控制
forceUpdate() 不会对状态进行修改
class LiftCycle extends React.Component {
constructor() {
console.log('constructor');
super()
this.state = {
count: 0
}
}
// 渲染组件
render() {
console.log('render');
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.add}>点我+1</button>
<button onClick={this.force}>强制更新组件</button>
</div>
)
}
add = () => {
let { count } = this.state
this.setState({ count: ++count })
}
force = () => {
// 强制更新
this.forceUpdate()
}
// 控制组件更新的阀门
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
// 即使阀门关闭,依然会强制更新
return false;
}
// 组件将要更新
componentWillUpdate() {
console.log('componentWillUpdate');
}
// 组件更新完毕
componentDidUpdate() {
console.log('componentDidUpdate');
}
}
ReactDOM.render(<LiftCycle />, document.getElementById('test'))
componentWillReceiveProps
在 A 组件里调用 B 组件,可以理解为 A 组件是 B 组件的父组件
父组件除了第一次执行 render() 时,子组件都会执行 componentWillReceiveProps() 函数
该函数接收一个参数,就是父组件传递进来的 props 对象
class A extends React.Component {
state = {
carName: '奔驰'
}
render() {
return (
<div>
<h1>我是A组件</h1>
<button onClick={this.changeCar}>点我换车</button>
{/* 在A组件里调用B组件,可以理解为A组件是B组件的父组件*/}
<B carName={this.state.carName} />
</div>
)
}
changeCar = () => {
this.setState({ carName: '宝马' })
}
}
class B extends React.Component {
// 组件将要接收新的Props(第一次接收时不算)
componentWillReceiveProps(props) {
console.log('componentWillReceiveProps', props);
}
render() {
return (
<div>
<h2>我是B组件,我接收到的车是{this.props.carName}</h2>
</div>
)
}
}
ReactDOM.render(<A />, document.getElementById('test'))
总结
- 初始化阶段:由 ReactDOM.render() 触发 -> 初次渲染
- constructor()
- componentWillMount()
- render()
- componentDidMount() -> 常用,一般在这个钩子中做一些初始化的事情,比如开启定时器、发送网络请求、订阅消失
- 更新阶段:由组件内部 this.setState() 或父组件 render() 触发
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
- 卸载组件:由 ReactDOM.unmountComponentAtNode() 触发
- componentWillUnmount() -> 常用,一般在这个钩子中做一些收尾的事,比如关闭定时器、取消消息订阅
生命周期流程(新)
React 17.x 版本之后,已不再推荐使用 componentWillMount()、componentWillReceiveProps()、componentWillUpdate() 这三个钩子函数,这些生命周期的代码在 React 的未来版本中可能出现 bug,尤其是在启用异步渲染之后。如果使用的话会报黄色警告,建议为这些钩子函数添加 “UNSAFE_” 前缀以消除警告
React 18.x 版本之后,直接删除了这三个钩子函数,如果仍要使用的话必须强制性添加 ‘UNSAFE_’ 前缀
新的生命周期流程图:
getDerivedStateFromProps()
getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用
它返回一个对象来更新 state,如果返回 null 则不更新任何内容
此方法适用于一个罕见的情况,即 state 的值在任何时候都取决于 props
此方法是由类调用的,所以要加 static 关键字修饰
class NewLifeCycle extends React.Component {
state = {
count: 0
}
// 此钩子函数可接收props和state参数
static getDerivedStateFromProps(props, state) {
// 此处返回的对象中如果包含了state中的count,所以state中的count值只能在此处更改,无法在他处更改
return { count: 3 }
// 利用此特性,我们可以让state中的值完全由props控制
// return porps
}
render() {
return (
<div>
<h1>当前count的值为:{this.state.count}</h1>
// 此处修改count无效,因为getDerivedStateFromProps函数返回的对象中已包含了count,所以count值完全由props决定
<button onClick={this.add}>点我+1</button>
</div>
)
}
add = () => {
this.setState({ count: ++this.state.count })
}
}
ReactDOM.render(<NewLifeCycle />, document.getElementById('test'))
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate(preProps, preState) 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate(preProps, preState, snapshot)
案例:我们循环向一个盒子中添加新的 item,当 item 溢出时会出现滚动条,并且内容随之向下滚动。我们希望内容不要自动滚动,而且鼠标滚轮滚到哪里,内容就停留到哪里
class NewsList extends React.Component {
state = {
newsList: []
}
render() {
const { newsList } = this.state
return (
<div className="list" ref="list">
{
newsList.map((v, i) => {
return (
<div className="news" key={i}>{v}</div>
)
})
}
</div>
)
}
// 组件更新前执行,返回值会返回给componentDidUpdate
// 接收两个参数,分别是更新前的props和state
getSnapshotBeforeUpdate(preProps, preState) {
return this.refs.list.scrollHeight
}
// 组件更新完成后执行
// 接收三个参数,分别是更新前的props和state,还有getSnapshotBeforeUpdate返回的快照
componentDidUpdate(preProps, preState, snapshot) {
this.refs.list.scrollTop += this.refs.list.scrollHeight - snapshot
}
// 组件挂载后,开启定时器,持续向盒子中添加内容
componentDidMount() {
setInterval(() => {
let { newsList } = this.state
console.log(newsList);
// 在newsList前添加一个元素,并返回添加后的数组
newsList = ['新闻' + newsList.length, ...newsList]
this.setState({ newsList })
}, 1000)
}
}
ReactDOM.render(<NewsList />, document.getElementById('test'))
总结
- 初始化阶段:由 ReactDOM.render() 触发 -> 初次渲染
- constructor()
- getDerivedStateFromProps()
- render()
- componentDidMount() -> 常用
- 更新阶段:由组件内部 this.setState() 或父组件重新 render 触发
- getDerivedStateFromProps
- shouldComponentUpdate()
- render
- getSnapshotBeforeUpdate
- componentDidUpdate()
- 卸载组件:由 ReactDOM.unmountComponentAtNode() 触发
- componentWillUnmount() -> 常用
遍历元素时key的作用
- 虚拟 DOM 中 key 的作用:
- 简单的说:key 是虚拟 DOM 对象的标识,在更新显示时 key 起着极其重要的作用
- 详细的说:当状态中的数据发生变化时,react 会根据【新数据】生产【新的虚拟 DOM】,随后 React 对【新虚拟 DOM】和【旧虚拟 DOM】进行 diffing 比较:
- 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:
- 若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
- 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
- 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key
- 用 index 作为 key 可能会引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序的操作
- 会产生没有必要的真实 DOM 更新,虽然界面效果没问题,但是效率大大降低
- 如果结构中还包含输入类的 DOM
- 输入类 DOM 中输入的值肯定是在渲染为真实 DOM 后输入的,所以在虚拟 DOM 对比的时候是拿不到的
- 会产生错误 DOM 更新,导致页面数据展示不正确
- 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表作展示
- 开发中如何选择 key:
- 最好使用每条数据的唯一标识作为 key,比如 id、手机号、身份证号、学号等唯一值
- 如果确定只是简单的展示数据,用 index 也是可以的
使用index作为key
无输入类 DOM 演示:
初始数据:
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
初始的虚拟DOM:
<li key=0>小张---18</li> // 格式:<li key={index}>name---age</li>
<li key=1>小李---19</li>
更新后的数据:
{id:3, name: '小王', age: 20} // 这里逆序添加了一个小王,现在小王处于数组中第一位
{id:1, name: '小张', age: 18}
{id:2, name: '小李', age: 19}
更新后的虚拟DOM:
<li key=0>小王---20</li> // 因为小王处于第一位,所以小王的key值(即index值)为1;此时用小王去和原虚拟DOM中key值相等的标签做diffing比较,发现内容不一样,所以要更新真实DOM
<li key=1>小张---18</li> // 原虚拟DOM中key为1的标签内容也变了,所以需要更新真实DOM
<li key=2>小李---19</li> // 原虚拟DOM中key为2的标签内容也变了,所以需要更新真实DOM
本来只添加了一个小王,但是却导致所有的真实DOM都要更新,影响效率!
有输入类 DOM 演示:
初始数据:
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
初始的虚拟DOM:
<li key=0>
小张---18
<input type="text"/> // 假如在此input框渲染为真实DOM后向其输入了abc
</li>
<li key=1>
小李---19
<input type="text"/>
</li>
更新后的数据:
{id:3, name: '小王', age: 20}
{id:1, name: '小张', age: 18}
{id:2, name: '小李', age: 19}
更新后的虚拟DOM:
<li key=0>
小王---20
<input type="text"/> // 用小王的key和小张的key作比较,发现内容不一样,但是input是一样的,所以只更新了内容,而没有更新input框,导致原来在小张那里输入的abc现在到了小王的后面
</li>
<li key=1>
小张---18
<input type="text"/>
</li>
<li key=2>
小李---19
<input type="text"/>
</li>
使用元素唯一键作为key
使用元素唯一键作为 key,则不会有任何问题,且效率最高
初始数据:
{id: 1, name: '小张', age: 18}
{id: 2, name: '小李', age: 19}
初始的虚拟DOM:
<li key=1>小张---18</li> // 格式:<li key={id}>name---age</li>
<li key=2>小李---19</li>
更新后的数据:
{id:3, name: '小王', age: 20} // 这里逆序添加了一个小王,现在小王处于数组中第一位
{id:1, name: '小张', age: 18}
{id:2, name: '小李', age: 19}
更新后的虚拟DOM:
<li key=3>小王---20</li> // 小王用3去原虚拟DOM中对比,发现没有此key,则更新真实DOM
<li key=1>小张---18</li> // 小张用1去原虚拟DOM中对比,发现有此key且标签内容一致,则不用重复更新
<li key=2>小李---19</li> // 小李用2去原虚拟DOM中对比,发现有此key且标签内容一致,则不用重复更新
脚手架
介绍
React 提供了一个用于创建 React 项目的脚手架库:create-react-app
脚手架项目的整体核心技术架构为:React + Webpack + ES6 + ESLint
脚手架项目的特点:模块化、组件化、工程化
创建项目并启动
- 全局安装脚手架库:
- npm install -g create-react-app(全局安装后可以在电脑上任何一个地方创建 React 脚手架项目)
- 切换到项目目录,执行命令:
- create-react-app hello-react
- 进入项目文件夹:
- 启动项目:
脚手架生成文件介绍
组件化项目结构
脚手架给我们生成的 public 和 src 文件夹中有很多没用的文件和代码,我们把这两个文件夹删掉,自己从零写一个 Hello 组件
文件结构图:
-
index.html(唯一页面)
<div id="root"></div>
-
index.js(程序入口文件)
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
-
App.js(所有组件的壳)
import React, { Component } from 'react'
import Hello from './components/Hello/Hello'
import Welcome from './components/Welcome'
export default class App extends Component {
render() {
return (
<div>
<Hello></Hello>
<Welcome />
</div>
)
}
}
-
Hello.js(Hello组件,和组件有关的文件名首字母都大写) import React from 'react'
import hello from './Hello.module.css'
export default class Hello extends React.Component {
render() {
return (
<h2 className={hello.text}>Hello, React!</h2>
)
}
}
-
Hello.module.css(模块化样式文件,首字母大写,中间添加.module) .text {
color: pink;
}
-
Welcome/index.jsx(Welcome 文件夹下 index.jsx / index.js 会被认为是 Welcome 组件,使用 .jsx 文件结尾,更容易区分) import React from 'react'
import './index.css'
export default class Welcome extends React.Component {
render() {
return (
<h2 className='text'>Welcome!</h2>
)
}
}
-
index.css(样式文件) .text {
color: skyblue;
}
组件化编码流程
- 拆分组件
- 实现静态组件
- 实现动态组件
ToDoList案例
功能介绍:
- 输入栏中输入任务名,按回车将其添加到列表中最上方
- 点击清除已完成任务可以删除所有选中项
- 左下角为全选框
拆分组件
我们将这个功能拆分为 4 个组件来实现
实现静态组件
使用组件化编码方式编写此案例,项目结构如下:
实现动态组件
-
App.js import React from 'react'
import Footer from './components/Footer'
import Header from './components/Header'
import List from './components/List'
import './App.css'
export default class App extends React.Component {
state = {
todos: [
{ id: 1, name: '吃饭', done: true },
{ id: 2, name: '睡觉', done: true },
{ id: 3, name: '打豆豆', done: false },
{ id: 4, name: '喝奶茶', done: false }
]
}
updateTodos = (id, done) => {
const { todos } = this.state
const newTodos = todos.map(v => {
if (v.id === id) {
return { ...v, done }
} else {
return v
}
})
this.setState({ todos: newTodos })
}
addTodos = (todo) => {
const { todos } = this.state
let newTodos = [todo, ...todos]
this.setState({ todos: newTodos })
}
deleteTodos = (id) => {
const { todos } = this.state
let newTodos = todos.filter(v => {
if (v.id !== id) {
return true
} else {
return false
}
})
this.setState({ todos: newTodos })
}
allTodosAct = (checked) => {
const { todos } = this.state
const newTodos = todos.map(v => {
return { ...v, done: checked }
})
this.setState({ todos: newTodos })
}
deleteAllChecked = () => {
const { todos } = this.state
const newTodos = todos.filter(v => {
return v.done === false
})
this.setState({ todos: newTodos })
}
render() {
return (
<div className='todo-container'>
{}
<Header addTodos={this.addTodos} />
<List updateTodos={this.updateTodos} todos={this.state.todos} deleteTodos={this.deleteTodos} />
<Footer todos={this.state.todos} allTodosAct={this.allTodosAct} deleteAllChecked={this.deleteAllChecked} />
</div>
)
}
}
-
Header.jsx import React, { Component } from 'react'
import { nanoid } from 'nanoid'
import { PropTypes } from 'prop-types'
import './index.css'
export default class Header extends Component {
// 限制传入此组件的props的类型及必要性
static propTypes = {
addTodos: PropTypes.func.isRequired
}
addTodo = (event) => {
const { addTodos } = this.props
// 如果按下的是回车键
if (event.keyCode === 13) {
// 如果输入框内容不为空
if (event.target.value.trim() !== '') {
let todo = { id: nanoid(), name: event.target.value, done: false }
// 因为状态在父组件里,所以需要调用父组件的添加todo方法
addTodos(todo)
}
event.target.value = ''
}
}
render() {
return (
<div className="todo-header">
<input type="text" onKeyUp={this.addTodo} placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
-
List.jsx import React, { Component } from 'react'
import Item from '../Item'
import './index.css'
export default class List extends Component {
render() {
const { todos, updateTodos, deleteTodos } = this.props
return (
<ul className="todo-main">
{
todos.map((v) => {
// 拿到父组件传递的函数后,继续向下一层子组件传
return <Item deleteTodos={deleteTodos} updateTodos={updateTodos} key={v.id} todo={v} />
})
}
</ul>
)
}
}
-
Item.jsx import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = {
// 鼠标是否悬浮
status: false
}
// 修改状态中保存的鼠标悬浮状态
mouseAct = (status) => {
return () => {
this.setState({ status: status })
}
}
inputAct = (id) => {
return (event) => {
const { updateTodos } = this.props
// 调用App组件传递过来的updateTodos方法,用于修改App组件的状态
updateTodos(id, event.target.checked)
}
}
render() {
console.log('RENDER');
const { id, name, done } = this.props.todo
const { status } = this.state
return (
// 根据状态中的鼠标悬浮状态,决定展示什么颜色
<li style={{ backgroundColor: status ? '#ddd' : 'white' }} onMouseOver={this.mouseAct(true)} onMouseLeave={this.mouseAct(false)} >
<label>
{/* 注意不要用defaultChecked,这个只会在首次挂载时控制是否选中,之后修改done值都不会进行控制 */}
<input onChange={this.inputAct(id)} type="checkbox" defaultChecked={done} />
<span>{name}</span>
</label>
{/* onClick需要接收一个函数,我们直接给它一个匿名函数,然后在这个匿名函数里调用其他方法 */}
<button onClick={() => { this.props.deleteTodos(id) }} style={{ display: status ? 'block' : 'none' }} className="btn btn-danger" >删除</button>
</li>
)
}
}
-
Footer.jsx import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
render() {
const { todos, allTodosAct, deleteAllChecked } = this.props
// 已完成的个数
const doneCount = todos.reduce((pre, cur) => { return pre + (cur.done ? 1 : 0) }, 0)
// 总个数
const totalCount = todos.length
return (
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneCount === totalCount && doneCount !== 0} onChange={(event) => { allTodosAct(event.target.checked); }} />
</label>
<span>
<span>已完成{doneCount}</span> / 全部{totalCount}
</span>
<button className="btn btn-danger" onClick={deleteAllChecked}>清除已完成任务</button>
</div>
)
}
}
总结
- 我们现在所学的知识还无法支持兄弟组件间传值,所以把列表数据都写到了共同的父级组件 App.js 中,由 App 组件传递给各个子组件
- 状态在哪里,修改状态的方法就写到哪里
- nanoid 可以理解为 uuid 的轻量型,安装:npm i nanoid
- 安装 prop-types 可以控制传入组件 props 的数据类型:npm i prop-types
- 为选择框添加 checked 属性时,必须指定 onClick 方法
- 为选择框添加 defaultChecked 属性时,需要知道它是非受控组件的属性,用于设置组件首次挂载时是否被选中,之后无法通过此值来控制组件的选中状态
头像搜索案例
拆分组件
注意:这里的头像就不用单独封装为组件了,而是直接在列表组件中遍历出来更为方便
实现静态组件
项目结构如下
实现动态组件
-
index.html <!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="./css/bootstrap.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
-
App.js import React, { Component } from 'react'
import Jumbotron from './component/Jumbotron'
import Row from './component/Row'
export default class App extends Component {
state = {
list: [],
err: '',
isFirst: true,
isLoading: false
}
// 因为state中属性太多,我们就不一一为每个属性都写一个更新的方法,而是直接写一个修改state的方法
updateState = (state) => {
this.setState(state)
}
render() {
return (
<div className='container'>
<Jumbotron updateState={this.updateState} />
<Row state={this.state} />
</div>
)
}
}
-
Row.jsx import React, { Component } from 'react'
import './index.css'
export default class Row extends Component {
render() {
const { state: { isFirst, isLoading, err, list } } = this.props
return (
<div className="row">
{
isFirst ? <h2>欢迎使用用户查询系统,请输入关键字进行搜索</h2> :
isLoading ? <h2>正在加载,请稍候...</h2> :
err ? <h2>{err}</h2> :
list.map(v => {
const { avatar_url, id, html_url, login } = v
return (
<div key={id} className="card">
<a href={html_url} rel='noreferrer' target="_blank">
<img alt='head_img' src={avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{login}</p>
</div>
)
})
}
</div>
)
}
}
-
Jumbotron.jsx import React, { Component } from 'react'
import axios from 'axios'
export default class Jumbotron extends Component {
search = () => {
const { updateState } = this.props
// 多级解构赋值,并以新的变量名接收
const { content: { value: str } } = this
// 发起请求(此处调用的GitHub通过名字查询用户信息的接口)
updateState({ isLoading: true, isFirst: false })
axios.get(`https://api.github.com/search/users?q=${str}`).then(
response => {
console.log('请求成功,响应数据为:', response)
updateState({ list: response.data.items, isLoading: false })
},
error => {
console.log('请求失败,响应数据为:', error)
updateState({ isLoading: false, err: error.message })
}
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<div>
<input ref={c => { this.content = c }} type="text" placeholder="enter the name you search" />
<button onClick={this.search}>Search</button>
</div>
</section>
)
}
}
React Ajax
React 本身只关注于界面,并不包含发送 ajax 请求的代码,所以我们需要借助于第三方库:axios
React 程序端口号为 3000,我们又在本地启了两个 node 服务器,端口号为 5000 和 5001,当直接使用 React 去请求 node 服务器获取数据时,会报跨域问题,所以需要配置代理
跨域问题
什么是跨域
CORS 全称 Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问就会遇到跨域问题
跨域指的是浏览器不能执行其它网站的脚本。是由浏览器的同源策略造成的,是浏览器对 JavaScript 施加的安全限制
同源策略
同源策略:是由 Netscape 提出的一个安全策略,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,则浏览器的正常功能可能都会受到影响,现在所有支持 JavaScript 的浏览器都会使用这个策略
在解析 Ajax 请求时,要求浏览器的路径与 Ajax 的请求的路径必须满足以下三个要求,则满足同源策略,可以访问服务器
- 协议相同
- 域名相同
- 端口号相同
同源策略案例:
- 满足同源策略.服务器可以正常访问
- 浏览器地址 http://localhost:8090/findAll
- Ajax 请求地址 http://localhost:8090/aaaa
- 不满足同源策略. 端口号不同. 属于跨域请求
- 浏览器地址 http://localhost:8091/findAll
- Ajax 请求地址 http://localhost:8090/aaaa
- 不满足同源策略. 协议不同. 属于跨域请求
- 浏览器地址 http://localhost:8090/findAll
- Ajax 请求地址 https://localhost:8090/aaaa
- 不满足同源策略. 域名不同(前提: IP与域名映射)
- 浏览器地址 http://www.baidu.com/findAll
- Ajax 请求地址 http://163.177.151.109/aaaa
- 满足同源策略. http协议,默认端口为80
- 浏览器地址 http://10.0.1.1:80/findAll
- Ajax 请求地址 http://10.0.1.1/aaaa
- 满足同源策略,https协议默认端口为443
- 浏览器地址 https://10.0.1.1/findAll
- Ajax 请求地址 https://10.0.1.1:443/aaaa
跨域解决方案
代理配置方案见下节,其他方案见:https://www.cnblogs.com/xikui/p/16071929.html
React脚手架代理配置
我在 localhost:3000 端口,要请求 localhost:5000 服务的数据
当我们发送一个 ajax 请求的时候,请求确实到达 5000 端口了,但是没有返回:
3000 端口的 ajax 引擎拒绝了返回的响应,所有产生了跨域
Q:为什么代理可以解决跨域呢?
A:因为我们在配置代理的时候 React 会帮我们生成一个中间人,通过中间人去跟服务端拿数据,那么跨域问题就解决了
Q:为什么中间人可以拿到 5000 端口上的数据,中间人不也是在 3000 端口上的吗
A:因为中间人不遵守 ajax 引擎策略,所以可能正常在 5000 端口拿到返回的数据,而客户端是直接向 3000 端口请求的数据,符合同源策略规范,跨域解决
方法一
在 package.json 中追加如下配置:
"proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了 3000 不存在的资源时,那么该请求会转发给 5000
- 比如说请求 localhost:3000/index.html,那么就会返回这个静态页面
- 如果请求 localhost:3000/aaa.html,在自己服务器上没找到,就会去 5000 服务器上找
方法二
-
第一步:创建代理配置文件 在src下创建配置文件:src/setupProxy.js
注意文件名必须写对,不然React找不着
-
编写 setupProxy.js 配置具体代理规则:
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
proxy('/api1', {
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: {'^/api1': ''}
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api2': ''}
})
)
}
const { createProxyMiddleware } = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
createProxyMiddleware('/api1', {
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: { '^/api1': '' }
}),
createProxyMiddleware('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: { '^/api2': '' }
}),
)
}
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理
- 缺点:配置繁琐,前端请求资源时必须加前缀
发起请求
import React, { Component } from 'react'
// 引入axios
import axios from 'axios'
export default class App extends Component {
getStudentData = () => {
// 1. 通过package.json配置代理后的请求方式:访问3000,会自动转发到5000
axios.get("http://localhost:3000/students").then(
response => { console.log('成功请求到数据:', response.data); },
error => { console.log('失败原因为:', error); }
)
// 2. 通过setupProxy.js配置代理后的请求方式:根据请求前缀的不同决定请求哪个端口
getStudentData = () => {
// 根据代理配置,api1前缀的请求会转发到5000服务器('/api1'必须写到最前面)
axios.get("http://localhost:3000/api1/students").then(
response => { console.log('成功请求到数据:', response.data); },
error => { console.log('失败原因为:', error); }
)
}
getCarData = () => {
// 根据代理配置,api2前缀会转发到5001服务器
axios.get("http://localhost:3000/api2/cars").then(
response => { console.log('成功请求到数据:', response.data); },
error => { console.log('失败原因为:', error); }
)
}
}
render() {
return (
<div>
<button onClick={this.getStudentData}>点我获取学生数据</button>
<button onClick={this.getCarData}>点我获取汽车数据</button>
</div>
)
}
}
消息订阅与发布
在我们之前做过的 ToDoList 和头像搜索案例中,为了实现兄弟组件通信,都需要将数据放到顶层 App 组件中,然后由 App 组件下发给各个子组件。可如果子组件的层级很多,这样写起来就会特别麻烦
我们通过 PubSub 库可以实现各个组件之间的相互通信
PubSub
-
A 组件: // 订阅search消息:每当收到名为search的消息时,就可以在回调函数中拿到消息名和数据内容
PubSub.subscribe('search', (msg, data) => {
this.setState(data)
})
-
B 组件: // 发布search消息,参数为消息名和数据内容
PubSub.publish('search', { isLoading: true, isFirst: false })
头像搜索案例优化
使用 PubSub 消息订阅与发布模式来优化之前的头像搜索案例:
-
搜索栏 Jumbotron.jsx(消息发布端) import React, { Component } from 'react'
import axios from 'axios'
import PubSub from 'pubsub-js'
export default class Jumbotron extends Component {
// 搜索框点击事件
search = () => {
const { content: { value: str } } = this
// 发起请求(此处调用的GitHub通过名字查询用户信息的接口)
PubSub.publish('search', { isLoading: true, isFirst: false })
axios.get(`https://api.github.com/search/users?q=${str}`).then(
response => {
// 请求到数据后,发送search消息,数据内容为刚刚请求到的用户信息list
PubSub.publish('search', { isLoading: false, list: response.data.items })
},
error => {
PubSub.publish('search', { isLoading: false, err: error.message })
}
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<div>
<input ref={c => { this.content = c }} type="text" placeholder="enter the name you search" />
<button onClick={this.search}>Search</button>
</div>
</section>
)
}
}
-
列表 Row.jsx(消息订阅端) import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'
export default class Row extends Component {
// 既然是该组件要展示数据,那么就把数据都存放到该组件的状态里,不需要再放到App组件中了
state = {
list: [],
err: '',
isFirst: true,
isLoading: false
}
// 订阅消息一般写到组件挂载完成后的钩子函数中
componentDidMount() {
/**
* 订阅search消息:只要有人发布名为search的消息,回调函数就能接收到
* 使用一个变量来接收,用于取消订阅
* msg:消息名
* data:消息数据
*/
this.token = PubSub.subscribe('search', (msg, data) => {
// 拿到搜索栏发送过来的数据,更新状态用于展示
this.setState(data)
})
}
componentWillUnmount(){
// 组件销毁时,取消订阅
PubSub.unsubscribe(this.token)
}
render() {
const { isFirst, isLoading, err, list } = this.state
return (
<div className="row">
{
isFirst ? <h2>欢迎使用用户查询系统,请输入关键字进行搜索</h2> :
isLoading ? <h2>正在加载,请稍候...</h2> :
err ? <h2>{err}</h2> :
list.map(v => {
const { avatar_url, id, html_url, login } = v
return (
<div key={id} className="card">
<a href={html_url} rel='noreferrer' target="_blank">
<img alt='head_img' src={avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{login}</p>
</div>
)
})
}
</div>
)
}
}
Fetch请求
我们常用的 Ajax 请求就是基于浏览器提供的 XHR(XMLHttpRequest)对象来实现的
与 XHR 对应的还有 Fetch,Fetch 请求方式使用了 Promise,运用了 “关注分离”(先看看服务器通不通,再取数据)的设计思想,且相比 XHR 更加简洁
但是 Fetch 兼容性不高
https://segmentfault.com/a/1190000003810652
React路由
SPA单页面应用
- 单页面 Web 应用:Single Page Web Application
- 整个应用只有一个完整的页面,但是包含了很多组件
- 点击页面中的链接不会刷新整个页面,只会做页面的局部更新
- 数据都需要通过 Ajax 请求获取,并在前端异步展现
什么是路由
一个路由就是一个映射关系(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 组件
工作原理
通过浏览器 BOM 对象中的 history 对象控制浏览器的 path,每当监听到 path 的变化时,就渲染对应的组件
history 对象的使用:(前提是运行在一个服务下,即必须以 IP:端口的形式访问)
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<a href="http://www.baidu.com" onclick="return push('/test1')">push test1</a>
<a onclick="return push('/test2')">push test2</a>
<a onclick="return replace('/test3')">replace test3</a>
<a onclick="forward()">前进</a>
<a onclick="back()">后退</a>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
let history = History.createBrowserHistory()
function push(path) {
history.push(path)
return false
}
function replace(path) {
history.replace(path)
}
function back() {
console.log('back');
history.goBack()
}
function forward() {
history.goForward()
}
history.listen((location) => {
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>
浏览器历史记录是一个栈的结构:
- 我们当前看到的页面肯定是栈顶的记录
- 调用 push 方法时,会向栈顶推入一个记录
- 调用 back 方法时,会把当前栈顶记录移出
- 调用 goForward 方法时,会把移出的记录移一个回来
- 调用 replace 方法时,会把当前栈顶的记录替换成新的记录,原来的栈顶记录直接丢掉了
react-router-dom
react-router-dom 是 React 的一个插件库,专门用来实现一个单页面 WEB 应用(还有其他路由版本,可用于原生应用)
此处讲解的是 react-router-dom@5 版本,安装:npm i react-router-dom@5
案例引入
用路由实现:通过点击左侧导航,切换右侧展示的组件
-
index.js import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
-
App.js import React, { Component } from "react";
import { Link, Route } from "react-router-dom";
import Home from "./component/Home";
import About from "./component/About";
export default class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 原生html中,用<a>跳转不同的页面 */}
{/* <a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中用路由链接实现切换组件 */}
{/* 浏览器肯定不认识Link标签,所以React会将其转成普通a标签,通过监听路径来阻止其默认行为 */}
<Link className="list-group-item" to="/about">About</Link>
{/* 标签体内容也可用children属性来指定 */}
<Link className="list-group-item" to="/Home" children="Home" />
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Route path="/about" component={About}></Route>
{/* 路由6.x版本以后写法:将component={Home}修改为:element={<Home/>} */}
<Route path="/home" component={Home}></Route>
</div>
</div>
</div>
</div>
</div>
)
}
}
知识点总结:
- React 中的 WEB 路由引入的是 react-router-dom
<Link> 标签用于编写路由链接
<Route> 标签用于注册路由,即展示组件
path 属性值为要监听的 path 路径component 属性值为要展示的组件 - 整个应用只能有一个路由器,即只能有一个
<BrowserRouter> 或 <HashRouter> 标签,所有的 <Link> 和 <Route> 标签都要写在这个标签里,所以我们直接把这个标签写到 <App /> 外面
路由组件和一般组件的区别
-
使用时的写法不同
- 一般组件:
<Demo /> - 路由组件:
<Route path="/demo" component={Demo} /> -
存放位置不同:
- 一般组件:components 目录下
- 路由组件:pages 目录下
-
接收到的 props 不同
-
一般组件:写组件标签时传递了什么,就能收到什么 -
路由组件:默认接收到三个固定的属性:
BrowserRouter和HashRouter的区别
- 底层原理不一样
- BrowserRouter 是使用 H5 的 history(React 中的 this.props.history 是对其二次封装过的,不要混淆) 实现的
- HashRouter 使用的是 URL 的哈希值实现的
- path 表现形式不一样
- BrowserRouter 的路径中没有
# - HashRouter 路径中有
# ,如 localhost:3000/#/demo - 刷新后对路由 state 参数的影响不一样
- BrowserRouter 没有任何影响,因为路由 state 参数保存在 history 对象中
- HashRouter 刷新后会导致路由 state 参数的丢失
- HashRouter 可以用来解决一些路径错误相关的问题(如本章最后一节的资源丢失问题),且其兼容性更强
NavLink
NavLink 是 Link 的升级版,可以指定一些属性,当该链接被选中时产生特定效果
// 当该链接被选中时,添加demo类名
<NavLink activeClassName="demo" className="list-group-item" {...this.props} ></NavLink>
师傅领进门,修行在个人,其他属性请参考官方文档
Switch
Switch 标签在路由 6.x 版本被移除
当切换路径时,React 会一个路由一个路由的去查找当前路径映射的哪个组件,当一个路径映射了多个组件时,则多个组件都会被展示:
// 当跳转至'/about'路径时,About和Home组件都会被渲染
<Route path="/about" component={About}></Route>
<Route path="/about" component={Home}></Route>
此时我们可以借助 <Switch> 标签,用该标签包裹起来的路由,只会找到第一个被映射的组件,可以提升效率,也能防止多个组件被渲染:
<Switch>
// 当跳转至'/about'路径时,只会渲染About组件
<Route path="/about" component={About}></Route>
<Route path="/about" component={Home}></Route>
</Switch>
Redirect
当所有路由都无法匹配时,则跳转到 <Redirect> 指定的路由
<NavLink className="list-group-item" to="/about">about</NavLink>
{/* 虽然在点击此链接时,此处'/def'路径通过Redirect标签间接匹配到了Home组件,但是该链接并不会处于active状态 */}
<NavLink className="list-group-item" to="/def">home</NavLink>
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
{/* 如果上面的的路径都没有匹配到,则自动跳转到/home路径 */}
<Redirect to="/home"></Redirect>
</Switch>
Redirect 还可以实现默认路由:
replace模式
路由链接默认为 push 模式,添加 replace 关键字可将其设置为 replace 模式(push 和 replace 的区别请见【前端路由 -> 工作原理】)
// 添加replace关键字
<Link replace to="/home/message/item">Item</Link>
点击此链接后,上一个链接的历史记录将消失,此时浏览器回退将直接回退到上上个链接
模糊匹配与严格匹配
模糊匹配:只要 <Route> 的路径处在路由链接路径的开头,就可以匹配到
严格匹配:<Route> 的路径必须和路由链接的路径完全一致,才能匹配到
- 使用
extact 关键字,或 exact={true}
例子:
// 模糊匹配1
<NavLink className="list-group-item" to="/home/a">home</NavLink>
<Route path="/home" component={Home}></Route> // 可以匹配到
// 模糊匹配2
<NavLink className="list-group-item" to="/home/a/b">home</NavLink>
<Route path="/home/a" component={Home}></Route> // 可以匹配到
// 模糊匹配3
<NavLink className="list-group-item" to="a/home">home</NavLink>
<Route path="/home" component={Home}></Route> // 不能匹配到,路径必须处在路由链接路径的开头
// 严格匹配1
<NavLink className="list-group-item" to="/home/a">home</NavLink>
<Route path="/home" component={Home}></Route> // 不能匹配到,路径不一致
// 严格匹配2
<NavLink className="list-group-item" to="/home">home</NavLink>
<Route path="/home" component={Home}></Route> // 可以匹配到
exact 关键字要谨慎使用,可能导致二级路由无法正常展示
嵌套路由
案例:先点击左侧 home 导航,右侧会出现 news 和 message 导航,再点击 news,下方出现 news 相关的内容
-
App.js <NavLink to="/about">about</NavLink>
<NavLink to="/home">home</NavLink>
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
</Switch>
-
Home.jsx <NavLink to="/home/news">news</NavLink>
<NavLink to="/home/message">message</NavLink>
<Route path="/home/news" component={News}></Route>
<Route path="/home/message" component={Message}></Route>
-
News.jsx <ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>
-
Message.jsx <ul>
<li>message01</li>
<li>message02</li>
<li>message03</li>
</ul>
嵌套路由匹配逻辑:
- 点击左侧 home 菜单时,路由跳转到 /home,渲染 Home 组件
- 点击右侧 news 菜单时,路由跳转到 /home/news,因为先注册的 about 和 home 路由,所以先对这两个进行匹配
- 通过模糊匹配,匹配到了 home(如果添加 exact 关键字则无法匹配),所以渲染 Home 组件
- Home 组件在挂载时又注册了 /home/news 和 /home/message 路由,因此又匹配到了 /home/news,故展示 News 组件
知识点:
- 注册子路由时,要在前面写上父路由的 path 值
- 哪个路由所在的组件先被挂载,则就先注册哪个路由
- 每当路由切换时,都会先从第一个注册的路由开始匹配
- 如果给路由添加了 exact 关键字,则无法使用嵌套路由
向路由组件传递参数
传递params参数
// 传递参数
<Link to="/home/message/item/zhangsan/18">Item</Link>
// 接收参数,命名为name和age
<Route path="/home/message/item/:name/:age" component={Item}></Route>
接收到参数会保存到路由组件的 props 中的 match 对象中:
刷新页面数据也不会丢失,因为参数保存在 url 里
此种路由传参方式最常用
传递search参数
// 传递search参数,在路径后跟 -> ?key=value
<Link to="/home/message/item?name=zhangsan&age=18">Item</Link>
// 注册路由无需修改
<Route path="/home/message/item" component={Item}></Route>
search 参数也会传递到路由组件的 props 对象中:
为了方便使用,我们需要把 ?key=value 的字符串形式转为一个对象
此时可以借助 qs 库:
import qs from 'qs'
let search = this.props.location.search // ?name=zhangsan&age=18
// 在解析前需要去掉前面的问号
qs.parse(search.slice(1)) // {name: zhangsan, age: 18}
// 也可以将对象转换为?name=key的形式:
qs.stringfy(obj)
刷新页面数据也不会丢失,因为参数保存在 url 里
传递state参数
注意:不要和状态 state 搞混了
// 传递stete参数时,to要使用对象形式,将原来的path路径写到pathname属性中,要传递的参数写到state对象中
<Link to={{ pathname: "/home/message/item", state: { name: 'zhangsan', age: 18 } }}>Item</Link>
// 注册路由不需要改
<Route path="/home/message/item" component={Item}></Route>
传递的参数会保存到路由组件的 props 对象中:
刷新页面数据也不会丢失,可以如果把浏览器缓存全部清空后再刷新就会丢失了
因此在使用 state 参数时最好加个空判断
编程式路由导航
我们之前讲的都是通过点击路由链接和浏览器的前进后退来切换路由的
下面我们来说说如何通过代码来实现路由跳转
需要借助于路由组件的 props 中的 history 对象,它有如下几个方法:
go(n) :前进或后退 n 个记录goBack() :后退 1 个记录goForward() :前进 1 个记录push(path, state) :push 模式切换路由,可以传递路由 path 和 state 参数replace(path, state) :replace 模式切换路由,可以传递路由 path 和 state 参数
// 点击按钮,跳转路由
<button onClick={this.jump}>button</button>
jump = () => {
// 1. 跳转路由
this.props.history.push("/home/message")
// 2. 跳转路由,并携带params参数
this.props.history.push("/home/message/zhangsan/18") // <Route>标签需要接收参数
// 3. 跳转路由,并携带state参数
this.props.history.push("/home/message", {name: 'zhangsan', age: 18})
}
withRouter
当我们想要在一般组件中控制路由的前进与回退时,发现无法实现,因为一般组件中拿不到 history 对象,这是路由组件特有的
但是 withRouter 可以加工一般组件,让一般组件具备路由组件所特有的 API,它是一个函数,返回加工后的新组件
import { withRouter } from "react-router-dom";
class App extends Component {
}
export default withRouter(App) // 加工App组件,这样就可以在App组件的props中拿到history对象
资源丢失问题
假如 bootstrap.css 文件夹放在 public/src 目录下:
当跳转的 path 值为多级时,如果刷新页面,bootstrap.css 样式会丢失,演示如下:
首先要配置多级 path 与组件映射:
<NavLink className="list-group-item" to="/zz/about">about</NavLink>
<NavLink className="list-group-item" to="/zz/home">home</NavLink>
<Route path="/zz/about" component={About}></Route>
<Route path="/zz/home" component={Home}></Route>
项目启动后,页面默认 url 为 http://localhost:3000,页面展示如下:
此时点击 about 链接,页面展示如下:
然后刷新页面,会发现所有样式都没了:
查看一下浏览器请求,发现在请求 bootstrap.css 时被添加了一个 zz 前缀,所以没请求到:
没请求到为什么状态码还是 200 呢?因为当 path 路径为 / 或者请求的资源没请求到时,则默认会将 index.html 返回:
在失去 bootstrap 样式后,页面就变成了光秃秃的样子
解决资源丢失问题有三种方式:
-
在引入资源时,不要用相对路径:
<link rel="stylesheet" href="./css/bootstrap.css">
<link rel="stylesheet" href="/css/bootstrap.css">
-
在引入资源时,使用 %PUBLIC_URL% (只适用于 React 脚手架中):
<link rel="stylesheet" href="./css/bootstrap.css">
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
-
使用 HashRouter 替换 BrowserRouter : // 修改前:
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
// 修改后:
// HashRouter会给url添加'#'号,'#'号后的路径浏览器是不会去请求的,所以相当于直接请求localhost:3000
root.render(
<HashRouter>
<App />
</HashRouter>
)
Ant Design组件库
Ant Design(简称 antd)是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品
- 安装:
npm i antd - 在 App 组件中引入 antd 样式文件:
import 'antd/dist/antd.min.css' - 引入 antd 组件:
import { Button } from 'antd' - 调用 antd 组件:
<Button type="primary">Button</Button>
除 antd 外,其他常用组件库还有 element-ui(Vue / React),vant-ui(移动端)
另外,如果觉得 antd 官网文档写的没那么详细,可以查看 3.x 版本的文档
样式按需引入
上面引入样式文件的方式加载了全部的 antd 组件的样式(gzipped 后一共大约 60kb)
我们希望 React 能够按照需要去加载组件代码和样式
https://www.bilibili.com/video/BV1wy4y1D7JT?p=95
自定义主题
antd 默认主题色是蓝色,其实是因为其 less 样式文件中有个变量定义的是蓝色,所以解析出来的 css 代码都带有蓝色,我们只需想办法修改那个变量即可
https://ant.design/docs/react/use-with-create-react-app-cn
Redux
介绍
- Redux 是什么
- Redux 是一个专门用于做状态管理的 js 库(并不属于 React 插件库)
- 它可以用在 React,Angular,Vue 等项目中,但基本上都是和 React 配合使用
- 作用:集中式管理 React 应用中多个组件共享的状态
- 类似于 Vue 中的 Vuex
- 什么情况下需要使用 Redux?
- 某个组件的状态,需要让其他组件可以随时拿到(共享)
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用,如果消息订阅模式用着比较吃力时才考虑使用
工作原理
Redux 工作流程:
- React Components(我们的组件)要修改状态
- 先由 Action Creators 创造一个 action 对象,用于表示【如何修改】和【修改的值是什么】
- 然后将此 action 对象发送给 Store,Store 再将上一次的状态和 action 对象传递给 Reducers 进行加工
- 如果是第一次传递状态,即初始化状态,则 previousState 值为 undefined,action 的 type 值为 @@init
- Reducers 返回加工后的新状态给 Store
- Store 再将新的状态返回给我们的组件,最终完成一次状态的修改
可以把 React Components 理解为餐厅的顾客(我要吃什么菜),Action Creators 就是服务员(记录菜单),Store 为传菜员(把菜单拿给厨子,厨子做好后再把菜上给顾客),Reducers 则为厨子(根据菜单做菜)
案例引入
这里通过一个计算器案例,依次引出 Redux 的各个 API 的使用方式
案例:有下图这样的一个计算器,上方展示计算结果,左侧下拉框可以选择要加或减的值,右方的按钮依次为加、减、奇数时再加、1 秒后再加
目录结构:
代码讲解:
-
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(<App></App>)
// 监听store的值变化
store.subscribe(() => {
// 直接重新渲染整个App
// 重新渲染所有组件,会不会有效率问题?不会的,有DOM的diffing算法
root.render(<App></App>)
})
-
App.js import React, { Component } from 'react'
import Count from './components/Count'
export default class App extends Component {
render() {
return (
<div>
<Count></Count>
</div>
)
}
}
-
constant.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
-
count_action.js
import { INCREMENT, DECREMENT } from "./constant";
export const createIncrementAction = data => ({ type: INCREMENT, data })
export const createDecrementAction = data => ({ type: DECREMENT, data })
export const createIncrementAsyncAction = (data, timeout) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data * 1))
}, timeout);
}
}
-
count_reducer.js
import { INCREMENT, DECREMENT } from "./constant";
const initState = 0;
export default function countReducer(preState = initState, action) {
const { type, data } = action;
switch (type) {
case INCREMENT:
return preState + data;
case DECREMENT:
return preState - data;
default:
return preState;
}
}
-
store.js
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import countReducer from './count_reducer'
export default createStore(countReducer, applyMiddleware(thunk))
-
Count.jsx import React, { Component } from "react";
import store from "../../redux/store";
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from "../../redux/count_action";
export default class Count extends Component {
state = {
name: "zhangsan",
};
/*
// 页面挂载后,监听store对象
// 如果每个组件都要通过修改状态来触发渲染,则每个组件都要写下面这段代码,所以我们把它提取到index.js中
componentDidMount(){
// 因为store的值改变并不能触发页面的渲染,而且我们也不能手动通过this.render()进行渲染
// 所以需要对store的值进行监听,只要发生改变,就手动调用一下setState方法触发页面渲染
store.subscribe(()=>{
this.setState({})
})
} */
add = () => {
const { value } = this.opt;
store.dispatch(createIncrementAction(value * 1));
};
sub = () => {
const { value } = this.opt;
store.dispatch(createDecrementAction(value * 1));
};
addIfOdd = () => {
const count = store.getState();
const { value } = this.opt;
if (count % 2 !== 0) {
store.dispatch(createIncrementAction(value * 1));
}
};
addAsync = () => {
const { value } = this.opt;
// 异步逻辑不再写到组件里,而是交给异步action处理
// dispatch本来只能接受一个普通action对象,但是这里传递的参数时一个函数,则需要redux-thunk中间件
store.dispatch(createIncrementAsyncAction(value * 1, 1000));
};
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select
ref={(v) => {
this.opt = v;
}}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<button onClick={this.add}>+</button>
<button onClick={this.sub}>-</button>
<button onClick={this.addIfOdd}>当前求和为奇数再加</button>
<button onClick={this.addAsync}>1秒后再加</button>
</div>
);
}
}
API
store.dispatch(action)
createStore(reducer,applyMiddleware(thunk))
- 暴露 store 对象,并为其注册 reducer 和 redux-thunk
store.substribe(callback)
总结
store 状态修改流程:
- 所有的公共状态都保存到 store 对象中
- 组件需要修改 store 状态时,需要通过
store.dispatch() 传递一个 action - action 由 count_action.js 文件专门创建, 需由组件调用其暴露的方法
- action 会被传递到 count_reducer.js 定义的 reducer 函数中,由 reducer 完成 store 状态的修改
注意:
- store 对象的状态更改并不能触发页面渲染,但可以通过
store.subscribe() 监听 store 状态的更改,然后重新渲染整个 App 组件 - 所有 type 类型值推荐定义到一个常量文件中,可以防止单词写错,也方便统一修改
- 异步逻辑可以不写到组件中,而是交给异步 action处理
- 传递的 action 如果是一个函数,则需要引入 redux-thunk 支持
React-redux
为了方便与 React 集成,Redux 官方提供了一个 react-redux 绑定库
安装:npm -i react-redux
原理图
基本使用
- 创建容器组件
- 将 redux状态传递给容器组件
- 绑定容器组件和 UI 组件
- 在容器组件中编写获取状态和操作状态的方法,并映射到 UI 组件
- 在 UI 组件中通过 props 获取状态和调用操作状态的方法
将 Redux 章节的计算器通过 react-redux 优化:
目录结构:
代码展示:
-
容器组件 Count.jsx
connect(mapStateToProps, mapDispatchToProps)(UIComponent)
- 创建并暴露一个绑定了 UIComponent 组件的容器组件
- mapStateToProps:映射状态到 UI 组件上,默认接收状态参数
- mapDispatchToProps:映射操作状态的方法到 UI 组件上,默认接收 dispatch 参数
// 引入Count的UI组件
import CountUI from "../../components/Count";
// 引入action creator
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from "../../redux/count_action";
// 引入connect用于连接UI组件与容器组件,以及映射状态和操作状态的方法
import { connect } from "react-redux";
// 映射状态(即该函数的返回值)到UI组件的props中
// 此函数是由react-redux帮我们调的,它在调的时候传了状态作为参数
function mapStateToProps(state) {
return { count: state };
}
// 映射操作状态的方法(即该函数的返回值)到UI组件的props中
// 此函数是由react-redux帮我们调的,它在调的时候传了dispatch作为参数
function mapDispatchToProps(dispatch) {
return {
add: (value) => {
// 调用redux的api操作状态
dispatch(createIncrementAction(value));
},
sub: (value) => {
dispatch(createDecrementAction(value));
},
addIfOdd: (value) => {
dispatch(createIncrementAction(value));
},
addAsync: (value, time) => {
dispatch(createIncrementAsyncAction(value, time));
},
};
}
// 使用connect()()创建并暴露一个绑定了CountUI组件的容器组件
// connect()是一个函数,它又返回了一个函数,我们在后面再加一个()来调用返回的函数
export default connect(mapStateToProps, mapDispatchToProps)(CountUI);
-
App.js import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store'
export default class App extends Component {
render() {
return (
<div>
{/* 将redux状态传递给容器组件,注意不是直接在容器组件中引入redux状态 */}
<Count store={store}></Count>
</div>
)
}
}
-
UI 组件 Count.jsx // UI组件中不再使用任何redux的API
import React, { Component } from "react";
export default class Count extends Component {
state = {
name: "zhangsan",
};
add = () => {
const { value } = this.opt;
{/* 调用容器组件传递过来的操作状态的方法 */}
this.props.add(value * 1);
};
sub = () => {
const { value } = this.opt;
this.props.sub(value * 1);
};
addIfOdd = () => {
const { value } = this.opt;
if (this.props.count % 2 !== 0) {
this.props.addIfOdd(value * 1);
}
};
addAsync = () => {
const { value } = this.opt;
this.props.addAsync(value * 1, 1000);
};
render() {
return (
<div>
{/* 从容器组件传递的props中获取状态 */}
<h1>当前求和为:{this.props.count}</h1>
<select
ref={(v) => {
this.opt = v;
}}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<button onClick={this.add}>+</button>
<button onClick={this.sub}>-</button>
<button onClick={this.addIfOdd}>当前求和为奇数再加</button>
<button onClick={this.addAsync}>1秒后再加</button>
</div>
);
}
}
编码优化
映射简写
/*
// connect()方法需要接收两个函数,我们不再单独定义
function mapStateToProps(state) {
return { count: state };
}
function mapDispatchToProps(dispatch) {
return {
add: (value) => {
dispatch(createIncrementAction(value));
},
sub: (value) => {
dispatch(createDecrementAction(value));
},
addIfOdd: (value) => {
dispatch(createIncrementAction(value));
},
addAsync: (value, time) => {
dispatch(createIncrementAsyncAction(value, time));
},
};
}
*/
export default connect(
// 原来的mapStateToProps简写可以为箭头函数
state => ({ count: state }),
// 原来的mapDispatchToProps可以简写为对象,因为react-redux帮我们做了处理
// 我们只需写上【方法名:action creator】即可,react-redux会自动接收UI组件传递的参数,并通过dispatch帮我们转发
{
add: createIncrementAction,
sub: createDecrementAction,
addIfOdd: createIncrementAction,
addAsync: createIncrementAsyncAction,
}
)(CountUI);
取消store监听
使用 react-redux 之后,可以不再通过监听 store 的变化来手动渲染 App,容器组件会自动监听并渲染
store传递简写
原来我们需要手动给容器对象传递 store:
export default class App extends Component {
render() {
return (
<div>
{/* 将redux状态传递给容器组件 */}
<Count store={store}></Count>
{/* 如果容器组件很多,就要传递很多次store */}
<A store={store}></A>
<B store={store}></B>
<C store={store}></C>
</div>
)
}
}
现在通过 <Provider/> 标签,可以自动将 store 传递给所有容器组件:
容器组件+UI组件整合
之前我们都是容器组件写到 containers 中,UI 组件写到 components 中
我们可以把这两个组件合为一个文件,使项目结构更清晰
一般合成后的文件还是写到 containers 目录中,因为该文件里面调用了 redux 的 API,已经不符合 UI 组件的定义了
-
Count.jsx import { connect } from "react-redux";
import React, { Component } from "react";
// 原UI组件内容
class Count extends Component {
render() {
return (
// ...
);
}
}
// 原容器组件内容
export default connect((state) => ({ count: state }), {
add: createIncrementAction,
sub: createDecrementAction,
addIfOdd: createIncrementAction,
addAsync: createIncrementAsyncAction,
})(Count); // 传入UI组件Count
多组件状态共享
在之前的 redux 案例中,我们只使用了一个 Count 组件,无法体会到多个组件共享状态的模式
本节编写了一个多组件状态共享案例:有 A、B 两个组件,A 组件展示求和结果,B 组件展示数组。A 组件可以向 B 组件的数组中添加元素,B 组件可以修改 A 组件的求和结果
文件目录:
-
index.js import React from "react";
import App from "./App";
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux/es/exports'
import store from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
-
App.js import React, { Component } from 'react'
import A from './containers/A'
import B from './containers/B'
export default class App extends Component {
render() {
return (
<div>
<A></A>
<hr></hr>
<B></B>
</div>
)
}
}
-
aReducer.js import { ADD } from "../constant";
const initState = 0;
export default function aReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case ADD:
return preState + data;
default:
return preState
}
}
-
bReducer.js import { PUSH } from "../constant";
const initState = []
export default function bReducer(preState = initState, action) {
const { type, data } = action;
switch (type) {
case PUSH:
return [...preState, data]
default:
return preState;
}
}
-
store.js import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import aReducer from "./reducers/a_reducer";
import bReducer from "./reducers/b_reducer";
const allReducers = combineReducers({
a: aReducer,
b: bReducer
})
export default createStore(allReducers, applyMiddleware(thunk))
-
constant.js export const ADD = 'add'
export const PUSH = 'push'
-
a_action.js import { ADD } from "../constant";
export const createAddAction = data => ({ type: ADD, data })
-
b_action.js import { PUSH } from "../constant";
export const createPushAction = data => ({ type: PUSH, data })
-
容器组件 A.jsx import React, { Component } from "react";
import { connect } from "react-redux";
import { createAddAction } from "../../redux/actions/a_action";
import { createPushAction } from "../../redux/actions/b_action";
class A extends Component {
render() {
console.log("a-props:", this.props);
return (
<div>
<h1>我是A组件,我的求和结果为{this.props.state.a}</h1>
<button
onClick={() => {
// 操作B组件的状态
this.props.push("item");
}}
>
向B组件数组添加元素
</button>
</div>
);
}
}
export default connect(
// 这里的参数state就是store中的状态,我们可以将整个state传过去,也可以只传store状态中的部分属性
(state) => ({ state }),
// 向UI组件传递操作状态的方法
{
add: createAddAction,
push: createPushAction
}
)(A);
-
容器组件 B.jsx import React, { Component } from "react";
import { connect } from "react-redux";
import { createAddAction } from "../../redux/actions/a_action";
import { createPushAction } from "../../redux/actions/b_action";
class B extends Component {
render() {
console.log("b-props:", this.props);
return (
<div>
<h1>我是B组件,我的数组元素为{this.props.state.b}</h1>
<button
onClick={() => {
// 操作A组件的状态
this.props.add(1);
}}
>
A组件结果+1
</button>
</div>
);
}
}
// (state) => ({ state }) 相当于:(state) => (state: state),这里是键值对同名时的简化写法,我们也可以用其他的键名来传递
export default connect((state) => ({ state }), {
add: createAddAction,
push: createPushAction,
})(B);
Redux开发者工具
安装步骤:
-
在浏览器中安装 Redux-DevTools 插件 -
在项目中安装插件扩展:npm i redux-devtools-extension -
在 store.js 中对插件进行支持
import { composeWithDevTools } from "redux-devtools-extension";
export default createStore(allReducers, composeWithDevTools())
组件通信方式总结
组件间的关系:
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
常用通信方式:
- props
- children props
- render props
- 消息订阅与发布
- 状态集中式管理
- context
通信方式选择:
- 父子组件:props
- 兄弟组件:消息订阅与发布、状态集中式管理
- 跨级组件:消息订阅与发布、状态集中式管理、context(开发中较少使用)
项目打包运行
打包:
- 停止 react 程序
- npm run build
- 生成 build 文件夹
运行:
- 生产环境中,肯定是将打包后的文件放到后台服务运行
- 如果要在我们自己电脑上要模拟一个服务的话,可以使用 serve 包:npm i serve -g
- 执行命令:进入 build 文件夹,执行 serve;或者在 build 的上级目录,执行 serve build
React扩展
setState
添加回调函数
setState(newState, [callback]) 可以接收第二个参数,即回调函数,它在状态更新完毕且界面重新 render 后才被调用
import React, { Component } from "react";
export default class SetState extends Component {
state = { count: 0 };
/*
add = () => {
// 修改状态
this.setState({ count: this.state.count + 1 });
// 打印修改后的状态值
console.log(this.state.count); // 因为setState是异步调用的,所以这里打印的永远是修改前的状态
};
*/
add = () => {
// 添加回调函数
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count); // 因为是状态更新完毕后执行,所以打印的是修改后的状态值
});
};
render() {
return (
<div>
<h2>Count值为{this.state.count}</h2>
<button onClick={this.add}>点我+1</button>
</div>
);
}
}
接收原state和props
setState(fn(preState, props), [callback]) :调用 setState 时可以不直接传新的 state 对象,而是传递一个函数,该函数可以接收原 state 状态和 props 对象,且返回值就是要修改的状态值
import React, { Component } from "react";
export default class SetState extends Component {
state = { count: 0 };
add = () => {
this.setState((state, props) => {
// 从原状态中取出count+1后,封装为对象返回
return { count: state.count + 1 };
});
};
render() {
return (
<div>
<h2>Count值为{this.state.count}</h2>
<button onClick={this.add}>点我+1</button>
</div>
);
}
}
lazyLoad
lazyLoad 一般用于路由的懒加载
当我们使用路由后第一次访问页面时,会一次性把所有资源都请求过来,然后在切换路由的时候不会再次请求资源
我们希望的是切换到哪个路由,就去请求哪个组件的资源
// 引入lazy和Suspense
import React, { Component, lazy, Suspense } from "react";
// 引入Loading组件,必须写到懒加载组件的上面
import Loading from "./component/Loading";
// 懒加载式引入组件,所有懒加载组件资源都会被分别打包
const Home = lazy(() => import('./component/Home'))
const About = lazy(() => import('./component/About'))
// 使用Suspense组件包裹路由组件,指定在加载得到路由打包文件前显示一个自定义Loading组件
<Suspense fallback={<Loading />}>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
</Suspense>
Hooks
Hook 是 React 16.8.0 版本增加的新特性 / 新语法
可以让你在函数组件中使用 state 以及其他的 React 特性
State Hook
State Hook 让函数组件也可以有 state 状态,并进行状态数据的读写操作
语法:const [xxx, setXxx] = React.useState(initValue)
useState(initValue) 说明:
- initValue 是 state 的初始化值,并且会在内部作缓存,即使修改 state 后引起视图渲染导致再次执行到 useState 的时候并不会重新初始化 state
- 返回值:包含 2 个元素的数组 —— 第1个元素为当前状态值,第 2 个为更新此状态值的函数
setXxx() 的 2 种写法:
setXxx(newValue) :参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值setXxx(value => newValue) :参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
import React from "react";
export default function Demo() {
// 有几个状态,就要写几个useState
// 并不能直接用一个对象来管理状态,因为修改状态的方法会直接把整个对象替换掉,而不是只替换掉对象中的某个属性
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("zhangsan");
function changeCount() {
setCount(count + 1);
}
function changeName() {
setName("lisi");
}
return (
<div>
<h2>当前Count为{count}</h2>
<h2>我的Name为{name}</h2>
<button onClick={changeCount}>点我修改Count</button>
<button onClick={changeName}>点我修改Name</button>
</div>
);
}
Effect Hook
Effect Hook 可以在函数组件中模拟生命周期钩子,包含 componentDidMount、componentDidUpdate、componentWillUnmount
语法:
React.useEffect(() => {
return () => {
}
}, [state1, state2])
使用:
import React from "react";
import ReactDOM from "react-dom";
export default function Demo() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("zhangsan");
React.useEffect(() => {
console.log("AAA");
return () => {
console.log("BBB");
};
}, [count]); // 只监听count状态
function changeCount() {
setCount((count) => count + 1);
}
function changeName() {
setName("lisi");
}
function unmount() {
// 卸载挂载到root上的组件
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
return (
<div>
<h2>当前count为{count}</h2>
<h2>我的Name为{name}</h2>
<button onClick={changeCount}>点我修改Count</button>
<button onClick={changeName}>点我修改Name</button>
<button onClick={unmount}>点我卸载组件</button>
</div>
);
}
Ref Hook
Ref Hook 可以在函数组件中存储 / 查找组件内的标签或任意其它数据,与之前讲的 createRef 功能是一样的
import React from "react";
export default function Demo() {
// 创建容器
const myRef = React.useRef();
function show() {
console.log(myRef.current.value);
}
return (
<div>
{/* 将该节点保存到容器 */}
<input type="text" ref={myRef} />
<button onClick={show}>点我展示输入框内容</button>
</div>
);
}
Fragment
<Fragment> 标签不会被解析到页面上,只能接收一个 key 属性用于遍历
// 引入Fragment标签
import React, { Component, Fragment } from 'react'
import Demo from './components/HookDemo'
export default class App extends Component {
render() {
return (
// 因为此处只能有一个根标签,如果用div的话又觉得多余,所以可以使用Fragment
<Fragment key={1}>
<Demo />
</Fragment>
)
}
}
context
context 是一种【祖组件】与【后代组件】之间的通信方式
import React, { Component } from "react";
// 1. 首先要创建Context
const myContext = React.createContext();
// 2. 从Context中取出Provider组件
const { Provider } = myContext;
export default class A extends Component {
state = { name: "tom" };
render() {
return (
<div>
<h2>我是A组件,我的用户名是:</h2>
{/* 3. 将name传递给所有子组件 */}
<Provider value={this.state.name}>
<B />
</Provider>
</div>
);
}
}
class B extends Component {
render() {
// 不声明是取不到的
console.log(this.context) // undefined
return (
<div>
<h2>我是B组件</h2>
<C />
</div>
);
}
}
// 第一种使用方式,只适用于类组件:
class C extends Component {
// 先声明,表示我需要使用Context
static contextType = myContext;
render() {
return (
<div>
<h2>我是C组件</h2>
{/* 使用this.context获取祖组件传递的值 */}
<h2>我从A组件接收到的用户名是:{this.context}</h2>
</div>
);
}
}
// 第二种使用方式,适用于类组件和函数组件
// 从Context中获取Consumer组件
const { Consumer } = myContext;
function C() {
return (
// 使用Consumer组件包裹
<Consumer>
{/* value值就是祖组件传递的值 */}
{(value) => {
return (
<div>
<h2>我是C组件</h2>
<h2>我从A组件接收到的用户名是{value}</h2>
</div>
);
}}
</Consumer>
);
}
React.Component优化
React.Component 存在两个问题,会影响效率:
- 只要执行了 setState(),即使不改变状态数据, 组件也会重新 render
- 只当前组件重新 render(),就会自动重新 render 子组件,纵使子组件没有用到父组件的任何数据
产生这两个问题的原因是因为:控制页面刷新的 shouldComponentUpdate() 函数总是返回 true
为了提高效率,我们希望只有当组件的 state 或 props 发生改变时,shouldComponentUpdate 函数才返回 true,具体实现为:
import React, { Component } from "react";
export default class Parent extends Component {
state = { car: "BMW" };
// 重新shouldComponentUpdate方法,比较状态是否真的改变了,只有改变时才返回true
shouldComponentUpdate(nextProps, nextState) {
return !(this.state.car === nextState.car);
}
render() {
return (
<div>
<h1>我的座驾:{this.state.car}</h1>
<button
onClick={() => {
// 执行了setState()方法,但是没有修改状态
this.setState({});
}}
>
点我换车
</button>
<Child car={this.state.car} />
</div>
);
}
}
class Child extends Component {
// 子组件需要比较props
shouldComponentUpdate(nextProps, nextState) {
return !(this.props.car === nextProps.car);
}
render() {
return <h1>我爸爸的座驾:{this.props.car}</h1>;
}
}
除了手动编写 shouldComponentUpdate 方法外,React 还为我们提供了 PureComponent 组件,该组件已经帮我们编写好了 shouldComponentUpdate 方法,它会帮我们比较 state 和 props 中的所有属性,不需要我们再一个一个去比较了
需要注意:PureComponent 在比较时使用的是浅比较
import React, { Component, PureComponent } from "react";
export default class Parent extends Component {
state = { car: "BMW" };
render() {
return (
<div>
<h1>我的座驾:{this.state.car}</h1>
<button
onClick={() => {
// 这里只是在原来的state上做修改,因此newState和state的地址是一样的
const newState = this.state;
// 将BMW替换成了Audi,父子组件都应该重新渲染
newState.car = "Audi";
this.setState(newState);
}}
>
点我换车
</button>
{/* 为了验证浅比较,这里就不传this.state.car了,而是直接传this.state,让PureComponent对新旧state对象进行比较 */}
<Child car={this.state} />
</div>
);
}
}
// 子组件继承PureComponent,它已经帮我们重写好了shouldComponentUpdate函数,它在渲染前会自动帮我们浅比较state和props
// 因为是浅比较,shouldComponentUpdate函数相当于返回了!(this.state === nextState),所以返回的是false,不会重新渲染页面
class Child extends PureComponent {
render() {
return <h1>Child</h1>;
}
}
render props
我们之前编写的父子关系组件,一般都是在父组件中引入子组件:
export default class A extends Component {
render() {
return (
// 在A组件中引入B组件,B就是A的子组件
<B />
);
}
}
还可以不将子组件写到父组件中,而是通过标签的嵌套来实现父子关系:
import React, { Component, PureComponent } from "react";
export default class Parent extends Component {
render() {
return (
// A标签包裹B标签,B就是A的子组件
// B标签会传入到A组件的props中
<A>
<B />
</A>
);
}
}
class A extends PureComponent {
render() {
return (
<div>
我是A组件
{/* 渲染B组件 */}
{this.props.children}
</div>
);
}
}
class B extends PureComponent {
render() {
return <div>我是B组件</div>;
}
}
页面效果:
虽然这种写法能够实现父子关系,但是 A 组件无法为 B 组件传值,此时我们需要借助 render props:
render props 的好处是比较灵活,不再将子组件写死到父组件中,而是像 Vue 中的插槽一样,想让谁做子组件,直接将其写到插槽中即可
import React, { Component, PureComponent } from "react";
export default class Parent extends Component {
render() {
return (
<A
// 为A组件的props传递render函数,并接收data参数
render={(data) => {
// 返回B组件,并将data传递至B组件的props中
return <B data={data}></B>;
// 好处:这种写法的好处是,我们想让谁做A组件的子组件,直接在这里return就行,而不用写死到A组件内部,比较灵活
// 这种写法类似于Vue中的slot插槽
return <C data={data}></C>;
}}
/>
);
}
}
class A extends PureComponent {
render() {
return (
<div>
我是A组件
{/* 调用render函数,并传递参数,此参数会传递至B组件的props中 */}
{this.props.render("abc")}
</div>
);
}
}
class B extends PureComponent {
render() {
return <div>我是B组件,我从A组件接收到的内容为{this.props.data}</div>;
}
}
错误边界
错误边界(Error Boundary):指限制错误的影响范围
一般情况下,页面上任何一个组件报错,整个页面都会无法渲染。我们希望报错的组件不要影响到其他组件,并在自己的位置展示一条提示信息,如 “服务器繁忙,请稍后再试 …”
注意:错误边界只能捕获后代组件的生命周期函数中产生的错误(render 函数属于生命周期函数)
实现错误边界需要借助两个生命周期函数:
- getDerivedStateFromError:只要后代组件中的生命周期函数报错,就会触发,一般用于修改错误标识,方便判断组件是否报错
- componentDidCatch:只要后代组件中的生命周期函数报错,就会触发,一般用于记录错误日志,反馈服务器
import React, { Component } from "react";
export default class Parent extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// 在render之前触发
// 返回的对象会去修改state
return { hasError: true };
}
componentDidCatch(error, info) {
// 统计页面错误信息
console.log(error, info);
}
render() {
return (
<div>
我是父组件
{/* 如果该组件报错,则展示提示语 */}
{this.state.hasError ? "服务器繁忙,请稍候再试..." : <Child />}
</div>
);
}
}
class Child extends Component {
render() {
// 随便调用一个不存在的函数,引发报错
abc();
return <div>我是子组件</div>;
}
}
注意:只有项目打包并放到服务器上运行时,错误边界才会起效果
ReactRouter@6
与Router@5版本不同
ReactRouter@6 删除了 5 版本中的一些标签和属性,同时新增了一些新标签和新属性
- 删除
<Switch> 标签,取而代之的是 <Routes/> 标签 - 删除
component 属性,取而代之的是 element 属性 - 删除
activeClassName 属性 - 新增
<Navigate> 标签 - 新增
caseSensitive 属性 - 新增
end 属性 - …
import React from "react";
import { Route, NavLink, Routes, Navigate } from "react-router-dom";
import Home from "./component/Home";
import About from "./component/About";
export default function App() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* Navlink组件在被选中时默认会添加'active'类名,如果想要修改类名,之前使用的是activeClassName属性 */}
{/* 路由6.x版本之后删除了这个属性,如果想要自定义类名,需要让className返回一个函数 */}
{/* 这个函数接收一个对象:{isActive: true/false},选中时isActive值为true,未选中时为false */}
{/* 该对象的返回值就是最终的类名 */}
{/* 选中时添加'abc'类名。注意:这里在接收参数时使用了解构赋值 */}
<NavLink className={({ isActive }) => isActive ? 'list-group-item abc' : 'list-group-item'} to="/about">about</NavLink>
{/* 给'/home'路由添加end属性后,当'/home'的子路由活跃时,会删除'/home'路由的active类名 */}
<NavLink className="list-group-item" end to="/home">home</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* Routes拥有之前Switch标签的所有功能,并且Route标签外必须包裹一个Routes */}
<Routes >
{/* 之前的component={About}替换为element={<About />} */}
<Route path="/about" element={<About />}></Route>
{/* 为Route标签添加caseSensitive后,在匹配路由路径时会区分大小写 */}
<Route caseSensitive path="/Home" element={<Home />}></Route>
{/* 当路由为'/'时,渲染Navigate组件,只要Navigate组件一渲染,就会跳到它指定的路径 */}
{/* 可以为Navigate添加replace属性指定是否使用replace模式跳转,默认为push模式 */}
<Route path="/" element={<Navigate to="/about" replace={true}></Navigate>}></Route>
</Routes>
</div>
</div>
</div>
</div>
</div>
)
}
路由表
之前在写路由组件的时候,会产生这样的代码:
<Routes >
<Route path="/about" element={<About />}></Route>
<Route caseSensitive path="/Home" element={<Home />}></Route>
<Route path="/xxx" element={<Xxx />}></Route>
<Route path="/xxx" element={<Xxx />}></Route>
<Route path="/xxx" element={<Xxx />}></Route>
<Route path="/" element={<Navigate to="/about" replace={true}></Navigate>}></Route>
</Routes>
所有的路由都挤在组件里面,看起来很乱,因此我们可以使用路由表,把他们统一到一个 js 文件中进行管理,需借助 useRoutes 方法
-
在 src 目录下新建 routes 文件夹,在里面新建 index.js,编写路由表: import About from '../component/About'
import Home from '../component/Home'
import { Navigate } from 'react-router-dom'
export default [
{
path: '/about',
element: <About />
},
{
path: '/home',
element: <Home />
},
{
path: '/',
element: <Navigate to="/about" />
}
]
-
在组件中使用路由表 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>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<NavLink className={({ isActive }) => isActive ? 'list-group-item abc' : 'list-group-item'} to="/about">about</NavLink>
<NavLink className="list-group-item" to="/home">home</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 使用路由表,被映射到的组件会展示在这里 */}
{element}
</div>
</div>
</div>
</div>
</div>
)
}
嵌套路由
我们在路由表中也可以编写嵌套路由:
import About from '../component/About'
import Home from '../component/Home'
import News from '../component/Home/News'
import Message from '../component/Home/Message'
import { Navigate } from 'react-router-dom'
export default [
{
path: '/about',
element: <About />
},
{
path: '/home',
element: <Home />,
children: [
{
path: 'news',
element: <News />
},
{
path: 'message',
element: <Message />
},
]
},
{
path: '/',
element: <Navigate to="/about" />
}
]
使用嵌套路由:
import React from "react";
// 引入Outlet组件
import { NavLink, Outlet } from "react-router-dom";
export default function Home() {
return (
<div>
<ul className="nav nav-tabs">
<li>
{/* 使用to属性指定路由路径时,直接写子路由的路径即可,不用写全量路径 */}
<NavLink className="list-group-item" to="news">
news
</NavLink>
</li>
<li>
<NavLink className="list-group-item" to="message">
message
</NavLink>
</li>
</ul>
{/* 使用Outlet指定嵌套路由组件呈现的位置 */}
<Outlet />
</div>
);
}
传递params参数
-
在路由路径中接收 params 参数: export default [
{
path: '/home',
element: <Home />,
children: [
{
path: 'message',
element: <Message />,
children: [
{
path: 'item/:name/:age',
element: <Item />
}
]
},
]
},
]
-
在路由链接中传递 params 参数: import React, { Component } from "react";
import { Link, Outlet } from "react-router-dom";
export default class Message extends Component {
render() {
return (
<div>
{/* 传递params参数 */}
<Link to="item/zhangsan/18">传递参数</Link>
<hr />
<Outlet />
</div>
);
}
}
-
在路由组件(函数式组件)中接收 params 参数: import React, { Component } from "react";
import { useParams, useMatch } from "react-router-dom";
export default function Item() {
// 第一种方式:使用useParams接收params参数
const params = useParams();
console.log(params); // {name: 'zhangsan', age: '18'}
// 第二种方式:使用useMatch先获取match对象,再拿到params参数;参数需要写全量路径,并声明接收参数
const match = useMatch("/home/message/item/:name/:age");
console.log(match); // {params: {…}, pathname: '/home/message/item/zhangsan/18', pathnameBase: '/home/message/item/zhangsan/18', pattern: {…}}
return (
<div>
我叫{params.name},我今年{params.age}岁
</div>
);
}
传递search参数
-
保证路由路径不要带任何参数 export default [
{
path: '/home',
element: <Home />,
children: [
{
path: 'message',
element: <Message />,
children: [
{
path: 'item',
element: <Item />
}
]
},
]
},
]
-
路由链接传递 search 参数: import React, { Component } from "react";
import { Link, Outlet } from "react-router-dom";
export default class Message extends Component {
render() {
return (
<div>
{/* 传递search参数 */}
<Link to={`item?name=zhangsan&age=18`}>传递参数</Link>
<hr />
<Outlet />
</div>
);
}
}
-
接收 search 参数: import React, { Component } from "react";
import { useSearchParams, useLocation } from "react-router-dom";
export default function Item() {
// 第一种方式:调用useSearchParams方法,返回一个数组,从数组的第一个参数中取search参数
const search = useSearchParams(); // [URLSearchParams, ?]
const [searchParams] = search;
console.log(searchParams.get("name")); // zhangsan
// 第二种方式:调用useLocation方法,返回location对象,从中获取search参数
const location = useLocation();
console.log(location.search); // ?name=zhangsan&age=18
return <div></div>;
}
传递state参数
-
保证路由路径不要带任何参数 -
传递 state 参数: import React, { Component } from "react";
import { Link, Outlet } from "react-router-dom";
export default class Message extends Component {
render() {
return (
<div>
{/* 传递state参数 */}
<Link to="item" state={{ name: "zhangsan", age: 18 }}>
传递参数
</Link>
<hr />
<Outlet />
</div>
);
}
}
-
接收 state 参数: import React, { Component } from "react";
import { useSearchParams, useLocation } from "react-router-dom";
export default function Item() {
// 先获取location对象
const location = useLocation();
// 从location对象中获取state
console.log(location.state); // {name: 'zhangsan', age: 18}
return <div></div>;
}
编程式路由导航
在 ReactRoute@5 中,只有路由组件才可以使用 history 对象实现编程式路由导航
而在 ReactRoute@6 中,任何组件都可以使用 navigate 函数实现编程式路由导航
import React from "react";
import { useNavigate, Link, Outlet } from "react-router-dom";
export default function Message() {
// 获取navigate函数
const navigate = useNavigate();
function showItem() {
// 调用navigate函数,第一个参数是路由路径,第二个参数可以传一些配置,也可传递state参数。但是params和search参数不能这样传,需要写到路由路径后面
navigate("item", {
replace: true,
state: {
name: "zhangsan",
age: 18,
},
});
}
function back() {
// 调用navigate函数,实现后退
navigate(-1);
}
return (
<div>
<button onClick={showItem}>展示Item</button>
<button onClick={back}>后退</button>
<Outlet />
</div>
);
}
useInRouterContext()
useInRouterContext() 钩子函数主要用于判断当前组件是否处于路由的上下文环境中
什么叫处于路由的上下文环境中?
// 只要是被BrowserRouter或HashRoute标签包裹的组件,就处于路由的上下文环境中
<BrowserRouter>
<App />
</BrowserRouter>
使用:
import React from "react";
import { useInRouterContext } from "react-router-dom";
export default function Demo() {
console.log(useInRouterContext()); // true
return <div>Demo</div>
}
useNavigationType
作用:返回当前的导航类型,即用户是如何来到当前页面的
返回值:POP ,PUSH ,REPLACE
备注:POP 是指在浏览器中直接打开了这个路由组件(如刷新页面)
import React from "react";
// 引入useNavigationType
import { useNavigationType } from "react-router-dom";
export default function Item() {
// 使用
console.log(useNavigationType()); // REPLACE
return <div>我是ITEM</div>;
}
useOutlet
作用:用于获取当前组件中已渲染的嵌套路由
如果嵌套路由还没有挂载,则返回 null;如果已经挂载,则返回嵌套路由对象
import React from "react";
import {
useNavigate,
Outlet,
useOutlet,
} from "react-router-dom";
export default function Message() {
const navigate = useNavigate();
// 获取当前组件中已渲染的嵌套路由
const res = useOutlet();
function showItem() {
navigate("item");
console.log("res", res); // {$$typeof: Symbol(react.element), type: {…}, key: null, ref: null, props: {…}, …}
}
return (
<div>
<button onClick={showItem}>展示Item</button>
<Outlet />
</div>
);
}
useResolvedPath
作用:给定一个 URL,解析其中的 path、search 和 hash 值
import { useResolvedPath } from "react-router-dom";
console.log(useResolvedPath('/user?name=zhangsan&age=18#abc')) // {pathname: '/user', search: '?name=zhangsan&age=18', hash: '#anc'}
|