深入理解 react 的渲染

什么是 React

React 是一个 UI Library,也就是用来构建用户界面的库。React 通过Javascript 完成了对 HTML DOM 的所有表达来实现UI的翻译和构建。

React 的渲染过程

React 的渲染过程就是把组件翻译出来的 DOM append 到 DOM tree 并绑定对应事件的过程。当 React 的 props 或者 state 发生变化之后,React 会对 props 和 state 进行 diff 比较,然后重新进行渲染,于是Render 过程会重新走一遍。

React 是如何保证 props 和 state 的变化始终能和UI保持一致的

在 function component 中,props、state、以及 component 内部出现的任何函数和变量都是以快照的形式出现,一旦 props 或 state 发生变化之后,所有函数内部的状态都会被重新刷新一遍形成新的快照,渲染的过程就是把快照还原到UI的过程,每个渲染过程都有自己的快照,不同快照之间相互不影响,因此不论component渲染多少次,都能保证UI和状态的一致性。通过延迟函数来理解快照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import "./styles.css";
import React, {useState} from 'react';


export default function App() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}

img
上面的代码中声明了 count 这个 state,当点击 Show Alert Button 之后再点击 Click Me 对 count 进行变更,会观察到 alert 显示的值和实际的值是不一样的。

为什么会是这种违反直觉的结果?

我们继续尝试剖析 React Component 本质上是一个数据状态的快照这句话的含义。所谓快照就是某一个时刻所有数据的状态,而在 React 中 UI 就是数据状态呈现的载体,数据状态有多少种变化,快照就会有多少个,UI 也对应呈现多少种变化,但是任意一个时刻 UI 只呈现一种数据状态。在上面的示例中,我们通过延迟函数捕获了数据变化过程中某一时刻的状态,把这一时刻的状态延迟到未来(timeout 函数中的延迟时间)呈现,可以看到虽然在 alter 出现的这一刻 B,Component 的数据状态是 B-state 但是 alert 呈现出来的数据状态却是 A-state,这个 A-state 就是 Component 在 A 的那一刻其数据状态被延迟函数捕获了,捕获之后的快照就是 A-state-snapshot。React 通过这种【捕获状态->生成快照->渲染快照】的机制保证了 UI 数据的一致性,也就是任意时刻 UI 呈现的状态和当时的数据状态始终一致。

Component 中的状态都包含哪些?

任何声明在 Component 内部顶层的变量,包含函数、state、变量、props 都是 Component 的状态,任何时候这些状态发生了变化都会触发 React 重新渲染 Component。当函数内依赖 props 或者 state 做逻辑处理时,函数会捕获对应 props 或者 state 的最新状态。通过多次改变state来理解快照理解了 Component 的状态是什么,我们一起继续看看这个例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import { useState } from 'react';

export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}

img
在 button 的 onClick 事件中,number 这个状态在三个 setNumber 调用中都指向的是相同的状态,它们都是某一时刻 nunber 的快照。因此原代码想要 number 自增 3 次是无法实现的,最终效果是调用三次 setNumber 和调用一次的效果是一样的。官方详细介绍 https://beta.reactjs.org/learn/state-as-a-snapshot

React 如何管理组件和状态的关系

React 管理的状态是由组件在UI DOM 树中的位置来决定的,UI Tree 就是组件组成的树

img

两个 Counter 分别在不同的位置,它们独享各自的状态互不影响

img

相同类型的组件在同一个位置,共享彼此的状态

img

相同位置不同类型组件,状态各自独立

img

相同位置的同类型的组件通过 key 实现状态重置,key的作用就是告诉 react 这是两个不同的组件,需要不同的各自独立的状态管理 使用 hook 和 reducer 来管理状态React hook 很多,最常用的就是 useState、useEffect、useCallback、useRef、useContext、useReducer,当业务变复杂时,需要管理的状态将非常多,这段代码来自琅琊阁,一个页面的状态多达 42 个

img

状态多带来了两个显著的问题:组件渲染性能受到影响状态之间的依赖关系复杂不明,容易制造bug复杂难以调试,开发效率降低因而又继续引申出新的疑问:需要这么多状态么?如果无法减少状态的数目,如何有效管理状态的依赖?

React 提供了 useEffect 和 reducer 来管理状态

使用 useEffect 管理状态的依赖关系。useEffect 有三种依赖管理形式:完全不依赖,每次 render 都会执行空的依赖列表,只在第一次 render 执行依赖某个具体的state或props,只在 props 或 state 发生变更之后才执行为了让 state 之间的关系能够尽量明确化,一定要避免在 useEffect 中隐式依赖 state或props上面的代码中依赖了 a 这个state,但是当 a 发生变化时,并不会执行,因而为了避免发生这种情况,我们建议useEffect 中的函数就在 useEffect 中完成定义和使用,这样能保证后续逻辑变动时,相关的代码总是在一起的。handleThis 重构成一个 useCallback 函数使用reducer实现状态管理的高内聚在琅琊阁的迭代管理中对一个状态的增删改查出现了在代码的多处,而且随着事件的变多这样重复的逻辑依然会继续出现

img

img

img

使用 reducer 重构之后,所有对状态的修改都集中在一起,代码的内聚性变强

img

reduce的使用非常简单:定义一个 reduce 函数,第一个参数是state,第二个参数是 action把所有对state的修改放到reduce函数中在component中使用reduce组件的通信大部分情况下,我们通过props来完成组件之间的通信(props支持传递组件、函数、state等),但是可能会遇到一个参数需要传递到很多个组件中或者一个参数被传递的层次特别深

img

最常见的就是用户信息,可能很多个组件都需要;如果网站有主题是动态的,主题相关的属性可能会传递的非常深。React 提供了 context 来实现多个组件之间的信息共享。Context 的使用也非常简单:定义 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { SprintModel } from '@/domainModel/sprint';

import React, { createContext } from 'react';

import { UserRole } from '@/domainModel/user';

export const SprintContext = createContext<{

currentSprint?: SprintModel;

userRole?: UserRole;

setCurrentSprint: React.Dispatch<React.SetStateAction<SprintModel>>;

setIndustryId: React.Dispatch<React.SetStateAction<string>>;

}>({

setCurrentSprint: () => { },

setIndustryId: () => { },

});

使用 context

1
2
const { currentSprint, setCurrentSprint,
userRole, setIndustryId } = useContext(SprintContext);

为 context 提供值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
< SprintContext.Providervalue={{ currentSprint, setCurrentSprint, userRole, setIndustryId } }>
<div className={styles['sprint-operation']}>

<SprintHeader demandLoading={loading} title="排期结果" type="sprintResults"/>

<SprintAlert alertStyle={{ margin: '15px 20px -10px 20px' }}/>

<Spin spinning={loading}>

<div className={styles.resultsLayout}>

<RightMain setLoading={setLoading}/>
</div>
</div >
</SprintContext.Provider >

使用context时,component从距离组件最近的context获取到对应的值,如果有多个context共存,距离component近的context会覆盖较远的context。

三月沙 wechat
扫描关注 wecatch 的公众号