# react-learn **Repository Path**: dushenyan/react-learn ## Basic Information - **Project Name**: react-learn - **Description**: React学习新篇章 - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-03-12 - **Last Updated**: 2022-03-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 邂逅 React 开发(放在后面是重点) > 我个人一直喜欢使用邂逅这个词,我希望每个人在和一个新技术接触的时候,都是一场美丽的邂逅。 > > 而 React 本身绝对是优雅和美丽的,所以从这里开始,我们和 React 来一场美丽的邂逅吧! ## 一. 认识 React ### 1.1. React 是什么? React 是什么呢?相信每个做前端的人对它都或多或少有一些印象。 这里我们来看一下官方对它的解释:用于构建用户界面的 JavaScript 库。 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsHCV89Z8DibK0m1sicq69pah3rsIqMzOBVUqRlPDMmibAu9mgch5QX3B4g/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 我们知道对于前端来说,主要的任务就是构建用于界面,而构建用于界面离不开三个技术: - HTML:构建页面的结构 - CSS:构建页面的样式 - JavaScript:页面动态内容和交互 那么使用最原生的 HTML、CSS、JavaScript 可以构建完整的用户界面吗?当然可以,但是会存在很多问题 - 比如操作 DOM 兼容性的问题; - 比如过多兼容性代码的冗余问题; - 比如代码组织和规范的问题; 所以,一直以来前端开发人员都在需求可以让自己开发更方便的 JavaScript 库: - 在过去的很长时间内,jQuery 是被使用最多的 JavaScript 库; - 在过去的一份调查中显示,全球前 10,000 个访问最高的网站中,有 65%使用了 jQuery,是当时最受欢迎的 JavaScript 库; - 但是越来越多的公司开始慢慢不再使用 jQuery,包括程序员使用最多的 GitHub; 现在前端领域最为流行的是三大框架: - Vue - React - Angular ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsbkB0xICJbmEibMnwzXjZyF3ASwZiaAYn4DHJTnD2eBA67BqibkoHR8QIg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) ​ 而 Angular 在国内并不是特别受欢迎,尤其是 Angular 目前的版本对 TypeScript 还有要求的情况下。 Vue 和 React 是国内最为流行的两个框架,而他们都是帮助我们来构建用户界面的 JavaScript 库。 - 关于它们的对比,我会另外再写一篇文章 ### 1.2. React 的起源 React 是 2013 年,Facebook 开源的 JavaScript 框架,那么当时为什么 Facebook 要推出这样一款框架呢? 这个源于一个需求,所产生的 bug: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6Zs7CLvbibh02ayVcKgVrWOOVV6Drt00GOic8KD3rwxKYIxPdtrA9F2iaJicg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 该功能上线之后,总是出现 bug: - 三个消息的数字在发生变化时,过多的操作很容易产生 bug; bug 是否可以修复呢?当然可以修复,但是 Facebook 的工程师并不满足于此; 他们开始思考为什么会产生这样的问题; - 在传统的开发模式中,我们过多的去操作界面的细节;(前端、iOS、Android) - - 比如说需要掌握和使用大量 DOM 的 API,当然我们可以通过 jQuery 来简化和适配一些 API 的使用; - 另外关于数据(状态),往往会分散到各个地方,不方便管理和维护; 他们就去思考,是否有一种新的模式来解决上面的问题: - 1.以组件的方式去划分一个个功能模块 - 2.组件内以 jsx 来描述 UI 的样子,以 state 来存储组件内的状态 - 3.当应用的状态发生改变时,通过 setState 来修改状态,状态发生变化时,UI 会自动发生更新 ### 1.3. React 的特点和优势 #### 1.3.1. React 的特点 声明式编程: - 声明式编程是目前整个大前端开发的模式:Vue、React、Flutter、SwiftUI; - 它允许我们只需要维护自己的状态,当状态改变时,React 可以根据最新的状态去渲染我们的 UI 界面; ![声明式编程](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6Zsx4UXdjKhrNDgicnKtPogSQW1sWndlbUDe6aHfIX6FPc6Y1wKiasu4JMA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 组件化开发: - 组件化开发页面目前前端的流行趋势,我们会讲复杂的界面拆分成一个个小的组件; - 如何合理的进行组件的划分和设计也是后面我会讲到的一个重点; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsqNp9Ay903Ju56ib9VHDxmpvSE3Vb8NeB57micoCbjrFTvzm8eeDo15vQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 多平台适配: - 2013 年,React 发布之初主要是开发 Web 页面; - 2015 年,Facebook 推出了 ReactNative,用于开发移动端跨平台;(虽然目前 Flutter 非常火爆,但是还是有很多公司在使用 ReactNative); - 2017 年,Facebook 推出 ReactVR,用于开发虚拟现实 Web 应用程序;(随着 5G 的普及,VR 也会是一个火爆的应用场景); ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsQ8MeT4JibovAXWianmU2Q0JdBWZ07sYpAXGsIicPGbfyTJ4VgUweVtgog/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) #### 1.3.2. React 的优势 React 由 Facebook 来更新和维护,它是大量优秀程序员的思想结晶: - React 的流行不仅仅局限于普通开发工程师对它的认可,大量流行的其他框架借鉴 React 的思想; Vue.js 框架设计之初,有很多的灵感来自 Angular 和 React。 - 包括 Vue3 很多新的特性,也是借鉴和学习了 React - 比如 React Hooks 是开创性的新功能(也是我们课程的重点) - Vue Function Based API 学习了 React Hooks 的思想 Flutter 的很多灵感都来自 React,来自官网的一段话:(SwiftUI 呢) ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsFH0d65IYpibjv4FBLtp0d0paWxVlMaH5PibTKicbao2vQIHeGDvuVxUXQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) ​ 来自 Flutter 官网 - 事实上 Flutter 中的 Widget – Element – RenderObject,对应的就是 JSX – 虚拟 DOM – 真实 DOM 所以 React 可以说是前端的先驱者,它总是会引领整个前端的潮流。 ### 1.4. React 的现状 另外在 HackerRank 中,2020 年有一份调用,你更想要学习的 framework(框架): ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsOEnSJ5JQPN38hGOia67MUCvhN760arlTIBo498aZicJOSefI8Q5HYG8Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)哪一个是你最想要学习的框架 国内外很多知名网站使用 React 开发: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsbMEMADmdhMNTKEg9ZGPyFYjuQCfPr0rrrvjjbyZ0GqEMS7HJ5fUsUQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)image-20200608115008557 目前国内在大型公司使用 React 的较多: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsmVj0gNQia5e9AsFRK9QNzYVGexbXibtohWwQ7V3nugXPHLiblpybQRS7A/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) ## 二. Hello React ### 2.1. 原生案例实现 为了演练 React,我们可以提出一个小的需求: - 在界面显示一个文本:Hello World - 点击下方的一个按钮,点击后文本改变为 Hello React - 在界面显示一个文本:Hello World - 点击下方的一个按钮,点击后文本改变为 Hello React ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsaEfbSic6Y3lOUG1rIIZzP5H5FibMPLFHwENChndo3JE7V2xx4pDx8fXg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)案例效果 但是,我们使用 React 实现之前,先使用原生代码来实现,这样更加方便大家对比 React 和原生: - 当然,你也可以使用 jQuery 和 Vue 来实现,对它们分别进行对比学习 原生实现代码如下: ```html

Hello World

``` ### 2.2. 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 和 react-native 所共同拥有的核心代码。 - react-dom 针对 web 和 native 所完成的事情不同: - - web 端:react-dom 会讲 jsx 最终渲染成真实的 DOM,显示在浏览器中 - native 端:react-dom 会讲 jsx 最终渲染成原生的控件(比如 Android 中的 Button,iOS 中的 UIButton)。 babel 是什么呢? - **Babel** ,又名 **Babel.js**。 - 是目前前端使用非常广泛的编辑器、转移器。 - 比如当下很多浏览器并不支持 ES6 的语法,但是确实 ES6 的语法非常的简洁和方便,我们**开发时**希望使用它。 - 那么编写源码时我们就可以使用 ES6 来编写,之后通过 Babel 工具,将 ES6 转成大多数浏览器都支持的 ES5 的语法。 React 和 Babel 的关系: - 默认情况下开发 React 其实可以不使用 babel。 - 但是前提是我们自己使用 `React.createElement` 来编写源代码,它编写的代码非常的繁琐和可读性差。 - 那么我们就可以直接编写 jsx(JavaScript XML)的语法,并且让 babel 帮助我们转换成 React.createElement。 - 后续还会讲到; 所以,我们在编写 React 代码时,这三个依赖都是必不可少的。 那么,如何添加这三个依赖: - 方式一:直接 CDN 引入 - - react 依赖:https://unpkg.com/react@16/umd/react.development.js - react-dom 依赖:https://unpkg.com/react-dom@16/umd/react-dom.development.js - babel 依赖:https://unpkg.com/babel-standalone@6/babel.min.js - 方式二:下载后,添加本地依赖 - 方式三:通过 npm 管理(后续脚手架再使用) 暂时我们直接通过 CDN 引入,来演练下面的示例程序: - 这里有一个 crossorigin 的属性,这个属性的目的是为了拿到跨域脚本的错误信息 ```jsx ``` ### 2.3. Hello World 下面我们通过一个 Hello World 的案例来看下如何使用 React 开发。 需求非常简单:通过 React,在界面上显示一个 Hello World - 注意:这里我们编写 React 的 script 代码中,必须添加 `type="text/babel"`,作用是可以让 babel 解析 jsx 的语法 ```jsx
``` 代码解析: - 依赖不需要多讲,开发 React 代码必须添加三个依赖; - ReactDOM.render 函数: - - 这里我们已经提前定义一个 id 为 app 的 div - 这里我们传入了一个 h2 元素,后面我们就会使用 React 组件 - 参数一:传递要渲染的内容,这个内容可以是 HTML 元素,也可以是 React 的组件 - 参数二:将渲染的内容,挂载到哪一个 HTML 元素上 显示效果: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuv8T0XGhQAv6Dp33VWkX6ZsN2Z5LzZUg9XibyreYMR1Nh4BglAFL6moNeSEOVQNLjfz32AL1xeKib0Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)Hello World 但是目前我们渲染的内容是定义死的,能否将其抽取到一个变量中呢? - 当然可以,我们可以通过{}语法来引入外部的变量或者表达式 ```jsx // 将数据定义到变量中 let message = "Hello World"; // 通过ReactDom对象来渲染内容 ReactDOM.render(

{message}

, document.getElementById("app")); ``` ### 2.4. Hello React 按照我们最初的案例,我们已经实现了 Hello World,但是我们希望点击一个按钮后,修改为 Hello React #### 2.4.1. 错误的方式 下面的代码是我们正常的执行逻辑,但是会报错: - 原因是默认情况下 `ReactDOM.render` 会覆盖挂载到的 app 原生中的所有内容; - 所以在执行完 `ReactDOM.render` 之后,就不存在 button 原生了; ```react
``` #### 2.4.2. 正确的方式 虽然可以实现,但是整个代码的流畅过于繁琐 ```react
``` #### 2.4.3. 组件的方式 整个逻辑其实可以看做一个整体,那么我们就可以将其封装成一个组件: - 我们说过 `ReactDOM.render` 第一参数是一个 HTML 原生或者一个组件; - 所以我们可以先将之前的业务逻辑封装到一个组件中,然后传入到 `ReactDOM.render` 函数中的第一个参数; 在 React 中,如何封装一个组件呢? - 这里我们暂时使用类的方式封装组件: - - render 当中返回的 jsx 内容,就是之后 React 会帮助我们渲染的内容 - 1.定义一个类,继承自 React.Component - 2.实现当前组件的 render 函数 具体的代码如下: ```react class App extends React.Component { render() { return (

Hello World

) } } ReactDOM.render(, document.getElementById("app")); ``` 如果我们的 Hello World 是依赖变量的,并且会根据按钮的点击而改变呢?这里涉及到几个核心点 1.数据在哪里定义 - 在组件中的数据,我们可以分成两类: - - 参与界面更新的数据:当数据变量时,需要更新组件渲染的内容 - 不参与界面更新的数据:当数据变量时,不需要更新将组建渲染的内容 - 参与界面更新的数据我们也可以称之为是参与数据流,这个数据是定义在当前对象的 state 中 - - 我们可以通过在构造函数中 `this.state = {定义的数据}` - 当我们的数据发生变化时,我们可以调用 `this.setState` 来更新数据,并且通知 React 进行 update 操作 - - 在进行 update 操作时,就会重新调用 render 函数,并且使用最新的数据,来渲染界面 2.事件绑定中的 this - 在类中直接定义一个函数,并且将这个函数绑定到 html 原生的 onClick 事件上,当前这个函数的 this 指向的是谁呢? - 默认情况下是 undefined - - 很奇怪,居然是 undefined; - 因为在正常的 DOM 操作中,监听点击,监听函数中的 this 其实是节点对象(比如说是 button 对象); - 这次因为 React 并不是直接渲染成真实的 DOM,我们所编写的 button 只是一个语法糖,它的本质 React 的 Element 对象; - 那么在这里发生监听的时候,react 给我们的函数绑定的 this,默认情况下就是一个 undefined; - 我们在绑定的函数中,可能想要使用当前对象,比如执行 `this.setState` 函数,就必须拿到当前对象的 this - - 我们就需要在传入函数时,给这个函数直接绑定 this - 类似于下面的写法:`` 我们一起来看一下代码是如何实现的: ```react class App extends React.Component { constructor() { super(); this.state = { message: "Hello World" }; } render() { return (

{this.state.message}

) } changeText() { this.setState({ message: "Hello React" }) } } ReactDOM.render(, document.getElementById("app")); ``` # 核心 JSX 语法一 ## 一. ES6 的 class > 虽然目前 React 开发模式中更加流行 hooks,但是依然有很多的项目依然是使用类组件(包括 AntDesign 库中); > > 但是有很多的同学对 ES6 中的类不太熟悉,所以这里我还是补充一下; ### 1.1. 类的定义 在 ES6 之前,我们通过 function 来定义类,但是这种模式一直被很多从其他编程语言(比如 Java、C++、OC 等等)转到 JavaScript 的人所不适应。 原因是,大多数面向对象的语言,都是使用 class 关键字来定义类的。 而 JavaScript 也从 ES6 开始引入了 class 关键字,用于定义一个类。 ES6 之前定义一个 Person 类: ```js function Person(name, age) { this.name = name; this.age = age; } Person.prototype.running = function () { console.log(this.name + this.age + "running"); }; var p = new Person("why", 18); p.running(); ``` 转换成 ES6 中的类如何定义呢? - 类中有一个 constructor 构造方法,当我们通过 new 关键字调用时,就会默认执行这个构造方法 - - 构造方法中可以给当前对象添加属性 - 类中也可以定义其他方法,这些方法会被放到 Person 类的 prototype 上 ```js class Person { constructor(name, age) { this.name = name; this.age = age; } running() { console.log(this.name + this.age + "running"); } } const p = new Person("why", 18); p.running(); ``` 另外,属性也可以直接定义在类中: - height 和 address 是直接定义在类中 ```js class Person { height = 1.88; address = "北京市"; constructor(name, age) { this.name = name; this.age = age; } studying() { console.log(this.name + this.age + "studying"); } } ``` ### 1.2. 类的继承 继承是面向对象的一大特性,可以减少我们重复代码的编写,方便公共内容的抽取(也是很多面向对象语言中,多态的前提)。 ES6 中增加了 extends 关键字来作为类的继承。 我们先写两个类没有继承的情况下,它们存在的重复代码: - Person 类和 Student 类 ```jsx class Person { constructor(name, age) { this.name = name; this.age = age; } running() { console.log(this.name, this.age, "running"); } } class Student { constructor(name, age, sno, score) { this.name = name; this.age = age; this.sno = sno; this.score = score; } running() { console.log(this.name, this.age, "running"); } studying() { console.log(this.name, this.age, this.sno, this.score, "studing"); } } ``` 我们可以使用继承来简化代码: - 注意:在 constructor 中,子类必须通过 super 来调用父类的构造方法,对父类进行初始化,否则会报错。 ```jsx class Student1 extends Person { constructor(name, age, sno, score) { super(name, age); this.sno = sno; this.score = score; } studying() { console.log(this.name, this.age, this.sno, this.score, "studing"); } } const stu1 = new Student1("why", 18, 110, 100); stu1.studying(); ``` ## 二. 案例练习 ### 2.1. 列表展示 真实开发中,我们的数据通常会从服务器获取,比较常见的是获取一个列表数据,保存到一个数组中进行展示 - 比如现在有一个电影列表,我们如何通过 React 进行展示呢? 我们还是通过一个组件来完成: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { movies: ["星际穿越", "大话西游", "盗梦空间", "少年派"], }; } render() { // var movieLis = []; // for (var i in this.state.movies) { // movieLis.push((
  • {this.state.movies[i]}
  • )); // } return (

    电影列表

      {this.state.movies.map((item, index) => { return
    • {item}
    • ; })}
    ); } } ReactDOM.render(, document.getElementById("app")); ``` ### 2.2. 计数器案例 电影列表的案例中并没有交互,我们再来实现一个计数器的案例: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { counter: 0, }; } render() { return (

    当前计数:{this.state.counter}

    ); } increment() { this.setState({ counter: this.state.counter + 1, }); } decrement() { this.setState({ counter: this.state.counter - 1, }); } } ReactDOM.render(, document.getElementById("app")); ``` ## 三. JSX 语法解析 ### 3.1. 认识 JSX 的语法 我们先来看一段代码: - 这段 element 变量的声明右侧赋值的标签语法是什么呢? - - 它不是一段字符串(因为没有使用引号包裹),它看起来是一段 HTML 原生,但是我们能在 js 中直接给一个变量赋值 html 吗? - 其实是不可以的,如果我们将 `type="text/babel"` 去除掉,那么就会出现语法错误; - 它到底是什么呢?其实它是一段 jsx 的语法; ```html ``` JSX 是什么? - JSX 是一种 JavaScript 的语法扩展(eXtension),也在很多地方称之为 JavaScript XML,因为看起就是一段 XML 语法; - 它用于描述我们的 UI 界面,并且其完全可以和 JavaScript 融合在一起使用; - 它不同于 Vue 中的模块语法,你不需要专门学习模块语法中的一些指令(比如 v-for、v-if、v-else、v-bind); 为什么 React 选择了 JSX? - React 认为渲染逻辑本质上与其他 UI 逻辑存在内在耦合 - - 比如 UI 需要绑定事件(button、a 原生等等); - 比如 UI 中需要展示数据状态,在某些状态发生改变时,又需要改变 UI; - 他们之间是密不可分,所以 React 没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component); - - 当然,后面我们还是会继续学习更多组件相关的东西; - 在这里,我们只需要知道,JSX 其实是嵌入到 JavaScript 中的一种结构语法; JSX 的书写规范: - JSX 的顶层**只能有一个根元素**,所以我们很多时候会在外层包裹一个 div 原生(或者使用后面我们学习的 Fragment); - 为了方便阅读,我们通常在 jsx 的外层包裹一个小括号(),这样可以方便阅读,并且 jsx 可以进行换行书写; - JSX 中的标签可以是单标签,也可以是双标签; - - 注意:如果是单标签,必须以/>结尾; JSX 的本质,我们后面再来讨论; ### 3.2. JSX 嵌入表达式 如果我们 jsx 中的内容是动态的,我们可以通过表达式来获取: - 书写规则:{表达式} - 大括号内可以是变量、字符串、数组、函数调用等任意 js 表达式; #### 3.2.1. jsx 中的注释 jsx 是嵌入到 JavaScript 中的一种语法,所以在编写注释时,需要通过 JSX 的语法来编写: ```html
    {/* 我是一段注释 */}

    Hello World

    ``` #### 3.2.2. JSX 嵌入变量 - 情况一:当变量是 Number、String、Array 类型时,可以直接显示 - 情况二:当变量是 null、undefined、Boolean 类型时,内容为空; - - 如果希望可以显示 null、undefined、Boolean,那么需要转成字符串; - 转换的方式有很多,比如 toString 方法、和空字符串拼接,String(变量)等方式; - 情况三:对象类型不能作为子元素(not valid as a React child) ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { name: "why", age: 18, hobbies: ["篮球", "唱跳", "rap"], test1: null, test2: undefined, flag: false, friend: { name: "kobe", age: 40 } } } render() { return (
    {/* 我是一段注释 */}

    Hello World

    {/* 1.可以直接显示 */}

    {this.state.name}

    {this.state.age}

    {this.state.hobbies}

    {/* 2.不显示 */}

    {this.state.test1}

    {this.state.test1 + ""}

    {this.state.test2}

    {this.state.test2 + ""}

    {this.state.flag}

    {this.state.flag + ""}

    {/* 3.不显示 */}

    123{this.state.friend}

    ) } } ReactDOM.render(, document.getElementById("app")); ``` **补充:为什么 null、undefined、Boolean 在 JSX 中要显示为空内容呢?** 原因是在开发中,我们会进行很多的判断; - 在判断结果为 false 时,不显示一个内容; - 在判断结果为 true 时,显示一个内容; 这个时候,我们可以编写如下代码: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { flag: false, }; } render() { return (
    {this.state.flag ?

    我是标题

    : null} {this.state.flag &&

    我是标题

    }
    ); } } ``` #### 3.3.3. JSX 嵌入表达式 JSX 中,也可以是一个表达式。 这里我们演练三个,其他的大家在开发中灵活运用: - 运算表达式 - 三元运算符 - 执行一个函数 ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { firstName: "kobe", lastName: "bryant", age: 20, }; } render() { return (
    {/* 运算表达式 */}

    {this.state.firstName + " " + this.state.lastName}

    {/* 三元运算符 */}

    {this.state.age >= 18 ? "成年人" : "未成年人"}

    {/* 执行一个函数 */}

    {this.sayHello("kobe")}

    ); } sayHello(name) { return "Hello " + name; } } ``` #### 3.3.4. jsx 绑定属性 很多时候,描述的 HTML 原生会有一些属性,而我们希望这些属性也是动态的: - 比如元素都会有 title 属性 - 比如 img 元素会有 src 属性 - 比如 a 元素会有 href 属性 - 比如元素可能需要绑定 class - - 注意:绑定 class 比较特殊,因为 class 在 js 中是一个关键字,所以 jsx 中不允许直接写 class - 写法:使用 className 替代 - 比如原生使用内联样式 style - - style 后面跟的是一个对象类型,对象中是样式的属性名和属性值; - 注意:这里会讲属性名转成驼峰标识,而不是连接符-; 我们来演示一下属性的绑定: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { title: "你好啊", imgUrl: "https://upload.jianshu.io/users/upload_avatars/1102036/c3628b478f06.jpeg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240", link: "https://www.baidu.com", active: false, }; } render() { return (

    Hello World

    百度一下
    你好啊
    你好啊
    我是文本
    ); } } ``` ### 3.3. jsx 事件监听 #### 3.3.1. 和原生绑定区别 如果原生 DOM 原生有一个监听事件,我们可以如何操作呢? - 方式一:获取 DOM 原生,添加监听事件; - 方式二:在 HTML 原生中,直接绑定 onclick; 我们这里演练一下方式二: - `btnClick()`这样写的原因是 onclick 绑定的后面是跟上 JavaScript 代码; ```html ``` 在 React 中是如何操作呢? 我们来实现一下 React 中的事件监听,这里主要有两点不同 - React 事件的命名采用小驼峰式(camelCase),而不是纯小写; - 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行; ```jsx class App extends React.Component { render() { return (
    ); } btnClick() { console.log("React按钮点击了一下"); } } ``` #### 3.3.2. this 绑定问题 在事件执行后,我们可能需要获取当前类的对象中相关的属性: - 比如我们这里打印:`this.state.message` - - 但是这里会报错:`Cannot read property 'state' of undefined` - 原因是 this 在这里是 undefined - 如果我们这里直接打印 this,也会发现它是一个 undefined ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { message: "你好啊,李银河", }; } render() { return (
    ); } btnClick() { console.log(this); console.log(this.state.message); } } ``` 为什么是 undefined 呢? - 原因是`btnClick`函数并不是我们主动调用的,而且当 button 发生改变时,React 内部调用了`btnClick`函数; - 而它内部调用时,并不知道要如何绑定正确的 this; **如何解决 this 的问题呢?** **方案一:bind 给 btnClick 显示绑定 this** 在传入函数时,我们可以主动绑定 this: - 这里我们主动将 btnClick 中的 this 通过 bind 来进行绑定(显示绑定) - 那么之后 React 内部调用 btnClick 函数时,就会有一个 this,并且是我们绑定的 this; ```jsx ``` 但是呢,如果我有两个函数都需要用到 btnClick 的绑定: - 我们发现 `bind(this)` 需要书写两遍; ```jsx ``` 这个我们可以通过在构造方法中直接给 this.btnClick 绑定 this 来解决: - 注意查看 `constructor` 中我们的操作:`this.btnClick = this.btnClick.bind(this);` ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { message: "你好啊,李银河", }; this.btnClick = this.btnClick.bind(this); } render() { return (
    ); } btnClick() { console.log(this); console.log(this.state.message); } } ``` **方案二:使用 ES6 class fields 语法** 你会发现我这里将 btnClick 的定义变成了一种赋值语句: - 这是 ES6 中给类定义属性的方法,称之为 class fields 语法; - 因为这里我们赋值时,使用了箭头函数,所以在当前函数中的 this 会去上一个**作用域**中查找; - 而上一个作用域中的 this 就是当前的对象; ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { message: "你好啊,李银河", }; } render() { return (
    ); } btnClick = () => { console.log(this); console.log(this.state.message); }; } ``` **方案三:事件监听时传入箭头函数(推荐)** 因为 `onClick` 中要求我们传入一个函数,那么我们可以直接定义一个箭头函数传入: - 传入的箭头函数的函数体是我们需要执行的代码,我们直接执行 `this.btnClick()`; - `this.btnClick()`中通过 this 来指定会进行隐式绑定,最终 this 也是正确的; ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { message: "你好啊,李银河", }; } render() { return (
    ); } btnClick() { console.log(this); console.log(this.state.message); } } ``` #### 3.3.3. 事件参数传递 在执行事件函数时,有可能我们需要获取一些参数信息:比如 event 对象、其他参数 情况一:获取 event 对象 - 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为) - 假如我们用不到 this,那么直接传入函数就可以获取到 event 对象; ```react class App extends React.Component { constructor(props) { render() { return ( ) } btnClick(e) { e.preventDefault(); console.log(e); } } ``` 情况二:获取更多参数 - 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数; ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { names: ["衣服", "鞋子", "裤子"], }; } render() { return (
    点我一下 {this.state.names.map((item, index) => { return ( this.aClick(e, item, index)}> {item} ); })}
    ); } aClick(e, item, index) { e.preventDefault(); console.log(item, index); console.log(e); } } ``` # 核心 JSX 语法二 ## 一. 条件渲染 某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容: - 在 vue 中,我们会通过指令来控制:比如 v-if、v-show; - 在 React 中,所有的条件判断都和普通的 JavaScript 代码一致; 常见的条件渲染的方式有哪些呢? ### 1.1. 条件判断语句 一种方式是当逻辑较多时,通过条件判断: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { isLogin: true, }; } render() { let titleJsx = null; if (this.state.isLogin) { titleJsx =

    欢迎回来~

    ; } else { titleJsx =

    请先登录~

    ; } return
    {titleJsx}
    ; } } ``` 当然,我们也可以将其封装到一个独立的函数中: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { isLogin: true, }; } render() { return
    {this.getTitleJsx()}
    ; } getTitleJsx() { let titleJsx = null; if (this.state.isLogin) { titleJsx =

    欢迎回来~

    ; } else { titleJsx =

    请先登录~

    ; } return titleJsx; } } ``` ### 1.2. 三元运算符 另外一种实现条件渲染的方法就是三元运算符:`condition ? true : false;` 三元运算符适用于没有太多逻辑的代码:只是根据不同的条件直接返回不同的结果 ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { isLogin: true, }; } render() { return (

    {this.state.isLogin ? "欢迎回来~" : "请先登录~"}

    ); } loginBtnClick() { this.setState({ isLogin: !this.state.isLogin, }); } } ``` ### 1.3. 与运算符&& 在某些情况下,我们会遇到这样的场景: - 如果条件成立,渲染某一个组件; - 如果条件不成立,什么内容也不渲染; 如果我们使用三元运算符,是如何做呢? ```jsx { this.state.isLogin ?

    {this.state.username}

    : null; } ``` 其实我们可以通过`逻辑与&&`来简化操作: ```jsx { this.state.isLogin &&

    {this.state.username}

    ; } ``` ### 1.4. v-show 效果 针对一个 HTML 原生,渲染和不渲染之间,如果切换的非常频繁,那么会相对比较损耗性能: - 在开发中,其实我们可以通过 display 的属性来控制它的显示和隐藏; - 在控制方式在 vue 中有一个专门的指令:v-show; - React 没有指令,但是 React 会更加灵活(灵活带来的代价就是需要自己去实现); 我来看一下如何实现: ```jsx render() { const { isLogin, username } = this.state; const nameDisplay = isLogin ? "block": "none"; return (

    {username}

    ) } ``` ## 二. jsx 列表渲染 ### 2.1. 列表渲染 真实开发中我们会从服务器请求到大量的数据,数据会以列表的形式存储: - 比如歌曲、歌手、排行榜列表的数据; - 比如商品、购物车、评论列表的数据; - 比如好友消息、动态、联系人列表的数据; 在 React 中并没有像 Vue 模块语法中的 v-for 指令,而且需要我们通过 JavaScript 代码的方式组织数据,转成 JSX: - 很多从 Vue 转型到 React 的同学非常不习惯,认为 Vue 的方式更加的简洁明了; - 但是 React 中的 JSX 正是因为和 JavaScript 无缝的衔接,让它可以更加的灵活; - 另外我经常会提到 React 是真正可以提高我们编写代码能力的一种方式; 如何展示列表呢? - 在 React 中,展示列表最多的方式就是使用数组的 map 高阶函数; 数组的 map 函数语法如下: - callback:生成新数组元素的函数,使用三个参数: - - `currentValue` `callback` 数组中正在处理的当前元素。 - `index`可选 `callback` 数组中正在处理的当前元素的索引。 - `array`可选 `map` 方法调用的数组。 - `thisArg`可选:执行 `callback` 函数时值被用作`this`。 ```js var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]) ``` 我们来演练一下之前的案例: ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { movies: [ "盗梦空间", "大话西游", "流浪地球", "少年派", "食神", "美人鱼", "海王", ], }; } render() { return (

    电影列表

      {this.state.movies.map((item) => { return
    • {item}
    • ; })}
    ); } } ReactDOM.render(, document.getElementById("app")); ``` ### 2.2. 数组处理 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理: - 比如过滤掉一些内容:filter 函数 - 比如截取数组中的一部分内容:slice 函数 比如我当前有一个数组中存放了一系列的数字:[10, 30, 120, 453, 55, 78, 111, 222] 案例需求:获取所有大于 50 的数字,并且展示前 3 个数组 ```jsx class App extends React.Component { constructor(props) { super(props); this.state = { numbers: [10, 30, 120, 453, 55, 78, 111, 222], }; } render() { return (

    数字列表

      {this.state.numbers .filter((item) => item >= 50) .slice(0, 3) .map((item) => { return
    • {item}
    • ; })}
    ); } } ReactDOM.render(, document.getElementById("app")); ``` ### 2.3. 列表的 key 我们会发现在前面的代码中只要展示列表都会报一个警告: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuucCPnW0chLZrf2O6SIP4UoOVBjPwM0eKrv8OLKGf9uY7iaeWialU39tNcMYSh37SsJEpCeicDWTQAqA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)列表展示警告 这个警告是告诉我们需要在列表展示的 jsx 中添加一个 key。 至于如何添加一个 key,为什么要添加一个 key,这个我们放到后面讲解 setState 时再来讨论; ## 三.JSX 原理解析 ### 3.1. JSX 转换本质 实际上,jsx 仅仅只是 `React.createElement(component, props, ...children)` 函数的语法糖。 - 所有的 jsx 最终都会被转换成`React.createElement`的函数调用。 React.createElement 在源码的什么位置呢? ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuucCPnW0chLZrf2O6SIP4Uoibcs6eGhUg6icsoeibzHNb4OXIHYj5icTvyDdPFrzTB62EQ7JgMiblXFG3Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)React.createElement 源码 createElement 需要传递三个参数: - 参数一:type - - 当前 ReactElement 的类型; - 如果是标签元素,那么就使用字符串表示 “div”; - 如果是组件元素,那么就直接使用组件的名称; - 参数二:config - - 所有 jsx 中的属性都在 config 中以对象的属性和值的形式存储 - 参数三:children - - 存放在标签中的内容,以 children 数组的方式进行存储; - 当然,如果是多个元素呢?React 内部有对它们进行处理,处理的源码在下方 对 children 进行的处理: - 从第二个参数开始,将其他所有的参数,放到 props 对象的 children 中 ```jsx const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } ``` 真实的转换过程到底长什么样子呢?我们可以从多个角度来查看。 #### 3.1.1. Babel 官网查看 我们知道默认 jsx 是通过 babel 帮我们进行语法转换的,所以我们之前写的 jsx 代码都需要依赖 babel。 - 可以在 babel 的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react 在这里我们编写一些 jsx 代码,来查看运行后的结果: ```jsx

    我是网站标题

    我是h2元素

    我是网站的尾部

    ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuucCPnW0chLZrf2O6SIP4UoPiaJS2Ko5hqlq1EMqPOYWPXIibq2C8mZibKKvxRo6IZDx95a8Tsm9q1Gw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)babel 转换 #### 3.1.2. 编写 createElement 还有一种办法是我们自己来编写 React.createElement 代码: ```jsx class App extends React.Component { constructor(props) { render() { /*#__PURE__*/ const result = React.createElement("div", { className: "app" }, /*#__PURE__*/React.createElement("div", { className: "header" }, /*#__PURE__*/React.createElement("h1", { title: "\u6807\u9898" }, "\u6211\u662F\u7F51\u7AD9\u6807\u9898")), /*#__PURE__*/React.createElement("div", { className: "content" }, /*#__PURE__*/React.createElement("h2", null, "\u6211\u662Fh2\u5143\u7D20"), /*#__PURE__*/React.createElement("button", { onClick: e => console.log("+1") }, "+1"), /*#__PURE__*/React.createElement("button", { onClick: e => console.log("+1") }, "-1")), /*#__PURE__*/React.createElement("div", { className: "footer" }, /*#__PURE__*/React.createElement("p", null, "\u6211\u662F\u7F51\u7AD9\u7684\u5C3E\u90E8"))); return result; } } ReactDOM.render(React.createElement(App, null) , document.getElementById("app")); ``` 上面的整个代码,我们就没有通过 jsx 来书写了,界面依然是可以正常的渲染。 另外,在这样的情况下,你还需要 babel 相关的内容吗?不需要了 - 所以,`type="text/babel"`可以被我们删除掉了; - 所以,``可以被我们删除掉了; ### 3.2. 虚拟 DOM #### 3.2.1. 虚拟 DOM 的创建过程 我们通过 `React.createElement` 最终创建出来一个 ReactElement 对象: ```js return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props ); ``` 这个 ReactElement 对象是什么作用呢?React 为什么要创建它呢? - 原因是 React 利用 ReactElement 对象组成了一个 JavaScript 的对象树; - JavaScript 的对象树就是大名鼎鼎的虚拟 DOM(Virtual DOM); 如何查看 ReactElement 的树结构呢? - 我们可以将之前的 jsx 返回结果进行打印; - 注意下面代码中我打 jsx 的打印; ```jsx render() { const jsx = (

    我是网站标题

    我是h2元素

    我是网站的尾部

    ) console.log(jsx); return jsx; } ``` 打印结果,在浏览器中查看: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuucCPnW0chLZrf2O6SIP4UoZxehW42hHmd8LiahFelezZEIQ4k2HAicY3icIDTsV3WwHic2EAict4ibJcBQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)ReactElement 对象结构 而 ReactElement 最终形成的树结构就是 Virtual DOM; 整体的转换过程如下: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuucCPnW0chLZrf2O6SIP4UoqHIh9cOibrhx0SWzVl28T4W4ib9VepmrN5WqqLfGeicu3BLqSQ7vBHB8A/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)jsx 转换流程 #### 3.2.2. 为什么采用虚拟 DOM 为什么要采用虚拟 DOM,而不是直接修改真实的 DOM 呢? - 很难跟踪状态发生的改变:原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试; - 操作真实 DOM 性能较低:传统的开发模式会进行频繁的 DOM 操作,而这一的做法性能非常的低; **DOM 操作性能非常低:** 首先,document.createElement 本身创建出来的就是一个非常复杂的对象; - https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement 其次,DOM 操作会引起浏览器的回流和重绘,所以在开发中应该避免频繁的 DOM 操作; **这里我们举一个例子:** 比如我们有一组数组需要渲染:[0, 1, 2, 3, 4],我们会怎么做呢? ```html
    • 0
    • 1
    • 2
    • 3
    • 4
    ``` 后来,我们又增加了 5 条数据:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ```js for (var i = 5; i < 10; i++) { var li = document.createElement("li"); li.innerHTML = arr[i]; ul.appendChild(li); } ``` 上面这段代码的性能怎么样呢?非常低效 - 因为我们通过 `document.createElement` 创建元素,再通过 `ul.appendChild(li)` 渲染到 DOM 上,进行了多次 DOM 操作; - 对于批量操作的,最好的办法不是一次次修改 DOM,而是对批量的操作进行合并;(比如可以通过 DocumentFragment 进行合并); **虚拟 DOM 帮助我们从命令式编程转到了声明式编程的模式** React 官方的说法:Virtual DOM 是一种编程理念。 在这个理念中,UI 以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的 JavaScript 对象,我们可以通过 ReactDOM.render 让 `虚拟DOM` 和 `真实DOM`同步起来,这个过程中叫做协调(Reconciliation); 这种编程的方式赋予了 React 声明式的 API:你只需要告诉 React 希望让 UI 是什么状态,React 来确保 DOM 和这些状态是匹配的。 你不需要直接进行 DOM 操作,只可以从手动更改 DOM、属性操作、事件处理中解放出来; **关于虚拟 DOM 的一些其他内容,在后续的学习中还会再次讲到;** # 阶段案例练习 ## 一. 书籍购物车说明 ### 1.1. 案例介绍 现在我们来做一个相对综合一点的练习:书籍购物车; 案例效果如下: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXut490MwgO1rHibr3icOwGe6OYfnR0oRicCbKn3v8o4c0aIiaic0Hp7WcBwrC9ibYic94k2RqrfYa6iaT29wmA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)案例效果 案例说明: - 1.在界面上以表格的形式,显示一些书籍的数据; - 2.在底部显示书籍的总价格; - 3.点击+或者-可以增加或减少书籍数量(如果为 1,那么不能继续-); - 4.点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~); ### 1.2. 项目的搭建 这里,我们使用 React 将默认的数据先展示出来: ```jsx Document
    ``` ## 二. 书籍购物车功能 ### 1.1. 价格的显示 我们可以封装一个工具函数,用于格式化价格: ```js function formatPrice(price) { if (typeof price !== "number") { price = Number(price) || 0; } return "¥" + price.toFixed(2); } ``` 对之前显示的价格进行格式化: ```html {formatPrice(item.price)} ``` 封装一个 App 中的方法,用于获取商品总价格显示的内容: ```js getTotalPrice() { let totalPrice = 0; for (let book of this.state.books) { totalPrice += book.count * book.price } return "总价格:" + formatPrice(totalPrice); } ``` 使用一个 h2 元素显示总价格: ```html

    {this.getTotalPrice()}

    ``` ### 1.2. 数量的变化 封装一个方法,用于改变书籍的数量: - 注意:在 React 中,要保证数据的不可变性; - 所以,我们是先复制一份 books,对其进行修改,再通过 setState 更新到最新的状态; ```jsx changeItem(index, counter) { const books = [...this.state.books]; this.setState({ books: books.map((item, indey) => { if (indey == index) { item.count += counter; } return item; }) }) } ``` 修改 jsx 对应位置的代码: ```html {item.count} ``` ### 1.3. 移除的操作 封装一个方法,用于移除对应的书籍: ```jsx removeItem(index) { const books = [...this.state.books]; this.setState({ books: books.filter((item, indey) => index !== indey) }) } ``` 修改对应的移除 jsx 代码: ```jsx ``` 如果所有的书籍移除完毕,那么要显示购物车为空: - 封装两个方法,一个用于获取显示购物车的 jsx 代码(后期会封装成一个组件),一个用于获取显示购物车为空的 jsx 代码(后期也可以封装为一个组件) ```jsx renderBooks() { const { books } = this.state; return (
    { books.map((item, index) => { return ( ) }) }
    书籍名称 出版日期 价格 购买数量 操作
    {index + 1} {item.name} {item.date} {formatPrice(item.price)} {item.count}

    {this.getTotalPrice()}

    ) } renderEmpty() { return

    购物车为空~

    } ``` 重新编写 render 方法代码: ```jsx render() { const { books } = this.state; return books.length ? this.renderBooks() : this.renderEmpty(); } ``` # React 脚手架 ## 一. 认识脚手架 ### 1.1. 前端工程的复杂化 如果我们只是开发几个小的 demo 程序,那么永远不需要考虑一些复杂的问题: - 比如目录结构如何组织划分; - 比如如何管理文件之间的相互依赖; - 比如如何管理第三方模块的依赖; - 比如项目发布前如何压缩、打包项目; - 等等... 现代的前端项目已经越来越复杂了: - 不会再是在 HTML 中引入几个 css 文件,引入几个编写的 js 文件或者第三方的 js 文件这么简单; - 比如 css 可能是使用 less、sass 等预处理器进行编写,我们需要将它们转成普通的 css 才能被浏览器解析; - 比如 JavaScript 代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在**成百上千**个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖; - 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等); 为了解决上面这些问题,我们需要再去学习一些工具: - 比如 babel、webpack、gulp。配置它们转换规则、打包依赖、热更新等等一些的内容; - 你会发现,你还没有开始做项目,你就面临一系列的工程化问题; **脚手架的出现,就是帮助我们解决这一系列问题的;** ### 1.2. 脚手架是什么呢? 传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIKKF8BcKMQKqlgJ8QYQIzJzv1I0HD5mTMpmqWwwFndllubBXJdviceHw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)脚手架 编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构; - 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的; - 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板; - 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可; - 这样也可以间接保证项目的基本机构一致性,方便后期的维护; 总结:**脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;** 对于现在比较流行的三大框架都有属于自己的脚手架: - Vue 的脚手架:vue-cli - Angular 的脚手架:angular-cli - React 的脚手架:create-react-app 它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好。 使用这些脚手架需要依赖什么呢? - 目前这些脚手架都是使用 node 编写的,并且都是基于 webpack 的; - 所以我们必须在自己的电脑上安装 node 环境; 这里我们主要是学习 React,所以我们还是以 React 的脚手架工具:create-react-app 作为讲解; ## 二. create-react-app ### 2.1. 安装相关的依赖 #### 2.1.1. 安装 node React 脚手架本身需要依赖 node,所以我们需要安装 node 环境: - 无论是 windows 还是 Mac OS,都可以通过 node 官网直接下载; - 官网地址:https://nodejs.org/en/download/ - 注意:这里推荐大家下载 LTS(_Long-term support_ )版本,是长期支持版本,会比较稳定; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIxhxhmTr1lUBWlfLq0mfEicAC66scdGHkGapAapQenHv2zicicxPA0YfCw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)nodejs 下载 下载后,双击安装即可: - 1.安装过程中,会自动配置环境变量; - 2.安装时,会同时帮助我们安装 npm 管理工具; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIYm442XUFCicQGd2mcKIUgz47kXicERSqX1jKySsmUx8BHicbOKQFl1ia4A/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)检测安装的版本 #### 2.1.2. 包管理工具 **什么是 npm?** - 全称 Node Package Manager,即“node 包管理器”; - 作用肯定是帮助我们管理一下依赖的工具包(比如 react、react-dom、axios、babel、webpack 等等); - 作者开发的目的就是为了解决“模块管理很糟糕”的问题; **另外,还有一个大名鼎鼎的 node 包管理工具 yarn:** - Yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具; - Yarn 是为了弥补 npm 的一些缺陷而出现的; - 早期的 npm 存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题; - 虽然从 npm5 版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用 yarn; - React 脚手架默认也是使用 yarn; 安装 yarn: ``` npm install -g yarn ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AI0SlVWoROIkVHwcyyEWz5FxAjaympAUbV8qCw4ou7ElqKXG3fEm5SVA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)检测 yarn 安装的版本 **yarn 和 npm 的命令对比** | Npm | Yarn | | :-------------------------------------- | :---------------------------- | | npm install | yarn install | | npm install [package] | yarn add [package] | | npm install --save [package] | yarn add [package] | | npm install --save-dev [package] | yarn add [package] [--dev/-D] | | npm rebuild | yarn install --force | | npm uninstall [package] | yarn remove [package] | | npm uninstall --save [package] | yarn remove [package] | | npm uninstall --save-dev [package] | yarn remove [package] | | npm uninstall --save-optional [package] | yarn remove [package] | | npm cache clean | yarn cache clean | | rm -rf node_modules && npm install | yarn upgrade | **cnpm 的使用** 在国内,某些情况使用 npm 和 yarn 可能无法正常安装一个库,这个时候我们可以选择使用 cnpm ``` npm install -g cnpm --registry=https://registry.npm.taobao.org ``` #### 2.1.3. 安装脚手架 最后一个需要安装的是创建 React 项目的脚手架: ``` npm install -g create-react-app ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIEkFibdMZu6NVyckHsKMgdwcVreFqm6VDfic3LrzicDzzDicKqicdd5wqE6w/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)检查 React 脚手架 ### 2.2. 创建 React 项目 #### 2.2.1. 创建 React 项目 现在,我们就可以通过脚手架来创建 React 项目了。 创建 React 项目的命令如下: - 注意:项目名称不能包含大写字母 ``` create-react-app 项目名称 ``` 另外还有更多创建项目的方式,可以参考 GitHub 的 readme - https://github.com/facebook/create-react-app; - 上面的创建方式,默认使用的 yarn 来管理整个项目包相关的依赖的; - 如果希望使用 npm,也可以在参数后面加上 --use-npm; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIiayTTPtCWuXfHeJzucTcS1JF73uVlLvEicJ6SbsRCic6e7r2UNZVuRN9w/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)image-20200615115822464 创建完成后,进入对应的目录,就可以将项目跑起来: ``` cd 01-test-react yarn start ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIOIsSFCX1YWicJNtIlPpuAmPk5pOb8Fo5B63CXkWoUpUDtzOCYadLjiaQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)项目效果 #### 2.2.2. 目录结构分析 我们可以通过 VSCode 打开项目: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIEibOoQZ2ohBfD4snZFsicRdCgDOkLG9JpJ592xnCknaTxIdbE0ajNy7g/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)项目目录结构 目录结构分析: ``` test-react ├─ README.md // readme说明文档 ├─ package.json // 对整个应用程序的描述:包括应用名称、版本号、一些依赖包、以及项目的启动、打包等等(node管理项目必备文件) ├─ public │ ├─ favicon.ico // 应用程序顶部的icon图标 │ ├─ index.html // 应用的index.html入口文件 │ ├─ logo192.png // 被在manifest.json中使用 │ ├─ logo512.png // 被在manifest.json中使用 │ ├─ manifest.json // 和Web app配置相关 │ └─ robots.txt // 指定搜索引擎可以或者无法爬取哪些文件 ├─ src │ ├─ App.css // App组件相关的样式 │ ├─ App.js // App组件的代码文件 │ ├─ App.test.js // App组件的测试代码文件 │ ├─ index.css // 全局的样式文件 │ ├─ index.js // 整个应用程序的入口文件 │ ├─ logo.svg // 刚才启动项目,所看到的React图标 │ ├─ serviceWorker.js // 默认帮助我们写好的注册PWA相关的代码 │ └─ setupTests.js // 测试初始化文件 └─ yarn.lock ``` 整个目录结构都非常好理解,只是有一个 PWA 相关的概念: - PWA 全称 Progressive Web App,即渐进式 WEB 应用; - 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能; - 这种 Web 存在的形式,我们也称之为是 Web App; PWA 解决了哪些问题呢? - 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏; - 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能; - 实现了消息推送; - 等等一系列类似于 Native App 相关的功能; 更多 PWA 相关的知识,可以自行去学习更多; #### 2.2.3. webpack 配置 我们说过 React 的脚手架是基于 Webpack 来配置的: - _webpack_ 是一个现代 JavaScript 应用程序的*静态模块打包器(module bundler)*; - 当 webpack 处理应用程序时,它会递归地构建一个*依赖关系图(dependency graph)*,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 _bundle_; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AInmKwldCjq7GHtMq4aS8r8icfSuOKibfYMjDnP8icqPYDFoQheBdQYYnMw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)webpack 在这里我们暂时不展开来讲 webpack,因为里面的内容是非常多的(后续会有专门讲 webpack 的专题); 但是,很奇怪:我们并没有在目录结构中看到任何 webpack 相关的内容? - 原因是 React 脚手架讲 webpack 相关的配置隐藏起来了(其实从 Vue CLI3 开始,也是进行了隐藏); 如果我们希望看到 webpack 的配置信息,应该怎么来做呢? - 我们可以执行一个 package.json 文件中的一个脚本:`"eject": "react-scripts eject"` - 这个操作是不可逆的,所以在执行过程中会给与我们提示; ``` yarn eject ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AI0JM1trSibvD4tv98wmSStaic3346WCrQHy4sicAeMnrxgPeWIeW59b0gQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)执行脚本 查看和学习 webpack 相关的配置信息: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIJC1r8RxS7oRtqlXjA126fZZFIMibJZ1SFic28BicicISicUoAKwqv8nYcUA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)webpack 相关配置 ### 2.3. 从零编写项目 #### 2.3.1. 文件的删减 通过脚手架创建完项目,很多同学还是会感觉目录结构过于复杂,所以我打算从零带着大家来编写代码。 我们先将不需要的文件统统删掉: - 1.将 src 下的所有文件都删除 - 2.将 public 文件下出列 favicon.ico 和 index.html 之外的文件都删除掉 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AItRDKhOtAP1H2XEm6k2ylH09kj1lM25ohMjbo7CicYqUGwK4cD62SYmQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)删除后的目录结构 修改 index.html 文件: - 我们需要删除选中的内容; - 这两行内容是我们之前引入的一个图标和 manifest 文件 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AICm05TjbPd7hzM5Vg9vlk0hQP7VbN6HmSvvQFWWhQ7QyuKKTwpbHuicA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)删除选中的两行内容 #### 2.3.2. 开始编写代码 在 src 目录下,创建一个 index.js 文件,因为这是 webpack 打包的入口。 在 index.js 中开始编写 React 代码: - 我们会发现和写的代码是逻辑是一致的; - 只是在模块化开发中,我们需要手动的来导入 React、ReactDOM,因为它们都是在我们安装的模块中; ```jsx import React from "react"; import ReactDOM from "react-dom"; ReactDOM.render(

    Hello React

    , document.getElementById("root")); ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AI0hrxfPsyIMnC4KpVLN2J9qxhjHEib7ibj4xRluibYaZI9j5aE9IbXicLibA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)展示效果 如果我们不希望直接在 `ReactDOM.render` 中编写过多的代码,就可以单独抽取一个组件 App.js: ```jsx import React, { Component } from "react"; export default class App extends Component { render() { return

    Hello App

    ; } } ``` 在 index.js 中引入 App,并且使用它: ```jsx import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render(, document.getElementById("root")); ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvHM8XhtYMaP62K8RWiaG6AIXEgXxvggGsWRTo6zGaXXOQCf4BRFmT1D59JD3EcBBTzKoJqDMxencw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)展示效果 # 组件化开发(一) > 现在可以说整个的大前端开发都是组件化的天下,无论从三大框架(Vue、React、Angular),还是跨平台方案的 Flutter,甚至是移动端都在转向组件化开发。 > > 所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。 ## 一. 认识组件化开发 ### 1.1. 什么是组件化? 人面对复杂问题的处理方式: - 任何一个人处理信息的逻辑能力都是有限的 - 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。 - 但是,我们人有一种天生的能力,就是将问题进行拆解。 - 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuicicEwkkD7zFNvR1BYib9OYbF2Wt0ASLxlEyqHAxZTv4zYiaBhVoVuyXib3NDfBgqut0leia6eJaNiaW2Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)image-20200617095345471 组件化也是类似的思想: - 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。 - 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuicicEwkkD7zFNvR1BYib9OYb8XuXdpX7EpdTx0jfqBzEKQ1xgw3jTEvE5PBUrrKGibejy8pX1VLBZeg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)组件化的拆分 我们需要通过组件化的思想来思考整个应用程序: - 我们将一个完整的页面分成很多个组件; - 每个组件都用于实现页面的一个功能块; - 而每一个组件又可以进行细分; - 而组件本身又可以在多个地方进行复用; ### 1.2. React 的组件化 组件化是 React 的核心思想,也是我们后续课程的重点,前面我们封装的 App 本身就是一个组件: - 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。 - 任何的应用都会被抽象成一颗组件树。 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuicicEwkkD7zFNvR1BYib9OYbia1dkicyWUajP6OSvYEvghiaCD8Vy1ibkZG2dWmF6oJKkUp6dX8kRXw8hw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)组件化开发 组件化思想的应用: - 有了组件化的思想,我们在之后的开发中就要充分的利用它。 - 尽可能的将页面拆分成一个个小的、可复用的组件。 - 这样让我们的代码更加方便组织和管理,并且扩展性也更强。 React 的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件: - 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component); - 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component); - 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component); 这些概念有很多重叠,但是他们最主要是关注数据逻辑和 UI 展示的分离: - 函数组件、无状态组件、展示型组件主要关注 UI 的展示; - 类组件、有状态组件、容器型组件主要关注数据逻辑; 当然还有很多组件的其他概念:比如异步组件、高阶组件等,我们后续再学习。 ## 二. 创建 React 组件 ### 2.1. 创建类组件 类组件的定义有如下要求: - 类组件需要继承自 `React.Component` - 类组件必须实现`render`函数 在 ES6 之前,可以通过`create-react-class` 模块来定义类组件,但是目前官网建议我们使用 ES6 的 class 类定义。 使用 class 定义一个组件: - constructor 是可选的,我们通常在 constructor 中初始化一些数据; - this.state 中维护的就是我们组件内部的数据; - `render()` 方法是 class 组件中唯一必须实现的方法; ```jsx import React, { Component } from "react"; export default class App extends Component { constructor() { super(); this.state = {}; } render() { return

    Hello App

    ; } } ``` 当 `render` 被调用时,它会检查 `this.props` 和 `this.state` 的变化并返回以下类型之一: - **React 元素**: - - 通常通过 JSX 创建。 - 例如,`
    ` 会被 React 渲染为 DOM 节点,`` 会被 React 渲染为自定义组件; - 无论是 `
    ` 还是 `` 均为 React 元素。 - **数组或 fragments**:使得 render 方法可以返回多个元素。 - **Portals**:可以渲染子节点到不同的 DOM 子树中。 - **字符串或数值类型**:它们在 DOM 中会被渲染为文本节点 - **布尔类型或 `null`**:什么都不渲染。 另外类组件有自己的生命周期,我们会在后面的章节中详细介绍。 ### 2.2. 创建函数组件 函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。 函数组件有自己的特点(当然,后面我们会讲 hooks,就不一样了): - 没有生命周期,也会被更新并挂载,但是没有生命周期函数; - 没有 this(组件实例); - 没有内部状态(state); 我们来定义一个函数组件: ```js export default function App() { return
    Hello World
    ; } ``` ## 三. 组件的生命周期 ### 3.1. 认识生命周期 很多的事物都有从创建到销毁的整个过程,这个过程称之为是**生命周期**; React 组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能; 生命周期和生命周期函数的关系: - 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段; - - 比如装载阶段(Mount),组件第一次在 DOM 树中被渲染的过程; - 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程; - 比如卸载过程(Unmount),组件从 DOM 树中被移除的过程; - React 内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数: - - 比如实现 componentDidMount 函数:组件已经挂载到 DOM 上时,就会回调; - 比如实现 componentDidUpdate 函数:组件已经发生了更新时,就会回调; - 比如实现 componentWillUnmount 函数:组件即将被移除时,就会回调; - 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能; 我们谈 React 生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过 hooks 来模拟一些生命周期的回调) ### 3.2. 生命周期解析 我们先来学习一下最基础、最常用的生命周期函数: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuicicEwkkD7zFNvR1BYib9OYbBk5vFia0sk82d1dJ77ys49egPl7KNoMZXyW8z3EOEshOHz07AAZT2wg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)React 基本生命周期 上图第一个区域解析: - 当我们挂载一个组件时,会先执行 constructor 构造方法来创建组件; - 紧接着调用 render 函数,获取要渲染的 DOM 结构(jsx),并且开始渲染 DOM; - 当组件挂载成功(DOM 渲染完成),会执行 componentDidMount 生命周期函数; 上图第二个区域解析: - 当我们通过修改 props,或者调用 setState 修改内部状态,或者直接调用 forceUpdate 时会重新调用 render 函数,进行更新操作; - 当更新完成时,会回调 componentDidUpdate 生命周期函数; 上图第三个区域解析: - 当我们的组件不再使用,会被从 DOM 中移除掉(卸载); - 这个时候会回调 componentWillUnmount 生命周期函数; ### 3.3. 生命周期函数 **constructor** ```js constructor(props); ``` 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。 constructor 中通常只做两件事情: - 通过给 `this.state` 赋值对象来初始化内部的 state; - 为事件绑定实例(this); **componentDidMount** ```js componentDidMount(); ``` `componentDidMount()` 会在组件挂载后(插入 DOM 树中)立即调用。 componentDidMount 中通常进行哪里操作呢? - 依赖于 DOM 的操作可以在这里进行; - 在此处发送网络请求就最好的地方;(官方建议) - 可以在此处添加一些订阅(会在 componentWillUnmount 取消订阅); **componentDidUpdate** ```js componentDidUpdate(prevProps, prevState, snapshot); ``` `componentDidUpdate()` 会在更新后会被立即调用,首次渲染不会执行此方法。 - 当组件更新后,可以在此处对 DOM 进行操作; - 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。 ```jsx componentDidUpdate(prevProps) { // 典型用法(不要忘记比较 props): if (this.props.userID !== prevProps.userID) { this.fetchData(this.props.userID); } } ``` **componentWillUnmount** ```jsx componentWillUnmount(); ``` `componentWillUnmount()` 会在组件卸载及销毁之前直接调用。 - 在此方法中执行必要的清理操作; - 例如,清除 timer,取消网络请求或清除在 `componentDidMount()` 中创建的订阅等; **代码验证所有的生命周期函数:** ```jsx import React, { Component } from "react"; class HYTestCpn extends Component { render() { return

    HYTestCpn

    ; } componentWillUnmount() { console.log("HYTestCpn componentWillUnmount"); } } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, }; console.log("调用constructor方法"); } render() { console.log("调用render方法"); return (

    当前计数: {this.state.counter}

    {this.state.counter <= 5 && }
    ); } increment() { this.setState({ counter: this.state.counter + 1, }); } componentDidMount() { console.log("调用componentDidMount方法"); } componentDidUpdate() { console.log("调用componentDidUpdate方法"); } componentWillUnmount() { console.log("调用componentWillUnmount方法"); } } ``` ### 3.4. 不常用生命周期 除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数: - getDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 state; - getSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置); - shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuicicEwkkD7zFNvR1BYib9OYbGWNw5Lj6QYkJL8LJbgYibp4TtEvQROlBnqV8ujd5L4y4DPn9ibicf8ueg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)完整的生命周期图 另外,React 中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。 更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html # 父子组件通信 ## 一. 认识组件的嵌套 组件之间存在嵌套关系: - 在之前的案例中,我们只是创建了一个组件 App; - 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护; - 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件; - 再将这些组件组合嵌套在一起,最终形成我们的应用程序; 我们来分析一下下面代码的嵌套逻辑: ```jsx import React, { Component } from "react"; function Header() { return

    Header

    ; } function Main() { return (
    ); } function Banner() { return
    Banner
    ; } function ProductList() { return (
    • 商品1
    • 商品2
    • 商品3
    • 商品4
    • 商品5
    ); } function Footer() { return

    Footer

    ; } export default class App extends Component { render() { return (
    ); } } ``` 上面的嵌套逻辑如下,它们存在如下关系: - App 组件是 Header、Main、Footer 组件的父组件; - Main 组件是 Banner、ProductList 组件的父组件; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutc0GrRs2tiaXjDWh4wQ88C25wVRAUic6FGGUONx6FEJbWAY3NBSCvD5fmicFvFoJugPdlcE9xsBPC7g/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)自定义组件的嵌套逻辑 在开发过程中,我们会经常遇到需要组件之间相互进行通信: - 比如 App 可能使用了多个 Header,每个地方的 Header 展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示; - 又比如我们在 Main 中一次性请求了 Banner 数据和 ProductList 数据,那么就需要传递给他们来进行展示; - 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件; 总之,在一个 React 项目中,组件之间的通信是非常重要的环节; 父组件在展示子组件,可能会传递一些数据给子组件: - 父组件通过 **属性=值** 的形式来传递给子组件数据; - 子组件通过 **props** 参数获取父组件传递过来的数据; ## 二. 父组件传递子组件 ### 2.1. 子组件是 class 组件 我们这里先演示子组件是 class 组件: ```jsx import React, { Component } from "react"; // 1.类子组件 class ChildCpn1 extends Component { constructor(props) { super(); this.props = props; } render() { const { name, age, height } = this.props; return (

    我是class的组件

    展示父组件传递过来的数据: {name + " " + age + " " + height}

    ); } } export default class App extends Component { render() { return (
    ); } } ``` 按照上面的结构,我们每一个子组件都需要写构造器来完成:`this.props = props;` 其实呢,大可不必,因为我们可以调用`super(props)`,我们来看一下 Component 的源码: - 这里我们先不关心 context、updater; - 我们发现传入的 props 会被 Component 设置到 this 中(父类的对象),那么子类就可以继承过来; - 补充一个思考题:为什么子类可以继承过来呢? ```jsx function Component(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } ``` 所以我们的构造方法可以换成下面的写法: ```js constructor(props) { super(props); } ``` 甚至我们可以省略,为什么可以省略呢? 如果不指定构造方法,则使用默认构造函数。对于基类,默认构造函数是 ```js constructor() {} ``` 对于派生类,默认构造函数是: ```js constructor(...args) { super(...args); } ``` ### 2.2. 子组件是 function 组件 我们再来演练一下,如果子组件是一个 function 组件: ```jsx function ChildCpn2(props) { const { name, age, height } = props; return (

    我是function的组件

    展示父组件传递过来的数据: {name + " " + age + " " + height}

    ); } export default class App extends Component { render() { return (
    ); } } ``` functional 组件相对来说比较简单,因为不需要有构造方法,也不需要有 this 的问题。 ### 2.3. 参数验证 propTypes 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说: - 当然,如果你项目中默认继承了 Flow 或者 TypeScript,那么直接就可以进行类型验证; - 但是,即使我们没有使用 Flow 或者 TypeScript,也可以通过 `prop-types` 库来进行参数验证; 从 React v15.5 开始,`React.PropTypes` 已移入另一个包中:`prop-types` 库 我们对之前的 class 组件进行验证: ```jsx ChildCpn1.propTypes = { name: PropTypes.string, age: PropTypes.number, height: PropTypes.number, }; ``` 这个时候,控制台就会报警告: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutc0GrRs2tiaXjDWh4wQ88C2bqkmrKnWg80PpSLFFUtFJhFkSylsXcibRlmgpn7aJvkkN7ONqsQrPHg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)类型验证警告 ```html ``` 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html - 比如验证数组,并且数组中包含哪些元素; - 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型; - 比如某个原生是必须的,使用 `requiredFunc: PropTypes.func.isRequired` **如果没有传递,我们希望有默认值呢?** - 我们使用`defaultProps`就可以了 ```jsx ChildCpn1.defaultProps = { name: "王小波", age: 40, height: 1.92, }; ``` ## 三. 子组件传递父组件 某些情况,我们也需要子组件向父组件传递消息: - 在 vue 中是通过自定义事件来完成的; - 在 React 中同样是通过 props 传递消息,只是让`父组件`给`子组件`传递一个`回调函数`,在子组件中调用这个函数即可; 我们这里来完成一个案例: - 将计数器案例进行拆解; - 将按钮封装到子组件中:CounterButton; - CounterButton 发生点击事件,将内容传递到父组件中,修改 counter 的值; ![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)Counter 案例 案例代码如下: ```jsx import React, { Component } from "react"; function CounterButton(props) { const { operator, btnClick } = props; return ; } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, }; } changeCounter(count) { this.setState({ counter: this.state.counter + count, }); } render() { return (

    当前计数: {this.state.counter}

    this.changeCounter(1)} /> this.changeCounter(-1)} />
    ); } } ``` ## 四. 组件通信案例练习 我们来做一个相对综合的练习: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutc0GrRs2tiaXjDWh4wQ88C29FDibRZn5WpcOLK7y4V5K4F34JqLQz6lqyc3SBCiaAmVv84DpSpXic5PA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)TabControl 案例练习 index.js 代码: ```jsx import React from "react"; import ReactDOM from "react-dom"; import "./style.css"; import App from "./App"; ReactDOM.render(, document.getElementById("root")); ``` App.js ```jsx import React, { Component } from "react"; import TabControl from "./TabControl"; export default class App extends Component { constructor(props) { super(props); this.titles = ["流行", "新款", "精选"]; this.state = { currentTitle: "流行", }; } itemClick(index) { this.setState({ currentTitle: this.titles[index], }); } render() { return (
    this.itemClick(index)} />

    {this.state.currentTitle}

    ); } } ``` TabControl.js ```jsx import React, { Component } from "react"; export default class TabControl extends Component { constructor(props) { super(props); this.state = { currentIndex: 0, }; } render() { const { titles } = this.props; const { currentIndex } = this.state; return (
    {titles.map((item, index) => { return (
    this.itemClick(index)}> {item}
    ); })}
    ); } itemClick(index) { this.setState({ currentIndex: index, }); this.props.itemClick(index); } } ``` style.css ```css .tab-control { height: 40px; line-height: 40px; display: flex; } .tab-control .tab-item { flex: 1; text-align: center; } .tab-control .title { padding: 3px 5px; } .tab-control .title.active { color: red; border-bottom: 3px solid red; } ``` ## 五. React 插槽实现 ### 5.1. 为什么使用插槽? 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div、span 等等这些元素。 我们应该让使用者可以决定某一块区域到底存放什么内容。 举个栗子:假如我们定制一个通用的导航组件 - NavBar - 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定; - 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示; - 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等; - 右边可能是一个文字,也可能是一个图标,也可能什么都不显示; ![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)京东导航 这种需求在 Vue 当中有一个固定的做法是通过 slot 来完成的,React 呢? - React 对于这种需要插槽的情况非常灵活; - 有两种方案可以实现:children 和 props; 我这里先提前给出 NavBar 的样式: ```css .nav-bar { display: flex; height: 44px; line-height: 44px; text-align: center; } .nav-bar .left, .nav-bar .right { width: 80px; background: red; } .nav-bar .center { flex: 1; background: blue; } ``` ### 5.2. children 实现 每个组件都可以获取到 `props.children`:它包含组件的开始标签和结束标签之间的内容。 比如: ```html Hello world! ``` 在 `Welcome` 组件中获取 `props.children`,就可以得到字符串 `Hello world!`: ```jsx function Welcome(props) { return

    {props.children}

    ; } ``` 当然,我们之前看过 props.children 的源码: - 如果只有一个元素,那么 children 指向该元素; - 如果有多个元素,那么 children 指向的是数组,数组中包含多个元素; 那么,我们的 NavBar 可以进行如下的实现: ```jsx import React, { Component } from "react"; class NavBar extends Component { render() { return (
    {this.props.children[0]}
    {this.props.children[1]}
    {this.props.children[2]}
    ); } } export default class App extends Component { render() { return (
    返回
    购物街
    更多
    ); } } ``` ### 5.3. props 实现 通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生; 另外一个种方案就是使用 props 实现: - 通过具体的属性名,可以让我们在传入和获取时更加的精准; ```jsx import React, { Component } from "react"; class NavBar extends Component { render() { const { leftSlot, centerSlot, rightSlot } = this.props; return (
    {leftSlot}
    {centerSlot}
    {rightSlot}
    ); } } export default class App extends Component { render() { const navLeft =
    返回
    ; const navCenter =
    购物街
    ; const navRight =
    更多
    ; return (
    ); } } ``` # 非父子组件通信 ## 一. Context 使用 ### 1.1. Context 应用场景 非父子组件数据的共享: - 在开发中,比较常见的数据传递方式是通过 props 属性自上而下(由父到子)进行传递。 - 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI 主题、用户登录状态、用户信息等)。 - 如果我们在顶层的 App 中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。 我们来看一个例子: ```jsx import React, { Component } from "react"; function ProfileHeader(props) { return (

    用户昵称: {props.nickname}

    用户等级: {props.level}

    ); } class Profile extends Component { render() { return (
    • 设置1
    • 设置2
    • 设置3
    • 设置4
    • 设置5
    ); } } export default class App extends Component { constructor() { super(); this.state = { nickname: "coderwhy", level: 99, }; } render() { const { nickname, level } = this.state; return (

    其他内容

    ); } } ``` 我这边顺便补充一个小的知识点:Spread Attributes - https://zh-hans.reactjs.org/docs/jsx-in-depth.html 下面两种写法是等价的: ```js function App1() { return ; } function App2() { const props = { firstName: "Ben", lastName: "Hector" }; return ; } ``` 那么我们上面的 Profile 的传递代码可以修改为如下代码: ```html ``` 但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的: - React 提供了一个 API:Context; - Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props; - Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言; ### 1.2. Context 相关的 API **React.createContext** ```js const MyContext = React.createContext(defaultValue); ``` 创建一个需要共享的 Context 对象: - 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 `Provider` 中读取到当前的 context 值; - defaultValue 是组件在顶层查找过程中没有找到对应的`Provider`,那么就使用默认值 **Context.Provider** ```html ``` 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化: - Provider 接收一个 `value` 属性,传递给消费组件; - 一个 Provider 可以和多个消费组件有对应关系; - 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据; 当 Provider 的 `value` 值发生变化时,它内部的所有消费组件都会重新渲染; **Class.contextType** ```jsx class MyClass extends React.Component { componentDidMount() { let value = this.context; /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* 基于 MyContext 组件的值进行渲染 */ } } MyClass.contextType = MyContext; ``` 挂载在 class 上的 `contextType` 属性会被重赋值为一个由 `React.createContext()` 创建的 Context 对象: - 这能让你使用 `this.context` 来消费最近 Context 上的那个值; - 你可以在任何生命周期中访问到它,包括 render 函数中; **Context.Consumer** ```html {value => /* 基于 context 值进行渲染*/} ``` 这里,React 组件也可以订阅到 context 变更。这能让你在 `函数式组件` 中完成订阅 context。 - 这里需要 函数作为子元素(function as child)这种做法; - 这个函数接收当前的 context 值,返回一个 React 节点; ### 1.3. Context 使用过程 我们先按照前面三个步骤来使用一个 Context: - 我们就会发现,这个过程中 Profile 是不需要有任何的数据传递的 ```jsx import React, { Component } from "react"; const UserContext = React.createContext({ nickname: "默认", level: -1 }); class ProfileHeader extends Component { render() { return (

    用户昵称: {this.context.nickname}

    用户等级: {this.context.level}

    ); } } ProfileHeader.contextType = UserContext; class Profile extends Component { render() { return (
    • 设置1
    • 设置2
    • 设置3
    • 设置4
    • 设置5
    ); } } export default class App extends Component { render() { return (

    其他内容

    ); } } ``` **什么时候使用默认值 defaultValue 呢?**如果出现了如下代码: - ``并没有作为 `UserContext.Provider` 的子组件; ```html ``` **什么时候使用 Context.Consumer 呢?** - 1.当使用 value 的组件是一个函数式组件时; - 2.当组件中需要使用多个 Context 时; 演练一: ```jsx function ProfileHeader(props) { return (
    {(value) => { return (

    用户昵称: {value.nickname}

    用户等级: {value.level}

    ); }}
    ); } ``` 演练二:当使用 value 的组件是一个函数式组件时; 1.创建一个新的 Context ```js const ThemeContext = React.createContext({ color: "black" }); ``` 2.Provider 的嵌套 ```html ``` 3.使用 Consumer 的嵌套 ```html {value => { return ( { theme => (

    用户昵称: {value.nickname}

    用户等级: {value.level}

    ) }
    ) }}
    ``` 更多用法可以参考官网:https://zh-hans.reactjs.org/docs/context.html 后续我们会学习 Redux 来进行全局的状态管理。 ## 二. 事件总线 ### 2.1. 事件总线的概述 前面通过 Context 主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢? - 在 Vue 中我们可以通过 Vue 的实例,快速实现一个事件总线(EventBus),来完成操作; - 在 React 中,我们可以依赖一个使用较多的库 `events` 来完成对应的操作; 我们可以通过 npm 或者 yarn 来安装 events: ``` yarn add events; ``` events 常用的 API: - 创建 EventEmitter 对象:eventBus 对象; - 发出事件:`eventBus.emit("事件名称", 参数列表);` - 监听事件:`eventBus.addListener("事件名称", 监听函数)`; - 移除事件:`eventBus.removeListener("事件名称", 监听函数)`; ### 2.2. 案例演练 我们通过一个案例来进行演练: ```jsx import React, { Component } from "react"; import { EventEmitter } from "events"; const eventBus = new EventEmitter(); class ProfileHeader extends Component { render() { return (
    ); } btnClick() { eventBus.emit("headerClick", "why", 18); } } class Profile extends Component { render() { return (
    • 设置1
    • 设置2
    • 设置3
    • 设置4
    • 设置5
    ); } } export default class App extends Component { componentDidMount() { eventBus.addListener("headerClick", this.headerClick); } headerClick(name, age) { console.log(name, age); } componentWillUnmount() { eventBus.removeListener("headerClick", this.headerClick); } render() { return (

    其他内容

    ); } } ``` ## 三. 临时知识补充 ### 为什么 constructor 中不是必须传入 props 也能使用 在进行 React 开发中,有一个很奇怪的现象: - 在调用 super()的时候,我没有传入 props,但是在下面的 render 函数中我依然可以使用; - 如果你自己编写一个基础的类,可以尝试一下:这种情况 props 应该是 undefined 的; ```jsx class ChildCpn extends Component { constructor(props) { super(); } render() { const { name, age, height } = this.props; return

    子组件展示数据: {name + " " + age + " " + height}

    ; } } ``` 为什么这么神奇呢? - 我一直喜欢说:计算机中没有黑魔法; - 之所以可以,恰恰是因为 React 担心你的代码会出现上面这种写法而进行了一些 `骚操作`; - React 不管你有没有通过 super 将 props 设置到当前的对象中,它都会重新给你设置一遍; 如何验证呢? - 这就需要通过源码来验证了; - React 的源码 packages 中有提供一个 Test Renderer 的 package; - 这个 package 提供了一个 React 渲染器,用于将 React 组件渲染成纯 JavaScript 对象,不需要依赖 DOM 或原生移动环境; 查看源码: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuPDakHqxuzFuqKicoP1qKiaTicIdImeBotRuzicsGW8AibYvyUNLdicWqAfhFShHKE4cKAKSNziaGHhw89Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)源码位置 我们来看一下这个组件是怎么被创建出来的: - 我们找到其中的 render 函数; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuPDakHqxuzFuqKicoP1qKiaTPOOXhN6rkVdG3rA971dAyNN5FZZpghSxcwjEEb5KoPUX56b0TRvzIQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)render 函数 - render 函数中有这样的一段代码; - - 这个\_instance 实例就是组件对象; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuPDakHqxuzFuqKicoP1qKiaTYlm85Fyqgg8zE7hrnPUbUPFq0kR9vF9eGfhVWgMIoV6qvAE257arAQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)创建\_instance 实例 - 我们再看一下,它在哪里重新赋值的: - - 这里还包括通过 this.\_instance 的方式回调生命周期函数; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuuPDakHqxuzFuqKicoP1qKiaT5XIAIJbx7H799p964WR9U9YJx3p9RYH359HIukF1NZibFFoo7pcy3Yg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)重新赋值的地方 **结论:你无论是否手动的将 props 保存到组件的实例上,React 内部都会帮你保存的,我只能说:骚操作;** # 深入理解 setState > setState 是 React 中使用频率最高的一个 API(当然 hooks 出现之前),它的用法灵活多样,并且也是 React 面试题经常会考的一个知识点。 > > 在这篇文章中,我对 React 的 setState 进行了很多解析,希望可以帮助大家真正理解 setState。(其中涉及到一个源码,我有贴出,但是没有详细展开,有机会我们再对源码进行解析,大家不是很懂也不影响你的学习,只需要知道 React 内部是这样做的即可,面试时也可以回答出来) ## 一. setState 的使用 ### 1.1. 为什么使用 setState 回到最早的案例,当点击一个 `改变文本` 的按钮时,修改界面显示的内容: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxLnVMoGhicB81ibhw9cwxXB5cPftzmeOn8ibHKxYyKwZqpwckepKHA2mbQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)案例 案例的基础代码如下: ```jsx import React, { Component } from "react"; export default class App extends Component { constructor(props) { super(props); this.state = { message: "Hello World", }; } render() { return (

    {this.state.message}

    ); } changeText() {} } ``` **关键是 changeText 中应该如何实现:** 我们是否可以通过直接修改 state 中的 message 来修改界面呢? - 点击不会有任何反应,为什么呢? - 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化; - React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化; - 我们必须通过 setState 来告知 React 数据已经发生了变化; ```js changeText() { this.state.message = "你好啊,李银河"; } ``` 我们必须通过 setState 来更新数据: - 疑惑:在组件中并没有实现 setState 的方法,为什么可以调用呢? - 原因很简单,setState 方法是从 Component 中继承过来的。 ```jsx Component.prototype.setState = function (partialState, callback) { invariant( typeof partialState === "object" || typeof partialState === "function" || partialState == null, "setState(...): takes an object of state variables to update or a " + "function which returns an object of state variables." ); this.updater.enqueueSetState(this, partialState, callback, "setState"); }; ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxhRFRXjhYz93pgwvibdbrmcibd53ddnhQ6og48icRdMyhgSyCCZ58tRIKQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)image-20200624165215233 所以,我们可以通过调用 setState 来修改数据: - 当我们调用 setState 时,会重新执行 render 函数,根据最新的 State 来创建 ReactElement 对象; - 再根据最新的 ReactElement 对象,对 DOM 进行修改; ```js changeText() { this.setState({ message: "你好啊,李银河" }) } ``` ### 1.2. setState 异步更新 我们来看下面的代码: - 最终打印结果是 Hello World; - 可见 setState 是异步的操作,我们并不能在执行完 setState 之后立马拿到最新的 state 的结果 ```js changeText() { this.setState({ message: "你好啊,李银河" }) console.log(this.state.message); // Hello World } ``` 为什么 setState 设计为异步呢? - setState 设计为异步其实之前在 GitHub 上也有很多的讨论; - React 核心成员(Redux 的作者)Dan Abramov 也有对应的回复,有兴趣的同学可以参考一下; - https://github.com/facebook/react/issues/11527#issuecomment-360199710; 我对其回答做一个简单的总结: - `setState`设计为异步,可以显著的提升性能; - - 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的; - 最好的办法应该是获取到多个更新,之后进行批量更新; - 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步; - - state 和 props 不能保持一致性,会在开发中产生很多的问题; 那么如何可以获取到更新后的值呢? - setState 接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行; - 格式如下:`setState(partialState, callback)` ```js changeText() { this.setState({ message: "你好啊,李银河" }, () => { console.log(this.state.message); // 你好啊,李银河 }); } ``` 当然,我们也可以在生命周期函数: ```js componentDidUpdate(prevProps, provState, snapshot) { console.log(this.state.message); } ``` ### 1.3. setState 一定是异步? 疑惑:setState 一定是异步更新的吗? 验证一:在 setTimeout 中的更新: ```js changeText() { setTimeout(() => { this.setState({ message: "你好啊,李银河" }); console.log(this.state.message); // 你好啊,李银河 }, 0); } ``` 验证二:原生 DOM 事件: ```jsx componentDidMount() { const btnEl = document.getElementById("btn"); btnEl.addEventListener('click', () => { this.setState({ message: "你好啊,李银河" }); console.log(this.state.message); // 你好啊,李银河 }) } ``` 其实分成两种情况: - 在组件生命周期或 React 合成事件中,setState 是异步; - 在 setTimeout 或者原生 dom 事件中,setState 是同步; React 中其实是通过一个函数来确定的:enqueueSetState 部分实现(react-reconciler/ReactFiberClassComponent.js) ```jsx enqueueSetState(inst, payload, callback) { const fiber = getInstance(inst); // 会根据React上下文计算一个当前时间 const currentTime = requestCurrentTimeForUpdate(); const suspenseConfig = requestCurrentSuspenseConfig(); // 这个函数会返回当前是同步还是异步更新(准确的说是优先级) const expirationTime = computeExpirationForFiber( currentTime, fiber, suspenseConfig, ); const update = createUpdate(expirationTime, suspenseConfig); ... } ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxc0Q0n6xicLAibO8ap93G4LPscnZlgwXDdkLclNHRwqQSzP2wfTibstPIQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)enqueueSetState 源码 computeExpirationForFiber 函数的部分实现: - Sync 是优先级最高的,即创建就更新; ```jsx currentTime: ExpirationTime, fiber: Fiber, suspenseConfig: null | SuspenseConfig, ): ExpirationTime { const mode = fiber.mode; if ((mode & BlockingMode) === NoMode) { return Sync; } const priorityLevel = getCurrentPriorityLevel(); if ((mode & ConcurrentMode) === NoMode) { return priorityLevel === ImmediatePriority ? Sync : Batched; } ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxg1eSZlNCaIOTeiafERwKqEQJzKPGxWYgIQUJK3cwsUW8G10aEoEVsDA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)computeExpirationForFiber ### 1.4. setState 的合并 #### 1.4.1. 数据的合并 假如我们有这样的数据: ```jsx this.state = { name: "coderwhy", message: "Hello World", }; ``` 我们需要更新 message: - 我通过 setState 去修改 message,是不会对 name 产生影响的; ```jsx changeText() { this.setState({ message: "你好啊,李银河" }); } ``` 为什么不会产生影响呢?源码中其实是有对 `原对象` 和 `新对象进行合并的:` - 事实上就是使用 `Object.assign(target, ...sources)` 来完成的; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxKV5LUp3u60p1nJH7lNglZ2YGjcnsxTdat6iaWvTCicen0Clz9vgwomicA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)React 源码合并数据 #### 1.4.2. 多个 setState 合并 比如我们还是有一个 counter 属性,记录当前的数字: - 如果进行如下操作,那么 counter 会变成几呢?答案是 1; - 为什么呢?因为它会对多个 state 进行合并; ```jsx increment() { this.setState({ counter: this.state.counter + 1 }); this.setState({ counter: this.state.counter + 1 }); this.setState({ counter: this.state.counter + 1 }); } ``` 其实在源码的 processUpdateQueue 中有一个 do...while 循环,就是从队列中取出多个 state 进行合并的; ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxMksAXMnwnbGicAvknB41BBIsxtvuPhk6MmZia8nuRKMA7F1bBZR7QLtw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)React 源码 如何可以做到,让 counter 最终变成 3 呢? ```jsx increment() { this.setState((state, props) => { return { counter: state.counter + 1 } }) this.setState((state, props) => { return { counter: state.counter + 1 } }) this.setState((state, props) => { return { counter: state.counter + 1 } }) } ``` 为什么传入一个函数就可以变出 3 呢? - 原因是多个 state 进行合并时,每次遍历,都会执行一次函数: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFx2MeibeyxLpQWhibp8D7ekHC3UfM1ptbkkuLic5rcSeFobag4ntzibuiaqWQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)传入的函数被多次执行 ## 二. setState 性能优化 ### 2.1. React 更新机制 我们在前面已经学习 React 的渲染流程: ![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)jsx 到虚拟 DOM 到真实 DOM 那么 React 的更新流程呢? ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxdOicRuqkm58AxubpBwO0nodxDgKxo4nPbEDBVoicsg6U52iaR87lyEDNQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)image-20200624090053224 React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树。 React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI: - 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量; - https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf; - 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围; - 这个开销太过昂贵了,React 的更新性能会变得非常低效; 于是,React 对这个算法进行了优化,将其优化成了 O(n),如何优化的呢? - 同层节点之间相互比较,不会垮节点比较; - 不同类型的节点,产生不同的树结构; - 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定; ### 2.2. Diffing 算法 #### 2.2.1. 对比不同类型的元素 当节点为不同的元素,React 会拆卸原有的树,并且建立起新的树: - 当一个元素从 `` 变成 ``,从 `
    ` 变成 ``,或从 `
    ); } insertMovie() {} } ``` 方式一:在最后位置插入数据 - 这种情况,有无 key 意义并不大 ```jsx insertMovie() { const newMovies = [...this.state.movies, "大话西游"]; this.setState({ movies: newMovies }) } ``` 方式二:在前面插入数据 - 这种做法,在没有 key 的情况下,所有的 li 都需要进行修改; ```js insertMovie() { const newMovies = ["大话西游", ...this.state.movies]; this.setState({ movies: newMovies }) } ``` 当子元素(这里的 li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素: - 在下面这种场景下,key 为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改; - 将 key 为 333 的元素插入到最前面的位置即可; ```html
    • 星际穿越
    • 盗梦空间
    • Connecticut
    • 星际穿越
    • 盗梦空间
    ``` key 的注意事项: - key 应该是唯一的; - key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字); - 使用 index 作为 key,对性能是没有优化的; ### 2.4. SCU 的优化 #### 2.4.1. render 函数被调用 我们使用之前的一个嵌套案例: - 在 App 中,我们增加了一个计数器的代码; - 当点击+1 时,会重新调用 App 的 render 函数; - 而当 App 的 render 函数被调用时,所有的子组件的 render 函数都会被重新调用; ```jsx import React, { Component } from "react"; function Header() { console.log("Header Render 被调用"); return

    Header

    ; } class Main extends Component { render() { console.log("Main Render 被调用"); return (
    ); } } function Banner() { console.log("Banner Render 被调用"); return
    Banner
    ; } function ProductList() { console.log("ProductList Render 被调用"); return (
    • 商品1
    • 商品2
    • 商品3
    • 商品4
    • 商品5
    ); } function Footer() { console.log("Footer Render 被调用"); return

    Footer

    ; } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, }; } render() { console.log("App Render 被调用"); return (

    当前计数: {this.state.counter}

    ); } increment() { this.setState({ counter: this.state.counter + 1, }); } } ``` ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxIicv5eQdicHoEcTrWvmibqot3XmkkMZTNCicnb6yxxqtprjq43EAUKTXcQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)嵌套树结构 那么,我们可以思考一下,在以后的开发中,我们只要是修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法,性能必然是很低的: - 事实上,很多的组件没有必须要重新 render; - 它们调用 render 应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的 render 方法; 如何来控制 render 方法是否被调用呢? - 通过`shouldComponentUpdate`方法即可; #### 2.4.2. shouldComponentUpdate React 给我们提供了一个生命周期方法 `shouldComponentUpdate`(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有返回值: - 该方法有两个参数: - - 参数一:nextProps 修改之后,最新的 props 属性 - 参数二:nextState 修改之后,最新的 state 属性 - 该方法返回值是一个 boolean 类型 - - 返回值为 true,那么就需要调用 render 方法; - 返回值为 false,那么久不需要调用 render 方法; - 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法; ``` shouldComponentUpdate(nextProps, nextState) { return true; } ``` 我们可以控制它返回的内容,来决定是否需要重新渲染。 比如我们在 App 中增加一个 message 属性: - jsx 中并没有依赖这个 message,那么它的改变不应该引起重新渲染; - 但是因为 render 监听到 state 的改变,就会重新 render,所以最后 render 方法还是被重新调用了; ```jsx export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, message: "Hello World", }; } render() { console.log("App Render 被调用"); return (

    当前计数: {this.state.counter}

    ); } increment() { this.setState({ counter: this.state.counter + 1, }); } changeText() { this.setState({ message: "你好啊,李银河", }); } } ``` 这个时候,我们可以通过实现 shouldComponentUpdate 来决定要不要重新调用 render 方法: - 这个时候,我们改变 counter 时,会重新渲染; - 如果,我们改变的是 message,那么默认返回的是 false,那么就不会重新渲染; ```jsx shouldComponentUpdate(nextProps, nextState) { if (nextState.counter !== this.state.counter) { return true; } return false; } ``` 但是我们的代码依然没有优化到最好,因为当 counter 改变时,所有的子组件依然重新渲染了: - 所以,事实上,我们应该实现所有的子组件的 shouldComponentUpdate; 比如 Main 组件,可以进行如下实现: - `shouldComponentUpdate`默认返回一个 false; - 在特定情况下,需要更新时,我们在上面添加对应的条件即可; ```jsx class Main extends Component { shouldComponentUpdate(nextProps, nextState) { return false; } render() { console.log("Main Render 被调用"); return (
    ); } } ``` #### 2.4.3. PureComponent 和 memo 如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么? - props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false; 事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢? - 将 class 基础自 PureComponent。 比如我们修改 Main 组件的代码: ```jsx class Main extends PureComponent { render() { console.log("Main Render 被调用"); return (
    ); } } ``` PureComponent 的原理是什么呢? - 对 props 和 state 进行浅层比较; **查看 PureComponent 相关的源码:** react/ReactBaseClasses.js 中: - 在 PureComponent 的原型上增加一个 isPureReactComponent 为 true 的属性 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxmFiaXcY9yib6osTds7P6yGibfxApAQINF3tpCVg0zZZdnr7biaXHtkib02Q/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)PureComponent React-reconcilier/ReactFiberClassComponent.js: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxZ43YSSGgnAbBSLOU3w45XkfibrR7yQob9xBdHaQkoCsfSlOvrA1tCgg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)checkShouldComponentUpdate 这个方法中,调用 `!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)`,这个 shallowEqual 就是进行浅层比较: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxpUwu62Uuq7HcKIFQNFePicviaI8J6tmzuUibiaVs3T6Vfwh7TicTRwKjrXA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)shallowEqual **那么,如果是一个函数式组件呢?** 我们需要使用一个高阶组件 memo: - 我们将之前的 Header、Banner、ProductList 都通过 memo 函数进行一层包裹; - Footer 没有使用 memo 函数进行包裹; - 最终的效果是,当 counter 发生改变时,Header、Banner、ProductList 的函数不会重新执行,而 Footer 的函数会被重新执行; ```jsx import React, { Component, PureComponent, memo } from "react"; const MemoHeader = memo(function () { console.log("Header Render 被调用"); return

    Header

    ; }); class Main extends PureComponent { render() { console.log("Main Render 被调用"); return (
    ); } } const MemoBanner = memo(function () { console.log("Banner Render 被调用"); return
    Banner
    ; }); const MemoProductList = memo(function () { console.log("ProductList Render 被调用"); return (
    • 商品1
    • 商品2
    • 商品3
    • 商品4
    • 商品5
    ); }); function Footer() { console.log("Footer Render 被调用"); return

    Footer

    ; } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, message: "Hello World", }; } render() { console.log("App Render 被调用"); return (

    当前计数: {this.state.counter}

    ); } increment() { this.setState({ counter: this.state.counter + 1, }); } shouldComponentUpdate(nextProps, nextState) { if (nextState.counter !== this.state.counter) { return true; } return false; } changeText() { this.setState({ message: "你好啊,李银河", }); } } ``` **memo 的原理是什么呢?** react/memo.js: - 最终返回一个对象,这个对象中有一个 compare 函数 ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXutB9lC7UoOnZk1MviaCCUqFxUccZHV1znRn92WbbPSl1rMXonnhWicJmzhTmUL7OfBpqYuiao3KDRyjQ/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)memo 函数 #### 2.4.4. 不可变数据的力量 我们通过一个案例来演练我们之前说的不可变数据的重要性: ```jsx import React, { PureComponent } from "react"; export default class App extends PureComponent { constructor(props) { super(props); this.state = { friends: [ { name: "lilei", age: 20, height: 1.76 }, { name: "lucy", age: 18, height: 1.65 }, { name: "tom", age: 30, height: 1.78 }, ], }; } render() { return (

    朋友列表

      {this.state.friends.map((item, index) => { return (
    • {`姓名:${item.name} 年龄: ${item.age}`}
    • ); })}
    ); } insertFriend() {} incrementAge(index) {} } ``` **我们来思考一下 inertFriend 应该如何实现?** 实现方式一: - 这种方式会造成界面不会发生刷新,添加新的数据; - 原因是继承自 PureComponent,会进行浅层比较,浅层比较过程中两个 friends 是相同的对象; ```jsx insertFriend() { this.state.friends.push({name: "why", age: 18, height: 1.88}); this.setState({ friends: this.state.friends }) } ``` 实现方式二: - `[...this.state.friends, {name: "why", age: 18, height: 1.88}]`会生成一个新的数组引用; - 在进行浅层比较时,两个引用的是不同的数组,所以它们是不相同的; ```jsx insertFriend() { this.setState({ friends: [...this.state.friends, {name: "why", age: 18, height: 1.88}] }) } ``` **我们再来思考一下 incrementAge 应该如何实现?** 实现方式一: - 和上面方式一类似 ```jsx incrementAge(index) { this.state.friends[index].age += 1; this.setState({ friends: this.state.friends }) } ``` 实现方式二: - 和上面方式二类似 ```jsx incrementAge(index) { const newFriends = [...this.state.friends]; newFriends[index].age += 1; this.setState({ friends: newFriends }) } ``` 所以,在真实开发中,我们要尽量保证 state、props 中的数据不可变性,这样我们才能合理和安全的使用 PureComponent 和 memo。 当然,后面项目中我会结合 immutable.js 来保证数据的不可变性。 # 受控非受控组件 ## 一. refs 的使用 在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 原生,但是某些特殊的情况,确实需要获取到 DOM 进行某些操作: - 管理焦点,文本选择或媒体播放。 - 触发强制动画。 - 集成第三方 DOM 库。 ### 1.1. 创建 ref 的方式 如何创建 refs 来获取对应的 DOM 呢?目前有三种方式: - 方式一:传入字符串 - - 使用时通过 `this.refs.传入的字符串`格式获取对应的元素; - 方式二:传入一个对象 - - 对象是通过 `React.createRef()` 方式创建出来的; - 使用时获取到创建的对象其中有一个`current`属性就是对应的元素; - 方式三:传入一个函数 - - 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存; - 使用时,直接拿到之前保存的元素对象即可; 代码演练: ``` import React, { PureComponent, createRef } from 'react' export default class App extends PureComponent { constructor(props) { super(props); this.titleRef = createRef(); this.titleEl = null; } render() { return (

    String Ref

    Hello Create Ref

    this.titleEl = element}>Callback Ref

    ) } changeText() { this.refs.title.innerHTML = "你好啊,李银河"; this.titleRef.current.innerHTML = "你好啊,李银河"; this.titleEl.innerHTML = "你好啊,李银河"; } } ``` ### 1.2. ref 节点的类型 ref 的值根据节点的类型而有所不同: - 当 `ref` 属性用于 HTML 元素时,构造函数中使用 `React.createRef()` 创建的 `ref` 接收底层 DOM 元素作为其 `current` 属性; - 当 `ref` 属性用于自定义 class 组件时,`ref` 对象接收组件的挂载实例作为其 `current` 属性; - **你不能在函数组件上使用 `ref` 属性**,因为他们没有实例; 这里我们演示一下 ref 引用一个 class 组件对象: ![图片](https://mmbiz.qpic.cn/mmbiz_jpg/O8xWXzAqXuvFsopQCEXnsr5PfAgan49KjZicPiaQY7FPSbeEnuvSIB1rsagSraseD0Nk09allmwckn0pnSibsEQjA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)class 组件案例 ``` import React, { PureComponent, createRef } from 'react'; class Counter extends PureComponent { constructor(props) { super(props); this.state = { counter: 0 } } render() { return (

    当前计数: {this.state.counter}

    ) } increment() { this.setState({ counter: this.state.counter + 1 }) } } export default class App extends PureComponent { constructor(props) { super(props); this.counterRef = createRef(); } render() { return (
    ) } increment() { this.counterRef.current.increment(); } } ``` 函数式组件是没有实例的,所以无法通过 ref 获取他们的实例: - 但是某些时候,我们可能想要获取函数式组件中的某个 DOM 元素; - 这个时候我们可以通过 `React.forwardRef` ,后面我们也会学习 hooks 中如何使用 ref; ## 二. 受控组件 ### 2.1. 认识受控组件 #### 2.1.1. 默认提交表单方式 在 React 中,HTML 表单的处理方式和普通的 DOM 元素不太一样:表单元素通常会保存在一些内部的 state。 比如下面的 HTML 表单元素: - 这个处理方式是 DOM 默认处理 HTML 表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面; - 在 React 中,并没有禁止这个行为,它依然是有效的; - 但是通常情况下会使用 JavaScript 函数来方便的处理表单提交,同时还可以访问用户填写的表单数据; - 实现这种效果的标准方式是使用“受控组件”; ```
    ``` #### 2.1.2. 受控组件提交表单 在 HTML 中,表单元素(如``、 `