IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    React Hooks 快速入门与开发体验(一)

    krimeshu发表于 2023-08-27 05:13:46
    love 0

    Vue 3 推出 Composition API 的时候,看到一些表示这和 React Hooks 很像的评论。

    正好最近有个项目改用了 React 的,于是趁机体验了一下 React Hooks,看看是否真是如此。

    简介

    说来惭愧,上次使用 React,还是几年前想在 React 项目里想要实现组件样式作用域,对比和选择 css-modules 和 styled-components 方案来着,最终实现体验还是不怎么样,后来大部分项目都是小程序、Vue 和 node.js 印象就还停留在那个年代。

    那什么是 React Hook 呢?官方的介绍如下:

    Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

    (来源链接:https://zh-hans.reactjs.org/docs/hooks-custom.html)

    其中的 class 指的应该是 ES Class 也就是类语法,而 state 应该就是指平时通过 setState 更新状态来触发重渲染的组件 state 属性了。

    并且官方保证它 没有破坏性改动:

    React Hook 是:

    • 完全可选的,可以轻松引入。如果你不喜欢,也可以不去学习和使用。
    • 100% 向后兼容,React Hook 不会包含任何破坏性改动。
    • 现在可用,Hook 已发布于 v16.8.0。

    第一条说明官方并不强制要求使用 React Hook。第二条则是说明,使用它不会影响旧版代码,确保存量项目代码的正常工作。

    至于支持 Hook 的 React 版本,大约发布于2018年底。到本文的2021年初算来,差不多已经过去两年时间了。

    不过需要注意 React Hook 的使用规则:

    • 只能在 函数最外层 调用 Hook。
    • 只能在 React 的函数组件 中调用 Hook。

    第二条很好理解,毕竟是为函数组件所设计的,第一条究竟为何,没有实际体验也很难说清楚,我们容后再叙。

    既然已经出来两年之久,这个 React Hook 实际使用起来究竟效果如何呢?

    一、基础 Hooks

    1. useState

    1-1. 基础示例

    比如一个简单的点击计数示例,其中使用到一个计数 state,在每次点击后将其 +1 后更新视图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import React, { Component } from 'react';

    class Example extends Component {
    constructor(props) {
    super(props);
    this.state = {
    count: 0,
    };
    }

    render () {
    const { count } = this.state;
    const setCount = (count) => this.setState({ count });

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

    如果使用 React Hooks 实现,就只需要这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import React, { useState } from 'react';

    function Example() {
    const [count, setCount] = useState(0);

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

    这个例子中可以看出,React Hook 相比组件类:

    • 将组件从带有多个生命周期函数的类声明,直接简化为一个渲染函数的函数组件。
    • 组件渲染时用到的属性和对应更新回调,通过一个名为 useState 的 Hook 来实现。
    • 对于组件类的生命周期函数,应该也可以通过其它 Hook 实现。

    其它生命周期函数我们稍后再叙,先来看看一些上面的例子没有提到的情况:

    1-2. 获取组件的 props

    对于组件 props 的获取很简单,函数组件的第一个传入参数就是了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function Child({ name }) {
    return (
    <p>Name: {name}</p>
    );
    }

    function Parent() {
    const children = [
    { id: 1, name: 'Adam' },
    { id: 2, name: 'Bernard' },
    ];

    return (
    <div>
    {children.map(({ id, name }) => (
    <Child key={id} name={name} />
    ))}
    </div>
    );
    }

    1-3. 更新数组/对象类型的 state

    对于简单的值类型 state,直接使用 useState 返回的更新函数就可以轻松完成更新了。

    对于数组和键值对(对象)类型的数据,又该怎么更新呢?

    难道直接把整个新的数组/对象传入更新函数?

    ——没错。

    不过这样操作可能会稍显繁琐,因为必须传入一个新的数组/对象才能触发更新。直接修改原对象后直接传入更新函数的话,并不会触发重渲染。

    所以我们需要创建一个数组/对象的拷贝,再传给更新函数,通常可以使用ES6数组方法和解构赋值对操作稍作简化:

    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
    function Example() {
    const [list, setList] = useState([
    'hello world',
    ]);
    const add = () => {
    setList([...list, 'new item']);
    };
    const remove = (removeItem) => {
    setList(list.filter(item => item !== removeItem));
    };
    return (
    <div>
    {list.map((item, idx) => (
    <p key={idx}>
    <span>{item}</span>
    <button onClick={() => remove(item)}>
    Remove
    </button>
    </p>
    ))}
    <button onClick={add}>
    Add item
    </button>
    </div>
    );
    }

    但对于更复杂些的情况(比如对象数组),这样还是不太方便,不过也没关系后面会有处理这类情况的其它 Hook。

    2. useEffect

    2-1. 基础示例

    使用 Hook 实现的函数组件(function component),其函数本身执行时机相当于 render 函数,执行较早。

    对于日常开发中常用的其它生命周期,通常使用 useEffect Hook 实现。这里的 effect,官方称呼为“副作用”:

    数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

    (来源链接:https://zh-hans.reactjs.org/docs/hooks-effect.html)

    它的第一个参数是个回调函数,称之为 副作用函数:

    1
    2
    3
    4
    5
    function Example() {
    useEffect(() => {
    // 副作用执行
    });
    }

    2-2. 依赖数组

    这样写的时候,副作用函数会在函数组件的每次 DOM 更新完毕后被调用,相当于类组件生命周期的 componentDidMount + componentDidUpdate。

    如果需要在其它时机执行副作用函数,就要靠第二个依赖数组字段了。

    如果存在依赖数组,React 就会在每次副作用函数执行前,检查依赖数组中的内容。当依赖数组与上次触发时完全没有变化,就会掉过此次执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Example({ name }) {
    const [count, setCount] = useState(0);
    useEffect(() => {
    // 当 count 更新时触发
    }, [count]);
    useEffect(() => {
    // 当 props.name 更新时触发
    }, [name]);
    useEffect(() => {
    // 当 count 或 props.name 更新时触发
    }, [count, name]);
    }

    依赖数组传空数组或者固定值的时候,每次触发的值都不会变化,所以这个副作用就只会在组件生命周期中执行一次。

    1
    2
    3
    4
    5
    function Example() {
    useEffect(() => {
    // 仅当组件创建挂载后触发一次
    }, []);
    }

    相当于类组件的 componentDidMount 生命周期函数。

    2-3. 清理函数

    对于副作用函数,我们还可以在其中返回一个对应的 清理函数:

    1
    2
    3
    4
    5
    6
    7
    8
    function Example() {
    useEffect(() => {
    // 副作用执行
    return () => {
    // 副作用清理
    };
    }, []);
    }

    清理函数将在当前副作用函数失效、下一个副作用函数设定之前被执行。

    上面的例子中,清理函数的执行时机相当于 componentWillUnmount。

    比如在组件挂载后添加一个对页面滚动做监听处理,并在卸载时清理监听器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Example() {
    useEffect(() => {
    const onScroll = () => {
    // Do something.
    };
    window.addEventListener('scroll', onScroll);
    return () => {
    window.removeEventListener('scroll', onScroll);
    };
    }, []);
    };

    2-4. 改进

    为什么要这样设计呢?官方给出了一个例子,就是根据 props 参数订阅数据源时,如果 props 参数发生变化,都需要清理旧订阅注册新订阅。

    在类组件的实现中,这需要把对应处理分散在多个生命周期函数中:

    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
    class Example extends Component {    
    constructor(props) {
    super(props);
    this.onDataSourceChange = this.onDataSourceChange.bind(this);
    }

    componentDidMount() {
    DataSource.subscribe(
    this.props.dependId,
    this.onDataSourceChange,
    );
    }

    componentDidUpdate(prevProps) {
    DataSource.unsubscribe(
    prevProps.dependId,
    this.onDataSourceChange,
    );
    DataSource.subscribe(
    this.props.dependId,
    this.onDataSourceChange,
    );
    }

    componentWillUnmount() {
    DataSource.unsubscribe(
    this.props.dependId,
    this.onDataSourceChange,
    );
    }

    onDataSourceChange(status) {
    // Do something.
    }
    }

    可以看到,对于同一个数据源的处理被分散得七零八落,其中 componentDidUpdate 的处理还经常被遗忘,导致一些本不应该产生的 bug。

    如果依赖于多个数据源的组件,或者还有其他相同生命周期的处理(如上面页面滚动事件的监听例子),还会让同一类数据源/事件的处理不能收拢到一起,反而因为发生时机而被混在其它不同数据源/事件的处理当中。导致组件编写过程中需要上下跳跃,而且后期维护中代码的阅读难度上升、可重构性下降。

    而通过 useEffect 实现,只需要放在同一个副作用处理内,再把相关参数放进依赖数组就行了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Example({ dependId }) {
    useEffect(() => {
    const onDataSourceChange = () => {
    // Do something.
    };
    DataSource.subscribe(dependId, onDataSourceChange);
    return () => {
    DataSource.unsubscribe(dependId, onDataSourceChange);
    };
    }, [dependId]);
    };

    只需要这样,不用再做 componentWillUpdate 的额外处理。而且最终同一类逻辑处理被收在同一个 effect 函数中,开发过程中聚焦单一问题,产出代码清晰可读,十分方便代码维护和重构。

    可以说是非常方便了。

    3. 小结

    基础的 React Hook 就是上面的 useState 和 useEffect 两个了,使用它们已经可以替代大部分以前使用类组件完成的功能,并且产出代码和执行效率都挺不错的。

    简单概括一下对于 React Hook 的第一印象:

    • 用来实现更简洁的函数组件,代替类组件;
    • 没有破坏性改动;
    • 但有一定使用规则;
    • 用副作用机制代替组件生命周期函数;
    • 对于同一类逻辑处理,可以按照比组件更细的粒度进行收拢。

    不过 React Hook 的设计也不是十全十美,有些问题通过简单例子可能无法体现出来,还需要通过更多使用场景的实践将其暴露出来。其它 Hooks 也将在新的例子中继续说明。

    敬请期待~

    To be continued.



沪ICP备19023445号-2号
友情链接