# react-hooks 的新特性
# 概念
我们知道,在react里面只有类组件才可以使用state,很多时候我们在编写函数组件的时候突然意识到需要state才可以完成特定需求的时候,最后不得不将函数组件转化为class组件的写法,而hooks就是让我们可以在函数组件当中使用state的新特性
# 注意事项
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用
# useState
# 初体验
我们先来看下react-hooks中的第一个简单用法, 函数组件中可以通过useState
来给组件添加state,但是和class组件不一样的地方在于他不会把新的state和旧的state进行合并,而每次渲染的时候都是一个独立的state,我们先来看下基本用法来理解一下他
function Counter1() {
const [number, setNumber] = useState(0)
return (
<>
<p>{ number }</p>
<button onClick={() => setNumber(number + 1)}>+</button>
</>
)
}
还是这个简单计数器例子,我们通过useState(0)
创建一个state,并且初始值为0,而返回值是一个数组,至于为什么是一个数组我们放在下一章节手写一个react-hooks
中来解释,这个数组的第一项就是我们想要的state中的值,第二项就是一个更新这个值的方法,它接收一个新的 state 值并将组件的一次重新渲染加入队。
# 每次渲染都是独立的闭包
怎么理解这句话呢,我们先写一个例子
function Counter2(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
alert(number);
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
我们会发现,当我们点击第一个按钮两次,在点击第二个按钮一次,最后点击第一个按钮两次,最后alert的值会是2而不是我们想象中的4
- 为什么会出现这个原因呢?
我们可以这么理解,当我们点击alertNumber的时候,调用了state中number的值,我们可以理解为相当于一个闭包,state里面的值一直更给引用,所有获取到的是点击时候当前的值(虽然正确的解释不是这样的~~)
# 函数式更新
还是老规矩我们先来看一个例子
function Counter3() {
const [number, setNumber] = useState(0)
function lazy1() {
setTimeout(() =>{
setNumber(number+1)
}, 3000)
}
return (
<>
<p>{ number }</p>
<button onClick={() => setNumber(number + 1)}>+</button>
<button onClick={lazy1}>lazy1 number+</button>
</>
)
}
TIP
当我们点击第一个按钮两次,在点击第二个按钮一次,最后点击第一个按钮两次的时候,页面最终显示的结果是啥
我们看下效果图
结果最后由4变成3吧,至于为什么,其实和上一个小节一样,我们只需记住每一次渲染都是独立的,获取的都是当前状态的值,这里在点击第二个按钮的时候,此时的numner的值为2,所以三秒后的页面上的值为 2 + 1
但是问题来了,我们如果想实现每次点击第二个按钮,获取到的都是最新的一个值,那我们可以通过setNumber(fn)
的写法来实现
function Counter3() {
const [number, setNumber] = useState(0)
function lazy2() {
setTimeout(() =>{
setNumber(number =>number + 1)
}, 3000)
}
return (
<>
<p>{ number }</p>
<button onClick={() => setNumber(number + 1)}>+</button>
<button onClick={lazy2}>lazy2 number+</button>
</>
)
}
我们来看下效果
是不是就可以实现我们的效果了,其中 setNumber(number =>number + 1)
中的形参number
表示上一个状态的number
,这也是我们这节所说的函数式更新,是不是和 this.setState
很像啊
# 惰性初始 state
在我们日常开发中,有时候我们需要给组件传入一些状态值作为组件的初始化state来使用吧,我们来看下hooks是怎么应对这种情况的
function Counter4(props) {
function initState() {
return {
number:props.number
}
}
const [counter, setCounter] = useState(initState)
return (
<>
<p>{ counter.number }</p>
<button onClick={() => setCounter({number : counter.number + 1})}>+</button>
</>
)
}
ReactDOM.render(<Counter4 number={5}/>, window.root)
从代码中我们可以看出,组件Counter4传入了一个number属性,而我们的useState
的用法是一种useState(fn)
的用法,这样的话就可以实现上述的功能了。
需要注意的是,fn只会在组件一开始第一个渲染的时候执行,只执行一次,后序渲染会直接忽略。
TIP
为什么叫 惰性初始 state ?
为什么叫惰性初始化呢,其实和我们的路由懒加载差不多,首先它是一个函数,只有在我们使用到counter的时候他才会执行这个函数,使用则不执行
# 性能优化
其实hooks里面集成了很多性能优化,这里我们就挑2个地方来简单的说一下吧
# 每次修改之后state没有发生变化,则页面不重新渲染
function Counter5(props) {
console.log('render')
// 如果当状态发生变化的时候,状态的值没有改变的话 不会重新渲染组件
const [number, setCounter] = useState(0)
return (
<>
<p>{ number }</p>
<button onClick={() => setCounter(number + 1)}>+</button>
<button onClick={() => setCounter(number)}>+</button>
</>
)
}
执行上述代码的时候我们发现,点击第一个按钮,state的值发生了改变,所以控制台打印了'render', 但是当我们点击第二个按钮的时候,因为state的值没有改变,我们发现控制台并没有输出东西,这也是hooks内部做的一个小优化,当调用setXXX()来修改state的值,如果值没有发生变化,组件不会重新渲染
# 使用 useMomo、useCallback 减少渲染次数
我们先来看一个比较简单的例子
function Counter6(props) {
console.log('Counter6 render')
const [number, setNumber] = useState(0)
const [counter, setCounter] = useState(0)
const data = {
counter,
}
const addClick = () => setCounter(counter + 1)
return (
<>
<input value={number} onChange={(e) => {setNumber(e.target.value)}} />
<SubCounter data={data} onClick={addClick} />
</>
)
}
function SubCounter(props) {
console.log('SubCounter render')
return (
<button onClick={props.onClick}>{props.data.counter}</button>
)
}
当我们在输入框中修改输入框的值,实际上是只修改了state里面的number的值,没有修改counter的值,但是页面上出现了2个console,这表明了两个组件Counter6
和SubCounter
都重新渲染了,其实这也是正常现象,这是因为我们hooks每次更新都是独立的,也就是每次拿到的data等值都是最新的,所以触发更新
但是啊,hooks给我们提供了2个可以减少这类情况渲染次数的方法,我们力求于修改state中的number的时候,组件SubCounter
没有重新渲染
SubCounter1 = memo(SubCounter1) // new
function Counter7(props) {
console.log('Counter7 render')
const [name, setName] = useState('CY')
const [counter, setCounter] = useState(0)
const data = useMemo(() => ({ counter }), [counter]) // new
const addClick = useCallback(() => setCounter(counter + 1), [counter])// new
return (
<>
<input value={name} onChange={(e) => {setName(e.target.value)}} />
<SubCounter1 data={data} onClick={addClick} />
</>
)
}
function SubCounter1(props) {
console.log('SubCounter1 render')
return (
<button onClick={props.onClick}>{props.data.counter}</button>
)
}
在代码中我们可以发现使用了memo(Component)
将我们的子组件包裹起来,使用useMemo()
和 useCallback()
将我们的数据和方法包裹起来,其中useMemo()
和 useCallback()
的第二个参数是依赖项的意思,表示只有在依赖项发生改变的时候才会重新计算值或者执行函数。这也就是说只有当counter改变的时候才会重新计算或者执行函数,配合memo(Component)
使用的时候,我们单纯的修改state里面的number的时候,是不会触发更新的
# useReducer
看到名字我们就知道是使用redux了,useReducer
内部帮我们集成了redux的整套流程,就不需要我们在组件内部绑定数据,添加订阅啥的,我们可以很优雅的在函数组件里面使用redux了,我们直接看用法吧
import React, { useReducer } from 'react'
import ReactDOM from 'react-dom'
function reducer(state, action) { //创建reducer
switch (action.type) {
case 'ADD':
return {
number: state.number + 1
}
default:
break;
}
}
const initState = 0 // 创建初始值
function Counter() {
const [state, dispatch] = useReducer(reducer, initState, () => ({number : initState}))
return (
<>
<p>{ state.number }</p>
<button onClick={() => (dispatch({type: 'ADD'}))}>+</button>
</>
)
}
ReactDOM.render(<Counter/>, window.root)
TIP
其实useState内部是靠useReducer来实现的
useReducer
的第一个参数是我们的reducer
了,第二个参数是我们state的初始值,第三个参数是我们的函数式更新的写法,可以在初次渲染的时候动态设置我们的初始值,然后我们就可以很简单的拿到 state, dispatch啦
# useContext
useContext
的用法我们就不举例子说了,很简单就可以让我们使用上下文,而不同于以前函数组件使用上下文的时候需要使用ContextConsumer
包裹起来
// 子组件
function SubCounter() {
const {xxx, YYY} = useContext(Context) // 传入context 就可以获取到上下文中的值了
return (
<>
<p>{state.number}</p>
<button onClick={() => {dispatch({type: 'ADD'})}}>+</button>
</>
)
}
# useEffect
# 初体验
useEffect
是我认为react-hooks里面较为新的东西,他的目的没有和useContext
这些一样是为了简化某些操作,或者为函数组件提供某些操作,useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。我们来看一个在以前只有类组件才可以实现的例子
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
componentDidMount() {
document.title = this.state.number;
}
componentDidUpdate() {
document.title = this.state.number;
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.setState({ number: this.state.number + 1 })}>
+
</button>
</div>
);
}
}
该例子的大概意思是,当我们点击按钮修改state的时候,我们需要同步的修改浏览器title的值,这个在函数组件是无法实现的,但是我们在使用hooks之后函数组件也拥有这个能力
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number,setNumber] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = number;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
这就是 useEffect
的大致用法了,我们只需要知道,函数组件也可以和class组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途
TIP
每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染
# 清除副作用
但是我们的useEffect
在某一些特定的情况下是有害的,如
function Counter(){
const [number,setNumber] = useState(0);
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
});
return (
<>
<p>{number}</p>
</>
)
}
如上述代码,我们希望页面上的数据每一秒加一,但是实际的情况确实页面上的数据几乎是成倍的增加,出现了内存泄漏,这是因为我们一直提到,hooks的每一次渲染都是独立的,上述代码相当于每一次渲染都增加了一个计时器,所以计时器叠加导致出现了内存泄漏,那么我们如何避免这种情况呢
function Counter(){
const [number,setNumber] = useState(0);
// 相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
return ()=>{
console.log('销毁老的定时器');
clearInterval($timer);
}
});
return (
<>
<p>{number}</p>
</>
)
}
我们只需要在useEffect(fn)
的fn里面返回一个函数就好了,这个函数会在下一个渲染一开始的时候执行,这个时候我们可以清楚页面上的定时器
我们还有一种方法
function Counter(){
const [number,setNumber] = useState(0);
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[]);
return (
<>
<p>{number}</p>
</>
)
}
useEffect(fn, [xx])
接受第二个参数依赖项,意思是fn的执行依赖于依赖项,当我们传入空数组[]的时候意味着fn只执行一次,所以就不会出现内存泄漏的情况了