使用 React 进行项目开发也有好几个项目了,趁着最近有空来对 React 的知识做一个简单的复盘。
完整目录概览
React 是单向数据流还是双向数据流?它还有其他特点吗?
React 是单向数据流,数据是从上向下流。它的其他主要特点时:
setState
React 通过什么方式来更新数据
React 是通过
setState
来更新数据的。调用多个
setState
不会立即更新数据,而会批量延迟更新后再将数据合并。
除了
setState
外还可以使用
forceUpdate
跳过当前组件的
shouldComponentUpdate
diff,强制触发组件渲染(避免使用该方式)。
React 不能直接修改 State 吗?
setState 是同步还是异步的?
出于性能的考虑,React 可能会把多个
setState
合并成一个调用。
不受 React 控制的代码快中使用
setState
是同步的,比如在
setTimeout
或是原生的事件监听器中使用。
setState 小测
1 |
componentDidMount() {
|
1 |
1 --> 0 |
React 生命周期
React 的生命周期主要是指组件 在特定阶段会执行的函数 。以下是 class 组件的部分 生命周期图谱 :
从上图可以看出:React 的生命周期按照类型划分,可分为 挂载时(Mounting)、更新时(Updating)、卸载时(Unmounting) 。图中的生命周期函数效果如下:
constructor (构造函数)
static getDerivedStateFromProps
-
触发条件
: 调用
render
函数之前 -
是否可以使用
setState
: X -
函数行为
: 函数可以返回一个对象用于更新组件内部的
state
数据,若返回null
则什么都不更新。 - 使用场景 : 用于 state 依赖 props 的情况,也就是状态派生。值得注意的是派生 state 会导致代码冗余,并使组件难以维护。
shouldComponentUpdate
-
触发条件
: 当
props
/state
发生变化 -
是否可以使用
setState
: X -
函数行为
: 函数的返回值决定组件是否触发
render
,返回值为true
则触发渲染,反之则阻止渲染。(组件内不写该函数的话,则调用默认函数。默认函数只会返回true
,即只要props
/state
发生变化,就更新组件) -
使用场景
: 组件的性能优化,仅仅是浅比较 props 和 state 的变化的话,可以使用内置的
PureComponent
来代替
Component
组件。
render
getSnapshotBeforeUpdate
- 触发条件 : 在最近一次渲染输出(提交到 DOM 节点)之前调用
-
是否可以使用
setState
: X -
函数行为
: 函数的返回值将传入给
componentDidUpdate
第三个参数中。若只实现了该函数,但没有使用componentDidUpdate
的话,React 将会在控制台抛出警告 - 使用场景 : 可以在组件发生更改之前从 DOM 中捕获一些信息(例如,列表的滚动位置)
componentDidMount
- 触发条件 : 组件挂载后(插入 DOM 树中)立即调用,该函数只会被触发一次
-
是否可以使用
setState
: Y (可以 直接调用 ,但会触发额外渲染) - 使用场景 : 从网络请求中获取数据、订阅事件等
componentDidUpdate
- 触发条件 : 组件更新完毕后(首次渲染不会触发)
-
是否可以使用
setState
: Y (更新语句须 放在条件语句 中,不然可能会造成死循环) - 使用场景 : 对比新旧值的变化,进而判断是否需要发送网络请求。比如监听路由的变化
componentWillUnmount
生命周期阶段
针对 React 生命周期中函数的调用顺序,笔者写了一个简易的 Demo 用于演示: React 生命周期示例
React 组件挂载阶段
先后会触发
constuctor
、
static getDerivedStateFromProps
、
render
、
componentDidMount
函数。若
render
函数内还有子组件存在的话,则会进一步递归:
1 |
[Parent]: constuctor |
React 组件更新阶段
主要是组件的 props 或 state 发生变化时触发。若组件内还有子组件,则子组件会判断是否也需要触发更新。默认情况下
component
组件是只要父组件发生了变化,子组件也会跟着变化。以下是更新父组件
state
数据时所触发的生命周期函数:
1 |
[Parent]: static getDerivedStateFromProps |
值得注意的是: 在本例 Demo 中没有给子组件传参,但子组件也触发了渲染。但从应用的角度上考虑,既然你子组件没有需要更新的东西,那就没有必要触发渲染吧?
因此
Component
组件上可以使用
shouldComponentUpdate
或者将
Component
组件替换为
PureComponment
组件来做优化。在生命周期图中也可以看到:
shouldComponentUpdate
返回
false
时,将不再继续触发下面的函数。
有时你可能在某些情况下想主动触发渲染而又不被
shouldComponentUpdate
阻止渲染该怎么办呢?可以使用
forceUpdate()
跳过
shouldComponentUpdate
的 diff,进而渲染视图。(需要使用强制渲染的场景较少,一般不推荐这种方式进行开发)
React 组件销毁阶段 也没啥好说的了。父组件先触发销毁前的函数,再逐层向下触发:
1 |
[Parent]: componentWillUnmount |
其他生命周期
除了上图比较常见的生命周期外,还有一些过时的 API 就没有额外介绍了。因为它们可能在未来的版本会被移除:
- UNSAFE_componentWillMount() : 在组件即将被挂载到页面的时刻自动执行。应该使用 componentDidUpdate 来代替该函数。
- UNSAFE_componentWillUpdate()
-
UNSAFE_componentWillReceiveProps()
:当父组件某个 props 更新前,可以调用
setState
覆盖内部的某个 state。
上图没有给出错误处理的情况,以下信息作为补充: 当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
React 组件通信
- 父组件通过 props 给子组件传递数据。子组件通过触发父组件提供的回调函数来给父组件传递消息或数据
-
React.Context
可以跨层级组件共享数据 - 自定义事件
-
引入
Redux
/Mobx
之类的状态管理器
React.Context 怎么使用
Context
可以共享对于组件树而言是全局的数据,比如全局主题、首选语言等。使用方式如下:
-
React.createContext
函数用于生成Context
对象。可以在创建时给Context
设置默认值:1
const ThemeContext = React.createContext('light');
-
Context
对象中有一个Provider(提供者)
组件,Provider
组件接受一个value
属性用以将数据传递给消费组件。1
2
3<ThemeContext.Provider value="dark">
<page />
</ThemeContext.Provider> -
获取
Context
提供的值可以通过contextType
或者Consumer(消费者)
组件中获取。contextType
只能用于类组件,并且只能挂载一个Context
:1
2
3
4
5
6
7
8
9
10
11class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 的值执行一些有副作用的操作 */
}
render() {
let value = this.context;
/* 基于 MyContext 的值进行渲染 */
}
}
MyClass.contextType = MyContext;若想给组件挂载多个
Context
, 或者在函数组件内使用Context
可以使用Consumer
组件:1
2
3
4
5
6
7
8
9<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
Context
通常适用于传递较为简单的数据信息,若数据太过复杂,还是需要引入状态管理(
Redux
/
Mbox
)。
函数组件是什么?与类组件有什么区别?
函数组件本质上是一个纯函数,它接受 props 属性,最后返回 JSX。
Hooks
Hook vs class
- 状态逻辑复用难,缺少复用机制。渲染属性和高阶组件导致层级冗余。
- 组件趋向复杂难以维护。生命周期函数混杂不相干逻辑,相干逻辑分散在不同生命周期中。
- this 指向令人困扰。内联函数过度创建新句柄,类成员函数不能保证 this。
Hooks 的使用
-
除此之外,
useEffect
还可以返回一个函数用于做清除操作,这个清除操作时可选的。常用于清理订阅事件、DOM 事件等。1
2
3
4
5
6
7
8
9
10// 绑定 DOM 事件
useEffect(() => {
document.addEventListener('click', handleClick);
// useEffect 回调函数的返回值是函数的话,当组件卸载时会执行该函数
// 若没有需要清除的东西,则可以忽略这一步骤
return () => {
document.removeEventListener('click', handleClick);
};
}, [handleClick]); -
useContext
: 接收一个Context
对象,并返回Context
的当前值。相当于类组件的static contextType = MyContext
。 -
useCallbck
用于缓存函数,避免函数被重复创建,它是useMemo
的语法糖。useCallback(fn, deps)
的效果相当于是useMemo(() => fn, deps)
。
Hook 之间的一些差异
-
React.useStatus 与 React.useRef :
React.useStatus
相当于类的state
;React.useRef
相当于类的内部属性。前者参与渲染,后者的修改不会触发渲染。
自定义 Hook 的使用
下面是 useLocalStorage 的实现,它将 state 同步到本地存储,以使其在页面刷新后保持不变。 用法与 useState 相似,不同之处在于我们传入了本地存储键,以便我们可以在页面加载时默认为该值,而不是指定的初始值。
1 |
import { useState } from 'react'; |
注意: 自定义 Hook 函数在定义时,也可以使用另一个自定义 Hook 函数。
Hook 使用约束
class 组件与 Hook 之间的映射与转换
函数组件相比 class 组件会缺少很多功能,但大多可以通过 Hook 的方式来实现。
生命周期
-
getDerivedStateFromProps :
getDerivedStateFromProps
一般用于在组件 props 发生变化时派生state
。Hooks 实现同等效果如下:1
2
3
4
5
6
7
8
9
10
11
12function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
} -
1
2
3const Button = React.memo((props) => {
return <button>{props.text}</button>
}); -
componentDidMount / componentDidUpdate / componentWillUnmount :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 没有依赖项,仅执行一次
useEffect(() => {
const subscription = props.source.subscribe();
// 相当于 componentWillUnmount
return () => {
subscription.unsubscribe();
};
}, []);
// 若有依赖项,相当于 componentDidUpdate
// 当 page 发生变化时会触发 effect 函数
useEffect(() => {
fetchList({ page });
}, [page]);
Hooks 没有实现的生命周期钩子
转换实例变量
强制更新 Hook 组件
设置一个
没有实际作用
的
state
,然后强制更新
state
的值触发渲染。
1 |
const Todo = () => { |
获取旧的 props 和 state
可以通过
useRef
来保存数据,因为渲染时不会覆盖掉可变数据。
1 |
function Counter() {
|
受控组件与非受控组件的区别
受控组件主要是指表单的值受到
state
的控制,它需要自行监听
onChange
事件来更新
state
。
由于受控组件每次都要编写事件处理器才能更新
state
数据、可能会有点麻烦,React 提供另一种代替方案是
非受控组件
。
非受控组件将
真实数据储存在 DOM 节点
中,它可以为表单项设置默认值,不需要手动更新数据。当需要用到表单数据时再通过
ref
从 DOM 节点中取出数据即可。