【ReactJS】关于钩子函数的一些思考

2026-03-28 22:43:22

目录:

useState 变量改变会重新渲染组件

如下代码: 父组件 Parent 中的文本框每次输入值时,浏览器控制台都会输出一行 ‘Child’,说明子组件也被渲染了。

import React, { useState } from 'react'

const Parent = () => {
    const [ inputValue, setInputValue ] = useState<string>('')

    return (
        <div>
            <input 
                type="text"
                value={inputValue}
                onChange={e => setInputValue(e.target.value)}
            />
            <Child />
        </div>
    )
}

const Child = () => {
    console.log('Child')

    return (
        <div>
            Child
        </div>
    )
}

export default Parent

结论: 组件内 useState 变量只要变化,就会触发组件重新渲染,子组件即便 props 没变,也会渲染。

显然,上面是个简单的示例,如果父组件内包含上百个子组件,父组件中只要状态变化,所有子组件都重新渲染一次,这看起来并不聪明。

ReactJS 官方提供了一种优化方式: 给子组件外层包上 React.memo(),作用是子组件只要 props 不变,就不会重新渲染。

现在再次修改输入框的值,控制只会在子组件第一次渲染时输出一次 ‘Child’,之后不会再输出 ‘Child’。

import React, { useState } from 'react'

const Parent = () => {
    const [ inputValue, setInputValue ] = useState<string>('')

    return (
        <div>
            <input 
                type="text"
                value={inputValue}
                onChange={e => setInputValue(e.target.value)}
            />
            <Child />
        </div>
    )
}

const Child = React.memo(() => {
    console.log('Child')

    return (
        <div>
            Child
        </div>
    )
})

export default Parent

想一想你的系统已经写了几百个组件,现在需要每个组件都修改一下,在外层包一层 React.memo(),简直是噩梦。

或者系统用到的第三方组件库,我们压根没有能力去修改别人的代码,加上 React.memo()。

基于组件渲染机制: useState 变量变化就会重新渲染组件,我们可以把状态相关部分提出去单独写成一个组件,还是上面的例子,将 input 相关操作写成 Input 组件,代码如下:

现在修改文本框值,只会重新渲染 Input 组件,是不会影响到 Child 组件的,即便 Child 外层没有包裹 React.memo()。

import React, { useState } from 'react'

const Parent = () => {
    return (
        <div>
            <Input />
            <Child />
        </div>
    )
}

const Input = () => {
    const [ inputValue, setInputValue ] = useState<string>('')

    return (
        <input 
            type="text"
            value={inputValue}
            onChange={e => setInputValue(e.target.value)}
        />
    )
}

const Child = () => {
    console.log('Child')

    return (
        <div>
            Child
        </div>
    )
}

export default Parent

useState 定义复杂类型变量需要引用变化才会重新渲染组件

对于数组或者对象字面量,直接改变值,不改变引用,组件是不会重新渲染的。

如下示例: 在 onAdd() 中只改变了数组变量 arr 的值,并没有改变引用,不会触发页面更新。

import React, { useState } from 'react'

const Parent = () => {
    const [ name, setName ] = useState('')
    const [ arr, setArr ] = useState([ { id: Math.random(), name: 'a' } ])

    const onAdd = () => {
        arr.push({ id: Math.random(), name })
        setArr(arr)
    }

    return (
        <div>
            <input 
                type="text" 
                value={name}
                onChange={e => setName(e.target.value)}
            />
            <button onClick={onAdd}>Add</button>
            {
                arr.map(item => (
                    <div key={item.id}>
                        {item.name}
                    </div>
                ))
            }
        </div>
    )
}

export default Parent

改变数组引用才会更新,下面三种方法都行。

第一种方法: 先改变数组引用,然后再进行相关操作

const [ arr, setArr ] = useState([ { id: Math.random(), name: 'a' } ])

const onAdd = () => {
  // 先改变数组引用
  const newArr = [ ...arr ]
  newArr.push({ id: Math.random(), name })
  setArr(newArr)
}

第二种方法: 直接用原数组进行相关操作,在赋值的时候改变数组引用

const [ arr, setArr ] = useState([ { id: Math.random(), name: 'a' } ])

const onAdd = () => {
  arr.push({ id: Math.random(), name })
  // 最后赋值时改变数组引用
  setArr([ ...arr ])
}

第三种方法: 使用回调函数,本质上也是在赋值时改变数组引用

const [ arr, setArr ] = useState([ { id: Math.random(), name: 'a' } ])

const onAdd = () => {
  setArr(prev => [ ...prev, { id: Math.random(), name } ])
}

props 变量改变会重新渲染组件

这看起来是一句废话,props 改变,当然会重新渲染组件。

如下示例:

在父组件内点击按钮不断改变子组件 props 变量 visible 的值,即便子组件 Child 压根没用到这个变量,也会重新渲染。

因此: 移除组件未使用到的 props 变量很重要,可以减少不必要的渲染。

import React, { useState } from 'react'

const Parent = () => {
    const [ visible, setVisible ] = useState(false)

    return (
        <div>
            <button onClick={() => setVisible(!visible)}>click</button>
            <Child visible={visible} />
        </div>
    )
}

const Child = React.memo(({ visible }: any) => {
    console.log('Child')

    return (
        <div>
            Child
        </div>
    )
})

export default Parent

memo 是浅比较/引用比较,引用改变时才会重新渲染组件

上面介绍过 React.memo(),不过子组件是没有 props 参数的。

如下示例: 两个子组件都用 React.memo() 进行包裹,接受 props 传参的,并且 props 都是复杂数据类型。

点击按钮 Child1 会渲染,Child2 不会渲染。

import React, { useState } from 'react'

const Parent = () => {
    const [ obj, setObj ] = useState({
        visible: false,
        child: [ 1, 2, 3 ],
    })

    const onChange = () => {
        setObj({ ...obj, visible: !obj.visible })
    }

    return (
        <div>
            <button onClick={onChange}>change {String(obj.visible)}</button>
            <Child1 data={obj} />
            <Child2 data={obj.child} />
        </div>
    )
}

const Child1 = React.memo(({ data }: any) => {
    console.log('Child 1')

    return (
        <div>
            Child 1
        </div>
    )
})

const Child2 = React.memo(({ data }: any) => {
    console.log('Child 2')

    return (
        <div>
            Child 2
        </div>
    )
})

export default Parent

原因是 Child1 的参数 obj 引用变了,而 Child2 的参数 obj.child 引用没有变。

下面这段代码能看懂,就能明白为什么会这样。

const obj = { visible: false, child: [ 1, 2, 3 ] }
const obj2 = { ...obj }
obj === obj2  // false
obj.child === obj2.child  // true

useEffect 执行机制

useEffect(callback, dependence)

  • 第一个参数是个回调函数,下面简称 useEffect 函数;
  • 第二个参数是个数组,下面简称 useEffect 依赖

当 useEffect 依赖是个空数组时,useEffect 函数只会在组件第一次渲染时执行一次,执行时可以获取 props 变量和 state 变量。

后续不论是组件的 props 改变,还是组件的 state 改变,都不会再次执行 useEffect 函数。

import React, { useEffect, useState } from 'react'

const Parent = () => {
    const [ visible, setVisible ] = useState(false)

    return (
        <div>
            <button onClick={() => setVisible(!visible)}>change visible</button>
            <Child visible={visible} />
        </div>
    )
}

const Child = ({ visible }: any) => {
    const [ firstName, setFirstName ] = useState('Ethan')
    const [ lastName, setLastName ] = useState('Williams')

    useEffect(() => {
        console.log('visible', visible)
        console.log('firstName', firstName)
        console.log('lastName', lastName)
    }, [])

    return (
        <div >
            Child
            <input type="text" value={firstName} onChange={e => setFirstName(e.target.value)} />
            <input type="text" value={lastName} onChange={e => setLastName(e.target.value)} />
        </div>
    )
}

export default Parent

按照如下示例在 useEffect 依赖中添加了 visible 变量。

含义是只要 Child 组件的 props 变量 visible 值变了,useEffect 函数就会被执行一次,并且每一次执行 useEffect 函数,useEffect 函数内部都可以获取最新的 props 变量值和 state 变量值,即便这些变量 (如: firstName, lastName) 没有写到 useEffect 依赖中。

useEffect(() => {
    console.log('visible', visible)
    console.log('firstName', firstName)
    console.log('lastName', lastName)
}, [ visible ])

这里有一个误区:

不是 useEffect 函数用到的所有 props 变量和 state 变量都要写到 useEffect 依赖中。

useEffect 渲染机制是:

写到 useEffect 依赖里的变量只要值变了就会触发 useEffect 函数

只要触发了 useEffect 函数,在函数内部就可以获取到最新的 props 变量值和 state 变量值

这里也容易造成页面死循环,如下示例:

  • firstName 数值变了,会触发 useEffect 函数;
  • useEffect 函数内部又会改变 firstName 的值;
  • firstName 数值变了,会触发 useEffect 函数;
  • useEffect 函数内部又会改变 firstName 的值;
  • ……
const [ firstName, setFirstName ] = useState('Ethan')

useEffect(() => {
    setFirstName(Math.random())
}, [ firstName ])

结论: 在 useEffect 函数内不要修改 useEffect 依赖中的变量的值。

state 变量批量更新对于 useEffect 的影响

如下代码: useEffect 依赖中包含了两个 state 变量,在按钮事件中先后修改这两个 state 变量值,理论上 useEffect 函数会执行两次,但实际运行 useEffect 函数只执行了一次。

import React, { useEffect, useState } from 'react'

const Parent = () => {
    const [ name, setName ] = useState('a')
    const [ age, setAge ] = useState(21)

    useEffect(() => {
        console.log('useEffect ...');
    }, [ name, age ])

    return (
        <button
            onClick={() => {
                setName('dkvirus')
                setAge(18)
            }}
        >
            change name and age
        </button>
    )
}

export default Parent

ReactJS 自己做了优化,在同一个 onClick 或者 onChange 中连续调用的多个 setState,React 会将这多个状态更新合并为一次批量更新。

因此,担心连续修改多个 state 变量会多次触发 useEffect 函数,进而将 state 变量合并为一个对象字面量的想法大可不必了。

上面的示例代码和下面的示例代码执行行为是一样的。

import React, { useEffect, useState } from 'react'

const Parent = () => {
    const [ people, setPeople ] = useState({ name: '', age: 21 })

    useEffect(() => {
        console.log('useEffect ...');
    }, [ people ])

    return (
        <button
            onClick={() => {
                setPeople({
                    name: 'dkvirus',
                    age: 18,
                })
            }}
        >
            change name and age
        </button>
    )
}

export default Parent

返回首页

本文总阅读量  次
皖ICP备17026209号-3
总访问量: 
总访客量: