使用Hook实现加减
React Hook
v16.8.0以后的版本可以使用Hook,Kook提供了API:props,state,context,refs以及生命周期。
函数副作用:是指函数在正常工作任务之外对外部环境所施加的影响。具体地说,函数副作用是指函数被调用,完成了函数既定的计算任务,但同时因为访问了外部数据,尤其是因为对外部数据进行了写操作,从而一定程度地改变了系统环境。
注意: Hook 在class 内部是不起作用的
Hook是一个特殊的函数,可以让你 钩入 React的特性。比如useState
是允许在函数组件中添加state的Hook;
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState<number>(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Hook将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。
class给目前的工具带来了一些问题,例如class不能很好的压缩,并且会使热重载出现不稳定的情况;Hook可以让使用者在非class的情况下使用更多的React特性;
副作用: 在React组件中执行过数据获取、订阅或者手动修改过DOM;这些操作成为“副作用”或者“作用”
。
useEffect是一个Effect Hook,给函数组件增加了操作副作用的能力;它和class组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个API;
Hook使用规则:
Hook就是JavaScript函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用Hook;不要再循环、条件判断或者子函数中调用;
- 只能在React的函数组件中调用Hook;不要在其他JS函数中调用;
自定义Hook
有时候会想在组件之间重用一些状态逻辑;目前为止,有两种方案解决:高阶组件
和render props
;自定义Hook可以在不增加组件的情况下,实现这个目标。
函数名字以“use”开头并调用其他Hook,就可以说这是一个自定义Hook。
其他Hook
useContext
:不使用组件嵌套就可以订阅React的context;
一、State Hook
useState方法的返回值是当前state和更新state的函数;这与 class 里面 this.state.count
和 this.setState
类似。
state只在组件首次渲染时被创建;下一次重新渲染时,useState返回给我们当前的state;所以叫useState
,而不叫createState
。
const [count, setCount] = useState()
二、Effect Hook
useEffect
Hook可以看做是componentDidMount
,componentDidUpdate
,componentWillUnmount
这三个函数的组合。
useEffect
会在每次渲染后都执行,默认:第一次渲染后和每次更新后都会执行。
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
声明了count
state变量,紧接着传递函数给useEffect Hook;我们把这个函数叫做 effect;
与componentDidMount
和 componentDidUpdate
不同,使用 useEffect
调度的 effect不会阻塞浏览器更新屏幕,这会让你的应用看起来响应更快。(多数情况下,effect不需要同步执行);
不需要清除的effect: 发送网络请求,手动变更DOM,记录日志;
需要清除的effect: 订阅外部数据源;(清除的目的是防止内存泄漏)
在React class中,通常会在 componentDidMount
中设置订阅,并在componentWillUnmount
中清除它;
在useEffect中返回一个函数:会在离开组件时执行;每次渲染时,在执行当前effect之前都会对上一个effect进行清除;
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
使用Hook其中一个目的就是:解决class生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同的方法中(比如:didmount需要访问数据,willUnmount需要清除数据)
useEffect可以设置多个,React将按照effect声明的顺序依次调用组件中的每一个effect。
通过跳过Effect进行性能优化:
在react中,每次渲染后,都执行清理或者执行effect可能导致性能问题,所以可以这样解决:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。
React会等待浏览器完成画面渲染之后才会延迟调用useEffect
。
三、Hook规则
只在最顶层使用Hook,不在循环、条件或嵌套函数中调用Hook。
Hook的调用顺序在多次渲染之间保持一致,React就能正确将内部state和对应Hook进行关联。
如果在条件语句中调用,下一次条件为假,不执行该Hook,那么在它下面的Hook 调用都被提前执行,就会导致bug的产生。如果想要有条件地执行effect,可以将判断放在Hook的内部。
四、自定义Hook
自定义Hook就是一个普通的函数,但是名字始终以 use
开头;
如何使用自定义Hook?
如果两个函数中有重复的逻辑,我们可以将重复的逻辑抽离出来,写在一个自定义Hook函数中。
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
FriendStatus
和 FriendListItem
组件都想知道好友是否在线:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
在两个组件中,使用相同的Hook 不会共享state;
自定义Hook解决了以前在React组件中无法灵活共享逻辑的问题,在某些场景下,我们可以利用自定义Hook,如表单处理、动画、订阅声明、计时器等。
五、Hook的API索引
useState
:
setState
函数用于更新state,它返回的第一个值始终是更新后最新的state;
setState可以传一个数据,也可以传一个函数。
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
与class组件中的setstate
方法不同,useState
不会自动合并并更新对象。
如果初始state需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始state;注意: 此函数只在初始渲染时被调用。
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
注意: 调用State Hook的更新函数并传入当前的state时,React将跳过子组件的渲染及effect的执行。(使用object.js比较算法来比较state)
useEffect
:
接收一个函数;
在函数组件主体内(这里指在React渲染阶段)改变DOM、添加订阅、设置定时器、记录日志以及包含其他副作用的操作都是不被允许的。
我们可以选择使用useEffect
完成副作用操作;**一定要注意:**赋值给useEffect
的函数会在组件渲染到屏幕之后执行。
理论上讲,effect只在每轮渲染结束后执行,但是可以选择让它只在某些值改变的时候才执行。这里要注意:useEffect会保证在任何新的渲染前执行。React将在组件更新前刷新上一轮渲染的effect。
(浏览器在完成布局和绘制之后,传给useEffect的函数会延迟调用,因此不能在函数中执行阻塞浏览器更新屏幕的操作)
通常来说,组件卸载时需要清除effect创建的诸如订阅或计时器ID等资源;要实现这一点,useEffect
函数需要返回一个清除函数:(为防止内存泄漏,清除函数会在组件卸载前执行;同时,在执行下一个effect之前,上一个effect就已经被清除)
这意味着:下面这个函数,组件每更新一次都会创建新的订阅。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
这里要注意:并非所有的effect都可以被延迟执行;例如:在浏览器执行下一次绘制前,用户可见的DOM变更必须同步执行,这样用户才不会感觉视觉上的不一致;因此React提供了useLayoutEffect
Hook来处理这类effect;它和useEffect
的结构相同,只是调用时机不同。
useContext
:
接收一个context
对象(React.createcontext
的返回值)并返回该context的当前值;当前的context值由上层组件中距离当前组件最近的 <MyContext.Provider>
的value
prop决定。
useReducer
:适合管理包含多个子值的state对象
useRef
:
不仅仅可以拿到节点,也可以将某一个数据设置为当前ref,这样的话,新一次渲染时,该数据不会被重新渲染;