Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。同时,它是100% 向后兼容的,不包含任何破坏性改动。
在我们讲解React Hook之前,先讲一下为什么要使用它,就好比谈对象,你得先搞清楚你喜欢他什么。我们知道,在没有 hook 之前,我们写一个 react 项目总是避免不了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; }
render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } } |
我想,对于很多做前端的朋友而言,如果学过后端语言还好,因为他们对类有一个明确的概念,对构造器和继承的理解也很容易,但对于很多直接就开始做前端的朋友(比如:非科班出生的一些自学者),突然让他们接受一个类的概念是很难适应的,虽然 ES6 已经有了 class。但是,如果你用 hook,就可以摆脱 class 了,上面的代码就变成了下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import React, { useState } from 'react';
function Example() { // 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0);
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } |
少了extends,class,constuctor,super等关键字,是不是感觉一下子轻松了很多,同时,onClick 的代码也精简了很多。并且,对于前端来说,一个组件实现的是一个功能,用 function 来定义一个组件,比起用 class 来说,更符合语境。
当然,这只是其中的一个点,它的好处还有很多:
Hook 使你在无需修改组件结构的情况下复用状态逻辑,这使得在组件间或社区内共享 Hook 变得更便捷;
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
Hook 使你在非 class 的情况下可以使用更多的 React 特性,让你的代码远离class。
废话到此结束,接下来,就分别讲讲各个 hook 的用法和作用。不过要记住一点,使用每个 hook 都得先从 react (16.8+版本) 中引入
1 | import { useState } from 'react'; |
1、useState
上面的代码已经展示过它的功能了,useState 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import React, { useState } from 'react';
function Example() { // 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0);
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } |
useState 支持我们在调用的时候直接传入一个值,来指定 state 的默认值,他支持可以作为js参数一切数据,包括函数,但如果是函数的话,函数必须要有返回值。
1 2 3 4 5 | const [ obj,setObj ] = useState({ a: 1 }), const [ arr,setArr ] = useState([ 1, 2 ]), const [ count, setCount ] = useState(() => { return props.count || 0 }) |
我们在使用 useState 时,如果想要获取上一轮该 state 的值的话,只需要在 useState 返回的第二个参数,传入一个参数,该函数的参数就是上一轮的 state 的值。
1 2 3 | setCount(count){ return count + 1 } |
当我们使用多个 useState 的时候,那 react 是如何识别哪个个是哪个呢?其实很简单,它是靠第一次执行的顺序来记录的,就相当于每个组件存放useState 的地方是一个数组,每使用一个新的 useState,就向数组中 push 一个 useState,所以,当我们在运行时改变 useState 的顺序,数据会混乱,增加 useState,程序会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function App (props) { let count, setCount let sum, setSum if (count > 2) { [ count, setCount ] = useState(0) [ sum, setSum ] = useState(10) } else { [ sum, setSum ] = useState(10) [ count, setCount ] = useState(0) } return ( <div> 点击次数: { count } 总计:{ sum } <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button> </div> ) } |
2、useEffect
如果你熟悉 React Class 的生命周期函数,你可以把 useEffect 看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合,并且它会根据你传递的第二个参数达到灵活多变的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 不使用useEffect class App extends PureComponent { componentDidMount() { document.title = count } componentDidUpdate() { document.title = count } } // 使用useEffect function App () { useEffect(() => { document.title = count }) } |
useEffect 支持第二个参数,分三种情况:
1、什么都不传,组件每次 render 之后 useEffect 都会调用,相当于 componentDidMount 和 componentDidUpdate;
2、传入一个空数组 [], 只会调用一次,相当于 componentDidMount 和 componentWillUnmount;
3、’传入一个数组,其中包括变量,只有这些变量变动时,useEffect里的方法才会执行。
如果 useEffect 最后 return 了一个方法,return 的方法会在 componentWillUnmount 阶段执行,比如定时器的清除,就可以通过 return 一个 clearTimer 方法来搞定。
在实际开发中,适当的把逻辑拆分成多个 effect,不仅业务清晰,再配合好第二个参数,生命周期的问题就迎刃而解了。且看下面例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | function App () { const [ count, setCount ] = useState(0) const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => { setWidth(document.body.clientWidth) }
useEffect(() => { // 相当于 componentDidMount window.addEventListener('resize', onChange, false)
return () => { // 相当于 componentWillUnmount window.removeEventListener('resize', onChange, false) } }, [])
useEffect(() => { // 相当于 componentDidUpdate document.title = count })
useEffect(() => { console.log(`count change: count is ${count}`) }, [ count ])
return ( <div> 页面名称: { count } 页面宽度: { width } <button onClick={() => { setCount(count + 1)}}>点我</button> </div> ) } |
3、useMemo
useMemo 主要用于一个变量依赖于另一个变量,有点类似于vue的计算属性,主要用于性能优化。同时它也支持传入第二个参数,用法和 useEffect 类似。不过需要注意的是,它的首次执行是在渲染的时候,而不是渲染完成之后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | function App(props) { const { pullUp, pullDown, onScroll } = props;
// 对传入的上拉和下拉函数进行防抖处理 let pullUpDebounce = useMemo(() => { return debounce(pullUp, 300) }, [pullUp]);
let pullDownDebounce = useMemo(() => { return debounce(pullDown, 300) }, [pullDown]);
// 绑定scrollEnd方法 useEffect(() => { bScroll.on('scrollEnd', () => { //判断是否滑动到了底部 if(bScroll.y <= bScroll.maxScrollY + 100){ pullUpDebounce(); } }); return () => { bScroll.off('scrollEnd'); } }, []);
// 绑定touchEnd方法 useEffect(() => { bScroll.on('touchEnd', (pos) => { //判断用户的下拉动作 if(pos.y > 50) { pullDownDebounce(); } }); return () => { bScroll.off('touchEnd'); } }, []); } |
4、useCallback
useCallback 可以说是 useMemo 的语法糖,能用 useCallback 实现的,都可以使用 useMemo,在 react 中我们经常面临一个子组件渲染优化的问题,尤其是在向子组件传递函数props时,每次 render 都会创建新函数,导致子组件不必要的渲染,浪费性能,这个时候,就是 useCallback 的用武之地了,useCallback 可以保证,无论 render 多少次,我们的函数都是同一个函数,减小不断创建的开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 例1 const onClick = useMemo(() => { return () => { console.log('button click') } }, [])
const onClick = useCallback(() => { console.log('button click') }, [])
// 例2 const [count1, changeCount1] = useState(0); const [count2, changeCount2] = useState(10);
const calculateCount = useCallback(() => { if (count1 && count2) { return count1 * count2; } return count1 + count2; }, [count1, count2])
useEffect(() => { const result = calculateCount(count, count2); message.info(`执行副作用,最新值为${result}`); }, [calculateCount]) |
useCallback 和直接使用 useEffect 不同的地方在于使用 useCallback 生成计算的回调后,在使用该回调的副作用中,第二个参数应该是生成的回调。其实这个问题是很好理解的,我们使用 useCallback 生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会重新生成新的回调,副作用监听到了回调的变化就会去重新执行副作用,此时 useCallback 和 useEffect 是按顺序执行的, 这样就实现了副作用逻辑的抽离。同样,useCallback 的第二个参数和 useMemo一样,没有区别。
5、useRef
useRef 总共有两种用法:
1、获取子组件的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | const Children = forwardRef((props, ref) => { <div> <p>{props.title}</p> </div> })
function App () { const [ count, setCount ] = useState(0) // 如果children组件不是一个forwardRef,这里会报错 const childrenRef = useRef(null) // const const onClick = useMemo(() => { return () => { console.log('button click') console.log(childrenRef.current) // 这里可以得到Children实例 setCount((count) => count + 1) } }, []) return ( <div> 点击次数: { count } <!-- ref得添加在一个forwardRef上才行 --> <Children ref={childrenRef} count={count}></Children> <button onClick={onClick}>点我</button> </div> ) } |
2、在函数组件中的一个全局变量,不会因为重复 render 而重复申明, 类似于类组件的 this.xxx。有些情况下,我们需要保证函数组件每次 render 之后,某些变量不会被重复申明,比如说 Dom 节点,定时器的 id 等等,在类组件中,我们完全可以通过给类添加一个自定义属性来保留,比如说 this.xxx, 但是函数组件没有 this,自然无法通过这种方法使用,有的朋友说,我可以使用 useState 来保留变量的值,但是 useState 会触发组件 render,在这里完全是不需要的,我们就需要使用 useRef 来实现了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function App () { const [ count, setCount ] = useState(0) // 注意,这里的ref并没有指定给任何元素 const timer = useRef(null) console.log(timer)
let timer2
useEffect(() => { let id = setInterval(() => { setCount(count => count + 1) }, 500)
timer.current = id timer2 = id return () => { clearInterval(timer.current) } }, [])
const onClickRef = useCallback(() => { clearInterval(timer.current) }, [])
const onClick = useCallback(() => { clearInterval(timer2) }, [])
return ( <div> 点击次数: { count } <button onClick={onClick}>普通</button> <button onClick={onClickRef}>useRef</button> </div> ) } |
6、useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值,说简单点就是,子组件可以选择性的暴露给父组件一些方法,这样可以隐藏一些私有方法和属性,官方建议,useImperativeHandle 应当与 forwardRef 一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function Kun (props, ref) { const kun = useRef()
const introduce = useCallback (() => { console.log('i can sing, jump, rap, play basketball') }, []) // 这里用useImperativeHandle暴露了一个方法出去 useImperativeHandle(ref, () => ({ introduce: () => { introduce() } }));
return ( <div ref={kun}> { props.count }</div> ) }
const KunKun = forwardRef(Kun)
function App () { const [ count, setCount ] = useState(0) const kunRef = useRef(null)
const onClick = useCallback (() => { setCount(count => count + 1) // 这里使用暴露出来的方法,执行子组件的内部逻辑 kunRef.current.introduce() }, []) return ( <div> 点击次数: { count } <KunKun ref={kunRef} count={count}></KunKun> <button onClick={onClick}>点我</button> </div> ) } |
7、useReducer
useReducer 有点类似 redux 中的功能,相较于 useState,它更适合一些逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等特定场景。
useReducer 总共有三个参数:
第一个参数是 一个 reducer,就是一个函数类似 (state, action) => newState 的函数,传入 上一个 state 和本次的 action;
第二个参数是初始 state,也就是默认值,是比较简单的方法;
第三个参数是惰性初始化,这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } }
function App() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <> 点击次数: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } |
好了,讲了那么多,我们似乎知道了 Hook 究竟是怎么一回事,说白了,其实 Hook 就是返回包含了更多逻辑的 State 以及改变 State 的方法。
接下来我们来自定义一个自己的钩子,以计数器来为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import React, { useState } from 'react';
// 编写我们自己的hook,名字以use开头 function useCounter(initialValue) { // 接受初始化的值生成state const [count, changeCount] = useState(initialValue); // 声明减少的方法 const decrease = () => { changeCount(count - 1); } // 声明增加的方法 const increase = () => { changeCount(count + 1); } // 声明重置计数器方法 const resetCounter = () => { changeCount(0); } // 将count数字与方法返回回去 return [count, { decrease, increase, resetCounter }] }
export default function myHooksView() { // 在函数组件中使用我们自己编写的hook生成一个计数器,并拿到所有操作方法的对象 const [count, controlCount] = useCounter(10); return ( <div> 当前数量:{count} <button onClick={controlCount.decrease}>减少</button> <button onClick={controlCount.increase}>增加</button> <button onClick={controlCount.resetCounter}>重置</button> </div> ) } |
本文主要参考文章:
https://www.cnblogs.com/ascoders/p/10591832.html、
https://blog.youkuaiyun.com/landl_ww/article/details/102158814