Hook 简介
Hook 出世之前 React 存在的问题
在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。有一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
复杂组件变得难以理解
组件常常在 componentDidMount
和 componentDidUpdate
中获取数据。但是,同一个 componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
难以理解的 class
class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this
的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。
Hook 带来的解决方案
你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
Hook API
useState
useState
是 react 自带的一个 hook 函数,它的作用就是用来声明状态变量。useState
这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]
项是当前当前的状态值,第[1]
项是可以改变状态值的方法函数。
初始化
//返回一个 state,以及更新 state 的函数 setState(接收一个新的 state 值并将组件的一次重新渲染加入队列)
const [state, setState] = useState(initialState);
复制代码
函数式更新
//如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
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>
</>
);
}
复制代码
惰性初始 state
//如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
复制代码
跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is
比较算法 来比较 state。)
useEffect
我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起 ajax 请求获取数据,添加一些监听的注册和取消注册,手动修改 dom 等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount
,componentDidUpdate
和componentWillUnmount
。而现在的 useEffect 就相当与这些声明周期函数钩子的集合体。它以一抵三。
简单例子
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}
复制代码
清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个清除函数。以下就是一个创建订阅的例子:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
复制代码
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。参考 前端进阶面试题详细解答
effect 的执行时机
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 useEffect
的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。在某些情况下,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
prop 改变时重新创建。要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。
//此时,只有当 props.source 改变后才会重新创建订阅。(要实现componentDidMount功能只需要设置第二个参数为[]即可)
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
复制代码
useContext
可以深层组件传值,父组件传给子孙组件。接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染
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>
);
}
复制代码
useReducer
useState
的替代方案,可以用于复杂状态处理。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
指定初始 state
有两种不同初始化 useReducer
state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer
是最简单的方法:
//nst [state, dispatch] = useReducer(reducer, initialArg, init);
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
复制代码
某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数 。
const initialState = {count: 0};
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 Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
复制代码
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count} <button
onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
复制代码
跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is
比较算法 来比较 state。)
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。memo
是浅比较,意思是,对象只比较内存地址,只要你内存地址没变,管你对象里面的值千变万化都不会触发 render。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useMemo
与 useCallback
类似,都是有着缓存的作用,useMemo 是缓存值的,useCallback 是缓存函数的。
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
useEffect
里面的 state 的值,是固定的,这个是有办法解决的,就是用useRef
,可以理解成useRef
的一个作用:就是相当于全局作用域,一处被修改,其他地方全更新。
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。然而,useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
请记住,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
const Hook =()=>{
const [count, setCount] = useState(0)
const btnRef = useRef(null)
useEffect(() => {
console.log('use effect...')
const onClick = ()=>{
setCount(count+1)
}
btnRef.current.addEventListener('click',onClick, false)
return ()=> btnRef.current.removeEventListener('click',onClick, false)
},[count])
return(
<div>
<div>
{count} </div>
<button ref={btnRef}>click me </button>
</div>
)
}
复制代码
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
复制代码
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
复制代码
在本例中,渲染 <FancyInput ref={inputRef} />
的父组件可以调用 inputRef.current.focus()
。
自定义 Hook
自定义 Hook 是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 Hook。
例如,下面的 useFriendStatus
是我们第一个自定义的 Hook:
import { useState, useEffect } from 'react';
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;
}
复制代码
自定义一个当 resize 的时候 监听 window 的 width 和 height 的 hook
import {useEffect, useState} from "react";
export const useWindowSize = () => {
const [width, setWidth] = useState()
const [height, setHeight] = useState()
useEffect(() => {
const {clientWidth, clientHeight} = document.documentElement
setWidth(clientWidth)
setHeight(clientHeight)
}, [])
useEffect(() => {
const handleWindowSize = () =>{
const {clientWidth, clientHeight} = document.documentElement
setWidth(clientWidth)
setHeight(clientHeight)
};
window.addEventListener('resize', handleWindowSize, false)
return () => {
window.removeEventListener('resize',handleWindowSize, false)
}
})
return [width, height]
}
复制代码
使用:
const [width, height] = useWindowSize()
const isOnline = useFriendStatus(id);
复制代码
评论