useState
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state
) 与传入的第一个参数 (initialState
) 值相同。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState);
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState
。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState
的两种用法:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1)
}
function handleClickFn() {
setCount((prevCount) => {
return prevCount + 1
})
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
两种方式的区别
注意上面的代码,handleClick
和handleClickFn
一个是通过一个新的 state 值更新,一个是通过函数式更新返回新的 state。现在这两种写法没有任何区别,但是如果是异步更新的话,那你就要注意了,他们是有区别的,来看下面例子:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
当我设置为异步更新,点击按钮延迟到3s之后去调用setCount
函数,当我快速点击按钮时,也就是说在3s多次去触发更新,但是只有一次生效,因为 count
的值是没有变化的。
当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是代码中的 prevCount
每次都是最新的值。
其实这个特点和类组件中 setState
类似,可以接收一个新的 state 值更新,也可以函数式更新。如果新的 state 需要通过使用先前的 state 计算得出,那么就要使用函数式更新。
因为setState更新可能是异步,当你在事件绑定中操作 state 的时候,setState更新就是异步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 这样写只会加1
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
当你在定时器中操作 state 的时候,而 setState 更新就是同步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 这样写是正常的,两次setState最后是加2
}, 3000);
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
注意这里的同步和异步指的是 setState 函数。因为涉及到 state 的状态合并,react 认为当你在事件绑定中操作 state 是非常频繁的,所以为了节约性能 react 会把多次 setState 进行合并为一次,最后在一次性的更新 state,而定时器里面操作 state 是不会把多次合并为一次更新的。
注意:与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。
性能优化
React 使用 Object.is 比较算法来比较 state。
在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表单的值
const addClick = () => setNumber(number + 1)
const data = { number }
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
如要避免不必要的子组件的重渲染,使用 React.memo
仅检查 props 变更。 默认情况下其只会对复杂对象做浅层对比。所有使用 memo 优化后的代码如下:
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child); // 在这里优化了
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表单的值
const addClick = () => setNumber(number + 1)
const data = { number }
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
你以为代码中的Child = memo(Child);
已经优化了吗,然而并没有,当你在更改了父组件的状态,子组件依然会重新渲染,因为这关系到了React是如何浅层比较的,在子组件中onButtonClick
和 data
都是引用类型,所以他们是始终都不相等的,也就是[]===[]
这样比较时始终返回false,在基本数据类型比较时memo才会起作用。
关于如何解决这个问题,我们就要使用两个新的API,useMemo和useCallback的Hook。下面是经过优化之后的代码。
function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child)
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表单的值
const addClick = useCallback(() => setNumber(number + 1), [number])
const data = useMemo(() => ({ number }), [number])
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
export default App;
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useCallback返回一个 memoized 回调函数。useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
useCallback 和 useMemo 参数相同,第一个参数是函数,第二个参数是依赖项的数组。主要区别是 React.useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数而不调用它。