0%

React 知识回顾 (使用篇)

使用 React 进行项目开发也有好几个项目了,趁着最近有空来对 React 的知识做一个简单的复盘。

完整目录概览

React 是单向数据流还是双向数据流?它还有其他特点吗?

React 是单向数据流,数据是从上向下流。它的其他主要特点时:

  • 数据驱动视图
  • 声明式编写 UI
  • 组件化开发

setState

React 通过什么方式来更新数据

React 是通过 setState 来更新数据的。调用多个 setState 不会立即更新数据,而会批量延迟更新后再将数据合并。

除了 setState 外还可以使用 forceUpdate 跳过当前组件的 shouldComponentUpdate diff,强制触发组件渲染(避免使用该方式)。

React 不能直接修改 State 吗?

  1. 直接修改 state 不会触发组件的渲染。
  2. 若直接修改 state 引用的值,在实际使用时会导致错误的值出现
  3. 修改后的 state 可能会被后续调用的 setState 覆盖

setState 是同步还是异步的?

出于性能的考虑,React 可能会把多个 setState 合并成一个调用。

React 内有个 batchUpdate(批量更新) 的机制,在 React 可以控制的区域 (如组件生命周期、React 封装的事件处理器) 设置标识位 isBatchingUpdate 来决定是否触发更新。

比如在 React 中注册的 onClick 事件或是 componentDidMount 中直接使用 setState 都是异步的。若想拿到触发更新后的值,可以给 setState 第二个参数传递一个函数,该函数在数据更新后会触发的回调函数,函数的参数就是更新后最新的值。

不受 React 控制的代码快中使用 setState 是同步的,比如在 setTimeout 或是原生的事件监听器中使用。

setState 小测

输出以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
componentDidMount() {
this.setState({ count: this.state.count + 1 });
console.log("1 -->", this.state.count);

this.setState({ count: this.state.count + 1 });
console.log("2 -->", this.state.count);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("3 -->", this.state.count);
}, 0);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("4 -->", this.state.count);
}, 0);
}

输出结果为:

1
2
3
4
1 --> 0
2 --> 0
3 --> 2
4 --> 3

解答: 调用 setState 后不会立即更新 state,开头两次调用会被异步合并调用,因此只有一次调用。一轮事件循环结束后,调用第 3、4 次 setState。由于在 setTimeout 中调用是同步更新的,因此都能正常的叠加数据。

React 生命周期

React 的生命周期主要是指组件在特定阶段会执行的函数。以下是 class 组件的部分生命周期图谱:

从上图可以看出:React 的生命周期按照类型划分,可分为 挂载时(Mounting)、更新时(Updating)、卸载时(Unmounting) 。图中的生命周期函数效果如下:

constructor (构造函数)

  • 触发条件: 组件初始化时
  • 是否可以使用 setState: X
  • 使用场景: 初始化 state 或者对方法绑定 this。在构造函数中便于自动化测试。

static getDerivedStateFromProps

Tips: 不常用方法

  • 触发条件: 调用 render 函数之前
  • 是否可以使用 setState: X
  • 函数行为: 函数可以返回一个对象用于更新组件内部的 state 数据,若返回 null 则什么都不更新。
  • 使用场景: 用于 state 依赖 props 的情况,也就是状态派生。值得注意的是派生 state 会导致代码冗余,并使组件难以维护。

shouldComponentUpdate

Tips: 不常用方法

  • 触发条件: 当 props/state 发生变化
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定组件是否触发 render,返回值为 true 则触发渲染,反之则阻止渲染。(组件内不写该函数的话,则调用默认函数。默认函数只会返回 true,即只要 props/state 发生变化,就更新组件)
  • 使用场景: 组件的性能优化,仅仅是浅比较 props 和 state 的变化的话,可以使用内置的 PureComponent 来代替 Component 组件。

render

  • 触发条件: 渲染组件时
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定视图的渲染效果
  • 使用场景: class 组件中唯一必须要实现的生命周期函数。

getSnapshotBeforeUpdate

Tips: 不常用方法

  • 触发条件: 在最近一次渲染输出(提交到 DOM 节点)之前调用
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值将传入给 componentDidUpdate 第三个参数中。若只实现了该函数,但没有使用 componentDidUpdate 的话,React 将会在控制台抛出警告
  • 使用场景: 可以在组件发生更改之前从 DOM 中捕获一些信息(例如,列表的滚动位置)

componentDidMount

  • 触发条件: 组件挂载后(插入 DOM 树中)立即调用,该函数只会被触发一次
  • 是否可以使用 setState: Y (可以直接调用,但会触发额外渲染)
  • 使用场景: 从网络请求中获取数据、订阅事件等

componentDidUpdate

  • 触发条件: 组件更新完毕后(首次渲染不会触发)
  • 是否可以使用 setState: Y (更新语句须放在条件语句中,不然可能会造成死循环)
  • 使用场景: 对比新旧值的变化,进而判断是否需要发送网络请求。比如监听路由的变化

componentWillUnmount

  • 触发条件: 组件卸载及销毁之前直接调用
  • 是否可以使用 setState: X
  • 使用场景: 清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等

生命周期阶段

针对 React 生命周期中函数的调用顺序,笔者写了一个简易的 Demo 用于演示: React 生命周期示例

React 组件挂载阶段先后会触发 constuctorstatic getDerivedStateFromPropsrendercomponentDidMount 函数。若 render 函数内还有子组件存在的话,则会进一步递归:

1
2
3
4
5
6
7
8
9
10
[Parent]: constuctor
[Parent]: static getDerivedStateFromProps
[Parent]: render
[Children]: constuctor
[Children]: static getDerivedStateFromProps
[Children]: render
[Children]: componentDidMount
[Children]: 挂载阶段结束!
[Parent]: componentDidMount
[Parent]: 挂载阶段结束!

React 组件更新阶段主要是组件的 props 或 state 发生变化时触发。若组件内还有子组件,则子组件会判断是否也需要触发更新。默认情况下 component 组件是只要父组件发生了变化,子组件也会跟着变化。以下是更新父组件 state 数据时所触发的生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
[Parent]: static getDerivedStateFromProps
[Parent]: shouldComponentUpdate
[Parent]: render
[Children]: static getDerivedStateFromProps
[Children]: shouldComponentUpdate
[Children]: render
[Children]: getSnapshotBeforeUpdate
[Parent]: getSnapshotBeforeUpdate
[Children]: componentDidUpdate
[Children]: 更新阶段结束!
[Parent]: componentDidUpdate
[Parent]: 更新阶段结束!

值得注意的是: 在本例 Demo 中没有给子组件传参,但子组件也触发了渲染。但从应用的角度上考虑,既然你子组件没有需要更新的东西,那就没有必要触发渲染吧?

因此 Component 组件上可以使用 shouldComponentUpdate 或者将 Component 组件替换为 PureComponment 组件来做优化。在生命周期图中也可以看到: shouldComponentUpdate 返回 false 时,将不再继续触发下面的函数。

有时你可能在某些情况下想主动触发渲染而又不被 shouldComponentUpdate 阻止渲染该怎么办呢?可以使用 force­Update() 跳过 shouldComponentUpdate 的 diff,进而渲染视图。(需要使用强制渲染的场景较少,一般不推荐这种方式进行开发)

React 组件销毁阶段也没啥好说的了。父组件先触发销毁前的函数,再逐层向下触发:

1
2
3
4
[Parent]: componentWillUnmount
[Parent]: 卸载阶段结束!
[Children]: componentWillUnmount
[Children]: 卸载阶段结束!

其他生命周期

除了上图比较常见的生命周期外,还有一些过时的 API 就没有额外介绍了。因为它们可能在未来的版本会被移除:

上图没有给出错误处理的情况,以下信息作为补充: 当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

React 组件通信

  1. 父组件通过 props 给子组件传递数据。子组件通过触发父组件提供的回调函数来给父组件传递消息或数据
  2. React.Context 可以跨层级组件共享数据
  3. 自定义事件
  4. 引入 Redux/Mobx 之类的状态管理器

React.Context 怎么使用

Context 可以共享对于组件树而言是全局的数据,比如全局主题、首选语言等。使用方式如下:

  1. React.createContext 函数用于生成 Context 对象。可以在创建时给 Context 设置默认值:

    1
    const ThemeContext = React.createContext('light');
  2. Context 对象中有一个 Provider(提供者) 组件,Provider 组件接受一个 value 属性用以将数据传递给消费组件。

    1
    2
    3
    <ThemeContext.Provider value="dark">
    <page />
    </ThemeContext.Provider>
  3. 获取 Context 提供的值可以通过 contextType 或者 Consumer(消费者) 组件中获取。contextType 只能用于类组件,并且只能挂载一个 Context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class 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。

与类组件的差别在于: 它没有实例、不能通过 extends 继承于其他方法、也没有生命周期和 state。以前函数组件常作为无状态组件,React 16.8+ 可以引入 Hooks 为函数组件支持状态和副作用操作。

Hooks

Hook vs class

类组件的不足:

  • 状态逻辑复用难,缺少复用机制。渲染属性和高阶组件导致层级冗余。
  • 组件趋向复杂难以维护。生命周期函数混杂不相干逻辑,相干逻辑分散在不同生命周期中。
  • this 指向令人困扰。内联函数过度创建新句柄,类成员函数不能保证 this。

Hooks 的优点:

  • 自定义 Hook 方便复用状态逻辑
  • 副作用的关注点分离
  • 函数组件没有 this 问题

Hooks 现有的不足:

  • 不能完全取代 class 组件的生命周期,部分不常用的生命周期暂时没有实现。
  • Hooks 的运作方式带来了一定的学习成本,需要转换现有的编程思维,增加了心智负担。

Hooks 的使用

描述 Hooks 有哪些常用的方法和大致用途

  1. useState: 使函数组件支持设置 state 数据,可用于代替类组件的 constructor 函数。

  2. useEffect: 使函数组件支持操作副作用 (effect) 的能力,Hook 第二个参数是 effect 的依赖项。当依赖项是空时,effect 函数仅会在组件挂载后执行一遍。若有一个或多个依赖项时,只要任意一个依赖项发生变化,就会触发 effect 函数的执行。effect 函数里可以做一些如获取页面数据、订阅事件等操作。

    除此之外,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]);
  3. useLayoutEffect: useEffect 的 effect 执行的时机是在浏览器完成布局和绘制之后会延迟调用。若想要 DOM 变更的同时同步执行 effect 的话可以使用 useLayoutEffect。它们之间只是执行的时机不同,其他都一样。

  4. useContext: 接收一个 Context 对象,并返回 Context 的当前值。相当于类组件的 static contextType = MyContext

  5. useReduceruseState 的代替方案,它的工作方式有点类似于 Redux,通过函数来操作 state。适合 state 逻辑较为复杂且包含多个子值,或是新的 state 依赖于旧的 state 的场景。

  6. useMemo 主要用于性能优化,它可以缓存变量的值,避免每次组件更新后都需要重复计算值。

  7. useCallbck 用于缓存函数,避免函数被重复创建,它是 useMemo 的语法糖。useCallback(fn, deps) 的效果相当于是 useMemo(() => fn, deps)

Hook 之间的一些差异

  1. React.memo 与 React.useMemo

    memo 针对一个组件的渲染是否重复执行,useMemo 定义一段函数逻辑是否重复执行。

  2. React.useMemo 与 React.useCallback

    useMemo(() => fn) 返回的是一个函数,将等同于 useCallback(fn)

  3. React.useStatus 与 React.useRef

    React.useStatus 相当于类的 stateReact.useRef 相当于类的内部属性。前者参与渲染,后者的修改不会触发渲染。

自定义 Hook 的使用

自定义 Hook 的命名规则是以 use 开头的函数,比如 useLocalStorage 就符合自定义 Hook 的命名规范。
使用自定义 Hook 的场景有很多,如表单处理、动画、订阅声明、定时器等等可复用的逻辑都能通过自定义 Hook 来抽象实现。

在自定义 Hook 中,可以使用 Hooks 函数将可复用的逻辑和功能提取出来,并将内部的 state 或操作的方法从自定义 Hook 函数中返回出来。函数组件使用时就可以像调用普通函数一祥调用自定义 Hook 函数, 并将自定义 Hook 返回的 state 和操作方法通过解构保存到变量中。

下面是 useLocalStorage 的实现,它将 state 同步到本地存储,以使其在页面刷新后保持不变。 用法与 useState 相似,不同之处在于我们传入了本地存储键,以便我们可以在页面加载时默认为该值,而不是指定的初始值。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useState } from 'react';

// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage('name', 'Bob');

return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}

// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};

return [storedValue, setValue];
}

注意: 自定义 Hook 函数在定义时,也可以使用另一个自定义 Hook 函数。

Hook 使用约束

  1. 只能在函数组件最顶层调用 Hook,不能在循环、条件判断或子函数中调用。
  2. 只能在函数组件或者是自定义 Hook 函数中调用,普通的 js 函数不能使用。

class 组件与 Hook 之间的映射与转换

函数组件相比 class 组件会缺少很多功能,但大多可以通过 Hook 的方式来实现。

生命周期

  • constructor:class 组件的构造函数一般是用于初始化 state 数据或是给事件绑定 this 指向的。函数组件内没有 this 指向的问题,因此可以忽略。而 state 可以通过 useState/useReducer 来实现。

  • getDerivedStateFromPropsgetDerivedStateFromProps 一般用于在组件 props 发生变化时派生 state。Hooks 实现同等效果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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}`;
    }
  • shouldComponentUpdate: 使用 React.memo 应用到函数组件中后,当 props 发生变化时,会对 props 的新旧值进行前对比,相当于是 PureComponent 的功能。如果你还想自己定义比较函数的话,可以给 React.memo 的第二个参数传一个函数,若函数返回 true 则跳过更新。

    1
    2
    3
    const Button = React.memo((props) => {
    return <button>{props.text}</button>
    });
  • render: 函数组件本身就是一个 render 函数。

  • componentDidMount / componentDidUpdate / componentWillUnmount:

    useEffect 第二个参数的依赖项为空时,相当于 componentDidMount,组件挂载后只会执行一次。每个 useEffect 返回的函数相当于是 componentWillUnmount 同等效果的操作。若有依赖,则 effect 函数相当于是 componentDidUpdate

    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 没有实现的生命周期钩子

  • getSnapshotBeforeUpdate
  • getDerivedStateFromError
  • componentDidCatch

转换实例变量

使用 useRef 设置可变数据。

强制更新 Hook 组件

设置一个没有实际作用state,然后强制更新 state 的值触发渲染。

1
2
3
4
5
6
7
8
9
10
const Todo = () => {
// 使用 useState,用随机数据更新也行
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
forceUpdate();
}

return <button click={handleClick}>强制更新组件</button>
}

获取旧的 props 和 state

可以通过 useRef 来保存数据,因为渲染时不会覆盖掉可变数据。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, []);

const prevCount = prevCountRef.current;

return <h1>Now: {count}, before: {prevCount}</h1>;
}

受控组件与非受控组件的区别

受控组件主要是指表单的值受到 state 的控制,它需要自行监听 onChange 事件来更新 state

由于受控组件每次都要编写事件处理器才能更新 state 数据、可能会有点麻烦,React 提供另一种代替方案是非受控组件

非受控组件将真实数据储存在 DOM 节点中,它可以为表单项设置默认值,不需要手动更新数据。当需要用到表单数据时再通过 ref 从 DOM 节点中取出数据即可。

注意: 多数情况下React 推荐编写受控组件。

扩展资料: 受控和非受控制使用场景的选择

Portals 是什么?

Portals 就像个传送门,它可以将子节点渲染到存在于父组件以外的 DOM 节点的方案。

比如 Dialog 是一个全局组件,按照传统渲染组件的方式,Dialog 可能会受到其容器 css 的影响。因此可以使用 Portals 让组件在视觉上渲染到 <body> 中,使其样式不受 overflow: hiddenz-index 的影响。

「请笔者喝杯奶茶鼓励一下」

欢迎关注我的其它发布渠道