目录:
如下代码: 父组件 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
对于数组或者对象字面量,直接改变值,不改变引用,组件是不会重新渲染的。
如下示例: 在 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 变量 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
上面介绍过 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(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 变量值
这里也容易造成页面死循环,如下示例:
const [ firstName, setFirstName ] = useState('Ethan')
useEffect(() => {
setFirstName(Math.random())
}, [ firstName ])
结论: 在 useEffect 函数内不要修改 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
↶ 返回首页 ↶