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

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

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 是前端开发人员必须掌握的两个框架。

- 下面的观点是一个目前比较普遍的共识,没有贬低任何框架的意思。
React 课堂体系

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


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

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

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

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

- 该案例的代码会在后面展示
- 当然,你也可以使用 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> </body>
</html>
|
1 2 3 4 5 6 7 8
| ReactDOM.render(<h2>Hello World</h2>, document.querySelector("#root"))
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
|
const root = ReactDOM.createRoot(document.querySelector("#root"))
let message = "hello World"
function btnClick() { message = "hello React" rootRender() }
function rootRender() { root.render(( <div> <h2>{message}</h2> <button onClick={btnClick}>修改文本</button> </div> )) }
rootRender()
|
VScode代码片段
- 我们在前面练习 React 的过程中,有些代码片段是需要经常写的,我们在 VSCode 中我们可以生成一个代码片段,方便我们快速生成。
- 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
| class App extends React.Component { constructor() { super() this.state = { message: "Hello World" } }
btnClick() { this.setState({ message: "Hello React" }) }
render() { return ( <div> <h2>{this.state.message}</h2> <button onClick={this.btnClick.bind(this)}>修改文本</button> </div> ) } }
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
| class App extends React.Component { constructor() { super() this.state = { movies: ["星际穿越", "流浪地球", "独行月球", "大话西游", "火星救援"] } }
render() { return ( <div> <h2>电影列表</h2> <ul> { this.state.movies.map((item,index) => { return <li key={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
| class App extends React.Component { constructor() { super() this.state = { message: "当前计数:", counter: 0 } }
increment = () => { this.setState({ counter: this.state.counter + 1 }) } decrement = () => { this.setState({ counter: this.state.counter - 1 }) }
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
| const element = <h2>Hello World</h2>
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
| 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
| 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
| class App extends React.Component { constructor() { super() this.state = { firstName: "jack", lastName: "jobe", age: 18, movies: ["星际穿越", "流浪地球", "独行月球", "大话西游", "火星救援"] } }
getMovieEls = () => { const liEls = this.state.movies.map(movie => <li key={movie}>{movie}</li>) return liEls }
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
| class App extends React.Component { 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" } } }
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-if
、v-show
;
- 在 React 中,所有的条件判断都和普通的 JavaScript 代码一致
- 常见的条件渲染的方式有哪些呢?
- 方式一:条件判断语句
- 方式二:三元运算符
- 方式三:与运算符
&&
- 适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染;
- v-show 的效果
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 原理
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;

声明式编程
- 虚拟 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 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空);

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
| 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,配置它们转换规则、打包依赖、热更新等等一些的内容;
- 脚手架的出现,就是帮助我们解决这一系列问题的;
什么是脚手架
- 传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;

- 编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构
- 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;
- 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板;
- 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可;
- 这样也可以间接保证项目的基本机构一致性,方便后期的维护;
- 总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;
前端脚手架
- 对于现在比较流行的三大框架都有属于自己的脚手架:
- Vue 的脚手架:**@vue/cli**
- Angular 的脚手架:@angular/cli
- React 的脚手架:create-react-app
- 它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好
- 使用这些脚手架需要依赖什么呢?
- 目前这些脚手架都是使用 node 编写的,并且都是基于 webpack 的;
- 所以我们必须在自己的电脑上安装 node 环境;
- 这里我们主要是学习 React,所以我们以 React 的脚手架工具:create-react-app 作为讲解;
安装 node
- React 脚手架本身需要依赖 node,所以我们需要安装 node 环境:
- 下载后,双击安装即可:
- 安装过程中,会自动配置环境变量;
- 安装时,会同时帮助我们安装 npm 管理工具;

创建 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 **
目录结构分析

了解 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
- 这个操作是不可逆的, 操作需谨慎,所以在执行过程中会给与我们提示;

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

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

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

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() { const { message } = this.state return <h2>{message}</h2> } }
export default App
|
index.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 来模拟一些生命周期的回调)
生命周期解析

生命周期函数
构造器
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) console.log('01 我是构造器中的执行的~') this.state = { message: '你好师姐', } } render() { console.log('02 我是 render 方法中执行的~') const { message } = this.state return <h2>{message}</h2> }
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) console.log('01 我是构造器中的执行的~') this.state = { message: '你好师姐', } }
btnClick = () => { this.setState({ message: '你好,世界', }) }
render() { console.log('02 我是 render 方法中执行的~') const { message } = this.state return ( <div> <h2 onClick={() => { this.btnClick() }} > {message} </h2> </div> ) }
componentDidMount() { console.log('03 componentDidMount') }
componentDidUpdate() { console.log('04 componentDidUpdate') } }
export default App
|
组件卸载
- componentWillUnmount
- componentWillUnmount() 会在组件卸载及销毁之前直接调用。
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
1 2 3 4 5
| componentWillUnmount() { console.log('componentWillUnmount') }
|
不常用的生命周期

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

- 上面的嵌套逻辑如下,它们存在如下关系:
- App 组件是 Header、Main、Footer 组件的父组件;
- Main 组件是 Banner、ProductList 组件的父组件;
认识组件间通信
父组件 : 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) 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) } render() { const { title, banner } = this.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 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) this.state = { banners: [], productList: [], } }
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 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 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
- 如果没有传递,我们希望有默认值呢?
父组件 : Main.jsx
1 2 3
| <MainBanner banners={banners} title={'轮播图'}></MainBanner> <MainBanner banners={banners}></MainBanner> {}
|
子组件 : 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'
export class MainBanner extends Component { render() { const { title, banners } = this.props return ( <div> <h2>{title}</h2> {/* 4. 根据数据渲染页面 */} <ul> {banners.map((item) => { return <li key={item.acm}>{item.title}</li> })} </ul> </div> ) } }
MainBanner.propTypes = { banners: PropTypes.array.isRequired, title: PropTypes.string, }
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 { static defaultProps = { banners: [], title: '我是默认标题', } render() { } }
|
子组件传递父组件
某些情况,我们也需要子组件向父组件传递消息:
- 在 vue 中是通过自定义事件来完成的;
- 在 React 中同样是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
我们这里来完成一个案例:

- 将计数器案例进行拆解;
- 将按钮封装到子组件中: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, } } changeCounter = (count) => { this.setState({ counter: this.state.counter + count, }) } 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 { 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
|

组件间通信案例

父组件 : 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: ['流行', '新款', '精选'], tabIndex: 0, } } tabClick = (index) => { this.setState({ tabIndex: index, }) } 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, } } itemClick = (index) => { this.setState({ currentIndex: index }) this.props.tabClick(index) } render() { const { titles } = this.props const { currentIndex } = this.state return ( <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; }
|

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

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

- 每个组件都可以获取到 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() { 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() { 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
|
作用域插槽的实现

父组件 : ./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) { this.setState({ currentIndex: index }) 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'
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() { console.log(this.context) return <div>HomeInfo :{this.context.color}</div> } }
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'
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'
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() { console.log('111', this.context) return <div>HomeInfo :{this.context.color}</div> } }
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 中继承过来的。
- 对应的源码 :

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 = () => { this.setState({ message: '你好,世界', }, () => { console.log(this.state.message)})
this.setState((state, props) => { console.log(state.message) return { count: 1, } })
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 = () => { setTimeout(() => { this.setState({ message: '你好,李银河' }) console.log(this.state.message) }, 0) }
|
- 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) }, 0) }
|
React的更新机制(难点)
React更新流程


React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树。
React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI:
于是,React 对这个算法进行了优化,将其优化成了 O(n),如何优化的呢?
- 同层节点之间相互比较,不会跨节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定;
keys的优化
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) { if (this.state.message !== newSate.message || this.props.message !== newProps.message) { return true } return false }
|
SCU生命周期方法
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'
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 就是进行浅层比较:

高阶组件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'
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, '点爆木星'] 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() this.titleEl = null } getNativeDOM = () => {
console.log(this.titleRef.current)
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) } 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 组件对象:

- 函数式组件是没有实例的,所以无法通过 ref 获取他们的实例:
- 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
- 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;
- 示例代码见上一节
受控组件
- 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state。
- 比如下面的HTML表单元素:
- 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在React中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”;

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

- textarea 标签
- 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) { event.preventDefault()
console.log('获取所有的输入内容') console.log(this.state.username, this.state.password)
}
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
| import React from 'react' import ReactDOM from 'react-dom/client'
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> ) } }
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 })
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 })
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 }, ], } }
handleSubmitClick(event) { event.preventDefault()
const hobbies = this.state.hobbies.filter((item) => item.isChecked).map((item) => item.value) console.log('获取爱好: ', hobbies)
}
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
|
对多表单回调优化
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) { event.preventDefault()
console.log('获取结果:', this.introRef.current.value)
}
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) { class NewCpn extends PureComponent { render() { return <Cpn name="why"/> } } return NewCpn
}
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'
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)
|
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') 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') 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 高阶函数;

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")) } }
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 元素:

- 我们又希望可以不渲染这样一个 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
|
严格模式