react hooks项目中的实践

1,719 阅读12分钟

引言

本文将介绍一些在日常开发中使用react hooks的心得体会,将着重介绍两个hooks的使用:useStateuseEffect这两个比较常用的使用方式。

1、在react class组件如此成熟的时代,我们为什么还要使用react hooks呢,

首先我们要明确几点:

  • react hooks完全是可选择的,你可以重写老的冗余组件,可以在新的组件使用,也可以啥都不学啥都不做,当一条咸鱼🐟。
  • 100% 向后兼容 Hooks不包含任何破坏式<颠覆式>更新。
  • class不会被移除,依然与hooks并存,但官方更建议使用hooks,因为在大部分情况下凡是class能做到的hooks都能做到

在使用react class组件时, 2、跨组件复用含状态(state)的逻辑十分困难,因为我们不得不通过实例对象this上挂载的各种方法来实现业务逻辑(如setState),而过多的this很容易让初级开发者产生一些疑惑(诸如这个this指向谁的问题,同样的问题也发生在vue2中)

3、复杂的组件变得冗余,在项目的后期,class组件很可能变得难以维护,试想一下,一个上千行的组件,内部会有上百个函数,由于每个函数都有自己的作用域,所以如果我们想在这些方法中使用state props的属性,我们就得不停的重定义,而hooks的functionComponent特性,让我们避免了这种事情的发生,从而减少了代码重复也提高了效率。(在不断地研究后发现其实这样做会存在一些性能问题也不好说,但是对于大部分项目而言并不太影响性能)

4、学习成本 这也是hooks的优势之一,对于掌握react的同学来说这简直小菜一碟,对于还没有使用react的同学来说也不会太难,因为vue3react hooks从思想设计程度上是一致的。当然,react多年来对比vue,业界普遍认为react上手较难,所以从招聘广告来看除了知名大厂使用react,其他公司用vue会更多,而hooks的出现一定程度上降低了难度。

5、hooks将逻辑更细粒度划分,就是使用hooks不仅仅可以封装带dom的组件,也可以封装一些公用的响应式逻辑方法(如实时监听返回一个用户登录状态,这个状态我们可以在任何hooks组件中复用),很显然class组件在处理这类事情的时候,写法稍微复杂一些。

useState与useEffect

我们先简单介绍一下 useStateuseEffect 是如何使用的

const [demo1Name, setDemo1Name] = useState('');

返回一个两项数组,第一项为useState返回的最新值,第二项可以理解为 具名化的setState,我们通过传 空字符串 作为 useState 唯一的参数来将其初始化为 空字符串

  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

提示 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

// 相当于每次都会触发 componentDidUpdate
    useEffect(() => {
        console.log('我一直在触发useEffect1')
    })

    // 只有组件初始化的时候触发 componentDidMount
    useEffect(() => {
        console.log('我初始化的时候触发了useEffect2')
    }, [])

    // 只要nameZyy变化了,以及初始化时就触发 类似于 vue中的 watch但比watch强大
    useEffect(() => {
        console.log('因为nameZyy变化了,我触发了useEffect3')
    }, [nameZyy])

    // 只要nameXyb变化了,以及初始化时就触发
    useEffect(() => {
        console.log('因为nameXyb变化了,我触发了useEffect4')
    }, [nameXyb])
    
    // 组件销毁时触发return 中的函数 componentWillUnmount
    useEffect(()=>{
        return () => {
            // 清除定时器、清除事件监听函数
        }
    }, [])

每次组件更新相当于重新执行组件function

分享会跳转vscode

  • class组件中,触发更新视图操作往往只需执行render函数以及特定update生命周期,它内部的状态通过this实例指向唯一内存地址,所以保证了组件状态在更新后的统一性。

  • hooks中,没有this,触发视图渲染后,需要重新执行这个组件函数,所导致的后果便是内部声明的变量会全部重新定义一遍,所以hooks组件借助了react 内部的方法让该组件从无状态变为有状态(useState),在useState内部保存着一个你初始化时定义的 变量。

为了更好的论证上面的理论,我这边借鉴了 网易云团队给出的概念 —— 类似动画帧的概念(我们将每次触发渲染称为一帧),更容易理解hooks的执行机制

每一帧定义的变量方法都是独立的,前后互不影响

为了解释帧的概念,这里有个定时器的例子

分享会跳转vscode

function Demo2() {
    const [num, setNum] = useState(0);
    // componentDidMount
    useEffect(() => {
        setInterval(() => {
            setNum(num + 1) // num一直是0,我拿的一直是第一帧即初始化时候的值
            console.log(num)
        }, 1000);
    }, [])
    useEffect(()=>{
        console.log(num,'我变成了1,之后一直是1,切不会触发渲染了')
    },[num])
    return (
        <Card title="demo2">
            <p>{num}</p>
        </Card>
    )
}

上方demo2是如何执行的呢,首先初始化第一帧 可以模拟为下方第一帧代码

function Demo2_1() {
    const num_1 = 0;
    // componentDidMount
    useEffect(() => {
        setInterval(() => {
            setNum(num_1 + 1)
            console.log(num_1)
        }, 1000);
    }, [])
    useEffect(()=>{
        console.log(num_1,'我初始化的时候执行以下0,第二帧我变成了1,之后一直是1,切不会触发渲染了')
    },[num_1])
    return (
        <Card title="demo2">
            <p>{num_1}</p>
        </Card>
    )
}

在demo2组件内部定义了 num_1 常量,两个 useEffect 函数,其中一个useEffect 只会在初始化的第一帧执行。 所导致的后果是:这个第一帧内部的代码就始终保持不变,即内部setInterval的回调函数中的变量num_1一直是0,从而导致setNum一直重复(0+1) 也导致了第二个useEffect console.log num_1 的值 只会增加到1,就不再触发渲染了。

分享会跳转vscode

如果上面的例子 你觉得没有说服力,ok!我给你一个网易大v的例子

const Demo3 = (props) => {
    const { count } = props;
    const handleClick = () => {
        setTimeout(() => {
            alert(count);
        }, 3000);
    };
    return (
        <Card title="demo3">
            <p>{count}</p>
            <button onClick={handleClick}>点我弹我</button>
        </Card>
    );
}
export default Demo3;

我们通过propscount传入子组件,初始值为0,每隔一秒加1,我们点击 点我弹我按钮,alert的count值,发现并不是页面上还在自增的值,而是等于点击的那一刻的值。 从而论证了每一帧的值都是 独立的,就是一个普普通通的数据而已

  • 当然这种情况只会在 hooks中出现,若使用class组件,由于this指向同一实例,this内部属性值是持续变化的。
class Demo4 extends Component {
    handleClick = () => {
        setTimeout(() => {
            alert(this.props.count);
        }, 3000);
    };
    render() {
        const {
            count
        } = this.props;
        return (
            <Card title="demo4">
                <p>{count}</p>
                <Button onClick={this.handleClick}>点我弹我4</Button>
            </Card>
        );
    }
}

小结

上面两个例子,通过动画帧的概念辅助理解每次触发renderhooks函数重新定义内部变量,并且通过useState等方式,react帮我们维护了一组状态,这些状态独立于函数存放。

需要明确的是,我们通过useState解构出来的变量 作为函数中的一个常量,就是普通的数据,并不存在诸如数据绑定这样的操作来驱使 DOM 发生更新。在调用 hooks中的setState 后,React 将重新执行 render 函数,仅此而已。

永远不要欺骗useEffect

对于 useEffect 来说,执行的时机是完成所有的 DOM 变更并让浏览器渲染页面后

const Demo5 = (props) => {
    const {
        id
    } = props;

    useEffect(() => {
            setTimeout(() => {
                console.log(id, '我是demo5中的id值')
            }, 100)
    }, [])

    return (
        <Card title="demo5">
            我是demo5 {id}
        </Card>
    )
}

通常我们会有上面的这种需求,id是由接口动态返回的,demo5作为子组件他也要使用id请求一次其他的接口,这时,实际上我们依赖了动态的id,但是在子组件中这么写是无法获得最新的id值,因为子组件在id值发生变化的时候,就已经渲染完毕了。

针对这种情况,我们有两种解决方案 1、useEffect第二个参数的数组中加上id依赖,并做空保护。

    useEffect(() => {
        if (id) {
            setTimeout(() => {
                console.log(id, '我是demo5中的id值')
            }, 100)
        }
    }, [id])

2、当id值发生变化后,再渲染子组件。 但是针对 诸如 wiki 这种 文章组件不会销毁的情况,则使用hooks的优势就体现出来了。

useMemo的实践

|  ![](https://assets.che300.com/wiki/2021-03-29/16170023709952396.png)
|_
import React, { useState } from 'react'
import { Button,Card } from 'antd';

export default () => {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('朱育仪');
    function add() {
        console.log('我又执行了');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }
    
    return (
        <Card title="demo6">
            <h4>{count}---{add()}</h4>
            {val}
            <div>
                <Button onClick={() => setCount(count + 1)}>+1</Button>
                <input value={val} onChange={event => setValue(event.target.value)} />
            </div>
        </Card>
    );
}

运行上面的demo6,我们会发现,无论我们点击Button或者在input添加值,add函数都会执行,然而add函数与val并无关系,这就造成了性能的损耗。所以针对这种情况我们就需要使用 useMemo这个 hook。

export default () => {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('朱育仪');
    const add = useMemo(() => {
        console.log('我只有count变了,我才执行');
        let sum = 0;
        for (let i = 0; i < count * 10; i++) {
            sum += i;
        }
        return sum;
    }, [count])

    return (
        <Card>
            <h4>{count}-{add}</h4>
            {val}
            <div>
                <Button onClick={() => setCount(count + 1)}>+c1</Button>
                <input value={val} onChange={e => setValue(e.target.value)} />
            </div>
        </Card>
    );
}

此时只有当我们点击 Button 时再触发 add 函数,节省了性能的消耗。 这里有一张题库项目中的实践图

这里将列表筛选项进行了缓存,减少了不必要的渲染,从而提高性能。(原先的class组件其实没有考虑到这种问题)

在过去的class组件中,我们通过 shouldComponentUpdate 判断当前属性和状态是否和上一次的相同,来避免组件不必要的更新。其中的比较是对于本组件的所有属性和状态而言的,无法根据 shouldComponentUpdate 的返回值来使该组件一部分 elements 更新,另一部分不更新。(公司所有的进件系统筛选项都有这个问题) 而函数组件中的 useMemo 其实就可以代替这一部分工作。

强大的自定义hooks

import { useEffect, useState, } from 'react'

export function useOnline() {

    const [online, setOnline] = useState(false);

    useEffect(() => {
        let timer = setInterval(() => {
            setOnline(c => {
                return !c
            })
        }, 3000)
        return () => {
            clearInterval(timer)
        }
    }, [])

    return online
}

上面的hooks 比较特殊,它定义了一个 online状态,每三秒变化一次true false 来模拟用户在线或者离线。我们可以直接返回 该状态。 如何使用呢?

export default () => {

    const online = useOnline();
    console.log(online)

    return (
        <Card title="demo8">
            <div>
                {online ? '我在线' : '我离线'}
            </div>
        </Card>
    );
}

该示例中直接调用 const online = useOnline(); ,即可获取到online的响应数据。 这就是自定义hooks的强大之处,聪明的开发者们也正是因为自定义hooks的存在,开发出了各种各样好用的hooks

关于父组件如何调取子组件方法

使用useRef useImperativeHandle forwardRef配合使用达到class组件的效果

interface useRefTypes {
    getTopicList: () => void
}

const topicRef = useRef<useRefTypes>(null);
...
const topicDom = (
     <div className={styles.describeContainer}>
          <Topic
               dispatch={dispatch}
               subjectId={id}
               loading={loading}
               userId={_id}
               ref={topicRef}
          />
     </div>
)
...
// 提交并发布评论区
const submitAndTopic = () => {
    if (!submitValue) {
        notification.warning({
            message: '提示',
            description: '请输入你的答案'
        })
        return
    }
    confirm({
        title: '提示',
        content: '是否将答案同步到评论区?',
        okText: '确定',
        cancelText: '取消',
        onOk: () => {
            submit();
            dispatch({
                type: 'subjectDetails/addTopic',
                payload: {
                    subjectId: id,
                    content: submitValue,
                    parentTopicId: 'topTopic'
                },
                callback: () => {
                    if (topicRef && topicRef.current) {
                        if (topicRef.current.getTopicList) {
                            topicRef.current.getTopicList();
                        }
                    }
                }
            })
        }
    })
}

在父组件中使用usRef(上方代码) 在子组件中使用useImperativeHandle forwardRef(下方代码)

// 将方法包裹在forwardRef中
const Topic: React.FC<propsType> = forwardRef((props, ref) => {
...
    useImperativeHandle(ref, () => ({
        getTopicList
    }));
}

将方法包裹在 forwardRef 中,内部调用 useImperativeHandleuseImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。)

问题

好学的同事在会前讨论中和我谈到的问题如下

1、Hooks 会因为在渲染时创建函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。 除此之外,可以认为 Hooks 的设计在某些方面更加高效:

  • Hooks 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
  • 符合语言习惯的代码在使用 Hooks 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

我们将 只会在useEffect中执行的代码放置于useEffect回调函数内部,这样只有依赖值发生变化的时候才会执行useEffect内部函数声明。

2、只在最顶层使用 Hook

是的没错!!! 不要在循环条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

3、useState在接口的回调函数中表现出同步特性,即每次setState触发了多次render

如何解决这个问题呢,使用 react 提供的 unstable_batchedUpdates 方法,这样两个不相关的setState就不会触发两次render,而变为一次render

import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';

dispatch({
    type:'xxxx',
    callback:()=>{
        batchedUpdates(()=>{
            // 两个不一样的state
            setIsMeThumbsUpState(flag => !flag);
            setThumbsUpCount(1);
        })
    }
})

参考文档

react官方中文网 juejin.cn/post/684490… blog.csdn.net/qq_44753552… useCallback useEffect和useLayoutEffect的区别 addeventlistener事件参数_函数式编程看React Hooks(二)事件绑定副作用深度剖析 为什么顺序调用对 React Hooks 很重要?

其他

格外强调在useEffect中编写事件监听器时,请时刻注意事件监听器回调函数中的state值。若 useEffect 不添加依赖项则state始终为初始值