React v 16
2025年11月6日, 开发一个旧项目, 使用 16.8.6 的 react.
本笔记只是一个知识点的概览
文档分为两个部分:
- 文档 docs
- 安装
- 主要概念
- 高级指南
- API 参考
- Hooks
- 测试
- 捐赠
- FAQ
- 教程 tutorial
1. 安装
1.1 开始 Getting Started
本节是概览. 算是后续内容的提纲. 可略过.
1.2 将 React 添加到网站中
本阶段的 react 还未形成完整的工程化项目, 给出的案例依旧是使用引入 js 的方式来完成. 本节范围两个部分:
- 一分钟添加 React
- 可选: 在 React 中使用 JSX
1.2.1 一分钟添加 React
主要步骤与要求: 在一个已有的 HTML 中, 通过引入 react.js 来添加 React 组件. 不用依赖其他库, 只需要网络和一点时间.
基本步骤:
- 在 HTML 中添加 DOM 容器 (这一点 react, vue 都一样), 需要 id
- 添加 script 标签 (带上
crossorigin特性), 引入 react.js 和 react-dom.js - 创建 react 组件, 通过一个 script 标签引入.
- 创建 like_button.js 文件.
- 然后找到一个代码 (代码细节不用在意), 拷贝一份到新建文件中. 然后再添加下面的代码.
- 获取容器 DOM 引用
domContainer - 调用
ReactDOM.render(e(LikeButton), domContainer)
- 完成.
补充一下:
react 的 js 文件采用 CDN 引入: react.js, react-dom.js.
拷贝的代码是:
'use strict'; const e = React.createElement; class LikeButton extends React.Component { constructor(props) { super(props); this.state = { liked: false }; } render() { if (this.state.liked) { return 'You liked this.'; } return e( 'button', { onClick: () => this.setState({ liked: true }) }, 'Like' ); } }案例中使用的是开发用 react 代码, 测试完成后可以换成生产代码. 主要区别就是 js 的压缩. 其他细节略.
1.2.2 使用 JSX (在浏览器中使用babel处理JSX)
上述案例可以直接在 浏览器中运行, 因为使用了纯 JS 代码. 但是 react 中常用 JSX. 如下:
return (
<button onClick={() => this.setState({ liked: true })}>
Like
</button>
)
但是浏览器不识别该代码. 可以使用在线转换工具(太麻烦).
1) 一个可行方案
引入一个 js 文件:
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
然后就可以在任意包含 type="text/babel" 的 script 标签中使用 JSX 了.
jk: 经过验证是可行的. 在浏览器中可以跑, 但需要注意
script标签的type属性
文档说, 这个方案仅做测试, 实际开发中应该使用预处理器来处理所有的 script 标签.
2) 在项目中添加 JSX
在项目中使用 JSX, 与预处理 CSS 一样, 也是需要一个预处理工具. 只需要安装 Node 即可. 然后执行下面两个命令:
npm init -y
npm install babel-cli@6 babel-preset-react-app@3
至此, 你的项目就可以处理 JSX 了.
3) 运行 JSX 预处理器
在项目根目录创建 src 目录, 然后运行:
npx babel --watch src --out-dir . --presets react-app/prod
该命令会阻塞终端, 不用管, 在你添加 JSX 代码后它会自动处理.
细节可以参考 babel 的文档.
1.3 创建一个新的 React App
主要介绍工具链, 来体验全新的开发方式. 工具迭代很快, 这里略了.
事实上工具链的使用是一个工程化的方法方式, 如果仅仅是传统的添加 script 标签的方式来开发, 是不需要它的. 完成使用浏览器引入 babel 就可以了. 这也是最简单的处理办法.
1.3.1 推荐的工具链
这里列举了一些在不同条件下使用的工具
- react 应用可以使用 create-react-app
- 服务端渲染可以使用 next.js
- 静态内容的网站, 可以使用 gatsby
- 组件库或集成以后的代码库, 可以使用 more flexible toolchains
1.3.2 create-react-app
react 专用脚手架, 很好用, 基于 webpack, 流程封装的很好.
2025年11月10日, 但似乎今天被 vite 取代.
工具不断的更新, 这里其他略.
1.4 CDN (略)
1.5 资源频道(略)
2 核心概念 (重点)
2.1 Hello World
给出了 React 标志性的代码:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
直观感受一下而已.
2.1.1 如何阅读本指南
本教程是创建一些可用的代码块. 还有一个在做中学的教程, 两个教程是互相补充的. 可以看看目录, 了解一个大概.
2.2 JSX 概述
开了下面变量的声明:
const element = <h1>Hello, World</h1>;
上面的值既不是字符串, 也不是 HTML, 它是 JSX, JSX 是用来创建 React 元素的.
实际上它可以看成一个语法糖, 会被预处理转换为 JS 代码.
2.2.1 为什么使用 JSX
React 将 UI 与逻辑处理混合在一起. React 不是必须使用 JSX, 但是, 编码中, JSX 让界面与代码更为直观.
2.2.2 在 JSX 中嵌入表达式
使用花括号即可.
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
在花括号中可以嵌入任意合法的 js 表达式, 甚至是函数调用.
2.2.3 JSX 实际上也是表达式
编译后, JSX 会编译成函数调用. 这表示可以将 JSX 嵌入到逻辑控制中, 分支, 循环, 作为函数参数, 作为函数返回值等.
2.2.4 在 JSX 中指定特性
标签特性 (attr) 使用双引号, 在 JSX 中使用花括号嵌入 JS 表达式. 例如:
const element = <img src={user.avatarUrl}></img>;
注意不要使用双引号将花括号括起来, 那会编程字符串, 而不是对象.
需要注意的是:
- React 标签必须闭合.
- React 属性使用骆驼命名规则.
class属性必须变成className.
2.2.5 在 JSX 中指定子元素
如果没有子元素, 标签必须闭合. 如果有, 和正常的 XML 设置子元素的方式一样.
2.2.6 避免跨站脚本攻击
React 中插值可以避免跨站脚本攻击. 例如:
const title = response.potentiallyMaliciousInput;
// 下面代码是安全的
const element = <h1>{title}</h1>;
2.2.7 渲染对象
babel 会将 JSX 编译成 React.createElement() 调用. 例如下面两段代码是等价的:
const element = (
<h1 className="greeting">
Hwllo, world!
</h1>
);
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);
使用 React.createElement() 更安全, 因为该方法会进行必要的校验. 实际上, 上述代码会生成下面的对象:
// 简化后的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
这个对象被称为 React 元素, React 就是使用它来渲染页面, 与更新 DOM.
2.3 渲染元素
元素是 React 中最小的构建块. React 元素就是一个纯对象. React 会负责处理渲染. 另外组件是元素的组合.
2.3.1 将元素渲染到 DOM
首先需要一个根元素作为容器, 其内的元素都会由 React 来维护. 要将元素渲染到页面中, 只需要将元素与根同时传递给 ReactDOM.render() 即可:
ReactDOM.render(element, root);
2.3.2 更新元素
React 元素是不可变的 (immutable), 一旦创建, 其属性与子元素都不允许修改. 文档将 React 元素比作电影中的一个帧, 要更新, 就创建一个新的元素, 替换上去, 再调一次 ReactDOM.render() 方法. 例如:
function tick() {
const element = ...
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
但是, 开发中一般只会调用一次
ReactDOM,render(), 下一节介绍状态组件.
2.3.3 React 只会更新我们需要的更新的
React 会比较新旧两个元素, 仅仅会更新不同的部分.
2.4 组件与属性
components and props
组件将 UI分隔中独立的, 可复用部分, 本节是一个概览, 详细可以参考组件API.
组件就是一个 JS 函数, 接收任意的参数 (被称为 props), 然后返回 React 元素, 来渲染页面.
2.4.1 函数与类组件
定义组件的最简单的方法是定义一个 JS 函数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
该函数是有效的 React 组件. 参数与返回值是标准的. 该组件被称为函数组件. 还可以使用 ES6 类来定义组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, { this.props.name }</h1>
}
}
上述两个组件语法是等价的.
2.4.2 渲染组件
之前定义 React 元素都是使用 HTML 标签, 实际上这里也可以是其他 React 元素.
文档中定义了一个
Welcome组件, 然后使用ReactDOM.render()渲染了Welcome, 最后解释了背后实际上进行了什么处理.
2.4.3 组合成组件
组件可以在内部使用其他组件. 利用组件组合 UI.
2.4.4 提取组件
给出一个定义组件的案例, 然后让组件可以复用. 这里有一个注意事项. 即组件的参数.
案例中没有明确定义可以有什么属性, 以及属性的类型. 只是在代码中使用了
props作为属性的引用, 然后在使用组件的时候传入对应名字的参数.props是所有属性的容器.
提取组件的原则是看是否存在多次复用.
2.4.5 属性 (props) 是只读的
无论是函数组件, 还是类组件, 都不要修改 props 中的数据. 保证 React 中的组件一定是纯函数.
2.5 状态与生命周期函数
本节介绍状态与生命周期. 细节可以参考 API.
文档中作者描述了一个案例, 写一个计时器, 每秒钟调用一次
RectDOM.render()来渲染页面, 更新时间显示.
这个方式一般开发不会使用 (实际项目只会调用一次 render). 本节会介绍封装组件的方法, 它拥有自己的计时器, 以及更新方式.
依旧是使用 setInterval() 调用 React.render(), 在每一秒渲染一次 Clock 组件.
然后将函数组件转换为类组件形式:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
每秒钟会调用一次 render() 方法. 但是 Clock 组件是单例的, 其他成员可以被复用. 因此可以在里面添加状态数据.
2.5.1 在类中添加本地状态
- 添加构造函数, 并在构造函数中调用父类构造函数 (同样传参)
- 在构造函数中初始化
state属性 (一个对象) - 在
render()中使用this.state.xxx作为数据源来渲染页面信息.
最后代码变成:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
然后准备为组件添加自己的计时器与更新方法.
2.5.2 在类中添加生命周期方法
通常组件中会在创建时创建资源对象, 并在组件失效被销毁时释放资源.
- 组件在第一次被渲染到 DOM 上时会初始化资源, 这个过程被称为
mounting. - 组件在被移除时释放资源, 这个过程被称为
unmounting.
这时有两个特殊的方法与之对应: componentDidMount() 和 componentWillUnmount. 这些方法被称为生命周期方法. 然后代码可以更新:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
然后作者简要解释了这段代码的执行过程. (略)
这里需要注意
this.setState(...)会更新数据, 然后渲染也会更新.
还应该注意有哪些生命周期函数.
2.5.3 正确使用 state
有三件事需要注意:
- 不要直接修改
state的属性, 通过this.setState()来更新数据. 只允许在构造器中对state赋值. state的更新可能是异步的. 这里引入setState()的第二种用法, 它可以接收一个函数, 返回对象即可. 该函数的第一个参数是前一个state, 而第二个参数是更新后的props.state的更新支持部分更新. react 会将跟新合并到完整的状态数据中去.
2.5.4 数据向下流动
组件中的数据对外是不透明的, 组件也不会知道作为参数的数据是怎么来的.
这算是一个对待数据的原则.
2.6 处理事件
与 DOM 的事件处理类似, 不同在于:
- react 中事件使用骆驼命名规则.
- 在 JSX 中传入函数来作为事件处理器, 而不是字符串.
然后文档对比了 DOM 中与 React 中处理事件的语法:
<tag onClick={eventHandler}></tag>
如果要阻止事件的默认行为, 无法再使用 return false, 而是需要显式调用 preventDefault. 例如:
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
这里有一点需要注意的是, 事件处理函数就是一个类方法, 需要关注方法内部的 this.
- 如果要使用
onClick={this.handler}的方式, 在构造器中需要this.handler.bind(this). - 如果想要避免
this的指向问题, 可以使用onClick={() => this.handler()}.
另外一个注意的事情是传递给事件处理函数的参数. 默认的第一个参数一定是事件处理参数 event 对象.
2.7 条件渲染
React 组件允许根据条件判断对页面进行不同的渲染. 基本逻辑是利用分支结构来返回不同的 React 元组 (组件).
2.8 列表与 key
文档中首先复习了 js 中列表的操作, 使用的是 map 方法.
2.8.1 渲染多个组件
利用 map 方法可以返回 React 元素数组.
注意 React 元素(组件) 本质是一个纯 JS 对象.
文档描述了一个利用数组生成 <li></li> 的数据, 然后将该数组放在 <ul></ul> 中的例子.
2.8.2 基本的列表组件
通常会在某个组件内部渲染一个列表. 参考上一节的例子, 可以定义接收数据数组, 渲染列表的组件. 需要注意的是, map 得到的组件数组, 需要有一个唯一的 key 属性.
key 的目的是让 React 能够识别哪些组件被更新了, 以便维护 DOM 结构. key 一定是唯一的, 可以使用数字或字符串. 如果数组的顺序是不变的, 也可以考虑使用索引.
这里的唯一是在兄弟元素中必须唯一. 但通常也会使用全局唯一.
注意 React 元素(组件) 就是 JS 对象, 因此 map 可以直接返回 React 元素 (组件).
2.9 表单
主要是介绍受控组件. 即在 HTML 中的组件本身携带数据. 而使用 状态, 可以控制组件中携带什么数据, 这便是受控组件. 例如:
<input
value={this.state.value}
onChange={(e) => this.setState(value: e.target.value)}
/>
2.9.1 file input
input:file 是一个特殊的组件, 它的 file 是只读的, 所以它不是一个受控组件. 一般需要配合其他受控组件来使用. 这部分后续介绍.
2.9.2 处理多个输入
这里文档中的多个 input 共用一个 onInputChange 事件, 然后介绍使用事件处理参数来区分不同的控件来更新不同的状态数据.
为了避免错误, 可以考虑为每一个控件添加自己的 change 处理函数, 或封装成组件使用.
2.9.3 注意 null
受控组件设置 value 后, 逻辑上不允许再编辑该组件, 除非不小心设置为 undefined 或 null.
2.10 状态提升 (Lifting State Up)
简单来说就是在处理组件中的事件时, 需要调用组件外部的处理函数将内部数据传递给外部, 来保证其他组件数据的更新.
简化后, 本节内容可以归结为: 在子组件中定义事件.
一般在一个容器组件中, 其多个子组件需要共享数据, 并且数据之间存在更新变化时才会使用到这个内容.
2.11 组合与继承
React 推荐使用组合的方式进行扩展, 而不是使用继承.
2.12 Thinking in React(略)
通过一个案例来解释使用 React 构建页面时的思考方式.
finally 思考:
有几个内容需要验证:
- 父组件数据更新, 子组件传入的数据如何更新?
- 状态提升的操作.
验证结束:
父组件的数据在更新的时候, 子组件的
componentDidUpdate()生命周期函数会被触发, 并且更新的数据也会伴随this.props传递给子组件. 渲染的内容会自动更新.状态提升(略)
补充一点, 当组件的状态数据是一个复杂对象是, 若要更新内部属性, 需要使用覆盖方式. 例如:
this.state = { data: { prop1: 'prop1', prop2: 'prop2' } }在更新属性
prop1的时候, 另一个属性可能会被覆盖.this.setState({ data: { ...this.state.data, prop1: '更新的值' } })这里利用解构拷贝了
this.state.data, 然后利用 覆盖 更新prop1属性.
3. 进阶指南
3.1 无障碍可访问性
Accessibility, 简称 a11y.
文档首先简要解释了 a11y 的含义, 然后说 React 完全支持, 并且通常使用标准 HTML 来实现.
3.1.1 标准与指南
WCAG 是 Web Content Accessibility Guideline, 它是 a11y 的指南. 细节略.
WAI-ARIA 是 Web Accessibility Initiative - Accessible Rich Internet Application, 它是在标签中添加 aria-* 的属性, 来补充标签功能. 细节略.
在 React 中属性都使用骆驼命名规则, 但是
aria-*采用kebab-case.
Semantic HTML 即语义化 HTML., 可参考 HTML 元素. 细节略.
通常在使用 JSX 生成 HTML 的时候会破坏语义化 (一些片段等). React 推荐使用
Fragment组件将其片段包起来. 例如:function ListItem({ item }) { return ( <Fragment> <dt>{item.term}</dt> <dd>{item.description}</dd> </Fragment> ); }特别是在使用 列表, 表格片段时常用. 细节可以参考文档.
本节后续是一些实现与注意, 暂略.
3.2 代码分离
code-splitting
文档首先介绍了代码会被打包工具打包到一起. 并且有很多打包工具可以使用.
然后文档说到随着项目的扩大, 第三包的引入, 打包也会变大, 首次加载也会变慢. 需要将其分离, 打包成多个小包, 按需加载, 懒加载. 提高首屏的加载速度.
实现编码分隔的最优方案是使用 import. 如下:
import('...').then(xxx => {
// 在此处使用导入模块
});
webpack 在遇到这段代码时, 会自动处理. 如果使用 create-react-app 或在 Next.js 可以直接使用.
如果是使用 Babel, 需要确保是否可以转换动态导入语法. 如果不能需要安装 @babel/plugin-syntax-dynamic-import.
React.lazy
需要注意的是
React.lazy和Suspense无法在服务端渲染中使用.
React.lazy 可以让动态加载组件像正常组件那样使用.
// 正常组件的导入
import OtherComponent from './OtherComponent';
// 使用 lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'));
该代码会保证组件第一次被渲染时加载对应的包. 需要注意的是, import 是异步的, 并且组件必须为默认导出.
lazy 组件需要在 Suspense 组件中渲染, 这允许在等待组件渲染的过程中添加等待动效
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
fallback 属性接收一个组件, 以显示加载等待动效. Suspense 组件中可以放置多个 lazy 组件, 该组件可以放在任意有 lazy 组件的位置.
如果 lazy 组件加载失败, 它会抛出一个错误, 可以使用 Error Bounderies 来处理.
Error Bounderies是什么? 后面有一个专题来说明.
基于路由来分割代码
怎么分割, 哪些需要懒加载, 哪些不要. 在不影响用户体验的情况下是比较难选择的. 使用路由来分割处理是一个不错的方案. 这主要是从大多数用户习惯中来的方案.
下面是一个示例:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
具名导出
lazy 组件现仅支持默认导出, 如果组件不是默认导出可以借助与中间文件:
// MyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
3.3 Context (上下文)
主要用于在组件中传递数据
传统 React 组件的数据传递是从上至下的, 即从父组件到子组件, 通过 props 来传递数据. 为了简化过深嵌套的组件的数据传递, 可以使用 context.
痛点:
- 数据必须一直传递下去, 嵌套层级过深.
- 在传递过程中, 数据也会传递到其他中间组件中.
Context 目的是为了在多层嵌套组件中传递数据, 但是使用依旧需要注意. 因为这种依赖使得组件复用变得困难 (不受参数影响, 有点类似于 Vue 中事件总线带来的问题).
猜测是让纯函数变得非纯了, 影响组件渲染的不再仅仅是参数.
然后再文档中推荐使用组件组合来构建页面, 而不是嵌套组件来构建页面.
组合组件同样存在一些问题, 传递的数据会变得更为复杂. 特别是仅在内部(嵌套较深)的组件需要增加某些数据时, 可能会依次向上添加新的参数. 一个处理办法是, 在上层组件构建复杂的模式, 将子组件作为组合组件进行传递, 这样将深层组件提升到较高层级来获得参数. 同样这会给组件提高复杂性.
function Page(props) { const user = props.user; const content = <Feed user={user} />; const topBar = ( <NavigationBar> <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> </NavigationBar> ); return ( <PageLayout topBar={topBar} content={content} /> ); }上述代码中, 相当于将
Avator提升到Page层级.
使用上下文可以将数据与层级(界面)解耦, 处理会更为灵活.
API
React.createContext
const MyContext = React.createContext(defaultValue);
改代码会创建一个上下文对象, 并提供默认值. React 在渲染组件时, 如果使用了该上下文, 子组件会在最近的父组件的 Provider 中读取对应数据. 如果没有捕获到 Provider, 则会使用默认值.
需要注意的是, 传递 undefined, 也是会覆盖默认值的.
Context.Provider
<MyContext.Provider value={/* 隐藏默认值 */}></MyContext.Provider>
并不会影响到上层组件中的默认值使用, 只会影响到其下层组件中对上下文的使用.
每一个上下文组件都有一个 Provider 组件. 当其 value 发生更新时, 其所有订阅的子组件都会重新渲染. 并且这个渲染会不受 shouldComponentUpdate() 影响. 包括 .contextType 和 useContext.
需要注意的是它采用
Object.is来判断对象, 如果传入对象包含value属性会有问题.
class.contextType
class MyComponent extends React.Component {
...
static contextType = MyContext
...
}
设置 .contextType 之后, 就可以在 this.context 中获取到数据了. 一个组件只允许使用一个 Context.
Context.Consumer
<Context.Consumer>
{value => /* 基于该 context 来渲染 */}
</Context.Consumer>
订阅了上下文更新的组件, 可以使用这个方法来渲染一个函数式组件.
使用了该语法就相当于完成了订阅, 不需要设置
contextType. 如果需要使用this.context, 那么就必须设置contextType.
Context.displayName
该属性是一个字符串, 用于在 ReactDevTools 中显示该上下文的名字.
需要在子组件中更新数据
只需要在 Context 绑定事件处理函数即可.
个人认为这个混乱的开始.
如果需要绑定多个 Context
那就需要使用嵌套的放置, 以 Context.Sonsumer 的方式来订阅. 这是为了更快的渲染.
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
如果两个上下文总是一起呈现, 就需要自定义渲染组件了 (复用).
警告
由于上下文的引用决定重新渲染, 所以有些陷阱会造成所有的子组件都会被重新渲染. 例如下面这样使用:
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
因为每次都会创建一个新的对象. 为了避开这个问题, 应该将其提升到状态中.
因为组件渲染的时候, 只是调用
render方法, 组件实例不会重新创建, 但是将对象写在 JSX 中, 每次渲染就会重新创建这个对象. 它影响的是子组件.
3.4 Error Boundaries
React 16 引入的错误处理方法.
3.4.1 Error Boundaries 简介
UI 中的 JavaScript 错误可能会影响到整个 app, 为了解决这个问题, React 16 引入了这个新概念.
Error Boundaries 是一个 React 组件, 它可以捕获所有子组件中的错误, 它会记录该错误并展示 fullback UI, 而不是组件内的内容. 会在渲染的时候捕获其下组件, 在生命周期中, 构造器中的错误.
整个页面是一棵树, 组件下就是树枝下的内容.
注意: 错误的捕获不包括
- 事件处理中的错误.
- 异步代码中的错误.
- 服务端渲染的错误.
- 自己本身抛出的错误 (只捕获子成员).
似乎现阶段只支持类组件.
一个类组件就是一个 Error Boundaries, 如果它定义了下面的方法(一个或两个):
static getDerivedStateFormError(), 用于在出现错误时渲染 fallback UI.componentDataCatch(), 用于在出现错误时记录日志, 打印错误消息.
下面是一个使用示例:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
使用该组件与其他正常组件的用法一样:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
Error Boundaries 的适用机制与 JS 中的 catch {} 块类似. 文档中明确说明了只有类组件才可以定义 Error Boundaries. 而且只用定义一次, 就可以在代码中随处使用.
需要注意的是, 仅会捕获在其树下的组件错误. 自己是无法捕获自己的错误. 错误会向上传播.
对于事件处理程序的错误, 可以自己用 try-catch 来处理.
其他略
3.5 传递 Ref (Forwarding Refs)
它主要用在将组件引用传递给其某个子组件上, 属于特殊用法, 一般比较少用.
正常情况下, 组件对外是隐藏内部实现的, 这样是合理的设计方案, 可以避免过度依赖组件内部 DOM 的结构 (算是一种解耦吧).
通常也不需要单独引用组件内部的某个具体的 DOM 元素. 但是某些自定义行为, 特效的时候不可避免的会进行 DOM 操作.
疑问是, 可能引用自定义组件吗? 逻辑上是可行的. 后续文档也描述了, 这是可以的.
传递 Ref 是一种解决方案. 它可以让一些组件通过 ref 来接收引用, 然后将其传递到子组件中, 这样父组件就可以利用 ref 来获得子组件的引用了. 例如下面代码:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
这样, 父组件就可以利用 ref 直接访问内部的 button 了, 就好像直接在使用该按钮一样.
有点像在函数外定义一个指针, 将指针传入函数中进行初始化, 然后在函数外就可以通过该指针来访问这个在函数内初始化的内容了.
上述代码的内部逻辑:
- 调用
React.createRef()定义一个 React ref, 并将其赋值给变量ref. - 将
ref作为参数, 利用<FancyButton ref={ref} >进行向下传递. - React 会将
ref作为, 由React.forwardRef()处理的函数组件的第二个参数传入. - 通过给子组件提供
ref={ref}来指定它引用什么组件. - 让
ref加载完成时,ref.current将会指向对应的<button>DOM 节点.
注意:
ref参数只会出现在React.forwardRef()包裹的函数式组件的第二个参数位置.- 标准的函数式组件, 以及类组件不会接收
ref参数.ref参数也不会出现在props中.ref不仅仅可以引用DOM元素, 也可以是其他组件.
然后文档中给出了一些使用的说明:
- 引入
ref的时候应该将项目作为一个重大更新进行维护 - 多在高阶组件中使用, 并且给出了一个 DOM 加以说明. 同时提供了一个将类组件转换为函数式组件的方案, 就是定义函数式组件
() => <类组件 />来处理. - 为了在
ReactDevTools方便查看, 可以在函数上定义displayName.
3.6 Fragment
其逻辑与
HTML中的DocumentFragment类似. React 要求每一个组件只允许有一个根节点. , 使用Fragment可以包装多个子组件, 而不会引入新的节点.
实际上就是 <></>.
文档首先引入一个 demo, 当封装 <td> 标签构成的组件时, 容易引入其他组件, 使得标准的 <table> 结构被破坏.
用法说明:
<React.Fragment></React.Fragment>
或者是:
<></>
该组件唯一的属性是 key, 在循环生成 Fragment 的时候使用.
3.7 高阶组件 (Higher-Order-Components, HOCs)
简单的说, 高阶组件就是一个函数, 其参数是一个组件, 返回值也是一个组件.
const EnhancedComponent = heigherOrderComponent(WrappedComponent);
组件将属性转换为 UI, 而高阶组件将组件转换为另一个组件. 高阶组件在第三方库中很常见. 例如 antd 中的 Form.create() 等.
首先文档使用了一个较大的篇幅介绍了一下背景, 并演示两个案例来说明构建组件时, 有些组件中存在同样的模式, 这样就可以将其行为抽象出来, 封装成一个通用的模式, 然后利用参数来确定模式该如何作用到对应的组件中. 这算是一个使用指南.
然后文档说明中有解释, 不要修改原始组件. 利用组合等方法派生新的组件.
然后文档说明了一些原则(约定), 以及一些陷阱. 细节略.
这个话题比较深入, 并且可以是长篇大论的讨论. 这里不作说明.
3.8 与第三方组件集成
3.8.1 集成 DOM 操作插件
在 React 系统之外对 DOM 的操作 React 是无法监视的, 因此为了保证 React 在渲染页面时不会破坏第三方库对 DOM 的更新, 就是让 React 不要更新对应的 DOM, 那么可以利用一个 React 无法更新的 DOM, 如空的 <div /> 来呈现其他工具处理的 DOM 即可.
然后文档使用 jQuery 插件来说明了这个方法的实现. 只要让 render 返回的元素没有属性, 那么 React 就没有理由重新渲染该组件.
class SomePlugin extends React.Component {
componentDidMount() {
this.$el = $(this.el);
this.$el.somePlugin();
}
componentWillUnmount() {
this.$el.somePlugin('destroy');
}
render() {
return <div ref={el => this.el = el} />;
}
}
需要注意, 在组件中, 使用 ref 来活动 DOM 引用的办法. 代码中 $el 相当于 jQuery 对象. 同时需要注意的是释放资源的逻辑.
然后文档又解释了怎么在 React 体系下, 利用 jQuery 绑定事件.
componentDidMount() {
this.$el = $(this.el);
this.$el.chosen();
this.handleChange = this.handleChange.bind(this);
this.$el.on('change', this.handleChange);
}
componentWillUnmount() {
this.$el.off('change', this.handleChange);
this.$el.chosen('destroy');
}
handleChange(e) {
this.props.onChange(e.target.value);
}
然后文档又说明了如何集成渲染函数. 例如:
function Button() {
return <button id="btn">Say Hello</button>;
}
ReactDOM.render(
<Button />,
document.getElementById('container'),
function() {
$('#btn').click(function() {
alert('Hello!');
});
}
);
本质上是利用 ReactDOM.render(组件, 容器[, 组件加载完成后执行的回调函数]) 函数的功能. 除了集成 jQuery, 又介绍了如何集成 Backbone. 这里细节略.
3.9 深入 JSX
JSX 本质上是
React.createElement(component, props, ...children)的语法糖.
每一个 JSX 都会被渲染成 React.createElement 调用, 每一个组件数据都会被转换成一个对象. 如果想要验证可以使用在线 Babel 编译工具.
babel 工具不知是否还可以使用.
3.9.1 执行 React 元素的类型
- 使用 React 组件, 其标签名即为类型名.
- 使用 React 组件, 必须保证组件在作用域中.
- 如果一个模块返回多个组件, 可以使用点分的命名规则来使用组件.
- 组件的名字必须使用首字母大写的命名规则, 全小写会被认定为 html 标签.
- 允许使用变量名来引用组件, 然后使用这个变量作为组件来实现动态控制 (变量也需要首字母大写).
3.9.2 props
js表达式可以作为props, 但需要使用花括号{}, 并且仅会取其值.- 字符串可以作为
props. 也可以是字符串字面值, 转义字符序列等. 需要注意{'字符串'}的表示方法. - 不带属性值的属性默认值为
true. Attribute解构.<Component {...props}>的用法 (与Vue的v-bind类似).- 子组件 (children in JSX)
- 可以是字符串 (会移动移除两端的空白)
- 可以是其他子组件.
- React 的 render 函数可以返回元素数组 (注意
key属性) - 子组件可以是
js表达式. - 也可以是一个函数 (函数式组件)
- 子组件位置如果是
null,undefined, 或布尔值都会被忽略. 如果要显示将其转换为字符串.
3.10 性能优化
略, 这部分不同版本与工具差异较大. 并且篇幅会很大.
3.11 Portals
简单来说就是在 DOM 节点染组件, 逻辑上类似于 DocumentFragment. 因为一般的 render 方法是返回一个 React 节点, 然后直接渲染到页面 DOM 树中. 这个方法是让其可以指定渲染在哪里. 通常使用在父组件处于隐藏等状态的情况下.
基本语法:
ReactDOM.createPortal(child, container)
第一个参数是一个 可渲染的节点. 可以理解为 React 组件. 第二个参数是容器, 是一个 DOM 节点. 它会将 child 渲染到 container 下. 例如:
render () {
return ReactDOM.createPortal(
this.props.children,
domNode
)
}
使用 Portal 需要注意的是管理键盘焦点的捕获.
然后文档构造了一个特殊的案例, 来说明事件冒泡.
- 创建一个逻辑树为
Parent,Modal,Child的组件模型. 其中Child中有一个按钮.Modal通过Portal将其挂载到另一个DOM节点上, 让物理树与逻辑树分离. - 然后在
Parent组件中注册onClick事件. - 触发
Child中按钮的点击事件. - 虽然物理上, 组件不在一棵
DOM树中, 但是事件冒泡依旧遵循逻辑树的层次进行传递.
3.12 Profiler API
注入一些测量用的方法, 专门用于性能调用, 并且在生产环境下无效, 仅用于开发性能调优.
用法:
Profiler标签可以用在任何位置, 它会监视对应节点的渲染时机.- 使用上只需要两个属性, 一个是
id, 另一个是onRender回调函数.
其中 onRender 的函数签名为:
function onRenderCallback(
id, // 提交 id, 用于唯一标识
phase, // 渲染动作: mounted/update
actualDuration, // 更新用时
baseDuration, // 渲染整个树的估计时间
startTime, // 开始渲染的时间
commitTime, // 完成渲染的时间
interactions // 属于本次更新的交互集合
) {
// 在此处聚合渲染时间
}
注意, 该方式尽可能少用, 会消耗大量性能.
3.13 在无 ES6 的环境下使用 React (略)
3.14 无 JSX 的 React (略)
JSX 本是语法糖, 最终都是对象与函数调用.
3.15 Reconciliation (略)
主要介绍了 React 中的差分算法的一些细节. 这里有时代背景, 略.
3.16 Refs 与 DOM
ref用于访问DOM或 React 节点的实例.
4. API 参考
5. Hook
5.1 Hook 简介与概览
Hook 是在 React 16.8 之后引入的.
基本用法
- 导入 Hook API
- 在函数组件中使用 (在函数顶层)
Hook 是为了解决函数式组件无状态控制而引入的.
5.1.1 常用的 Hook
本阶段的 Hook 包括
- State Hook. 用于提供状态数据的控制.
- Effect Hook. 用于补充生命周期函数:
componentDidMount,componentDidUpdate, 以及componentWillUnmount. - Context Hook.
- Reducer Hook.
5.1.2 Hook 使用规则
Hook 就是 JavaScript 的函数, 使用的时候有两点必须满足的规则
- 只在顶层使用 Hook. 不能将 Hook 放在循环, 分支, 以及嵌套函数中.
- 仅在 React 函数式组件中使用 Hook. 不要在其他 JavaScript 函数中调用 Hook, 除非自定义 Hook 的时候 (自定义 Hook 是一个 JavaScript 函数).
自定义 Hook 是在复用一些有状态的组件时才会使用. 实际上就是将重复的状态控制打包到一起. 但是应该避免这种打包多个 Hook 到自定义 Hook 中包含其他复杂逻辑. 应尽可能简洁.