2020-12-02
React
00

目录

前言
1. React 是什么 ?
1.1 React 的特点一:声明式编程
1.2 React 的特点二:组件化开发
1.2 React 的特点三:多平台适配
2. 邂逅:Hello World 案例说明
2.1 React 的开发依赖
2.2 React 依赖的引入
2.3 Hello World
2.4 Hello World 组件化开发
3. JSX 语法
3.1 认识 JSX
3.2 为什么 React 选择了 JSX
3.3 JSX 的使用
3.4 React 事件绑定
3.5 条件渲染
3.6 列表渲染
3.7 JSX 的本质
3.8 虚拟 DOM 的创建过程
4. 脚手架解析
4.1 脚手架是什么呢?
4.2 前端脚手架
4.3 创建 React 项目
4.4 目录结构分析
4.5 了解PWA
5. React 的组件化开发
5.1 类组件
5.2 函数组件
5.3 React 组件常见的生命周期
5.3.1 Constructor
5.3.2 componentDidMount
5.3.3 componentDidUpdate
5.3.4 componentWillUnmount
5.4 React 组件不常见的生命周期
5.5 React 父子组件间的通信
5.5.1 父组件传递子组件
5.5.2 子组件传递父组件
5.6 React 中的插槽
5.7 React 非父子组件之间的通信
5.8 关于 setState
5.8.1 为什么使用 setState ?
5.8.2 setState 异步更新
5.8.3 如何获取异步的结果
5.9 React 性能优化 SCU
5.10 获取 DOM 方式 refs

前言

本文适合零基础的同学、技术栈主Vue的同学快速入门 React,对于有一定的 React 基础同学,相信看完也能有所收获。

1. React 是什么 ?

React 的官方文档 首页有一行说明

image.png

理解为 React 是一个用于构建用户界面的 JavaScript

这里的 web and native 可以理解为 web 浏览器端,以及指定特定平台 iosandroidwindows

React 的特点:

  1. 声明式
  2. 组件化
  3. 一次学习,跨平台编写

1.1 React 的特点一:声明式编程

声明式编程:

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

image.png

1.2 React 的特点二:组件化开发

组件化开发:

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

image.png

1.2 React 的特点三:多平台适配

多平台适配:

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

image.png

2. 邂逅:Hello World 案例说明

为了演练 React,我们可以提出一个小的需求:

  1. 在界面显示一个文本: Hello World
  2. 点击下方的一个按钮,点击后文本改变为 Hello React

在这个案例中我们只是初体验一下 react 开发,如果在这个过程中你碰到有些不理解的内容,没关系跳过,我们后面会有详细的讲解。

image.png

当然,你也可以使用 jQueryVue 来实现,甚至是原生方式来实现,对它们分别进行对比学习

2.1 React 的开发依赖

开发 React 必须依赖三个库:

  1. react:包含 react 所必须的核心代码
  2. react-dom: react 渲染在不同平台所需要的核心代码
  3. babel:将 jsx 转换成 React 代码的工具

FQA:

1. 你可能会问,我学习 Vue 只要依赖一个 vue.js 文件就可以了,为什么 React 要依赖这么多东西呢?

其实呢,这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情,在 React0.14 版本之前是没有 react-dom 这个概念的,所有功能都包含在 react 里。

2. 那么 React 为什么要进行拆分呢?

原因就是 react-nativereact 包中包含了 react webreact-native 所共同拥有的核心代码。

react-dom 针对 webnative 所完成的事情不同:

  1. web 端: react-dom 会将 jsx 最终渲染成真实的 DOM,显示在浏览器中
  2. native 端: react-dom 会将 jsx 最终渲染成原生的控件(比如Android中的Button,iOS中的UIButton)。

3. 为什么还要引入 babel 呢?

babel 是目前前端使用非常广泛的编译器、转移器。比如当下很多浏览器并不支持 ES6 的语法,但是确实 ES6 的语法非常的简洁和方便,我们开发时希望使用它。那么编写源码时我们就可以使用 ES6 来编写,之后通过 Babel 工具,将ES6 转成大多数浏览器都支持的 ES5 的语法。

默认情况下开发 React 其实可以不使用 babel。但是前提是我们自己使用 React.createElement 来编写源代码,它编写的代码非常的繁琐和可读性差。那么我们就可以直接编写 jsx (JavaScript XML)的语法,并且让 babel 帮助我们转换成 React.createElement。关于 jsx 后续还会详细讲到;

2.2 React 依赖的引入

对于上面三个包的依赖,我们可以通过以下三种方式引入:

  1. 方式一:直接CDN引入
  2. 方式二:下载后,添加本地依赖
  3. 方式三:通过npm管理(后续脚手架再使用)

暂时我们下载到本地引入

  1. react 包地址:https://unpkg.com/react@18/umd/react.development.js
  2. react-dom 包地址:https://unpkg.com/react-dom@18/umd/react-dom.development.js
  3. babel 包地址:https://unpkg.com/babel-standalone@6/babel.min.js

image.png

2.3 Hello World

  1. 第一步:在界面上通过 React 显示一个 Hello World
html
<div id="app"></div> <script type="text/babel"> // 1. 定义变量 const message = "Hello World"; const root = ReactDOM.createRoot(document.querySelector("#app")); // 2. 渲染内容 root.render(<h1>{message}</h1>); </script>
  • 在上面的代码中:
    1. ReactDOM.createRoot 函数:用于创建一个 React 根,之后渲染的内容会包含在这个根中。参数:将渲染的内容,挂载到哪一个 HTML 元素上
    2. root.render 函数:渲染内容。参数:要渲染的根组件
    3. 我们可以通过 {} 语法来引入外部的变量或者表达式
  1. 第二步:添加 修改文本 按钮,添加它的点击事件修改 message 变量
html
<div id="app"></div> <script type="text/babel"> // 1. 定义变量 let message = "Hello World"; const handleClick = () => { message = "Hello React"; console.log(message); }; const root = ReactDOM.createRoot(document.querySelector("#app")); // 2. 渲染内容 root.render( <div> <h1>{message}</h1> <button onClick={handleClick}>改变文本</button> </div> ); </script>

上面的代码修改完之后我们可以在控制台中看到message 变成了 Hello React 但是界面上却依然是发现 Hello World,这是为什么呢?

image.png

原因就是我们虽然已经修改了 message,但是并没有重新渲染内容,正确的做法是在 handleClick 方法种再次调用 root.render 方法,但是这样子我们就重复调用了两次 root.render 方法了,所以我们可以将这段逻辑封装一下:

  1. render 函数封装
html
<div id="app"></div> <script type="text/babel"> // 1. 定义变量 let message = "Hello World"; const handleClick = () => { message = "Hello React"; render(); }; const root = ReactDOM.createRoot(document.querySelector("#app")); // 2. 渲染内容 render(); function render() { root.render( <div> <h1>{message}</h1> <button onClick={handleClick}>改变文本</button> </div> ); } </script>

2.4 Hello World 组件化开发

在上一节中我们可以将整个 render 函数逻辑其实可以看做一个整体,将其封装成一个组件:

  1. 我们说过 root.render 参数是一个 HTML 元素或者一个组件;
  2. 所以我们可以先将之前的业务逻辑封装到一个组件中,然后传入到 ReactDOM.render 函数中的第一个参数;

那么,在 React 中,如何封装一个组件呢?这里我们暂时使用类的方式封装组件

  1. 定义一个类(类名大写,组件的名称是必须大写的,小写会被认为是HTML元素),继承自 React.Component
  2. 实现当前组件的 render 函数
    • render 当中返回的 jsx 内容,就是之后 React 会帮助我们渲染的内容
html
<div id="app"></div> <script type="text/babel"> // 1. 定义根组件 class App extends React.Component { render() { return <h1>Hello World</h1>; } } // 2. 渲染跟组件 const root = ReactDOM.createRoot(document.querySelector("#app")); root.render(<App />); </script>

1. 组件化问题一:数据在哪里定义?

在组件中的数据,我们可以分成两类:

  • 参与界面更新的数据:当数据变量时,需要更新组件渲染的内容;
  • 不参与界面更新的数据:当数据变量时,不需要更新将组建渲染的内容;

参与界面更新的数据我们也可以称之为是参与数据流,这个数据是定义在当前对象的 state 中:

  • 我们可以通过在构造函数中 this.state = {定义的数据}
  • 当我们的数据发生变化时,我们可以调用 this.setState 来更新数据,它会通知 React 进行 update 操作。在进行 update 操作时,就会重新调用render 函数,并且使用最新的数据,来渲染界面。
js
class App extends React.Component { constructor() { super(); this.state = { message: "Hello World", }; } render() { return <h1>{this.state.message}</h1>; } }

2. 组件化问题二:事件绑定中的 this

在类中直接定义一个函数,并且将这个函数绑定到元素的 onClick 事件上,当前这个函数的 this 指向的是谁呢?

默认情况下是 undefined,因为在正常的 DOM 操作中,监听点击,监听函数中的 this 其实是节点对象(比如说是button对象)。但是因为 React 并不是直接渲染成真实的 DOM,我们所编写的 button 只是一个语法糖,它的本质 是ReactElement 对象; React 在执行函数时并没有绑定 this,所以默认情况下就是一个 undefined;

我们在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的 this,我们就需要在传入函数时,给这个函数直接绑定this,类似于下面的写法

image.png

好了,至此我们这个案例就已经完成了。

3. JSX 语法

3.1 认识 JSX

js
// 1. 定义根组件 const element = <h1>hello jsx</h1>; // 2. 渲染跟组件 const root = ReactDOM.createRoot(document.querySelector("#app")); root.render(element);

这段 element 变量的声明右侧赋值的标签语法是什么呢?

  • 它不是一段字符串(因为没有使用引号包裹);
  • 它看起来是一段 HTML 元素,但是我们能在 js 中直接给一个变量赋值 html 吗?
  • 其实是不可以的,如果我们将 type="text/babel" 去除掉,那么就会出现语法错误;
  • 它到底是什么呢?其实它是一段jsx的语法;

image.png

JSX 是什么?

  • JSX一种 JavaScript 的语法扩展( extension ),也在很多地方称之为 JavaScript XML,因为看起就是一段 XML 语法;
  • 它用于描述我们的 UI 界面,并且其完成可以和 JavaScript 融合在一起使用;
  • 不同于 Vue 中的模块语法,你不需要专门学习模块语法中的一些指令(比如 v-forv-ifv-elsev-bind);

3.2 为什么 React 选择了 JSX

  • React 认为渲染逻辑本质上与其他 UI 逻辑存在内在耦合

    • 比如 UI 需要绑定事件(button、a原生等等);
    • 比如 UI 中需要展示数据状态;
    • 比如在某些状态发生改变时,又需要改变UI;
  • 他们之间是密不可分,所以 React 没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component);

    • 当然,后面我们还是会继续学习更多组件相关的东西;
  • 在这里,我们只需要知道,JSX 其实是嵌入到 JavaScript 中的一种结构语法;

  • JSX 的书写规范

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

注意:如果是单标签,必须以 /> 结尾;

3.3 JSX 的使用

  1. jsx 中的注释

jsx 中的注释要在一个大括号的包裹下书写

image.png

  1. JSX 嵌入变量作为子元素
  • 情况一:当变量是 NumberStringArray 类型时,可以直接显示

image.png

  • 情况二:当变量是 nullundefinedboolean 类型时,内容为空;

image.png

  • 情况三:Object 对象类型不能作为子元素(not valid as a React child)

image.png

  1. JSX 嵌入表达式

image.png

3.4 React 事件绑定

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

问题一:this 的绑定问题?

在上一章我们也提到了 this 默认指向的是 undefined

  • 原因是 handleClick 函数并不是我们主动调用的,而且当 button 发生改变时,React 内部调用了 btnClick 函数;
  • 而它内部调用时,并不知道要如何绑定正确的 this;

解决 this 的问题有以下三种方法:

  • 方案一:使用箭头函数(个人推荐)
  • 方案二:在事件绑定时通过 bind 改变 this 指向
  • 方案三:在构造方法中通过 bind 改变 this 指向

问题二:事件参数传递问题?

在执行事件函数时,有可能我们需要获取一些参数信息:比如 event对 象、其他参数

  • 情况一:获取 event 对象
    • 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为)
    • 那么默认情况下,event 对象有被直接传入,函数就可以获取到 event 对象;
  • 情况二:获取更多参数
    • 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;
js
class App extends React.Component { btnClick(e, name, age) { console.log(this, event, name, age); } render() { return ( <div> <button onClick={(e) => this.btnClick(e, "coder", 18)}>按钮</button> </div> ); } }

3.5 条件渲染

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

3.6 列表渲染

  • 真实开发中我们会从服务器请求到大量的数据,数据会以列表的形式存储:
    • 比如歌曲、歌手、排行榜列表的数据;
    • 比如商品、购物车、评论列表的数据;
    • 比如好友消息、动态、联系人列表的数据;
  • React 中并没有像 Vue 模块语法中的 v-for 指令,而且需要我们通过 JavaScript 代码的方式组织数据,转成 JSX:
    • 很多从 Vue 转型到 React 的同学非常不习惯,认为 Vue 的方式更加的简洁明了;
    • 但是 React 中的 JSX 正是因为和 JavaScript 无缝的衔接,让它可以更加的灵活;
    • 另外我经常会提到 React 是真正可以提高我们编写代码能力的一种方式;
  • 如何展示列表呢?
    • React 中,展示列表最多的方式就是使用数组的 map 高阶函数;
  • 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理:
    • 比如过滤掉一些内容:filter 函数
    • 比如截取数组中的一部分内容:slice 函数

image.png

列表中的 key

我们会发现在前面的代码中,在 li 标签上添加了一个 key,如果没有添加,React 会报下面的警告

image.png

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

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

3.7 JSX 的本质

  • 实际上,jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。
    • 所有的 jsx 最终都会被转换成 React.createElement 的函数调用。
  • createElement 需要传递三个参数:
  • 参数一:type
    • 当前 ReactElement 的类型;
    • 如果是标签元素,那么就使用字符串表示 “div”;  如果是组件元素,那么就直接使用组件的名称;
  • 参数二:config
    • 所有jsx中的属性都在config中以对象的属性和值的形式存储;  比如传入className作为元素的class;
  • 参数三:children
    • 存放在标签中的内容,以children数组的方式进行存储;

我们知道默认 jsx 是通过 babel 帮我们进行语法转换的,所以我们之前写的jsx 代码都需要依赖 babel

image.png

拿到 babel 编译的代码后,我们不再需要引入 babel 依赖

image.png

3.8 虚拟 DOM 的创建过程

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

  • 这个 ReactElement 对象是什么作用呢? React 为什么要创建它呢?
  • 原因是 React 利用 ReactElement 对象组成了一个 JavaScript 的对象树;
  • JavaScript 的对象树就是虚拟DOM(Virtual DOM);
  • 如何查看 ReactElement 的树结构呢?
    • 我们可以将之前的 jsx 返回结果进行打印;
    • 注意下面代码中我打 jsx 的打印;
  • ReactElement 最终形成的树结构就是 Virtual DOM;

image.png

虚拟DOM帮助我们从命令式编程转到了声明式编程的模式

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

4. 脚手架解析

如果我们只是开发几个小的 demo 程序,那么永远不需要考虑一些复杂的问题

  1. 比如目录结构如何组织划分
  2. 比如如何管理文件之间的相互依赖
  3. 比如如何管理第三方模块的依赖
  4. 比如项目发布前如何压缩、打包项目等等。

现代的前端项目已经越来越复杂了,不会再是在 HTML 中引入几个 css 文件,引入几个编写的 js 文件或者第三方的 js 文件这么简单

  1. 比如 css 可能是使用 lesssass 等预处理器进行编写,我们需要将它们转成普通的 css 才能被浏览器解析
  2. 比如 JavaScript 代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖。
  3. 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等)。

为了解决上面这些问题,我们需要再去学习一些工具,比如 babelwebpackgulp,配置它们转换规则、打包依赖、热更新等等一些的内容,脚手架的出现,就是帮助我们解决这一系列问题的;

4.1 脚手架是什么呢?

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

image.png

编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构;

  1. 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;
  2. 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板
  3. 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可
  4. 这样也可以间接保证项目的基本机构一致性,方便后期的维护;

总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷

4.2 前端脚手架

对于现在比较流行的三大框架都有属于自己的脚手架:

  1. Vue的脚手架:@vue/cli
  2. Angular的脚手架:@angular/cli
  3. React的脚手架:create-react-app

它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好。

使用这些脚手架需要依赖什么呢?

  • 目前这些脚手架都是使用node编写的,并且都是基于webpack的;
  • 所以我们必须在自己的电脑上安装node环境,无论是windows还是Mac OS,都可以通过node官网直接下载:https://nodejs.org/en/download/

image.png

这里推荐大家下载LTS(Long-term support )版本,是长期支持版本,会比较稳定;

4.3 创建 React 项目

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

  1. 创建React项目的命令如下:

    • 注意:项目名称不能包含大写字母
    • 另外还有更多创建项目的方式,可以参考GitHub的readme
  2. 创建完成后,进入对应的目录,就可以将项目跑起来:

create-react-app 项目名称 cd 01-test-react yarn start

image.png

4.4 目录结构分析

我们可以通过VSCode打开项目:

image.png

4.5 了解PWA

整个目录结构都非常好理解,只是有一个PWA相关的概念:

  • PWA全称Progressive Web App,即渐进式WEB应用;
  • 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用;
  • 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能;
  • 这种Web存在的形式,我们也称之为是 Web App;

PWA解决了哪些问题呢?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
  • 实现了消息推送;
  • 等等一系列类似于Native App相关的功能;

更多PWA相关的知识,可以自行去学习更多;

5. React 的组件化开发

组件化是一种分而治之的思想

  • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
  • 但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

React 的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件

  1. 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
  2. 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
  3. 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);

当然还有很多组件的其他概念:比如异步组件、高阶组件等等...

5.1 类组件

类组件的定义有如下要求:

  1. 组件的名称是大写字符开头(无论类组件还是函数组件)
  2. 类组件需要继承自 React.Component
  3. 类组件必须实现render函数

ES6 之前,可以通过 create-react-class 模块来定义类组件,但是目前官网建议我们使用 ES6class 类定义。

使用 class 定义一个组件:

  1. constructor 是可选的,我们通常在 constructor 中初始化一些数据;
  2. this.state 中维护的就是我们组件内部的数据;
  3. render() 方法是 class 组件中唯一必须实现的方法;

image.png

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

5.2 函数组件

函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。

函数组件有自己的特点(当然,后面我们会讲 hooks,就不一样了):

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
  • this 关键字不能指向组件实例(因为没有组件实例);
  • 没有内部状态(state);

我们来定义一个函数组件:

image.png

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

5.3 React 组件常见的生命周期

生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;

  • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
  • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
  • 比如卸载过程(Unmount),组件从DOM树中被移除的过程;

React 内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:

  • 比如实现 componentDidMount 函数:组件已经挂载到 DOM 上时,就会回调;
  • 比如实现 componentDidUpdate 函数:组件已经发生了更新时,就会回调;
  • 比如实现 componentWillUnmount 函数:组件即将被移除时,就会回调;
  • 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;

我们谈 React 生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过 hooks 来模拟一些生命周期的回调)

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

image.png

5.3.1 Constructor

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

constructor 中通常只做两件事情:

  1. 通过给 this.state 赋值对象来初始化内部的 state
  2. 为事件绑定实例(this);

image.png

5.3.2 componentDidMount

  • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
  • componentDidMount 中通常进行哪里操作呢?
    1. 依赖于 DOM 的操作可以在这里进行;
    2. 在此处发送网络请求就最好的地方;(官方建议)
    3. 可以在此处添加一些订阅(会在 componentWillUnmount 取消订阅)

image.png

5.3.3 componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

  • 当组件更新后,可以在此处对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网 络请求)。

5.3.4 componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。

  • 在此方法中执行必要的清理操作;
  • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;

5.4 React 组件不常见的生命周期

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

  1. getDerivedStateFromPropsstate 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新state;
  2. getSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置);
  3. shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;

image.png

5.5 React 父子组件间的通信

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

  • 比如 App 可能使用了多个 Header,每个地方的 Header 展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示;

  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件

总之,在一个 React 项目中,组件之间的通信是非常重要的环节

父组件在展示子组件,可能会传递一些数据给子组件:

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

5.5.1 父组件传递子组件

  1. 类组件
js
import React, { Component } from "react"; class ChildCpn extends Component { constructor(props) { super(); this.props = props; } render() { const { name, age } = this.props; return ( <div> <h2>我是class的组件</h2> <p>展示父组件传递过来的数据:{name + "" + age}</p> </div> ); } } class App extends Component { render() { return ( <div> <ChildCpn name="coder" age="25" /> </div> ); } } export default App;
  1. 函数组件
js
function ChildCpn(props) { const { name, age } = props; return ( <div> <h2>我是class的组件</h2> <p>展示父组件传递过来的数据:{name + "" + age}</p> </div> ); } class App extends Component { render() { return ( <div> <ChildCpn name="coder" age="25" /> </div> ); } }

对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:

  • 当然,如果你项目中默认继承了 Flow 或者 TypeScript,那么直接就可以进行类型验证;
  • 但是,即使我们没有使用 Flow 或者 TypeScript,也可以通过 prop-types 库来进行参数验证;

React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types

js
import PropTypes from 'prop-types'; class ChildCpn extends React.Component { render() { return ( <h1>Hello, {this.props.name}</h1> ); } } ChildCpn.propTypes = { name: PropTypes.string };

更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

  • 比如验证数组,并且数组中包含哪些元素;
  • 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型;
  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

如果我们没有传递,可以使用 defaultProps 设置默认值

js
ChildCpn.defaultProps = { name: 'coder' };

5.5.2 子组件传递父组件

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

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

例如,假设我们有一个父组件 Parent和一个子组件 ChildParent 中包含一个按钮和一个 Child 组件。当用户点击按钮时,我们希望 Child 组件能够向 Parent 组件发送一条消息。

下面是一个实现的例子:

  1. 类组件
js
// Parent.js import React, { Component } from "react"; import Child from "./Child"; class Parent extends Component { constructor(props) { super(props); this.state = { message: "" }; this.handleMessage = this.handleMessage.bind(this); } handleMessage(msg) { this.setState({ message: msg }); } render() { return ( <div> <button onClick={() => this.handleMessage("Hello from Child!")}> Send message to Child </button> <Child onMessage={this.handleMessage} /> <p>Received message from Child: {this.state.message}</p> </div> ); } } export default Parent; // Child.js import React, { Component } from "react"; class Child extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.onMessage("Hello from Child!"); } render() { return <button onClick={this.handleClick}>Send message to Parent</button>; } } export default Child;

在这个例子中,我们使用了类组件来定义了父组件 Parent 和子组件 Child 。在 Parent 中,我们定义了一个名为 handleMessage 的函数,用于接收从子组件 Child 发送的消息。我们将这个函数作为 onMessage 属性传递给 Child 组件。

在子组件 Child 中,我们在点击按钮时调用 props.onMessage 函数,并将消息作为参数传递给它。当用户点击按钮时,Parent 中的 handleMessage 函数被调用,并将接收到的消息存储在 message 状态中。我们最后在 Parent 中展示了接收到的消息。

这样,我们就实现了子组件向父组件通信的功能。

5.6 React 中的插槽

Vue 当中有一个固定的做法是通过 slot 来完成的插槽的,而在 React 对于需要插槽的情况非常灵活,有两种方案可以实现:

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

  1. 组件的 children` 子元素;
js
class NavBar extends Component { render(){ const { children } = this.props reutrn ( <div className="nav-bar"> <div className="left"> {children[0]} </div> <div className="center"> {children[1]} </div> <div className="right"> {children[2]} </div> </div> ) } }
js
<NavBar> <button>按钮</button> <h2>我是标题</h2> <span>span</span> </NavBar>
  1. props 属性传递 React 元素;

通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

另外一个种方案就是使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;

js
class NavBar extends Component { render(){ const { leftSlot, centerSlot, rightSlot } = this.props reutrn ( <div className="nav-bar"> <div className="left"> {leftSlot} </div> <div className="center"> {centerSlot} </div> <div className="right"> {rightSlot} </div> </div> ) } }
js
<NavBar leftSlot={<button>按钮</button>} centerSlot={<h2>我是标题</h2>} rightSlot={<span>span</span>} />

5.7 React 非父子组件之间的通信

非父子组件数据的共享:

  • 在开发中,比较常见的数据传递方式是通过 props 属性自上而下(由父到子)进行传递。
  • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
  • 如果我们在顶层的 App 中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

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

Context 相关 API

  1. React.createContext
  • 创建一个需要共享的Context对象:
  • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
js
const MyContext = React.createContext(defaultValue)
  1. Context.Provider
  • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
  • Provider 接收一个 value 属性,传递给消费组件;
  • 一个 Provider 可以和多个消费组件有对应关系;
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
js
<MyContext.Provider value={/*某个值*/}>
  1. Class.contextType
  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
  • 这能让你使用 this.context 来消费最近 Context 上的那个值;
  • 你可以在任何生命周期中访问到它,包括 render 函数中;
js
MyClass.contextType = MyContext
  1. Context.Consumer
  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context
  • 这里需要 函数作为子元素(function as child)这种做法;
  • 这个函数接收当前的 context 值,返回一个 React 节点;
js
<MyContext.Consumer> { value => /*基于 context 值进行渲染*/} </MyContext.Consumer>

5.8 关于 setState

5.8.1 为什么使用 setState ?

开发中我们并不能直接通过修改state的值来让界面发生更新:

  • 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改React 并不知道数据发生了变 化;
  • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化;
  • 我们必须通过 setState 来告知 React 数据已经发生了变化;

疑惑:在组件中并没有实现 setState 的方法,为什么可以调用呢?

  • 原因很简单,setState 方法是从 Component 中继承过来的

image.png

5.8.2 setState 异步更新

setState 的更新是异步的吗?

js
changeText() { this.setState({ message: 'Hello World' }) console.log(this.state.message) }

上面的代码最终打印结果并不是是 Hello World。可见 setState 是异步的操作,我们并不能在执行完setState 之后立马拿到最新的 state 的结果

为什么 setState 设计为异步呢?

  1. 因为 setState 设计为异步,可以显著的提升性能;

    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  2. 如果同步更新了 state,但是还没有执行 render 函数,那么 stateprops 不能保持同步。 stateprops 不能保持一致性,会在开发中产生很多的问题;

setState 一定是异步吗?

其实要看情况

React18 之前分两种情况:

  1. 在组件生命周期或 React 合成事件中,setState 是异步;
  2. setTimeout 或者原生 dom 事件中,setState 是同步;

React18 之后,默认所有的操作都被放到了批处理中(异步处理)。

如果希望代码可以同步会拿到,则需要执行特殊的 flushSync 操作:

js
flushSync(() => { this.setState({ counter: 8888 }) }) console.log(this.state.counter)

5.8.3 如何获取异步的结果

方式一:setState 的回调

setState 接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行; 格式如下:setState(partialState, callback)

js
changeText() { this.setState({ message: 'Hello World' }, () => { console.log(this.state.message) }) }

方式二: 可以在生命周期函数中获取

js
componentDidUpdate(prevProps, provState, snapShot){ console.log(this.state.message) }

5.9 React 性能优化 SCU

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

该方法有两个参数:

  1. 参数一:nextProps 修改之后,最新的 props 属性
  2. 参数二:nextState 修改之后,最新的 state 属性

该方法返回值是一个 boolean 类型:

  1. 返回值为 true,那么就需要调用 render方法;
  2. 返回值为 false,那么久不需要调用 render方法;
  3. 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;

比如我们在 App 中增加一个 message 属性:

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

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

  • 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
  • props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者false

事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢?

那就是将 class 继承自 PureComponent 即可

针对类组件可以使用 PureComponent,而函数组件可以使用 高阶组件 React.memo() 来实现

5.10 获取 DOM 方式 refs

在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

  • 管理焦点,文本选择或媒体播放
  • 触发强制动画
  • 集成第三方 DOM 库
  • 我们可以通过refs获取DOM;

如何创建refs来获取对应的DOM呢?目前有三种方式:

  1. 方式一:传入字符串

使用时通过 this.refs.传入的字符串格式获取对应的元素;

  1. 方式二:传入一个对象

对象是通过 React.createRef() 方式创建出来的;

使用时获取到创建的对象其中有一个current属性就是对应的元素;

  1. 方式三:传入一个函数

该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;

使用时,直接拿到之前保存的元素对象即可;

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!