# React

React 简介

React 简介

  • React 是什么呢?
    • 相信每个做开发的人对它都或多或少有一些印象;
    • 这里我们来看一下官方对它的解释:用于构建用户界面的 JavaScript 库

1663993425782

  • 目前对于前端开发来说,几乎很少直接使用原生的 JavaScript 来开发应用程序,而是选择一个 JavaScript 库(框架)。
    • 在过去的很长时间内,jQuery 是被使用最多的 JavaScript 库;
    • 在过去的一份调查中显示,全球前 10,000 个访问最高的网站中,有 65% 使用了 jQuery,是当时最受欢迎的 JavaScript 库;
    • 但是,目前甚至已经处于淘汰的边缘了;
  • 而无论是国内外,最流行的其实是三大框架:Vue、React、Angular

React 在前端的地位

  • 目前前端最流行的是三大框架:Vue、React、Angular

1663993643418

React 的技术特点

  • React 由 Facebook 来更新和维护,它是大量优秀程序员的思想结晶:

    • React 的流行不仅仅局限于普通开发工程师对它的认可;
    • 大量流行的其他框架借鉴 React 的思想
  • Vue.js 框架设计之初,有很多的灵感来自 Angular 和 React。

    • 包括 Vue3 很多新的特性,也是借鉴和学习了 React;
    • 比如 React Hooks 是开创性的新功能(也是我们课程的重点);
    • Vue Composition API 学习了 React Hooks 的思想
  • Flutter 的很多灵感都来自 React,来自官网的一段话:(SwiftUI 呢)

    • 事实上 Flutter 中的 Widget – Element – RenderObject;
    • 对应 React 的就是 JSX – 虚拟 DOM – 真实 DOM;
  • 所以 React 可以说是前端的先驱者,它总是会引领整个前端的潮流

Vue和React的选择

  • 首先,React 和 Vue 是前端开发人员必须掌握的两个框架。

1663993978733

  • 下面的观点是一个目前比较普遍的共识,没有贬低任何框架的意思。
    • 大中型公司选择 React 会较多,灵活和稳定;

    • 中小型公司选择 Vue 会较多,易上手和代码统一;

    • 难度:React 难度大于 Vue

    • 工资:React 工资大于 Vue

React 课堂体系

1663994136592

  • React 和 Vue 都是前端工程师必须掌握的两个框架:
    • 大多数同学都是学习了 Vue,并且刚开始工作都是使用的 Vue,所以通常对 Vue 是有深入的感情的(某些同学可能是小程序);
    • 但是在前端整个职业发展的过程中,不能仅仅将自己局限在某一个框架或者技术中;
    • 并且 React 是作为前端进阶来说自己必须要掌握的一个框架;
  • 本次课程要求:
    • 本课程要求掌握前端的核心开发语言:HTML、CSS、JavaScript
    • React 本身是 JavaScript 的要求相对会更高一些,所以也需要掌握一些高级的 JavaScript 语法,比如 ES6 以上的语法、this 绑定规则等等;
    • 整个课程从零讲解 React,所以并不要求之前学习过 React 相关的知识。
    • 如果你之前已经掌握了一些 React,也可以从课程中学习到非常多其他的核心知识和实战细节,也包括原理、源码、架构等知识内容。
  • 所以无论你目前处于前端哪一个阶段,都可以在这个过程中有很多的收获。

邂逅React开发

React 的介绍

1663998324637

1663998337353

React 的特点

声明式编程

  • 声明式编程:
    • 声明式编程是目前整个大前端开发的模式:Vue、React、Flutter、SwiftUI;
    • 它允许我们只需要维护自己的状态,当状态改变时,React 可以根据最新的状态去渲染我们的 UI 界面

1663998444461

组件化开发

  • 组件化开发:
    • 组件化开发页面目前前端的流行趋势,我们会将复杂的界面拆分成一个个小的组件;
    • 如何合理的进行组件的划分和设计也是后面我会讲到的一个重点;

1663998520366

多平台适配

  • 多平台适配:
    • 2013 年,React 发布之初主要是开发 Web 页面;
    • 2015 年,Facebook 推出了 ReactNative,用于开发移动端跨平台;(虽然目前 Flutter 非常火爆,但是还是有很多公司在使用 ReactNative);
    • 2017 年,Facebook 推出 ReactVR,用于开发虚拟现实 Web 应用程序;( VR 也会是一个火爆的应用场景);

1663998570934

React 开发依赖

React 开发依赖

  • 开发 React 必须依赖三个库:
    • react:包含 react 所必须的核心代码
    • react-dom:react 渲染在不同平台所需要的核心代码
    • babel:将 jsx 转换成 React 代码的工具
  • 第一次接触 React 会被它繁琐的依赖搞蒙,居然依赖这么多东西:
    • 对于 Vue 来说,我们只是依赖一个 vue.js 文件即可,但是 react 居然要依赖三个包。
    • 其实呢,这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情;
    • 在 React 的 0.14 版本之前是没有 react-dom 这个概念的,所有功能都包含在 react 里;
  • 为什么要进行拆分呢?原因就是 react-native。
    • react 包中包含了 react web 和 react-native 所共同拥有的核心代码。
    • react-dom 针对 web 和 native 所完成的事情不同:
      • web 端:react-dom 会将 jsx 最终渲染成真实的 DOM,显示在浏览器中
      • native 端:react-dom 会将 jsx 最终渲染成原生的控件(比如 Android 中的 Button,iOS 中的 UIButton)。

Babel与React的关系

  • babel 是什么呢?
    • Babel ,又名 Babel.js。
    • Babel 是目前前端使用非常广泛的编译器、转移器
    • 比如当下很多浏览器并不支持 ES6 语法,TS, JSX 等语法,但是这些语法确实非常的简洁和方便,我们开发时希望使用它。
    • 那么编写源码时我们就可以使用 ES6 等这些高级语法来编写,之后通过 Babel 工具,将 ES6 等这么高级语法转成大多数浏览器都支持的 ES5 的语法。
  • React 和 Babel 的关系:
    • 默认情况下开发 React 其实可以不使用 babel。
    • 但是前提是我们自己使用 React.createElement 来编写源代码,它编写的代码非常的繁琐和可读性差。
    • 那么我们就可以直接编写 jsx(JavaScript XML)的语法,并且让 babel 帮助我们转换成 React.createElement
    • 后续还会详细讲到;

React 的依赖引入

  • 所以,我们在编写 React 代码时,这三个依赖都是必不可少的,并且引入顺序也分先后顺序
  • 那么,如何添加这三个依赖:
    • 方式一:直接 CDN 引入
    • 方式二:下载后,添加本地依赖
    • 方式三:通过 npm 管理(后续脚手架再使用)
  • 暂时我们直接通过 CDN 引入,来演练下面的示例程序:
    • 这里有一个 crossorigin 的属性,这个属性的目的是为了拿到跨域脚本的错误信息
1
2
3
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

React 小案例

案例需求分析

  • 为了演练 React,我们可以提出一个小的需求:
    • 在界面显示一个文本:Hello World
    • 点击下方的一个按钮,点击后文本改变为 Hello React

1663998673323

  • 该案例的代码会在后面展示
  • 当然,你也可以使用 jQuery 和 Vue 来实现,甚至是原生方式来实现,对它们分别进行对比学习

Hello World 案例

  • 第一步:在界面上通过 React 显示一个 Hello World
    • 注意:这里我们编写 React 的 script 代码中,必须添加 type=”text/babel”作用是可以让 babel 解析 jsx 的语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>

<body>
<div id="root"></div>
<script type="text/babel"> // script 标签的类型必须是 type="text/babel"
// ...
</script>
</body>

</html>
1
2
3
4
5
6
7
8
// React 18 之前的写法: 
ReactDOM.render(<h2>Hello World</h2>, document.querySelector("#root"))

//========================================================================

// React 18 之后的写法:
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<h2>Hello World</h2>)
  • ReactDOM.createRoot 函数:用于创建一个 React 根,之后渲染的内容会包含在这个根中
    • 参数:将渲染的内容,挂载到哪一个 HTML 元素上
      • 这里我们已经提定义一个 id 为 app 的 div
  • root.render 函数:
    • 参数:要渲染的根组件
  • 我们可以通过 { } 语法来引入外部的变量或者表达式

Hello React 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  // React 18 之后的写法:
// 创建 root 实例对象
const root = ReactDOM.createRoot(document.querySelector("#root"))

// 01 定义变量
let message = "hello World"

// 02 定义按钮的点击事件回调
function btnClick() {
// 05 修改数据
message = "hello React"
// 06 重新渲染页面
rootRender()
}

// 03 封装一个渲染函数
function rootRender() {
// 渲染页面
root.render((
<div>
<h2>{message}</h2>
<button onClick={btnClick}>修改文本</button>
</div>
))
}

// 04 初次渲染页面
rootRender()

VScode代码片段

  • 我们在前面练习 React 的过程中,有些代码片段是需要经常写的,我们在 VSCode 中我们可以生成一个代码片段,方便我们快速生成。
  • VSCode 中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。
  • 具体的步骤如下:
    • 第一步,复制自己需要生成代码片段的代码;
    • 第二步,https://snippet-generator.app/ , 在该网站中生成代码片段;
    • 第三步,在 VSCode 中配置代码片段;

组件化开发

Hello 案例组件化

  • 整个逻辑其实可以看做一个整体,那么我们就可以将其封装成一个组件:
    • 我们说过 root.render 参数是一个 HTML 元素或者一个组件
    • 所以我们可以先将之前的业务逻辑封装到一个组件中,然后传入到 ReactDOM.render 函数中的第一个参数;
  • 在React中,如何封装一个组件呢?这里我们暂时使用类的方式封装组件:
    • 定义一个类(类名大写,组件的名称是必须首字母大写,小写会被认为是 HTML 元素),并且继承自 React.Component
    • 实现当前组件的 render 函数
      • render 当中返回的 jsx 内容,就是之后 React 会帮助我们渲染的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1. 定义一个类组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "Hello World"
}
}

// 1.2 组件方法 , setState 方法: 内部会进行数据的修改,并会重新执行 render 函数,重新渲染页面
btnClick() {
// 使用 ES6 的函数语法定义的事件回调,如果在绑定事件时,没有通过 bind 修改 this 的指向,则 this 为 undefined
// 因为类中的方法,默认开启局部严格模式,这是类的特性,和 react 没有关系,经过 babel 编译后的代码也会开启严格模式
// setState 方法是从父类 Component 继承来的,并不是当前组件实例对象所有
this.setState({
message: "Hello React"
})
}

// 1.3 渲染内容 , render 函数中的 this 指向当前组件实例对象
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick.bind(this)}>修改文本</button>
</div>
)
}
}

// 2. 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

数据依赖

  • 组件化问题一:数据在哪里定义?
  • 在组件中的数据,我们可以分成两类:
    • 参与界面更新的数据:当数据变量时,需要更新组件渲染的内容;
    • 不参与界面更新的数据:当数据变量时,不需要更新将组建渲染的内容;
  • 参与界面更新的数据我们也可以称之为是参与数据流,这个数据是定义在当前对象的 state 中
    • 我们可以通过在构造函数中 this.state = { 定义的数据 }
    • 当我们的数据发生变化时,我们可以调用 this.setState 来更新数据,并且通知 React 进行 update 操作;
      • 在进行 update 操作时,就会重新调用 render 函数,并且使用最新的数据,来渲染界面

方法的 this 指向问题

  • 组件化问题二:事件绑定中的 this
    • 在类中直接定义一个函数,并且将这个函数绑定到元素的 onClick 事件上,当前这个函数的this指向的是谁呢?
  • 默认情况下是 undefined
    • 很奇怪,居然是 undefined;
    • 因为在正常的 DOM 操作中,监听点击,监听函数中的 this 其实是节点对象(比如说是 button 对象);
    • 这次因为 React 并不是直接渲染成真实的 DOM,我们所编写的 button 只是一个语法糖,它的本质是 React 的 Element 对象;
    • 那么在这里发生监听的时候,react 在执行函数时并没有绑定 this,默认情况下就是一个 undefined;
    • 类组件中的方法,默认会开启局部严格模式,类中的方法中的 this 就是 undefined , 这是类的特性,和 react 没有关系
    • 经过 babel 编译后的代码,也会开启严格模式
  • 我们在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的 this
    • 我们就需要在传入函数时,给这个函数直接绑定 this
    • 类似于下面的写法:
1
<button onClick={this.changeText.bind(this)}>改变文本</button>
  • 后面事件绑定的章节中会详细解析解决 this 的指向问题的多种方法

组件化开发案例

电影列表展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 定义一个类组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
movies: ["星际穿越", "流浪地球", "独行月球", "大话西游", "火星救援"]
}
}

// 1.2 渲染内容
render() {
return (
<div>
<h2>电影列表</h2>
<ul>
{
this.state.movies.map((item,index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
}
}

// 2. 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

计数器案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "当前计数:",
counter: 0
}
}

// 1.2 组件方法
increment = () => {
this.setState({
counter: this.state.counter + 1 // 不能使用自增 ++
})
}
decrement = () => {
this.setState({
counter: this.state.counter - 1 // 不能使用自减 --
})
}

// 1.3 渲染内容
render() {
const { message, counter } = this.state
return (
<div>
<h2>{message}</h2>
<h2>{counter}</h2>
<button onClick={this.increment}>+1</button>
<button onClick={this.decrement}>-1</button>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

React JSX语法

认识 JSX

1
2
3
4
5
6
7
// 1. 创建虚拟 DOM
const element = <h2>Hello World</h2> // JSX

// 2. 将虚拟 DOM渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(element)

  • 这段 element 变量的声明右侧赋值的标签语法是什么呢?
    • 它不是一段字符串(因为没有使用引号包裹);
    • 它看起来是一段 HTML 元素,但是我们能在 js 中直接给一个变量赋值 html 吗?
      • 其实是不可以的,如果我们将 type="text/babel" 去除掉,那么就会出现语法错误;
      • 它到底是什么呢?其实它是一段 JSX 的语法
  • JSX 是什么?
    • JSX 是一种 JavaScript 的语法扩展(extension),也在很多地方称之为 JavaScript XML,因为看起就是一段 XML 语法;
    • 用于描述我们的 UI 界面,并且可以和 JavaScript 融合在一起使用;
    • 它不同于 Vue 中的模块语法,你不需要专门学习模块语法中的一些指令(比如 v-for、v-if、v-else、v-bind);

为什么选择 JSX

  • React 认为渲染逻辑本质上与其他 UI 逻辑存在内在耦合
    • 比如 UI 需要绑定事件(button、a 原生等等);
    • 比如 UI 中需要展示数据状态;
    • 比如在某些状态发生改变时,又需要改变 UI;
  • 他们之间是密不可分,所以 React 没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component)
    • 当然,后面我们还是会继续学习更多组件相关的东西;
  • 在这里,我们只需要知道,JSX 其实是嵌入到 JavaScript 中的一种结构语法

JSX 的书写规范

  • JSX 的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个 div 元素(或者使用后面我们学习的 Fragment );
  • 为了方便阅读,我们通常在 jsx 的外层包裹一个小括号 (),这样可以方便阅读,并且 jsx 可以进行换行书写
  • JSX 中的标签可以是单标签,也可以是双标签;
    • 注意:如果是单标签,也必须结束
      • 比如 img , input 等标签,可以加/闭合,也可以写结束标签

JSX的使用

JSX 中的注释

1
2
3
4
5
6
7
8
9
10
11
// 1.3 渲染内容
render() {
const { message } = this.state
return (
<div>
{/* 我是 jsx 的一个注释 */}
<h2>{message}</h2>
</div>
)
}

JSX 嵌入变量

  • 情况一:当变量是 Number、String、Array 类型时,可以直接显示
  • 情况二:当变量是 null、undefined、Boolean 类型时,内容为空
    • 如果希望可以显示 null、undefined、Boolean,那么需要转成字符串;
    • 转换的方式有很多,比如 toString 方法、和空字符串拼接,String (变量)等方式;
  • 情况三:不能直接将 Object 对象类型作为子元素(not valid as a React child),否则会报错,但是可以读取对象中的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 定义 App 根组件
class App extends React.Component {
// 组件数据
constructor() {
super()
this.state = {
counter: 0,
message: "Hello World",
names: ["acb", "cba", "nba"],

aaa: undefined,
bbb: null,
ccc: true,

friend: {
name: "zhangsan"
}
}
}

// 渲染内容
render() {
const { counter, message, names, friend } = this.state
const { aaa, bbb, ccc } = this.state

return (
<div>
{/* 1. 当变量是 Number、String、Array 类型时,可以直接显示*/}
<h2>{counter}</h2>
<h2>{message}</h2>
<h2>{names}</h2>

{/* 2. 当变量的值是 null、undefined、Boolean 类型时,页面内容为空;可以通过转为字符串类型显示*/}
<h2>{aaa}</h2>
<h2>{bbb}</h2>
<h2>{ccc}</h2>

{/* 3. 直接放一个对象会报错 , 对象类型不能作为子元素进行显示,但是可以读取对象中的属性 */}
<h2>{friend.name}</h2>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

JSX 嵌入表达式

  • 运算表达式
  • 三元运算符
  • 执行一个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
firstName: "jack",
lastName: "jobe",
age: 18,
movies: ["星际穿越", "流浪地球", "独行月球", "大话西游", "火星救援"]
}
}

// 1.2 组件方法
getMovieEls = () => {
const liEls = this.state.movies.map(movie => <li key={movie}>{movie}</li>)
return liEls
}

// 1.3 渲染内容
render() {
const { firstName, lastName, age, movies } = this.state
const fullName = firstName + lastName
const ageText = age >= 18 ? "成年人" : "未成年人"
const liEls = this.state.movies.map(movie => <li key={movie}>{movie}</li>)

return (
<div>
{/* 4.可以插入对应的表达式*/}
<h2>{10 + 20}</h2>
<h2>{firstName + " " + lastName}</h2>
<h2>{fullName}</h2>

{/* 5.可以插入三元运算符*/}
<h2>{ageText}</h2>
<h2>{age >= 18 ? "成年人" : "未成年人"}</h2>

{/* 6.可以调用方法获取结果*/}
<ul>{liEls}</ul>
<ul>{this.state.movies.map(movie => <li key={movie}>{movie}</li>)}</ul>
<ul>{this.getMovieEls()}</ul>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

JSX 绑定属性

  • 比如元素都会有 title 属性
  • 比如 img 元素会有 src 属性
  • 比如 a 元素会有 href 属性
  • 比如元素可能需要绑定 class
  • 比如原生使用内联样式 style
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
title: "我是标题",
imgURL: "https://img1.baidu.com/it/u=133990705,310893509&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1667667600&t=a29e7543c3f08fa9b38d6931b7529f95",
href: "http://www.baidu.com",
isActive: true,
objStyle: { color: "red", fontSize: "50px" }
}
}

// 1.2 组件方法

// 1.3 渲染内容
render() {
const { title, imgURL, href, isActive, objStyle } = this.state
const className = `abc cba ${isActive ? "active" : ""}`
const classList = ["abc", "edf", "ghi"]

if (isActive) {
classList.push("active")
}

return (
<div>
{ /* 1.基本属性绑定 */}
<h2 title={title}>我是h2元素</h2>
<img src={imgURL} alt="" />
<a href={href}>百度一下</a>

{ /* 2.绑定class属性: 需要使用 className */}
<h2 className={className}>哈哈哈哈</h2>
<h2 className={classList.join(" ")}>哈哈哈哈</h2>

{ /* 3.绑定style属性: 绑定对象类型 */}
<h2 style={{ color: "red", fontSize: "50px" }}>呵呵呵呵</h2>
<h2 style={objStyle}>呵呵呵呵</h2>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

React 事件绑定

React 事件绑定

  • 如果原生 DOM 原生有一个监听事件,我们可以如何操作呢?
    • 方式一:获取 DOM 原生,添加监听事件
    • 方式二:在 HTML 原生中,直接绑定 onclick
  • 在 React 中是如何操作呢?我们来实现一下 React 中的事件监听,这里主要有两点不同
    • React 事件的命名采用小驼峰式(camelCase),而不是纯小写
    • 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;

this 绑定

  • 在事件执行后,我们可能需要获取当前类的对象中相关的属性,这个时候需要用到 this
    • 如果我们这里直接打印 this,也会发现它是一个 undefined
  • 为什么是 undefined 呢
    • 原因是 btnClick 函数并不是我们主动调用的,而且当 button 发生改变时,React 内部调用了 btnClick 函数;
    • 而它内部调用时,并不知道要如何绑定正确的 this;
  • 如何解决 this 的问题呢?
    • 方案一:bind 给 btnClick 显示绑定 this
    • 方案二:使用 ES6 class fields 语法
    • 方案三:事件监听时传入箭头函数(个人推荐)

解决 this 指向问题

  • 类中的方法,默认开启局部严格模式 , this 是 undefined , 这是类的特性,和 react 没有关系

方法一

  • 使用 ES6 的函数增强语法定义的事件回调, 可以在构造器中,通过 bind 方法修改方法的 this 指向,绑定事件时无需再修改 this 指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 定义 App 根组件
class App extends React.Component {
// 01 组件数据
constructor() {
super()
this.state = {
message: "hello world"
}
// 使用 ES6 的函数语法定义的事件回调,可以在构造器中修改 this 的指向,绑定事件时无需再修改 this 指向
this.btnClick = this.btnClick.bind(this)
}

// 02 组件方法 , setState 方法: 内部会进行数据的修改,并会重新执行 render 函数,重新渲染页面
btnClick() {
this.setState({
message: "Hello React"
})
}

// 03 渲染内容
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick}>修改文本</button>
</div>
)
}
}

方法二

  • 使用 ES6 的函数增强语法定义的事件回调, **可以在绑定事件时,通过 bind 修改 this 指向,**无需再在构造器中多写一行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "hello world"
}
}

// 1.2 组件方法 , setState 方法: 内部会进行数据的修改,并会重新执行 render 函数,重新渲染页面
btnClick() {
this.setState({
message: "Hello React"
})
}

// 1.3 渲染内容
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick.bind(this)}>修改文本</button>
</div>
)
}
}

方法三

  • 使用箭头函数定义事件回调,在绑定事件时即可直接绑定事件回调,无需再修改 this 指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "当前计数:",
counter: 0
}
}

// 1.2 组件方法
increment = () => {
this.setState({
counter: this.state.counter + 1
})
}
decrement = () => {
this.setState({
counter: this.state.counter - 1
})
}

// 1.3 渲染内容
render() {
const { message, counter } = this.state
return (
<div>
<h2>{message}</h2>
<h2>{counter}</h2>
<button onClick={this.increment}>+1</button>
<button onClick={this.decrement}>-1</button>
</div>
)
}
}

方法四

  • 在绑定事件时,绑定一个箭头函数,在箭头函数中加括号调用需要执行的事件回调 , 注意事件处理函数要加括号调用
    • 则定义事件回调时,使用 ES6 增强语法定义方法 或者 使用箭头函数定义方法均可
    • 推荐使用,方便传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "当前计数:",
counter: 0
}
}

// 1.2 组件方法
increment = () => {
this.setState({
counter: this.state.counter + 1
})
}
decrement = () => {
this.setState({
counter: this.state.counter - 1
})
}

// 1.3 渲染内容
render() {
const { message, counter } = this.state
return (
<div>
<h2>{message}</h2>
<h2>{counter}</h2>
<button onClick={this.increment}>+1</button>
<button onClick={() => { this.decrement() }}>-1</button>
</div>
)
}
}

事件参数传递

  • 在执行事件函数时,有可能我们需要获取一些参数信息:比如 event 对象、其他参数
  • 情况一:获取 event 对象
    • 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为)
    • 那么默认情况下,event 对象有被直接传入,函数就可以获取到 event 对象;
  • 情况二:获取更多参数
    • 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数
    • 极其不推荐通过 bind 修改 this 指向的同时,还传递参数,复杂还容易混乱….(详细参考 JS 高级 bind 修改 this 指向)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "Hello World"
}
}

// 1.2 组件方法
btnClick = (event, name, age) => {
console.log("btnClick", event, this)
console.log("name:", name, "age:", age)
}

// 1.3 渲染内容
render() {
return (
<div>
{/* 01. 不传递参数,也会默认传递 event 事件对象 */}
<button onClick={this.btnClick}>按钮1</button>

{/* 02. event 事件对象的传递, 若实参不写 event , 则第一个形参为 undefined */}
<button onClick={(event) => { this.btnClick(event) }}>按钮2</button>

{/* 03. 其他参数的传递 */}
<button onClick={(event) => { this.btnClick(event, "why", 18) }}>按钮3</button>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

事件绑定案例

  • 点击电影列表,选中项变为红色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
movies: ["星际穿越", "流浪地球", "独行月球", "大话西游", "火星救援"],
currentIndex: 0
}
}

// 1.2 组件方法
btnClick = (index) => {
this.setState({
currentIndex: index
})
}

// 1.3 渲染内容
render() {
const { movies, currentIndex } = this.state
return (
<div>
<ul>
{
movies.map((item, index) => {
return (
<li
className={index === currentIndex ? "active" : ""}
key={index}
onClick={() => { this.btnClick(index) }}
>{item}</li>
)
})
}
</ul>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

React 渲染

React 条件渲染

  • 某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容:
    • 在 vue 中,我们会通过指令来控制:比如 v-ifv-show
    • 在 React 中,所有的条件判断都和普通的 JavaScript 代码一致
  • 常见的条件渲染的方式有哪些呢?
    • 方式一:条件判断语句
      • 适合逻辑较多的情况
    • 方式二:三元运算符
      • 适合逻辑比较简单
    • 方式三:与运算符 &&
      • 适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染;
    • v-show 的效果
      • 主要是控制 display 属性是否为 none

if 条件渲染

三元运算符

逻辑运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
isReady: true,
friend: {
name: "why",
age: 18
}
}
}

// 1.2 渲染内容
render() {
// if 条件判断
const { isReady, friend } = this.state
let showElement = null
if (isReady) {
showElement = <h2>准备开始</h2>
} else {
showElement = <h2>提前准备</h2>
}

return (
<div>
{/* 01 if 条件渲染 */}
<div>{showElement}</div>

{/* 02 三元运算符 */}
<div>{isReady ? <h1>开始</h1> : <h1>等待</h1>}</div>

{/* 03 逻辑与运算符 应用场景: 当某一个值有可能为 undefined 时, 使用 && 进行条件判断 */}
<div>{friend && <div>{friend.name + " " + friend.age}</div>}</div>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

条件渲染案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "hello world",
isShow: true
}
}

// 1.2 组件方法
btnClick() {
this.setState({ isShow: !this.state.isShow }) // 状态取反
}

// 1.3 渲染内容
render() {
const { message, isShow } = this.state

let showElement = null
if (isShow) {
showElement = <h2>{message}</h2>
}

return (
<div>
<button onClick={() => { this.btnClick() }}>切换</button>
{/* 01 if 结构实现 */}
{showElement}

{/* 02 逻辑与实现 */}
{isShow && <h2>{message}</h2>}

{/* 03 实现类似于 vue 中的 v-show 的效果 */}
<h2 style={{ display: isShow ? 'block' : 'none' }}>你好,李银河</h2>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

React 列表渲染

React 列表渲染

  • 真实开发中我们会从服务器请求到大量的数据,数据会以列表的形式存储:
    • 比如歌曲、歌手、排行榜列表的数据;
    • 比如商品、购物车、评论列表的数据;
    • 比如好友消息、动态、联系人列表的数据;
  • 在 React 中并没有像 Vue 模块语法中的 v-for 指令,而且需要我们通过 JavaScript 代码的方式组织数据,转成 JSX:
    • 很多从 Vue 转型到 React 的同学非常不习惯,认为 Vue 的方式更加的简洁明了;
    • 但是 React 中的 JSX 正是因为和 JavaScript 无缝的衔接,让它可以更加的灵活;
    • 另外我经常会提到 React 是真正可以提高我们编写代码能力的一种方式;
  • 如何展示列表呢?
    • 在 React 中,展示列表最多的方式就是使用数组的 map 高阶函数
  • 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理:
    • 比如过滤掉一些内容:filter 函数
    • 比如截取数组中的一部分内容:slice 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
message: "电影列表",
movies: ["星际穿越", "盗梦空间", "大话西游", "流浪地球"],
currentIndex: 0
}
}

// 1.2 组件方法
itemClick(index) {
this.setState({ currentIndex: index })
}

// 1.3 渲染内容
render() {
const { message, movies, currentIndex } = this.state
return (
<div>
<h2>{message}</h2>
<ul>
{
movies.map((item, index) => {
return (
<li
className={currentIndex === index ? "active" : ""}
key={index}
onClick={() => { this.itemClick(index) }}
>
{item}
</li>
)
})
}
</ul>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

列表渲染案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义 App 根组件
class App extends React.Component {
// 组件数据
constructor() {
super()
this.state = {
message: "列表渲染案例",
students: [
{ id: 111, name: "why", score: 199 },
{ id: 112, name: "kobe", score: 98 },
{ id: 113, name: "james", score: 199 },
{ id: 114, name: "curry", score: 120 },
]
}
}

// 渲染内容
render() {
const { message, students } = this.state
// 对数据进行过滤操作
const filterStudents = students.filter((item) => {
return item.score > 100
})

// 渲染结构
return (
<div>
<h2>{message}</h2>
<ul>
{
filterStudents.map((item, index) => {
return <li key={item.id}>{`姓名:${item.name},成绩:${item.score}`}</li>
})
}
</ul>
</div>
)
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

列表中的 key

  • 我们会发现在前面的代码中只要展示列表都会报警告:

  • 这个警告是告诉我们需要在列表展示的 jsx 中添加一个 key。

    • key 主要的作用是为了提高 diff 算法时的效率;
    • 这个我们在后续内容中再进行讲解;

JSX 原理

JSX的本质

  • 实际上,jsx 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。
    • 所有的 jsx 最终都会被转换成 React.createElement 的函数调用
  • createElement 需要传递三个参数:
    • 参数一:type
      • 当前 ReactElement 的类型;
      • 如果是标签元素,那么就使用字符串表示 “div”;
      • 如果是组件元素,那么就直接使用组件的名称;
    • 参数二:config 对象,即创建的元素的属性和属性值 (没有属性就写 null )
      • 所有 jsx 中的属性都在 config 中以对象的属性和值的形式存储;
      • 比如传入 className 作为元素的 class;
    • 参数三:children
      • 存放在标签中的内容,以 children 数组的方式进行存储;
      • 当然,如果是多个元素呢?React 内部有对它们进行处理,处理的过程见源码
  • 我们知道默认 jsx 是通过 babel 帮我们进行语法转换的,所以我们之前写的 jsx 代码都需要依赖 babel。
  • 可以在 babel 的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react

直接编写 JSX

  • 我们自己来编写 React.createElement 代码:
    • 我们就没有通过 jsx 来书写了,界面依然是可以正常的渲染。
    • 另外,在这样的情况下,你还需要 babel 相关的内容吗?不需要了
      • 所以,type="text/babel" 可以被我们删除掉了;
      • 所以,<script src="../react/babel.min.js"></script>可以被我们删除掉了;

虚拟DOM的创建过程

  • 我们通过 React.createElement 最终创建出来一个 ReactElement 对象:

  • 这个 ReactElement 对象是什么作用呢?React 为什么要创建它呢?

    • 原因是 React 利用 ReactElement 对象组成了一个 JavaScript 的对象树;
    • JavaScript 的对象树就是虚拟 DOM(Virtual DOM);
  • 如何查看 ReactElement 的树结构呢?

    • 我们可以将之前的 jsx 返回结果进行打印;
    • 注意下面代码中我打 jsx 的打印;
  • 而 ReactElement 最终形成的树结构就是 Virtual DOM;

1663997001745

声明式编程

  • 虚拟 DOM 帮助我们从命令式编程转到了声明式编程的模式
  • React 官方的说法:Virtual DOM 是一种编程理念。
    • 在这个理念中,UI 以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的 JavaScript 对象
    • 我们可以通过 ReactDOM.render 让 虚拟 DOM 和 真实 DOM 同步起来,这个过程中叫做协调(Reconciliation);
  • 这种编程的方式赋予了 React 声明式的 API :
    • 你只需要告诉 React 希望让 UI 是什么状态;
    • React 来确保 DOM 和这些状态是匹配的;
    • 你不需要直接进行 DOM 操作,就可以从手动更改 DOM、属性操作、事件处理中解放出来;
  • 关于虚拟 DOM 的一些其他内容,在后续的学习中还会再次讲到;

购物车案例练习

  • 1 在界面上以表格的形式,显示一些书籍的数据;
  • 2 在底部显示书籍的总价格;
  • 3 点击 + 或者 - 可以增加或减少书籍数量(如果为 1,那么不能继续 - );
  • 4 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空);

1663998003145

1
2
3
4
5
6
<script src="../lib/react.js"></script>
<script src="../lib/react-dom.js"></script>
<script src="../lib/babel.js"></script>
<script src="./data.js"></script>
<script src="./format.js"></script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 定义 App 根组件
class App extends React.Component {
// 1.1 组件数据
constructor() {
super()
this.state = {
books: books
}
}

// 组件方法
// 总价格
getTotalPrice() {
const { books } = this.state
// 计算总价格,方式一
// let totalPrice = 0
// for (let i = 0; i < books.length; i++) {
// const book = books[i]
// totalPrice += book.price * book.count
// }
// 方式二:
let totalPrice = books.reduce((preValue, item) => {
return preValue + item.count * item.price
}, 0)
return totalPrice
}

// 加减按钮的点击
// increment = (index) => {
// const newBooks = [...this.state.books]
// newBooks[index].count += 1
// this.setState({ books: newBooks })
// }
// decrement = (index) => {
// const newBooks = [...this.state.books]
// newBooks[index].count -= 1
// this.setState({ books: newBooks })
// }
changeCount = (index, count) => {
const newBooks = [...this.state.books]
newBooks[index].count += count
this.setState({ books: newBooks })
}

// 删除按钮的回调
removeItem = (index) => {
const newBooks = [...this.state.books]
newBooks.splice(index, 1)
this.setState({ books: newBooks })
}

// 渲染购物车列表
renderBookList = () => {
const { books } = this.state
return (
<div>
<table>
<thead>
<tr>
<th>序号</th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{
books.map((item, index) => {
return (
<tr key={item.id}>
<td>{index + 1}</td>
<td>{item.name}</td>
<td>{item.date}</td>
<td>{formatPrice(item.price)}</td>
<td>
<button disabled={item.count <= 1} onClick={() => { this.changeCount(index, -1) }}>-</button>
{item.count}
<button onClick={() => { this.changeCount(index, 1) }}>+</button>
</td>
<td><button onClick={() => { this.removeItem(index) }}>删除</button></td>
</tr>
)
})
}
</tbody>
</table>
<h2>总价格:{formatPrice(this.getTotalPrice())}</h2>
</div>
)
}

// 渲染购物车为空时的结构
renderBookEmpty = () => {
return <div><h2>购物车数据为空,请添加书籍</h2></div>
}

// 渲染内容
render() {
const { books } = this.state
return books.length ? this.renderBookList() : this.renderBookEmpty()
}
}

// 将组件渲染到页面中
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

format.js

1
2
3
4
function formatPrice(price) {
return '¥' + Number(price).toFixed(2)
}

data.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const books = [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.00,
count: 1
},
]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /* CSS */ 
table {
border-collapse: collapse;
text-align: center;
}

thead {
background-color: #f2f2f2;
}

td,
th {
padding: 10px 16px;
border: 1px solid #aaa;
}

React 脚手架

前端工程的复杂化

  • 如果我们只是开发几个小的 demo 程序,那么永远不需要考虑一些复杂的问题:
    • 比如目录结构如何组织划分;
    • 比如如何管理文件之间的相互依赖;
    • 比如如何管理第三方模块的依赖;
    • 比如项目发布前如何压缩、打包项目;
    • 等等…
  • 现代的前端项目已经越来越复杂了:
    • 不会再是在 HTML 中引入几个 css 文件,引入几个编写的 js 文件或者第三方的 js 文件这么简单;
    • 比如 css 可能是使用 less、sass 等预处理器进行编写,我们需要将它们转成普通的 css 才能被浏览器解析;
    • 比如 JavaScript 代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖;
    • 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等);
  • 为了解决上面这些问题,我们需要再去学习一些工具:
    • 比如 babel、webpack、gulp,配置它们转换规则、打包依赖、热更新等等一些的内容;
    • 脚手架的出现,就是帮助我们解决这一系列问题的;

什么是脚手架

  • 传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;

1664621537937

  • 编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构
    • 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;
    • 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板;
    • 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可;
    • 这样也可以间接保证项目的基本机构一致性,方便后期的维护
  • 总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;

前端脚手架

  • 对于现在比较流行的三大框架都有属于自己的脚手架:
    • Vue 的脚手架:**@vue/cli**
    • Angular 的脚手架:@angular/cli
    • React 的脚手架:create-react-app
  • 它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好
  • 使用这些脚手架需要依赖什么呢?
    • 目前这些脚手架都是使用 node 编写的,并且都是基于 webpack 的;
    • 所以我们必须在自己的电脑上安装 node 环境;
  • 这里我们主要是学习 React,所以我们以 React 的脚手架工具:create-react-app 作为讲解;

安装 node

  • React 脚手架本身需要依赖 node,所以我们需要安装 node 环境:
    • 无论是 windows 还是 Mac OS,都可以通过 node 官网直接下载;
    • 官网地址:https://nodejs.org/en/download/
    • 注意:这里推荐大家下载 LTS(Long-term support )版本,是长期支持版本,会比较稳定;
  • 下载后,双击安装即可:
    • 安装过程中,会自动配置环境变量;
    • 安装时,会同时帮助我们安装 npm 管理工具;

1664631012799

创建 React 项目

  • 安装脚手架 : npm install create-react-app -g

  • 现在,我们就可以通过脚手架来创建 React 项目了。

  • 创建 React 项目的命令如下:

    • create-react-app 项目名称 或者 npx create-react-app 项目名称 , 后者无需安装脚手架, npx 指令会联网调用最新脚手架版本创建项目
      • 注意:项目名称不能包含大写字母
      • 另外还有更多创建项目的方式,可以参考 GitHub 的 readme
  • 创建完成后,进入对应的目录,执行命令,就可以将项目跑起来:

    • cd 01-test-react
    • **npm run start **

目录结构分析

1664631059744

了解 PWA

  • 整个目录结构都非常好理解,只是有一个 PWA 相关的概念:
    • PWA 全称 Progressive Web App,即渐进式 WEB 应用
    • 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用;
    • 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能;
    • 这种 Web 存在的形式,我们也称之为是 Web App;
  • PWA 解决了哪些问题呢?
    • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
    • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
    • 实现了消息推送;
    • 等等一系列类似于 Native App 相关的功能;
  • 更多 PWA 相关的知识,可以自行去学习更多;

脚手架中的 Webpack

  • React 脚手架默认是基于 Webpack 来开发的;
  • 但是,很奇怪:我们并没有在目录结构中看到任何 Webpack 相关的内容?
    • 原因是 React 脚手架将 Webpack 相关的配置隐藏起来了(其实从 Vue CLI3 开始,也是进行了隐藏);
  • 如果我们希望看到 Webpack 的配置信息,应该怎么来做呢?
    • 我们可以执行一个 package.json 文件中的一个脚本:"eject": "react-scripts eject" , 执行指令 : npm eject
    • 这个操作是不可逆的, 操作需谨慎,所以在执行过程中会给与我们提示;

1664631217363

文件结构删除

  • 通过脚手架创建完项目,很多同学还是会感觉目录结构过于复杂,所以我打算从零带着大家来编写代码。
  • 我们先将不需要的文件统统删掉:
    • 01 将 src 下的所有文件都删除
    • 02 将 public 文件下除了 favicon.ico 和 index.html 之外的文件都删除掉

1664631300449

  • 如果删除了目录中的 ./pulic/manifest.json 文件,运行时控制台报错
    • ./public/index.html 中将 <link *rel*="manifest" *href*="%PUBLIC_URL%/manifest.json" /> 删掉即可

开始书写代码

  • 在 src 目录下,创建一个 index.js 文件,因为这是 webpack 打包的入口。
  • 在 index.js 中开始编写 React 代码:
    • 我们会发现和写的代码是逻辑是一致的;
    • 只是在模块化开发中,我们需要手动的来导入 React、ReactDOM,因为它们都是在我们安装的模块中;
  • 如果我们不希望直接在 root.render 中编写过多的代码,就可以单独抽取一个组件 App.js:

index.js

1
2
3
4
5
6
7
import ReactDOM from 'react-dom/client'

import App from './App.jsx'

const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App></App>)

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
import HelloWorld from './Components/HelloWorld.jsx'

class App extends React.Component {
constructor() {
super()
this.state = {
message: 'Hello React',
}
}
render() {
const { message } = this.state
return (
<div>
<h2>{message}</h2>
<HelloWorld></HelloWorld>
</div>
)
}
}

export default App

./Components/HelloWorld.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'

class HelloWorld extends React.Component {
constructor() {
super()
this.state = {
message: '你好,师姐',
}
}
render() {
const { message } = this.state
return (
<div>
<h2>{message}</h2>
</div>
)
}
}

export default HelloWorld

组件化开发

什么是组件化开发

  • 组件化是一种分而治之的思想:
    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
    • 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
  • 我们需要通过组件化的思想来思考整个应用程序:
    • 我们将一个完整的页面分成很多个组件;
    • 每个组件都用于实现页面的一个功能块;
    • 而每一个组件又可以进行细分;
    • 而组件本身又可以在多个地方进行复用;

1664707966668

React的组件化

  • 组件化是 React 的核心思想,也是我们后续课程的重点,前面我们封装的 App 本身就是一个组件:
    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    • 任何的应用都会被抽象成一颗组件树。

1664708003911

React组件的分类

  • 组件化思想的应用:

    • 有了组件化的思想,我们在之后的开发中就要充分的利用它。
    • 尽可能的将页面拆分成一个个小的、可复用的组件。
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
  • React 的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:

    • 根据组件的定义方式,可以分为:函数组件 (Functional Component ) 和类组件 (Class Component);
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件 (Stateless Component ) 和有状态组件 (Stateful Component);
    • 根据组件的不同职责,可以分成:展示型组件 (Presentational Component) 和容器型组件 (Container Component);
  • 这些概念有很多重叠,但是他们最主要是关注数据逻辑和 UI 展示的分离:

    • 函数组件、无状态组件、展示型组件主要关注 UI 的展示;
    • 类组件、有状态组件、容器型组件主要关注数据逻辑;
  • 当然还有很多组件的其他概念:比如异步组件、高阶组件等,我们后续再学习。

类组件

类组件

  • 类组件的定义有如下要求:
    • 组件的名称是大写字符开头(无论类组件还是函数组件)
    • 类组件需要继承自 React.Component
    • 类组件必须实现 render 函数
  • 在 ES6 之前,可以通过 create-react-class 模块来定义类组件,但是目前官网建议我们使用 ES6 的 class 类定义。
  • 使用 class 定义一个组件:
    • constructor 是可选的,我们通常在 constructor 中初始化一些数据;
    • this.state 中维护的就是我们组件内部的数据;
    • render() 方法是 class 组件中唯一必须实现的方法;

./App.js 定义一个类组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类组件
import React, { Component } from 'react'

// 定义并分别暴露组件
export class App extends Component {
constructor(props) {
super(props)
this.state = {
message: '你好师姐',
}
}
render() {
// 从 state 中解构出需要的数据
const { message } = this.state
// 将数据渲染到页面
return <h2>{message}</h2>
}
}

// 默认暴露组件
export default App

index.js 引入 App.js 类组件并使用,无需注册

  • App.js 即为最大的容器组件
1
2
3
4
5
6
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

render函数的返回值

  • 当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
    • React 元素
      • 通常通过 JSX 创建。
      • 例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件;
      • 无论是 <div /> 还是<MyComponent />均为 React 元素。
    • 数组或 fragments:使得 render 方法可以返回多个元素。
    • Portals:可以渲染子节点到不同的 DOM 子树中。
    • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
    • 布尔类型或 null:什么都不渲染。

函数组件

  • 函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。
  • 函数组件有自己的特点(当然,后面我们会讲 hooks,就不一样了):
    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
    • this 关键字不能指向组件实例(因为没有组件实例);
    • 没有内部状态(state);
  • 我们来定义一个函数组件:

./Components/HelloReact.jsx 定义一个函数式组件 , 返回值跟 类组件的 render 函数的返回值一样

1
2
3
4
5
6
// 函数式组件
function HelloReact() {
return <h1>HelloReact,师姐,你好</h1>
}
export default HelloReact

App.jsx 引入函数式组件并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
import HelloReact from './Components/HelloReact.jsx'

class App extends React.Component {
constructor() {
super()
this.state = {
message: 'Hello React',
}
}
render() {
const { message } = this.state
return (
<div>
<h2>{message}</h2>
<HelloReact></HelloReact>
</div>
)
}
}

export default App

  • 在前面的学习中,我们主要讲解类组件,后面学习 Hooks 时,会针对函数式组件进行更多的学习。

组件生命周期

认识生命周期

  • 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
  • React 组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
  • 生命周期和生命周期函数的关系:
  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;
    • 比如装载阶段(Mount),组件第一次在 DOM 树中被渲染的过程;
    • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
    • 比如卸载过程(Unmount),组件从 DOM 树中被移除的过程;
  • React 内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
    • 比如实现 componentDidMount 函数:组件已经挂载到 DOM 上时,就会回调;
    • 比如实现 componentDidUpdate 函数:组件已经发生了更新时,就会回调;
    • 比如实现 componentWillUnmount 函数:组件即将被移除时,就会回调;
    • 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
  • 我们谈 React 生命周期时,主要谈的类组件的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过 hooks 来模拟一些生命周期的回调)

生命周期解析

  • 我们先来学习一下最基础、最常用的生命周期函数:

1664708339180

生命周期函数

构造器

render

组件挂载

  • Constructor
    • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
    • constructor 中通常只做两件事情:
      • 通过给 this.state 赋值对象来初始化内部的 state;
      • 为事件绑定实例(this);
  • componentDidMount
    • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
    • componentDidMount 中通常进行哪里操作呢?
      • 依赖于 DOM 的操作可以在这里进行;
      • 在此处发送网络请求就最好的地方;(官方建议)
      • 可以在此处添加一些订阅(会在 componentWillUnmount 取消订阅);

任意组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react'

// 定义并分别暴露组件
export class App extends Component {
constructor(props) {
super(props)
// 1. 构造器 constructor 会最先被调用
console.log('01 我是构造器中的执行的~')
this.state = {
message: '你好师姐',
}
}
render() {
// 2. render 方法紧随其后调用
console.log('02 我是 render 方法中执行的~')
const { message } = this.state
return <h2>{message}</h2>
}

// 3. 组件被渲染到 DOM 时 , 即被挂载到 DOM 时,该生命周期函数被调用
componentDidMount() {
console.log('03 componentDidMount')
}
}

// 默认暴露组件
export default App

组件更新

  • componentDidUpdate
    • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
    • 当组件更新后,可以在此处对 DOM 进行操作;
    • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { Component } from 'react'

// 定义并分别暴露组件
export class App extends Component {
constructor(props) {
super(props)
// 1. 构造器会最先被调用
console.log('01 我是构造器中的执行的~')
this.state = {
message: '你好师姐',
}
}

// 事件回调
btnClick = () => {
this.setState({
message: '你好,世界',
})
}

render() {
// 2. render 方法紧随其后调用
console.log('02 我是 render 方法中执行的~')
const { message } = this.state
return (
<div>
<h2
onClick={() => {
this.btnClick()
}}
>
{message}
</h2>
</div>
)
}

// 3. 组件被渲染到 DOM 时 , 即被挂载到 DOM 时,该生命周期函数被调用
componentDidMount() {
console.log('03 componentDidMount')
}

// 4. 组件的 DOM 更新时被调用, 数据更新,页面重新渲染, render 也会在该生命周期函数之前调用一次
componentDidUpdate() {
console.log('04 componentDidUpdate')
}
}

// 默认暴露组件
export default App

组件卸载

  • componentWillUnmount
    • componentWillUnmount() 会在组件卸载及销毁之前直接调用。
    • 在此方法中执行必要的清理操作;
    • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
1
2
3
4
5
// 5. 组件从 DOM 中卸载时调用 , 那个组件被卸载,该生命周期函数就写在那个组件中...
componentWillUnmount() {
console.log('componentWillUnmount')
}

不常用的生命周期

  • 除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
    • getDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 state;
    • getSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置);
    • shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
  • 另外,React 中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。
  • 更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
  • 或者可以查阅 : https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

1664708543095

1
2
3
4
5
// 通过返回值 true/false ,决定数据更新后是否要重新渲染页面
shouldComponentUpdate() {
return true
}

组件间通信

组件的嵌套

  • 组件之间存在嵌套关系:
    • 在之前的案例中,我们只是创建了一个组件 App;
    • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
    • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
    • 再将这些组件组合嵌套在一起,最终形成我们的应用程序;

1664708619829

  • 上面的嵌套逻辑如下,它们存在如下关系:
    • App 组件是 Header、Main、Footer 组件的父组件;
      • Main 组件是 Banner、ProductList 组件的父组件;

认识组件间通信

  • 在开发过程中,我们会经常遇到需要组件之间相互进行通信:

    • 比如 App 可能使用了多个 Header,每个地方的 Header 展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示;
    • 又比如我们在 Main 中一次性请求了 Banner 数据和 ProductList 数据,那么就需要传递给他们来进行展示;
    • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
    • 总之,在一个 React 项目中,组件之间的通信是非常重要的环节;
  • 父组件在展示子组件,可能会传递一些数据给子组件:

    • 父组件通过 属性=值 的形式来传递给子组件数据;
    • 子组件通过 props 参数获取父组件传递过来的数据;
  • 组件间通信父传子

父组件 : Main.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { PureComponent } from 'react'
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'

export class Main extends PureComponent {
constructor(props) {
super(props)
// 1. 准备的数据
this.state = {
banner: ['新歌曲', '新MV', '新歌单'],
productList: ['推荐商品', '热门商品', '流行商品'],
}
}
render() {
return (
<div>
<h2>Main</h2>
{/* 2. 将数据传递给子组件 */}
<MainBanner title="轮播图" banner={this.state.banner}></MainBanner>
<MainProductList title="商品列表" productList={this.state.productList}></MainProductList>
</div>
)
}
}

export default Main

子组件 : MainBanner.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from 'react'

export class MainBanner extends Component {
constructor(props) {
super(props)
console.log(props) // 也可以在构造器中获取到父组件传递来的数据 , 相当于 this.props = props
}
render() {
const { title, banner } = this.props // 3. 子组件从 props 中解构出需要的数据
return (
<div>
<h2>{title}</h2>
{/* 4. 根据数据渲染页面 */}
<ul>
{banner.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
)
}
}

export default MainBanner

子组件 MainProductList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { PureComponent } from 'react'

export default class MainProductList extends PureComponent {
render() {
const { title, productList } = this.props // 3. 子组件从 props 中解构出需要的数据
return (
<div>
{/* 4. 根据数据渲染页面 */}
<h2>{title}</h2>
<ul>
{productList.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
)
}
}

服务器动态数据展示

  • 需要安装 axios : npm install axios

父组件 : Main.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { PureComponent } from 'react'
import axios from 'axios'
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'

export class Main extends PureComponent {
constructor(props) {
super(props)
// 1. 数据初始化
this.state = {
banners: [],
productList: [],
}
}

// 2. 在组件挂载完毕时,发起网络请求获取数据
componentDidMount() {
axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
const banners = res.data.data.banner.list
const recommend = res.data.data.recommend.list
this.setState({
banners,
productList: recommend,
})
})
}

render() {
return (
<div>
<h2>Main</h2>
{/* 2. 将数据传递给子组件 */}
<MainBanner title="轮播图" banners={this.state.banners}></MainBanner>
<MainProductList title="商品列表" productList={this.state.productList}></MainProductList>
</div>
)
}
}

export default Main

子组件 : MainBanner.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'

export class MainBanner extends Component {
render() {
const { title, banners } = this.props // 3. 子组件从 props 中解构出需要的数据
return (
<div>
<h2>{title}</h2>
{/* 4. 根据数据渲染页面 */}
<ul>
{banners.map((item) => {
return <li key={item.acm}>{item.title}</li>
})}
</ul>
</div>
)
}
}

export default MainBanner

子组件 MainProductList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { PureComponent } from 'react'

export default class MainProductList extends PureComponent {
render() {
const { title, productList } = this.props // 3. 子组件从 props 中解构出需要的数据
return (
<div>
{/* 4. 根据数据渲染页面 */}
<h2>{title}</h2>
<ul>
{productList.map((item) => {
return <li key={item.acm}>{item.title}</li>
})}
</ul>
</div>
)
}
}

参数校验与默认值

  • 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
    • 当然,如果你项目中默认继承了 Flow 或者 TypeScript,那么直接就可以进行类型验证;
    • 但是,即使我们没有使用 Flow 或者 TypeScript,也可以通过 prop-types 库来进行参数验证;
  • 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
  • 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
    • 比如验证数组,并且数组中包含哪些元素;
    • 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型;
    • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
  • 如果没有传递,我们希望有默认值呢?
    • 我们使用 defaultProps 就可以了

父组件 : Main.jsx

1
2
3
<MainBanner banners={banners} title={'轮播图'}></MainBanner>
<MainBanner banners={banners}></MainBanner> {/* 未传递 title 给子组件,将会使用子组件的默认值*/}

子组件 : MainBanner.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component } from 'react'
import PropTypes from 'prop-types' // 0. 导入依赖,提供数据校验,脚手架已内置

export class MainBanner extends Component {
render() {
const { title, banners } = this.props // 3. 子组件从 props 中解构出需要的数据
return (
<div>
<h2>{title}</h2>
{/* 4. 根据数据渲染页面 */}
<ul>
{banners.map((item) => {
return <li key={item.acm}>{item.title}</li>
})}
</ul>
</div>
)
}
}

// 1. 数据类型校验
MainBanner.propTypes = {
banners: PropTypes.array.isRequired, // 要求传递一个数组,必须项
title: PropTypes.string, // 要求传递一个字符串
}

// 2. 默认值,父组件没有传递数据过来时,就会使用默认值
MainBanner.defaultProps = {
banners: [],
title: '默认标题',
}

export default MainBanner

  • 默认值的另一种扩展写法 :
1
2
3
4
5
6
7
8
9
10
11
12
13
export class MainBanner extends Component {
// 在 ES 高级中,也可以将默认值写到类中,作为类的静态属性,这是 ES 13 的新特性,目前还比较少使用该写法
// 即, 为该组件提供默认值
static defaultProps = {
banners: [],
title: '我是默认标题',
}

render() {
//...
}
}

子组件传递父组件

  • 某些情况,我们也需要子组件向父组件传递消息:

    • 在 vue 中是通过自定义事件来完成的;
    • 在 React 中同样是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
  • 我们这里来完成一个案例:

    1666498585648

    • 将计数器案例进行拆解;
    • 将按钮封装到子组件中:AddCounter;
    • AddCounter 发生点击事件,将内容传递到父组件中,修改 counter 的值;

父组件 : app.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { Component } from 'react'
import AddCounter from './components/AddCounter'

export class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 100, // 1. 数据初始化
}
}
// 2. 定义一个方法,用于接收子组件传来的数据,形参就是接收到的数据
changeCounter = (count) => {
this.setState({
counter: this.state.counter + count,// 6. 修改父组件中的数据
})
}
render() {
const { counter } = this.state
return (
<div>
<h2>当前计数:{counter}</h2>
{ /* 3. 传递一个函数给子组件,在函数中调用定义的方法,接收数据 */}
<AddCounter addClick={(count) => { this.changeCounter(count)}}></AddCounter>
</div>
)
}
}

export default App

子组件: ./Components/AddCounter.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'

export class AddCounter extends Component {
// 5. 在事件回调中调用父组件传递来的方法,并传递参数
addCount = (count) => {
this.props.addClick(count)
}
render() {
return (
<div>
{ /* 4. 给按钮添加点击事件并传递参数 */}
<button onClick={()=>{this.addCount(1)}}>+1</button>
<button onClick={()=>{this.addCount(5)}}>+5</button>
<button onClick={()=>{this.addCount(10)}}>+10</button>
</div>
)
}
}

export default AddCounter

  • 图解:

1671432799322

组件间通信案例

1666501397200

父组件 : app.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component } from 'react'
import TabControl from './components/TabControl'

export class App extends Component {
constructor(props) {
super(props)
this.state = {
titles: ['流行', '新款', '精选'], // 1. 数据初始化
tabIndex: 0,
}
}
// 2. 定义一个方法,传递给子组件,并接收来自子组件的数据
tabClick = (index) => {
this.setState({
tabIndex: index, // 8. 接收到子组件的传参,并修改状态
})
}
render() {
const { titles, tabIndex } = this.state
return (
<div className="app">
{/** 3. 渲染页面,并将数据和方法传递给子组件 */}
<TabControl
titles={titles}
tabClick={(index) => {
this.tabClick(index)
}}
></TabControl>
<h1>{titles[tabIndex]}</h1>
</div>
)
}
}

export default App

子组件 : ./Components/TabControl.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { Component } from 'react'
import './style.css'

export class TabControl extends Component {
constructor(props) {
super(props)
this.state = {
currentIndex: 0, // 4. 数据初始化
}
}
// 7. 在事件回调中,修改状态值,并调用父组件传递来的方法,并传参
itemClick = (index) => {
this.setState({ currentIndex: index })
this.props.tabClick(index)
}
render() {
const { titles } = this.props // 5. 接收来自父组件的数据
const { currentIndex } = this.state
return (
// 6. 根据数据渲染页面,并添加点击事件并传递参数,动态绑定类名
<div className="tab-control">
{titles.map((item, index) => {
return (
<div key={index} onClick={() => {this.itemClick(index)}}
className={`item ${index === currentIndex ? 'active' : ''}`}
>
{item}
</div>
)
})}
</div>
)
}
}

export default TabControl

对应的 CSS 样式: ./TabControl.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.tab-control {
display: flex;
align-items: center;
height: 40px;
text-align: center;
}
.tab-control .item {
flex: 1;
}
.tab-control .item.active {
color: red;
}
.tab-control .item.active .text {
padding: 3px;
border-bottom: 3px solid red;
}

  • 图解 :

1667714276858

插槽的实现

插槽的作用

  • 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div、span 等等这些元素。
  • 我们应该让使用者可以决定某一块区域到底存放什么内容。

image-20230312112213109

  • 这种需求在 Vue 当中有一个固定的做法是通过 slot 来完成的,React 呢?
  • React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
    • 组件的 children 子元素;
    • props 属性传递 React 元素;

children实现插槽

1666502946860

  • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

父组件 : app.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import NavBar from './Components/NavBar/NavBar.jsx'

export class App extends Component {
render() {
return (
<div>
<NavBar>
{/* 在子组件使用时,传递多个 DOM 结构,则在子组件中解构出来的 children 是一个数组,元素为传递的 DOM 结构 */}
{/* 如果只传递了一个 DOM 结构,则在子组件中解构出来的 children 就是传递的 DOM 结构,不再是一个数组*/}
<button>返回</button>
<h2>标题</h2>
<i>分享</i>
</NavBar>
</div>
)
}
}

export default App

子组件 : ./Components/NavBar/NavBar.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
import './NavBar.css'

export class NavBar extends Component {
render() {
// 从 props 中解构出 children , 类型为一个数组
const { children } = this.props
// 渲染页面
return (
<div className="nav-bar">
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
)
}
}

export default NavBar

对应的 CSS 样式: ./NavBar.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
* {
margin: 0;
padding: 0;
}
.nav-bar {
display: flex;
height: 40px;
text-align: center;
line-height: 40px;
}
.left,
.right {
width: 80px;
background-color: #f00;
}
.center {
flex: 1;
background-color: #f80;
}

props实现插槽

  • 通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
  • 另外一个种方案就是使用 props 实现,该方法使用较多:
    • 通过具体的属性名,可以让我们在传入和获取时更加的精准;

父组件: App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react'
import NavBar from './Components/NavBar/NavBar.jsx'

export class App extends Component {
constructor(props) {
super(props)
this.state = {
btn: <button>按钮</button>,
}
}
render() {
const { btn } = this.state
return (
<div>
{/** 将 DOM 结构作为数据传递给子组件 */}
<NavBar
leftSlot={btn}
centerSlot={<h2>标题</h2>}
rightSlot={<i>斜体</i>}
></NavBar>
</div>
)
}
}

export default App

子组件 : ./Components/NavBar/NavBar.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
import './NavBar.css'

export class NavBar extends Component {
render() {
// 从 props 中解构出需要的数据
const { leftSlot, centerSlot, rightSlot } = this.props
// 渲染页面
return (
<div className="nav-bar">
<div className="left">{leftSlot}</div>
<div className="center">{centerSlot}</div>
<div className="right">{rightSlot}</div>
</div>
)
}
}

export default NavBar

作用域插槽的实现

1671003519262

父组件 : ./App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { Component } from 'react'
import TabControl from './TabControl'

export class App extends Component {
constructor() {
super()
this.state = {
titles: ['流行', '新款', '精选'],
tabIndex: 0,
}
}

// 通过方法接收到子组件传递来的数据
tabClick(tabIndex) {
this.setState({ tabIndex })
}

// 通过方法接收到子组件传递来的数据,并控制渲染结构
getTabItem(item) {
if (item === '流行') {
return <span>{item}</span>
} else if (item === '新款') {
return <button>{item}</button>
} else {
return <i>{item}</i>
}
}

render() {
const { titles, tabIndex } = this.state

return (
<div className="app">
<TabControl titles={titles} tabClick={(i) => this.tabClick(i)} itemType={(item) => this.getTabItem(item)} />
<h1>{titles[tabIndex]}</h1>
</div>
)
}
}

export default App

子组件 ./TabControlindex.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from 'react'
import './style.css'

export class TabControl extends Component {
constructor() {
super()
this.state = {
currentIndex: 0,
}
}

itemClick(index) {
// 1.自己保存最新的index
this.setState({ currentIndex: index })
// 2.让父组件执行对应的函数
this.props.tabClick(index)
}

render() {
const { titles, itemType } = this.props
const { currentIndex } = this.state

return (
<div className="tab-control">
{titles.map((item, index) => {
return (
<div className={`item ${index === currentIndex ? 'active' : ''}`} key={item} onClick={(e) => this.itemClick(index)}>
{itemType(item)}
</div>
)
})}
</div>
)
}
}

export default TabControl

Context

Context 基本使用

  • 非父子组件数据的共享:
    • 在开发中,比较常见的数据传递方式是通过 props 属性自上而下(由父到子)进行传递。
    • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI 主题、用户登录状态、用户信息等)。
    • 如果我们在顶层的 App 中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
  • 我们实现一个一层层传递的案例:
    • 我这边顺便补充一个小的知识点:Spread Attributes
1
2
<Home {...info}></Home> {/* info 是一个对象 */}

  • 但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
    • React 提供了一个 API:Context
    • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
    • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;

./context/theme-context.js

1
2
3
4
5
6
7
import React from 'react'

// 01. 创建 context
const ThemeContext = React.createContext()

export default ThemeContext

提供数据的父组件 : App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
import Home from './Components/Home'

import ThemeContext from './context/theme-context'

export class App extends Component {
render() {
return (
<div>
{/* 02. 通过 ThemeContext.Provider 的 value 属性给后代组件提供数据 */}
<ThemeContext.Provider value={{ color: 'red', size: '30' }}>
<Home></Home>
</ThemeContext.Provider>
</div>
)
}
}

export default App

需要数据的后代组件: HomeInfo.jsx , 该组件是上面使用的 home 组件的后代组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react'
import ThemeContext from '../context/theme-context'

export class HomeInfo extends Component {
render() {
// 04. 通过 this.context 获取到数据
console.log(this.context)
return <div>HomeInfo :{this.context.color}</div>
}
}

// 03. 在需要数据的地方,设置组件的 contextType 为某一个 context
HomeInfo.contextType = ThemeContext

export default HomeInfo

  • 在函数式子组件中获取到共享的数据 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import ThemeContext from '../context/theme-context'

// 函数式组件中使用 Context 共享的数据
function HomeBanner() {
return (
<div>
<h2>HomeBanner</h2>
<ThemeContext.Consumer>
{(value) => {
return <h2>{value.color}</h2>
}}
</ThemeContext.Consumer>
</div>
)
}

export default HomeBanner

Context相关API

  • React.createContext

    • 创建一个需要共享的 Context 对象:
    • 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 context 值;
    • defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值
  • Context.Provider

    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
    • Provider 接收一个 value 属性,传递给消费组件;
    • 一个 Provider 可以和多个消费组件有对应关系;
    • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
    • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
  • Class.contextType

    • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
    • 这能让你使用 this.context 来消费最近 Context 上的那个值;
    • 你可以在任何生命周期中访问到它,包括 render 函数中;
  • Context.Consumer

    • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
    • 这里需要 函数作为子元素(function as child)这种做法;
    • 这个函数接收当前的 context 值,返回一个 React 节点;

Context的默认值

  • 什么时候使用默认值 defaultValue 呢?
1
2
3
4
5
6
7
import React from 'react'

// 01. 创建 context,并提供一个默认值
const ThemeContext = React.createContext({ color: 'blue', size: "10" })

export default ThemeContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import Home from './Components/Home'

import ThemeContext from './context/theme-context'

export class App extends Component {
render() {
return (
<div>
{/* 02. 通过 ThemeContext.Provider 的 value 属性给后代组件提供数据 */}
<ThemeContext.Provider value={{ color: 'red', size: '30' }}>
<Home></Home>
</ThemeContext.Provider>
<Home></Home>{/** 该组件没有在 ThemeContext.Provider 组件中,将使用默认值 */}
</div>
)
}
}

export default App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react'
import ThemeContext from '../context/theme-context'

export class HomeInfo extends Component {
render() {
// 04. 通过 this.context 获取到数据
console.log('111', this.context) // {color: 'blue', size: "10"} , 获取到的是默认值
return <div>HomeInfo :{this.context.color}</div>
}
}

// 03. 在需要数据的地方,设置组件的 contextType 为某一个 context
HomeInfo.contextType = ThemeContext

export default HomeInfo

  • 什么时候使用 Context.Consumer 呢?
    • 当使用 value 的组件是一个函数式组件时;
    • 当组件中需要使用多个 Context 时;

全局事件总线

  • 安装依赖 : npm install hy-event-store

创建实例

1
2
3
4
5
6
import { HYEventBus } from 'hy-event-store'

const eventBus = new HYEventBus()

export default eventBus

任意组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react'
import eventBus from '../utils/event-bus'

export class HomeInfo extends Component {
btnClick = () => {
eventBus.emit('bannerPrev', 'why', 18, 1.88) // 发射一个自定义事件,并携带参数
}
render() {
return (
<div>
<h2>HomeInfo</h2>
<button onClick={() => { this.btnClick() }}> 按钮 </button>
</div>
)
}
}

export default HomeInfo

任意组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { Component } from 'react'
import Home from './Components/Home'
import eventBus from './utils/event-bus'

export class App extends Component {
// 组件挂载
componentDidMount() {
eventBus.on('bannerPrev', this.bannerPrevClick) // 监听事件 事件回调
}

// 事件回调
bannerPrevClick = (name, age, height) => {
console.log('app中监听到 bannerPrev ', name, age, height) // 获取到传递来的数据
}

// 组件卸载
componentWillUnmount() {
eventBus.off('bannerPrev', this.bannerPrevClick) // 移除监听 要移除的事件回调
}

render() {
return (
<div>
<Home></Home>
</div>
)
}
}

export default App

setState(难点)

为什么使用setState

  • 开发中我们并不能直接通过修改 state 的值来让界面发生更新:
    • 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化;
    • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化;
    • 我们必须通过 setState 来告知 React 数据已经发生了变化
  • 疑惑:在组件中并没有实现 setState 的方法,为什么可以调用呢?
    • 原因很简单,setState 方法是从 Component 中继承过来的
  • 对应的源码 :

1666529986004

setState异步更新

  • setState 的更新是异步的?
    • 最终打印结果是 Hello World;
    • 可见 setState 是异步的操作,我们并不能在执行完 setState 之后立马拿到最新的 state 的结果
  • 为什么 setState 设计为异步呢?
  • 我对其回答做一个简单的总结:
  • setState 设计为异步,可以显著的提升性能
    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的
    • 最好的办法应该是获取到多个更新,之后进行批量更新
      • 即在一个回调中,多次调用 setState 方法,会将每个 setState 加入到任务队列中,再统一进行批量更新,数据只会更新一次
        • 如果 setState 是同步的,则在一个回调中多次调用,则会多次执行 render 方法进行多次渲染,但实际并不是这样
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
    • state 和 props 不能保持一致性,会在开发中产生很多的问题;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { Component } from 'react'

export class App extends Component {
constructor(props) {
super(props)
this.state = {
message: '你好师姐',
count: 0,
}
}

btnClick = () => {
// 1. 给 setState 传递一个对象,会将属性与 state 中的合并,同名覆盖 ,底层是调用 Object.assign(this.state,newState)
this.setState({
message: '你好,世界',
}, () => { console.log(this.state.message)}) // 你好,世界 , 获取到的是新值

// 2. 给 setState 传递一个回调函数,新的 state 由函数的返回值决定,同样是合并到旧 state 中,同名覆盖,调用 Object.assign
// 2.1 可以在回调中对新的 state 编写对应的业务逻辑
// 2.2 当前的回调可以访问到之前旧的 state 和 props
this.setState((state, props) => {
console.log(state.message) // 你好师姐
return {
count: 1,
}
})

// 3. setState 是异步调用的,执行该行时,state 还没有更新数据
// 如果希望在 state 数据更新之后获取到新的数据,则可以给 setState 传递第二个参数,该参数为一个函数,state 更新完后会自己调用
console.log(this.state.message) // 你好师姐 , 获取到的是旧值
}

render() {
return (
<div>
<button onClick={() => {this.btnClick()}}>按钮</button>
</div>
)
}
}

export default App

获取异步的结果

  • 那么如何可以获取到更新后的值呢?
  • setState 的回调
    • setState 接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
    • 格式如下:setState (partialState, callback)
    • 代码见上一小节

setState的异步

  • React18 之前 , 其实分成两种情况:

    • 在组件生命周期或 React 合成事件中,setState 是异步;
    • 在 setTimeout 或者原生 dom 事件中,setState 是同步;
    • 在 react 18 之前 , promise , setTimeout, 原生事件绑定中, setSate 是同步的
      • 但是到了 react 18 后,这些都是异步的,都会进行批处理
  • 验证一:在 setTimeout 中的更新:

1
2
3
4
5
6
7
8
9
btnClick = () => {
// 在react18之前, setTimeout 中 setState 操作,是同步操作
// 在react18之后,setTimeout 中 setState 异步操作(批处理)
setTimeout(() => {
this.setState({ message: '你好,李银河' })
console.log(this.state.message) // 你好师姐
}, 0)
}

  • 验证二:原生 DOM 事件:
1
2
// ...

  • React18 之后,默认所有的操作都被放到了批处理中(异步处理)。如果希望代码可以同步会拿到,则需要执行特殊的 flushSync 操作:
1
2
3
4
5
6
7
8
9
10
11
import { flushSync } from 'react-dom'

btnClick = () => {
setTimeout(() => {
flushSync(() => {
this.setState({ message: '你好,李银河' })
})
console.log(this.state.message) // 你好,李银河 ,此时 setState 是同步的
}, 0)
}

React的更新机制(难点)

React更新流程

  • 我们在前面已经学习 React 的渲染流程:

1666522719562

  • 那么 React 的更新流程呢?

1666522755713

  • React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树。

  • React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI:

    • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n³),其中 n 是树中元素的数量;
    • https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
    • 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
    • 这个开销太过昂贵了,React 的更新性能会变得非常低效;
  • 于是,React 对这个算法进行了优化,将其优化成了 O(n),如何优化的呢?

    • 同层节点之间相互比较,不会跨节点比较;
    • 不同类型的节点,产生不同的树结构;
    • 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定;

keys的优化

  • 我们在前面遍历列表时,总是会提示一个警告,让我们加入一个 key 属性

  • 方式一:在最后位置插入数据

    • 这种情况,有无 key 意义并不大
  • 方式二:在前面插入数据

    • 这种做法,在没有 key 的情况下,所有的 li 都需要进行修改;
  • 当子元素(这里的 li )拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

    • 在下面这种场景下,key 为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改;
    • 将 key 为 333 的元素插入到最前面的位置即可;
  • key 的注意事项:

    • key 应该是唯一的;
    • key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字);
    • 使用 index 作为 key,对性能是没有优化的;

render函数的优化

  • 我们使用之前的一个嵌套案例:
    • 在 App 中,我们增加了一个计数器的代码;
    • 当点击 +1 时,会重新调用 App 的 render 函数;
    • 而当 App 的 render 函数被调用时,所有的子组件的 render 函数都会被重新调用;
  • 那么,我们可以思考一下,在以后的开发中,我们只要是修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法,性能必然是很低的:
    • 事实上,很多的组件没有必须要重新 render;
    • 它们调用 render 应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的 render 方法;
  • 如何来控制 render 方法是否被调用呢?
    • 通过 shouldComponentUpdate 方法即可;

任意类组件

1
2
3
4
5
6
7
8
9
// 生命周期函数
shouldComponentUpdate(newProps, newSate) {
// 通过新旧的 state 值, props 值进行对比,决定 render 方法是否需要重新执行,性能优化
if (this.state.message !== newSate.message || this.props.message !== newProps.message) {
return true // 重新渲染
}
return false // 不重新渲染
}

  • 以下是对该生命周期函数的详解

SCU生命周期方法

  • React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU ),这个方法接受参数,并且需要有返回值:

  • 该方法有两个参数:

    • 参数一:nextProps 修改之后,最新的 props 属性
    • 参数二:nextState 修改之后,最新的 state 属性
  • 该方法返回值是一个 boolean 类型:

    • 返回值为 true,那么就需要调用 render 方法;
    • 返回值为 false,那么久不需要调用 render 方法;
    • 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;
  • 比如我们在 App 中增加一个 message 属性:

    • jsx 中并没有依赖这个 message,那么它的改变不应该引起重新渲染;
    • 但是因为 render 监听到 state 的改变,就会重新 render,所以最后 render 方法还是被重新调用了;

PureComponent

  • 如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
    • 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
    • props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;
  • 事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢?
    • 将 class 继承自 PureComponent。

任意类组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { PureComponent } from 'react'

// 类组件在创建组件时,继承 PureComponent , 则内部会自动判断 state 和 props 有没有改变
// 如果没有改变,就不会重新渲染该组件
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
message: '你好师姐',
}
}

render() {
return (
<div>
<button>按钮</button>
</div>
)
}
}

export default App

shallowEqual方法

  • 这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个 shallowEqual 就是进行浅层比较:

1666590254588

高阶组件memo

  • 目前我们是针对类组件可以使用 PureComponent,那么函数式组件呢?
    • 事实上函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的
  • 我们需要使用一个高阶组件 memo:
    • 我们将之前的 Header、Banner、ProductList 都通过 memo 函数进行一层包裹;
    • Footer 没有使用 memo 函数进行包裹;
    • 最终的效果是,当 counter 发生改变时,Header、Banner、ProductList 的函数不会重新执行;
    • 而 Footer 的函数会被重新执行;

任意函数组件

1
2
3
4
5
6
7
8
9
10
11
12
import { memo } from 'react'

// 某一个子组件,函数式组件
// 该子组件所依赖的 message 数据来自 props
// 如果父组件中 message 没有被改变,则该函数式组件不会被重新渲染
const Profile = memo(function (props) {
console.log('profile render')
return <h2>Profile:{props.message}</h2>
})

export default Profile

数据的不可变

  • 对于 state 中,数组或对象这些引用类型的数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
movies: ['流浪地球', '红海行动', '战狼2'],
}
}

btnClick = () => {
// 浅拷贝,产生一个全新的数组/对象
const newMovies = [...this.state.movies, '点爆木星'] // 浅拷贝,数组展开

// 对于数组或对象的多层结构的数据改变,也需要先进行浅拷贝,再更新深层次的数据
// books[index].count++ 仅举例

// 更新 state
this.setState({
movies: newMovies,
})
}
render() {
return (
<div>
<button onClick={() => {this.btnClick() }}> 按钮 </button>
</div>
)
}
}

export default App

其他高级内容

如何使用ref

  • 在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 原生,但是某些特殊的情况,确实需要获取到 DOM 进行某些操作:
    • 管理焦点,文本选择或媒体播放;
    • 触发强制动画;
    • 集成第三方 DOM 库;
    • 我们可以通过 refs 获取 DOM;
  • 如何创建 refs 来获取对应的 DOM 呢?目前有三种方式:
  • 方式一:传入字符串
    • 使用时通过 this.refs 传入的字符串格式获取对应的元素;
  • 方式二:传入一个对象
    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个 current 属性就是对应的元素;
  • 方式三:传入一个函数
    • 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存;
    • 使用时,直接拿到之前保存的元素对象即可;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component, createRef } from 'react'

export class App extends Component {
constructor(props) {
super(props)
this.titleRef = createRef() // 1. 创建一个 ref 对象
this.titleEl = null
}
getNativeDOM = () => {
// 方式一 : 只需要在 react 元素或组件上绑定一个 ref="why" 属性 ,能用,但是已经不推荐使用
// console.log(this.refs.why)

// 方式二: 提前用 createRef 创建一个 ref 对象,并绑定到元素上,推荐使用
console.log(this.titleRef.current)

// 方式三 : 给元素的 ref 属性传递一个函数,该函数的形参就是该 DOM 元素
console.log(this.titleEl)
}
render() {
return (
<div>
<h2 ref="why">App</h2>
{/** 2. 将创建的 ref 对象绑定到元素上 */}
<h2 ref={this.titleRef}>你好,李银河</h2>
<h2 ref={(el) => { this.titleEl = el}}>你好,师姐</h2>
<button onClick={() => { this.getNativeDOM() }}> 按钮 </button>
</div>
)
}
}

export default App

ref获取类组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component, createRef } from 'react'

// 子组件
export class HelloWorld extends Component {
test() {
console.log('test~~~~~')
}
render() {
return <h2>HelloWorld</h2>
}
}

// 父组件
export class App extends Component {
constructor(props) {
super(props)
this.hwRef = createRef()
}

// 事件回调
getComponent = () => {
console.log(this.hwRef.current)
this.hwRef.current.test() // 调用子组件中的实例方法
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef}></HelloWorld>
<button onClick={() => { this.getComponent() }}> 获取组件实例</button>
</div>
)
}
}

export default App

ref获取函数组件的DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component, createRef, forwardRef } from 'react'

// 函数式子组件
const HelloWorld = forwardRef(function (props, ref) {
return (
<div>
<h2 ref={ref}>师姐,你好</h2>
</div>
)
})

// 父组件
export class App extends Component {
constructor(props) {
super(props)
this.hwRef = createRef()
}

// 事件回调
getComponent = () => {
console.log(this.hwRef.current) // <h2>师姐,你好</h2>
}
render() {
return (
<div>
<HelloWorld ref={this.hwRef}></HelloWorld>
<button onClick={() => { this.getComponent()}}> 获取函数组件 DOM </button>
</div>
)
}
}

export default App

ref的类型

  • ref 的值根据节点的类型而有所不同:
    • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
    • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
    • 你不能在函数组件上使用 ref 属性,因为他们没有实例;
  • 这里我们演示一下 ref 引用一个 class 组件对象:

1666590499139

  • 函数式组件是没有实例的,所以无法通过 ref 获取他们的实例:
    • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
    • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;
    • 示例代码见上一节

受控组件

  • 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state。
  • 比如下面的HTML表单元素:
    • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
    • 在React中,并没有禁止这个行为,它依然是有效的;
    • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
    • 实现这种效果的标准方式是使用“受控组件”;

1666591399902

常见的受控组件

  • 在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。
  • 而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
    • 我们将两者结合起来,使 React 的 state 成为“唯一数据源”;
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
    • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
  • 由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。
  • 由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

1666591479283

  • textarea 标签
    • texteare 标签和 input 比较相似:
  • select 标签
    • select 标签的使用也非常简单,只是它不需要通过 selected 属性来控制哪一个被选中,它可以匹配 state 的 value 来选中。
  • 处理多个输入
    • 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
    • 这里我们可以使用 ES6 的一个语法:计算属性名(Computed property names)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { Component } from 'react'

export class App extends Component {
constructor(props) {
super(props)
this.state = {
username: 'coderwhy',
}
}
// 事件回调
inputChange = (event) => {
console.log(event.target.value)
this.setState({
username: event.target.value,
})
}
// 渲染页面
render() {
const { username } = this.state
return (
<div>
<input type="text" value={username} onChange={(e) => {this.inputChange(e)}}/>
<h2>{username}</h2>
</div>
)
}
}

export default App

文本框

  • 01 将 state 中的变量绑定到输入框的 value 属性
  • 02 绑定 onChange 事件,在回调中修改 state 中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
username: '',
password: '',
}
}

// 事件回调
handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()

// 2.获取到所有的表单数据, 对数据进行组件
console.log('获取所有的输入内容')
console.log(this.state.username, this.state.password)

// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}

// handleUsernameChange(event) {
// this.setState({ username: event.target.value })
// }

// handlePasswordChange(event) {
// this.setState({ password: event.target.value })
// }

// 事件回调
handleInputChange(event) {
this.setState({
[event.target.name]: event.target.value,
})
}

render() {
const { username, password } = this.state
return (
<div>
<form onSubmit={(e) => this.handleSubmitClick(e)}>
{/* 用户名和密码 */}
<label htmlFor="username">
用户名:<input id="username" type="text" name="username" value={username} onChange={(e) => this.handleInputChange(e)} />
</label>
<label htmlFor="password">
密码:<input id="password" type="password" name="password" value={password} onChange={(e) => this.handleInputChange(e)} />
</label>

<button type="submit">注册</button>
</form>
</div>
)
}
}

export default App

文本域

  • 01 与上面的同理 , 将 state 中的变量绑定到文本域的 value 属性
  • 02 绑定 onChange 事件,在回调中修改 state 中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 01 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom/client'

// 02 创建组件
class App extends React.Component {
state = {
desc: '',
}
handleDesc = (e) => {
this.setState({
desc: e.target.value,
})
}

render() {
return (
<div>
描述:<textarea value={this.state.desc} onChange={this.handleDesc}></textarea>
</div>
)
}
}

// 03 渲染react元素
const root = ReactDOM.createRoot(document.getElementById('root')) // 指定渲染到那个容器
root.render(<App></App>) // 渲染组件到页面

select 单选

  • 01 与上面的同理 , 将 state 中的变量绑定到下拉框的 value 属性
  • 02 绑定 onChange 事件,在回调中修改 state 中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
fruit: 'orange',
}
}

handleFruitChange(event) {
const options = Array.from(event.target.selectedOptions)
const values = options.map((item) => item.value)
this.setState({ fruit: values })

// 额外补充: Array.from(可迭代对象)
// Array.from(arguments)
const values2 = Array.from(event.target.selectedOptions, (item) => item.value)
console.log(values2)
}

render() {
const { fruit } = this.state
return (
<div>
<form>
{/* select */}
<select value={fruit} onChange={(e) => this.handleFruitChange(e)}>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
</form>
</div>
)
}
}

export default App

select 多选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
fruit: ['orange'],
}
}

// 事件回调
handleFruitChange(event) {
const options = Array.from(event.target.selectedOptions)
const values = options.map((item) => item.value)
this.setState({ fruit: values })

// 额外补充: Array.from(可迭代对象)
// Array.from(arguments)
const values2 = Array.from(event.target.selectedOptions, (item) => item.value)
console.log(values2)
}

render() {
const { fruit } = this.state
return (
<div>
<form>
{/* select */}
<select value={fruit} onChange={(e) => this.handleFruitChange(e)} multiple>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<div>
<button type="submit">注册</button>
</div>
</form>
</div>
)
}
}

export default App

复选框单选

  • 01 与上面的同理 , 将 state 中的变量绑定到复选框的 checked 属性
  • 02 绑定 onChange 事件,在回调中修改 state 中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
isAgree: false,
}
}

// 事件回调
handleAgreeChange(event) {
this.setState({ isAgree: event.target.checked })
}
render() {
const { isAgree } = this.state
return (
<div>
<form>
{/* checkbox单选 */}
<label htmlFor="agree">
<input id="agree" type="checkbox" checked={isAgree} onChange={(e) => this.handleAgreeChange(e)} />
同意协议
</label>
</form>
</div>
)
}
}

export default App

复选框多选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
hobbies: [
{ value: 'sing', text: '唱', isChecked: false },
{ value: 'dance', text: '跳', isChecked: false },
{ value: 'rap', text: 'rap', isChecked: false },
],
}
}

// form 表单的提交事件回调
handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()

// 2.获取到所有的表单数据, 对数据进行组件
const hobbies = this.state.hobbies.filter((item) => item.isChecked).map((item) => item.value)
console.log('获取爱好: ', hobbies)

// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}

// 事件回调
handleHobbiesChange(event, index) {
const hobbies = [...this.state.hobbies]
hobbies[index].isChecked = event.target.checked
this.setState({ hobbies })
}

render() {
const { hobbies } = this.state

return (
<div>
<form onSubmit={(e) => this.handleSubmitClick(e)}>
{/* checkbox多选 */}
<div>
您的爱好:
{hobbies.map((item, index) => {
return (
<label htmlFor={item.value} key={item.value}>
<input type="checkbox" id={item.value} checked={item.isChecked} onChange={(e) => this.handleHobbiesChange(e, index)} />
<span>{item.text}</span>
</label>
)
})}
</div>
<div>
<button type="submit">注册</button>
</div>
</form>
</div>
)
}
}

export default App

对多表单回调优化

  • 问题:每个表单元素都需要一个单独的事件处理程序,处理太繁琐

  • 优化:使用一个事件处理程序处理多个表单元素

  • 步骤:

    • 给表单元素添加 name 属性,名称与 state 属性名相同
    • 根据表单元素类型获取对应的值
    • 在事件处理程序中通过[name]修改对应的 state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 01 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom/client'

// 02 创建组件
class App extends React.Component {
state = {
username: '',
desc: '',
city: '2',
isSingle: true,
}

// 事件回调
handleChange = (e) => {
// 通过 type 属性判断表单的类型
let { name, type, value, checked } = e.target
this.setState({
// 在 ES6 之前,对象的属性名必须指定,属性值可以是任意表达式
// 在 ES6 之后,对象的属性名和和属性值都可以是任意表达式,但是表达式作为属性使用,必须放在[]中
[name]: type === 'checkbox' ? checked : value, // [name] : 动态属性 , 该 name 值来自表单的属性,且与 state 中的变量同名
})
}

render() {
return (
<div>
姓名:
<input type="text" name="username" value={this.state.username} onChange={this.handleChange} />
<br />
描述:
<textarea name="desc" value={this.state.desc} onChange={this.handleChange}></textarea>
<br />
城市:
<select name="city" value={this.state.city} onChange={this.handleChange}>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
</select>
<br />
是否单身:
<input type="checkbox" name="isSingle" checked={this.state.isSingle} onChange={this.handleChange} />
</div>
)
}
}

// 03 渲染react元素
const root = ReactDOM.createRoot(document.getElementById('root')) // 指定渲染到那个容器
root.render(<App></App>) // 渲染组件到页面

非受控组件

  • React 推荐大多数情况下使用受控组件 来处理表单数据:

    • 一个受控组件中,表单数据是由 React 组件来管理的;
    • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
  • 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。

    • 我们来进行一个简单的演练:
    • 使用 ref 来获取 input 元素;
  • 在非受控组件中通常使用 defaultValue 来设置默认值;

    • 同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked,<select> <textarea> 支持 defaultValue。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { createRef, PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
intro: '哈哈哈',
}
this.introRef = createRef()
}

handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()

// 2.获取到所有的表单数据, 对数据进行组件
console.log('获取结果:', this.introRef.current.value)

// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}

render() {
const { intro } = this.state
return (
<div>
<form onSubmit={(e) => this.handleSubmitClick(e)}>
{/* 5.非受控组件 */}
<input type="text" defaultValue={intro} ref={this.introRef} />
<div>
<button type="submit">注册</button>
</div>
</form>
</div>
)
}
}

export default App

高阶组件

高阶函数

  • 什么是高阶组件呢?
    • 相信很多同学都知道(听说过?),也用过 高阶函数
    • 它们非常相似,所以我们可以先来回顾一下什么是 高阶函数。
  • 高阶函数的维基百科定义:至少满足以下条件之一:
    • 接受一个或多个函数作为输入;
    • 输出一个函数;
  • JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。
  • 那么什么是高阶组件呢?
    • 高阶组件的英文是 Higher-Order Components,简称为 HOC;
    • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数
  • 我们可以进行如下的解析:
    • 首先,高阶组件 本身不是一个组件,而是一个函数;
    • 其次,这个函数的参数是一个组件,返回值也是一个组件;

高阶组件的定义

  • 高阶组件的编写和调用过程类似于这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { PureComponent } from 'react'

// 定义一个高阶组件
function hoc(Cpn) {
// 1.定义类组件
class NewCpn extends PureComponent {
render() {
return <Cpn name="why"/>
}
}
return NewCpn

// 定义函数组件
// function NewCpn2(props) {

// }
// return NewCpn2
}

class HelloWorld extends PureComponent {
render() {
return <h1>Hello World</h1>
}
}

const HelloWorldHOC = hoc(HelloWorld)

export class App extends PureComponent {
render() {
return (
<div>
<HelloWorldHOC/>
</div>
)
}
}

export default App

  • 组件的名称问题:
    • 在 ES6 中,类表达式中类名是可以省略的;
    • 组件的名称都可以通过 displayName 来修改;
  • 高阶组件并不是 React API 的一部分,它是基于 React 的组合特性而形成的设计模式;
  • 高阶组件在一些 React 第三方库中非常常见:
    • 比如 redux 中的 connect;(后续会讲到)
    • 比如 react-router 中的 withRouter;(后续会讲到)

应用一:props的增强

  • 修改原有代码的情况下,添加新的props
  • 利用高阶组件来共享Context

./enhanced_props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { PureComponent } from 'react'

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props)
this.state = {
userInfo: {
name: 'coderwhy',
level: 99,
},
}
}

render() {
return <OriginComponent {...this.props} {...this.state.userInfo} />
}
}
return NewComponent
}

export default enhancedUserInfo

./About.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { PureComponent } from 'react'
import enhancedUserInfo from '../enhanced_props'

export class About extends PureComponent {
render() {
return (
<div>About: {this.props.name}</div>
)
}
}

export default enhancedUserInfo(About)

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { PureComponent } from 'react'
import enhancedUserInfo from './enhanced_props'
import About from './components/About'

const Home = enhancedUserInfo(function(props) {
return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})

const Profile = enhancedUserInfo(function(props) {
return <h1>Profile: {props.name}-{props.level}</h1>
})

const HelloFriend = enhancedUserInfo(function(props) {
return <h1>HelloFriend: {props.name}-{props.level}</h1>
})

export class App extends PureComponent {
render() {
return (
<div>
<Home banners={["轮播1", "轮播2"]}/>
<Profile/>
<HelloFriend/>
<About/>
</div>
)
}
}

export default App

应用二:Context

./context/theme_context.js

1
2
3
4
5
6
import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext

../hoc/with_theme.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import ThemeContext from '../context/theme_context'

function withTheme(OriginComponment) {
return (props) => {
return (
<ThemeContext.Consumer>
{(value) => {
return <OriginComponment {...value} {...props} />{/* 3. 给传递进来的子组件注入数据 */}
}}
</ThemeContext.Consumer>
)
}
}

export default withTheme

./components/Product.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { PureComponent } from 'react'
import withTheme from '../hoc/with_theme'

export class Product extends PureComponent {
render() {
const { color, size } = this.props
return (
<div>
<h2>Product: {color}-{size}</h2>
</div>
)
}
}

export default withTheme(Product) // 2. 将子组件传递给高阶组件进行处理

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './components/Product'

export class App extends PureComponent {
render() {
return (
<div>
{/* 1. 为后代组件提供数据 */}
<ThemeContext.Provider value={{ color: 'red', size: 30 }}>
<Product />
</ThemeContext.Provider>
</div>
)
}
}

export default App

应用三:登录鉴权

  • 在开发中,我们可能遇到这样的场景:
    • 某些页面是必须用户登录成功才能进行进入;
    • 如果用户没有登录成功,那么直接跳转到登录页面;
  • 这个时候,我们就可以使用高阶组件来完成鉴权操作:

../hoc/login_auth.js

1
2
3
4
5
6
7
8
9
10
11
12
13
function loginAuth(OriginComponent) {
return (props) => {
const token = localStorage.getItem('token') // 读取 token
if (token) {
return <OriginComponent {...props} />
} else {
return <h2>请先登录, 再进行跳转到对应的页面中</h2>
}
}
}

export default loginAuth

./components/Cart.jsx

1
2
3
4
5
6
7
8
9
10
11
import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'

export class Cart extends PureComponent {
render() {
return <h2>Cart Page</h2>
}
}

export default loginAuth(Cart) // 将当前组件传入高阶组件进行处理

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { PureComponent } from 'react'
import Cart from './components/Cart'

export class App extends PureComponent {
constructor() {
super()
}

// 按钮点击事件回调
loginClick() {
localStorage.setItem('token', 'coderwhy') // 存储 token
this.forceUpdate() // 强制更新组件
}

render() {
return (
<div>
App
<button onClick={(e) => this.loginClick()}>登录</button>
<Cart />
</div>
)
}
}

export default App

应用四:生命周期劫持

  • 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

../hoc/log_render_time.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { PureComponent } from "react";

function logRenderTime(OriginComponent) {
return class extends PureComponent {
UNSAFE_componentWillMount() {
this.beginTime = new Date().getTime()
}

componentDidMount() {
this.endTime = new Date().getTime()
const interval = this.endTime - this.beginTime
console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`)
}

render() {
return <OriginComponent {...this.props}/>
}
}
}

export default logRenderTime

./components/Detail.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { PureComponent } from 'react'
import logRenderTime from '../hoc/log_render_time'

export class Detail extends PureComponent {
render() {
return (
<div>
<h2>Detail Page</h2>
<ul>
<li>数据列表1</li>
<li>数据列表2</li>
<li>数据列表3</li>
<li>数据列表4</li>
<li>数据列表5</li>
<li>数据列表6</li>
<li>数据列表7</li>
<li>数据列表8</li>
<li>数据列表9</li>
<li>数据列表10</li>
</ul>
</div>
)
}
}

export default logRenderTime(Detail)

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { PureComponent } from 'react'
import Detail from './components/Detail'

export class App extends PureComponent {
render() {
return (
<div>
<Detail />
</div>
)
}
}

export default App

高阶组件的意义

  • 我们会发现利用高阶组件可以针对某些 React 代码进行更加优雅的处理。
  • 其实早期的 React 有提供组件之间的一种复用方式是 mixin,目前已经不再建议使用:
    • Mixin 可能会相互依赖,相互耦合,不利于代码维护;
    • 不同的 Mixin 中的方法可能会相互冲突;
    • Mixin 非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;
  • 当然,HOC 也有自己的一些缺陷:
    • HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
    • HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;
  • Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题
    • 比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;
  • 后续我们还会专门来学习 hooks 相关的知识,敬请期待;

ref的转发

  • 在前面我们学习 ref 时讲过,ref 不能应用于函数式组件:
    • 因为函数式组件没有实例,所以不能获取到对应的组件对象
  • 但是,在开发中我们可能想要获取函数式组件中某个元素的 DOM,这个时候我们应该如何操作呢?
    • 方式一:直接传入 ref 属性(错误的做法)
    • 方式二:通过 forwardRef 高阶函数;

1671527308472

Portals

Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
    • 第二个参数(container)是一个 DOM 元素;

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"

export class App extends PureComponent {
render() {
return (
<div className='app'>
<h1>App H1</h1>
{
createPortal(<h2>App H2</h2>, document.querySelector("#why")) {/* h2 结构会被挂载到另一个容器 #why 下面 */}
}
</div>
)
}
}

export default App

  • 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
  • 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

Modal组件案例

  • 比如说,我们准备开发一个 Modal 组件,它可以将它的子组件渲染到屏幕的中间位置:
  • 步骤一:修改 index.html , 添加新的容器节点 div#modal
  • 步骤二:编写这个节点的样式
  • 步骤三:编写组件代码

./Modal.jsx

1
2
3
4
5
6
7
8
9
10
11
import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"

export class Modal extends PureComponent {
render() {
return createPortal(this.props.children, document.querySelector("#modal")) // 给该组件传入的结构将会挂载到容器 div#modal 下面
}
}

export default Modal

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { PureComponent } from 'react'
import Modal from './Modal'

export class App extends PureComponent {
render() {
return (
<div className='app'>
<h1>App H1</h1>

{/* 2.Modal组件 */}
<Modal>
<h2>我是标题</h2>
<p>我是内容, 哈哈哈</p>
</Modal>
</div>
)
}
}

export default App

fragment

  • 在之前的开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:

1666592279401

  • 我们又希望可以不渲染这样一个 div 应该如何操作呢?
    • 使用 Fragment
    • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
  • React 还提供了 Fragment 的短语法:
    • 它看起来像空标签 <> </>
    • 但是,如果我们需要在 Fragment 中添加 key,那么就不能使用短语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { PureComponent, Fragment } from 'react'

export class App extends PureComponent {
constructor() {
super()
this.state = {
sections: [
{ title: "哈哈哈", content: "我是内容, 哈哈哈" },
{ title: "呵呵呵", content: "我是内容, 呵呵呵" },
{ title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
{ title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
]
}
}

render() {
const { sections } = this.state
return (
<>
<h2>我是App的标题</h2>
<p>我是App的内容, 哈哈哈哈</p>
<hr />
{
sections.map(item => {
return (
<Fragment key={item.title}>
<h2>{item.title}</h2>
<p>{item.content}</p>
</Fragment>
)
})
}
</>
)
}
}

export default App

严格模式

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具:

    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
    • 它为其后代元素触发额外的检查和警告;
    • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
  • 可以为应用程序的任何部分启用严格模式:

    • <StrictMode></StrictMode>组件将, 需要开启严格模式的组件进行包裹,即可运行严格模式对指定组件进行检查;
    • 对 app 根组件开启严格模式: <StrictMode> <App /> </StrictMode>
  • 但是检测,到底检测什么呢?

    • 识别不安全的生命周期
    • 使用过时的 ref API
    • 检查意外的副作用
      • 这个组件的 constructor 会被调用两次;
      • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
      • 在生产环境中,是不会被调用两次的;
    • 使用废弃的 findDOMNode 方法
      • 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,不过已经不推荐使用了,严格模式下已废弃 , 可以自行学习演练一下
    • 检测过时的 context API
      • 早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的;
      • 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法;