咱们组的前端妹子在组内分享时谈到了 react 的钩子,趁此机会我也对我所理解的内容进行下总结,方便更多的同窗了解。在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API,咱们很是有必要了解下他的使用方法,并可以结合咱们的业务编写几个自定义的 hooks。javascript
官方中提供了几个内置的钩子,咱们简单了解下他们的用法。html
须要更新页面状态的数据,咱们能够把他放到 useState
的钩子里。例如点击按钮一下,数据加 1 的操做:前端
const [count, setCount] = useState(0); return (<> <p>{ count}</p> <button onClick = { () => setCount(count + 1) }> add 1 </button> </> ); 复制代码
在 typescript 的体系中,count 的类型,默认就是当前初始值的类型,例如上面例子中的变量就是 number
类型。若是咱们想自定义这个变量的类型,能够在 useState 后面进行定义:java
const [count, setCount] = useState<number | null>(null); // 变量count为number类型或者null类型 复制代码
同时,使用 useState 改变状态时,是整个把 state 替换掉的,所以,若状态变量是个 object 类型的数据,我只想修改其中的某个字段,在以前 class 组件内调用 setState 时,他内部会自动合并数据。react
class Home extends React.Component { state = { name: 'wenzi', age: 20, score: 89 }; update() { this.setState({ score: 98 }); // 内部自动合并 } } 复制代码
但在 function 组件内使用 useState
时,须要本身先合并数据,而后再调用方法,不然会形成字段的丢失。web
const [person, setPerson] = useState({ name: 'wenzi', age: 20, score: 89 }); setPerson({ ...person, { score: 98 } }); // 先合并数据 { name: 'wenzi', age: 20, score: 98 } setPerson({ score: 98 }); // 仅传入要修改的字段,后name和age字段丢失 复制代码
useEffect 能够看作是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。typescript
useEffect 钩子在组件初始化完毕时,必定会执行一次,在组件从新渲染的过程当中,是否还要 update,还要看传入的第 2 个参数。后端
- 当只有回调函数这一个参数时,组件的每次更新,回调都会执行;
- 当有 2 个参数时,只有第 2 参数里的数据发生变化时,回调才执行;
- 只想在组件初始化完毕时只执行一次,第 2 个参数能够传入一个空的数组;
咱们能够看下这个例子,不管点击 add按钮
仍是 settime按钮
,useEffect 的回调都会执行:数组
const Home = () => { const [count, setCount] = useState(0); const [nowtime, setNowtime] = useState(0); useEffect(() => { console.log('count', count); console.log('nowtime', nowtime); }); return ( <> <p>count: {count} </p> <p>nowtime: {nowtime} </p> <button onClick = {() => setCount(count + 1)}> add 1 </button> <button onClick = {() => setNowtime(Date.now())} > set now time </button> </>); }; 复制代码
若改为下面的这样,回调仅会在 count 发生变化时才会在控制台输出,仅修改 nowtime 的值时没有输出:websocket
useEffect(() => { console.log('count', count); console.log('nowtime', nowtime); }, [count]); 复制代码
useEffect 的回调函数还能够返回一个函数,这个函数在 effect 生命周期结束以前调用。为防止内存泄漏,清除函数会在组件卸载前执行。另外,若是组件屡次渲染,则在执行下一个 effect 以前,上一个 effect 就已被清除。
基于上面的代码,咱们稍微修改一下:
useEffect(() => { console.log('count', count); console.log('nowtime', nowtime); return () => console.log('effect callback will be cleared'); }, [count]); 复制代码
基于这个机制,在一些存在添加绑定和取消绑定的案例上特别合适,例如监听页面的窗口大小变化、设置定时器、与后端的 websocket 接口创建链接和断开链接等,均可以预计 useEffect 进行二次的封装,造成自定义的 hook。关于自定义 hook,下面咱们会讲到。
function 组件中定义的变量和方法,在组件从新渲染时,都会从新从新进行计算,例以下面的这个例子:
const Home = () => { const [count, setCount] = useState(0); const [nowtime, setNowtime] = useState(0); const getSum = () => { const sum = ((1 + count) * count) / 2; return sum + ' , ' + Math.random(); // 这个random是为了看到区别 }; return ( <> <p> count: {count}< /p> <p> sum: {getSum()}</p> <p> nowtime: {nowtime}</p> <button onClick = {() => setCount(count + 1)} > add 1 </button> <button onClick = {() => setNowtime(Date.now())}> set now time </button> </>); }; 复制代码
这里有 2 个按钮,一个是 count+1,一个设置当前的时间戳, getSun()
方法是计算从 1 到 count 的和,咱们每次点击 add 按钮后,sum 方法都会从新计算和。但是当咱们点击 settime 按钮时,getSum 方法也会从新计算,这是没有必要的。
这里咱们可使用 useMemo
来修改下:
const sum = useMemo(() => ((1 + count) * count) / 2 + ' , ' + Math.random(), [count]); <p> {sum} </p>; 复制代码
点击查看样例,修改后就能够看到,sum 的值只有在 count 发生变化的时候才从新计算,当点击 settime 按钮的时候,sum 并无从新计算。这要得益于 useMemo
钩子的特性:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); 复制代码
useMemo 返回回调里 return 的值,并且 memoizedValue 它仅会在某个依赖项改变时才从新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。若是没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
在上面的例子里,只有 count 变量发生变化时,才从新计算 sum,不然 sum 的值保持不变。
useCallback 与 useMemo 类型,只不过 useCallback 返回的是一个函数,例如:
const fn = useCallback(() => { return ((1 + count) * count) / 2 + ' , ' + nowtime; }, [count]); 复制代码
在官方文档里,实现了好友的在线与离线功能。这里咱们本身也学着实现几个 hook。
咱们经过监听resize
事件来获取实时获取window窗口的宽高,对这个方法进行封装后能够在生命周期结束前能自动解绑resize事件:
const useWinResize = () => { const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }); const resize = useCallback(() => { setSize({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) }, []) useEffect(() => { window.addEventListener('resize', resize); return () => window.removeEventListener('resize', resize); }, []); return size; } 复制代码
使用起来也很是方便:
const Home = () => { const {width, height} = useWinResize(); return <div> <p>width: {width}</p> <p>height: {height}</p> </div>; }; 复制代码
点击连接useWinResize的使用能够查看demo演示。
在前端中使用定时器时,一般要在组件生命周期结束前清除定时器,若是定时器的周期发生变化了,还要先清除定时器再从新按照新的周期来启动。这种最经常使用的场景就是九宫格抽奖,用户点击开始抽奖后,先缓慢启动,而后逐渐变快,接口返回中奖结果后,再开始减速,最后中止。
咱们很容易想到用 useEffect
来实现这样的一个 hook:
const useInterval = (callback, delay) => { useEffect(() => { if (delay !== null) { let id = setInterval(callback, delay); return () => clearInterval(id); } }, [delay]); }; 复制代码
咱们把这段代码用到项目中试试:
const Home = () => { const [count, setCount] = useState(0); useInterval(() => { console.log(count); setCount(count + 1); }, 500); return <div > { count } < /div>; }; 复制代码
但是这段运行后很奇怪,页面从 0 到 1 后,就不再变了, console.log(count)
的输出代表代码并无卡死,那么问题出在哪儿了?
React 组件中的 props 和 state 是能够改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果,它们之间再也不有相关性。
useEffect() Hook 也「丢弃」上一次渲染结果,它会清除上一次 effect 再创建下一个 effect,下一个 effect 锁住新的 props 和 state,这也是咱们第一次尝试简单示例能够正确工做的缘由。
但 setInterval 不会「丢弃」。 它会一直引用老的 props 和 state 直到你把它换掉 —— 不重置时间你是没法作到的。
这里就要用到 useRef
这个 hook 了,咱们把 callback 存储到 ref 中,当 callback 更新时去更新 ref.current
的值:
const useInterval = (callback, delay) => { const saveCallback = useRef(); useEffect(() => { // 每次渲染后,保存新的回调到咱们的 ref 里 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); }; 复制代码
当咱们使用新的 useInterval
时,发现就能够自增了,点击查看样例useInterval 的简单使用。
这里咱们使用一个变量来控制增长的速度:
const [count, setCount] = useState(0); const [diff, setDiff] = useState(500); useInterval(() => { setCount(count + 1); }, diff); return ( <div> <p> count: {count} </p> <p> diff: {diff}ms </p> <p> <button onClick = {() => setDiff(diff - 50)}> 加快50ms </button> <button onClick = {() => setDiff(diff + 50)} > 减慢50ms </button> </p> </div>); 复制代码
分别点击两个按钮,能够调整count增长的速度。
使用react hook能够作不少有意思的事情,这里咱们也仅仅是举几个简单的例子,后续咱们也会更加深刻了解hook的原理。
▼ 我是来腾讯的小小前端开发工程师,长按识别二维码关注,与你们共同窗习、讨论 ▼