作者 | 马剑光
居安思危,思则有备,有备无患
下面将介绍这些 hook 的用法:useLayoutEffect、useReducer、useImperativeHandle、useRef、useContext、useCallback、useDebugValue;在介绍当中也会加入一些组合使用的进阶用法等;
1、 useRef
它可以用来获取组件实例对象或者是 DOM 对象;useRef 这个 hooks 函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据。
1.1 定义
const ref = useRef()
1.2 和 createRef 的区别
官网定义如下:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
换句话说 , useRef 在 hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西。createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。这也就是说明为什么它能跨渲染周期保存数据的原因;
1.3 示例
import React, {useState, useEffect,useRef} from 'react'
const App = () => {
const [state, setState] = useState(1)
const ref1 = useRef()
const timerRef = useRef()
const ref2 = createRef()
if (!ref1.current) {
ref1.current = state;
}
if (!ref2.current) {
ref2.current = state;
}
<!--通过useRef保留了timmer,即使多次渲染后timmer值也不会变-->
useEffect(() => {
timerRef.current = setInterval(()=>{
setCount(state => state + 1);
}, 1000);
}, []);
useEffect(()=>{
if(count > 10){
clearInterval(timerRef.current);
}
});
return (
<>
<span>current state: {state}</span>
<span>ref1 value: {ref1.current}</span>
<span>ref2 value: {ref2.current}</span>
</>
)
}
通过运行示例可以看到,ref1 一直保留着state的初始值,timmerRef 保留着对 setInterval 的初始引用,但是 ref2 每次渲染都会获取新的引用。
1.4 总结
useRef 不仅仅是用来获取 DOM 或组件对象的,它还相当于 this , 可以存放任何变量(当然 useMemo 也能实现类似的效果)。
useRef 可以很好的解决闭包带来的不方便性。
2、 useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:
2.1 定义
useImperativeHandle(ref, createHandle, [deps])
ref:定义 current 对象的 ref;
createHandle:一个函数,返回值是一个对象,即这个 ref 的 current 对象;
deps:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件 ref 的 current 属性上,如果为空数组,则不会重新输出。
2.2 示例
const FancyInput =forwardRef( (props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
})
const App = () => {
const inputRef = useRef()
const handleClick = useCallback(()=>inputRef.current.focus(),[inputRef])
return (
<>
<FancyInput ref={inputRef} />
<button onClick={handleClick}>获取焦点</button>
</>
)
}
2.3 总结
函数组件不像 class 组件,可以很方便的拿到组件对象,所以通过u seImperativeHandle 函数可以很方便拿到函数组件的对父组件暴露的方法。
3、useCallback
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, [deps]) 相当于 useMemo(() => fn, [deps])。
3.1 定义
const cb = useCallback(fn, [deps])
fn:返回的处理函数
deps:依赖项,当监听的依赖发生变化返回新的 fn
3.2 和 useMemo 区别
不同的:
useMemo 计算结果是 return 回来的值, 主要用于 缓存计算结果的值 ,应用场景如:需要 计算的状态
useCallback 计算结果是函数,主要用于缓存函数,应用场景如:需要缓存的函数,因为函数式组件每次任何一个 state 的变化整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
相同点:
只要依赖数据发生变化,才会重新计算结果,也就是起到缓存的作用。
useCallback 注意:
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
3.3 示例
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
3.4 总结
当父组件传递一个函数给子组件的时候,可以使用 useCallback 进行缓存,可以避免在父组件更新的时候,子组件不进行非必要的更新,提供渲染效率。
4、useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
4.1 定义
const value = useContext(MyContext);
别忘记 useContext 的参数必须是 context 对象本身:
正确:useContext(MyContext)
错误:useContext(MyContext.Consumer)
错误:useContext(MyContext.Provider)
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
4.2 使用提示
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。
4.3 示例
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
4.4 总结
目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;而不是通过 props 层层传递;
5、useReducer
5.1 定义
const [state, dispatch] = useReducer(reducer, initialArg, init);
reducer: 处理函数,相当于 redux 的 action,是一个处理函数;
initialArg:初始化 state 的默认值,相当于 redux 的 store,作为初始状态传入 reducer;
init:惰性初始化,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg);这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
5.2 作用
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer 会比 useState 更适用,例如 state 是一个复杂的对象、或者每次更新都需要更新多个 state 值、或是当前 state 依赖上一个 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
而且对于深层次的组件,可以结合 useContext 实现数据共享。
注意
React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
5.3 示例
结合 useContext 使用的示例
const MyContext = createContext({})
const defaultState = {
count: 1,
name: 'xiaoming',
age: 12,
}
const reducer = (state=defaultSgtate, action) => {
if(action.type === 'add') {
return {...state, count: state.count+1}
}
if(action.type === 'changeName') {
return {...state, name: action.value}
}
return state
}
<!--父组件-->
const App = () => {
const [store, dispatch] = useReducer(reducer, defaultState)
return (
<MyContext.Provider value={{store, dispatch}}>
<CompA />
<CompB />
</MyContext.Provider>
)
}
const CompA = props => {
const {store,dispatch} = useContext(MyContext);
return (
<>
<div>名字:{store.name}</div>
<div>年龄:{store.age}</div>
<div>次数:{store.count}</div>
<Button onClick={()=>dispatch({type:'add'})}>新增对象</Button>
</>
)
}
const CompB = props => {
const {store,dispatch} = useContext(MyContext);
return (
<>
<div>名字:{store.name}</div>
<div>年龄:{store.age}</div>
<div>次数:{store.count}</div>
<Button onClick={()=>dispatch({type:'changeName', value: '隔壁老王'})}>改名字了</Button>
</>
)
}
5.4 总结
虽然 react 推荐每个组件维护自己的 state,但是针对一些通用的、使用地方比较多的属性,而且有复杂逻辑交互的统一封装到 useReducer 中,可以更好的区分业务和 UI 交互,以及方便维护;
6、useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。
6.1 定义
useLayoutEffect(fn, [deps])
fn: 处理函数;
deps:依赖项,当依赖项发生变化,执行处理函数 fn;
6.2 与 useEffect 区别
先看看 useEffect 的解释:
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
区别:
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
useLayoutEffect 在渲染时是同步执行,callback 函数会在 DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。其执行时机与 componentDidMount,componentDidUpdate 一致。
6.3 示例
const App = props => {
const [state, setState] = useState(0)
<!--useEffect方式-->
useEffect(()=>{
if(state===1) {
setState(Math.random()*100)
}
}, [state])
<!--useLayoutEffect方式-->
useLayoutEffect(()=>{
if(state===1) {
setState(Math.random()*100)
}
}, [state])
return (
<>
<div onClick={()=>setState(1)}>当前:{state}</div>
</>
)
}
如果使用 useEffect 方式会先看到 1 之后才会变成一个随机数;而使用 useLayoutEffect 的不会看到 1。
6.4 总结
当我们需要做一些 UI 上的操作修改的时候,建议放到 useLayoutEffect;如果放到 useEffect,useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗;而且会看到 UI 的闪烁,对用户体验不是很友好。
7、useDebugValue
7.1 定义
useDebugValue(value)
7.2 作用
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
注意:
不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
7.3 示例
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
7.4 总结
非必要就不要用这个 hook 函数了
8、文末思考
总的来说,hook 函数有很多个,但我们需要根据我们实际业务来选用合适的 hook 函数,可以很好的帮助我们维护代码、提高代码可读性、页面渲染性能等等;而且多个函数结合使用效果可能更好,不必一个函数用到天荒地老。
全文完
零基础玩转 Serverless
iOS 开发:深入理解 Xcode 工程结构(一)
三大报表:财务界的通用语言
Tcl 和 Raft 发明人的软件设计哲学
基于 Apollo 的 配置中心 Matrix 2.0 实践总结
ZooKeeper 分布式锁实践(下篇)读写锁
ZooKeeper 分布式锁实践(上篇)排它锁
浅析 InnoDB 存储引擎的工作流程
使用 RabbitMQ 实现 RPC
原来你是这样的 Stream —— 浅析 Java Stream 实现原理
分布式锁实践之一:基于 Redis 的实现
数据介绍一个 MySQL 自动化运维利器 - Inception
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。