hooks 是 react 在16.8版本开始引入的一个新功能,它扩展了函数组件的功能,使得函数组件也能实现状态、生命周期等复杂逻辑。
import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
上面是 react 官方提供的 hooks 示例,使用了内置hookuseState
,对应到<u>Class Component</u>应该这么实现
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> ); } }
简而言之,hooks 就是钩子,让你能更方便地使用react相关功能。
看完上面一段,你可能会觉得除了代码块精简了点,没看出什么好处。别急,继续往下看。
过去,我们习惯于使用<u>Class Component</u>,但是它存在几个问题:
状态逻辑复用困难
side effect 复用和组织困难
class FriendStatusWithCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0, isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { document.title = `You clicked ${this.state.count} times`; ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { return ( <div> <p>You clicked {this.state.count} times</p> <p>Friend {this.props.friend.id} status: {this.state.isOnline}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } }
复用的问题就不说了,跟状态逻辑一样,主要说下代码组织的问题。1. 为了在组件刷新的时候更新文档的标题,我们在componentDidMount
和componentDidUpdate
中各写了一遍更新逻辑; 2. 绑定朋友状态更新和解绑的逻辑,分散在componentDidMount
和componentWillUnmount
中,实际上这是一对有关联的逻辑,如果能写在一起最好;3. componentDidMount
中包含了更新文档标题和绑定事件监听,这2个操作本身没有关联,如果能分开到不同的代码块中更利于维护。
Javascript Class 天生缺陷
this
的指向问题,需要记得手动 bind 事件处理函数,这样代码看起来很繁琐,除非引入@babel/plugin-proposal-class-properties
(这个提案目前还不稳定)。为了解决上述问题,hooks 应运而生。让我们使用 hooks 改造下上面的例子
import React, { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); 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); }; }); return isOnline; } function FriendStatusWithCounter(props) { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); const isOnline = useFriendStatus(props.friend.id); return ( <div> <p>You clicked {count} times</p> <p>Friend {props.friend.id} status: {isOnline}</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } function FriendStatus(props) { // 通过自定义hook复用逻辑 const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
看,问题都解决了!
hooks 一般配合<u>Function Components</u>使用,也可以在内置 hooks 的基础上封装自定义 hook。
先介绍下 react 提供的内置 hooks。
const [count, setCount] = useState(0);
useState
接收一个参数作为初始值,返回一个数组,数组的第一个元素是表示当前状态值的变量,第二个参数是修改状态的函数,执行的操作类似于this.setState({ count: someValue })
,当然内部的实现并非如此,这里仅为了帮助理解。
useState
可以多次调用,每次当你需要声明一个state时,就调用一次。
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
需要更新某个具体状态时,调用对应的 setXXX 函数即可。
useEffect的作用是让你在<u>Function Components</u>里面可以执行一些 side effects,比如设置监听、操作dom、定时器、请求等。
useEffect(() => { document.title = `You clicked ${count} times`; });
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); }; });
需要注意,上面这种写法,每次组件更新都会执行 effect 的回调函数和清理函数,顺序如下:
// Mount with { friend: { id: 100 } } props ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect // Update with { friend: { id: 200 } } props ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect // Update with { friend: { id: 300 } } props ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect // Unmount ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
这个效果等同于在componentDidMount
、componentDidUpdate
和componentWillUnmount
实现了事件绑定和解绑。如果只是组件的 state 变化导致重新渲染,同样会重新调用 cleanup 和 effect,这时候就显得没有必要了,所以 useEffect 支持用第2个参数来声明依赖
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]);
第2个参数是一个数组,在数组中传入依赖的 state 或者 props,如果依赖没有更新,就不会重新执行 cleanup 和 effect。
如果你需要的是只在初次渲染的时候执行一次 effect,组件卸载的时候执行一次 cleanup,那么可以传一个空数组[]
作为依赖。
context
这个概念大家应该不陌生,一般用于比较简单的共享数据的场景。useContext
就是用于实现context
功能的 hook。
来看下官方提供的示例
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> ); }
代码挺长,但是一眼就能看懂了。把 context 对象传入useContext
,就可以拿到最新的 context value。
需要注意的是,只要使用了useContext
的组件,在 context value 改变后,一定会触发组件的更新,哪怕他使用了React.memo
或是shouldComponentUpdate
。
useReducer(reducer, initialArg)
返回[state, dispatch]
,跟 redux 很像。
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> </> ); }
除此之外,react 内置的 hooks 还包括useCallback
、useMemo
、useRef
、useImperativeHandle
、useLayoutEffect
和useDebugValue
,这里就不再赘述了,可以直接参考官方文档。
基于内置 hook,我们可以封装自定义的 hook,上面的示例中已经出现过useFriendStatus
这样的自定义 hook,它能帮我们抽离公共的组件逻辑,方便复用。注意,自定义 hook 也需要以use
开头。
我们可以根据需要创建各种场景的自定义 hook,如表单处理、计时器等。后面实战场景的章节中我会具体介绍几个例子。
hooks 的使用需要遵循几个规则:
之所以有这些规则限制,是跟 hooks 的实现原理有关。
这里我们尝试实现一个简单的版本的useState
和useEffect
用来说明。
const memoHooks = []; let cursor = 0; function useState(initialValue) { const current = cursor; const state = memoHooks[current] || initialValue; function setState(val) { memoHooks[current] = val; // 执行re-render操作 } cursor++; return [state, setState]; } function useEffect(cb, deps) { const hasDep = !!deps; const currentDeps = memoHooks[cursor]; const hasChanged = currentDeps ? !deps.every((val, i) => val === currentDeps[i]) : true; if (!hasDep || hasChanged) { cb(); memoHooks[cursor] = deps; } cursor++; }
此时我们需要构造一个函数组件来使用这2个 hooks
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); const [name, setName] = useState('Joe'); useEffect(() => { console.log(`Your name is ${name}`); }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
[]
,cursor 为0
第一次渲染
const [count, setCount] = useState(0);
,memoHooks 为[0]
,cursor 为0
useEffect(() => { document.title =
You clicked ${count} times; }, [count]);
,memoHooks 为[0, [0]]
,cursor 为1
const [name, setName] = useState('Joe');
,memoHooks 为[0, [0], 'Joe']
,cursor 为2
useEffect(() => { console.log(
Your name is ${name}); });
,memoHooks 为[0, [0], 'Joe', undefined]
,cursor 为3
点击按钮
setCount(count + 1)
,memoHooks 为[1, [0], 'Joe', undefined]
,cursor 为0
re-render
const [count, setCount] = useState(0);
,memoHooks 为[1, [0], 'Joe', undefined]
,cursor 为0
useEffect(() => { document.title =
You clicked ${count} times; }, [count]);
,memoHooks 为[1, [1], 'Joe', undefined]
,cursor 为1
。这里由于hooks[1]
的值变化,会导致 cb 再次执行。const [name, setName] = useState('Joe');
,memoHooks 为[1, [1], 'Joe', undefined]
,cursor 为2
useEffect(() => { console.log(
Your name is ${name}); });
,memoHooks 为[1, [1], 'Joe', undefined]
,cursor 为3
。这里由于依赖为 undefined,导致 cb 再次执行。通过上述示例,应该可以解答为什么 hooks 要有这样的使用规则了。
memoHooks
和cursor
其实都跟当前渲染的组件实例绑定,脱离了<u>Function Components</u>,hooks 也无法正确执行。当然,这些只是为了方便理解做的一个简单demo,react 内部实际上是通过一个单向链表来实现,并非 array,有兴趣可以自行翻阅源码。
实现一个hook,支持自动获取输入框的内容。
function useInput(initial) { const [value, setValue] = useState(initial); const onChange = useCallback(function(event) { setValue(event.currentTarget.value); }, []); return { value, onChange }; } // 使用示例 function Example() { const inputProps = useInput('Joe'); return <input {...inputProps} /> }
实现一个网络请求hook,能够支持初次渲染后自动发请求,也可以手动请求。参数传入一个请求函数即可。
function useRequest(reqFn) { const initialStatus = { loading: true, result: null, err: null }; const [status, setStatus] = useState(initialStatus); function run() { reqFn().then(result => { setStatus({ loading: false, result, err: null }) }).catch(err => { setStatus({ loading: false, result: null, err }); }); } // didMount后执行一次 useEffect(run, []); return { ...status, run }; } // 使用示例 function req() { // 发送请求,返回promise return fetch('http://example.com/movies.json'); } function Example() { const { loading, result, err, run } = useRequest(req); return ( <div> <p> The result is {loading ? 'loading' : JSON.stringify(result || err)} </p> <button onClick={run}>Reload</button> </div> ); }
上面2个例子只是实战场景中很小的一部分,却足以看出 hooks 的强大,当我们有丰富的封装好的 hooks 时,业务逻辑代码会变得很简洁。推荐一个github repo,这里罗列了很多社区产出的 hooks lib,有需要自取。
根据官方的说法,在可见的未来 react team 并不会停止对 class component 的支持,因为现在绝大多数 react 组件都是以 class 形式存在的,要全部改造并不现实,而且 hooks 目前还不能完全取代 class,比如getSnapshotBeforeUpdate
和componentDidCatch
这2个生命周期,hooks还没有对等的实现办法。建议大家可以在新开发的组件中尝试使用 hooks。如果经过长时间的迭代后 function components + hooks 成为主流,且 hooks 从功能上可以完全替代 class,那么 react team 应该就可以考虑把 class component 移除,毕竟没有必要维护2套实现,这样不仅增加了维护成本,对开发者来说也多一份学习负担。