这个标题很大,但是落点很小,只是我,一个开发者在学习和使用 hooks 中的一点感受和总结。
React hook 的由来
在组件之间复用状态逻辑很难 复杂组件变得难以理解 难以理解的 class
难以琢磨的 this 关联的逻辑被拆分 熟练记忆众多的生命周期,在合适的生命周期里做适当的事情 代码量相对更多,尤其是写简单组件时
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); // 要手动绑定this } componentDidMount() { ChatAPI.subscribeToFriendStatus( // 订阅和取消订阅逻辑的分散 this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { // 要熟练记忆并使用各种生命周期,在适当的生命周期里做适当的事情 ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return 'Loading...'; } return this.state.isOnline ? 'Online' : 'Offline'; } }
React hook 的实现
function App () { const [num, setNum] = useState(0); const [age, setAge] = useState(18); const clickNum = () => { setNum(num => num + 1); // setNum(num => num + 1); // 是可能调用多次的 } const clickAage = () => { setNum(age => age + 3); // setNum(num => num + 1); // 是可能调用多次的 } return <div> <button onClick={clickNum}>num: {num}</button> <button onClick={clickAage}>age:{age}</button> </div> }
function App () { const [num, setNum] = useState(0); const [age, setAge] = useState(10); console.log(isMount ? '初次渲染' : '更新'); console.log('num:', num); console.log('age:', age); const clickNum = () => { setNum(num => num + 1); // setNum(num => num + 1); // 是可能调用多次的 } const clickAge = () => { setAge(age => age + 3); // setNum(num => num + 1); // 是可能调用多次的 } return { clickNum, clickAge } }
之所以能保持住state,是在一个函数组件之外的地方,保存了一个「对象」,这个对象里记录了之前的状态。
// 组件是分初次渲染和后续更新的,那么就需要一个东西来判断这两个不同阶段,简单起见,我们是使用这个变量好了。 let isMount = true; // 最开始肯定是true // 我们在组件中,经常是使用多个useState的,那么需要一个变量,来记录我们当前实在处理那个hook。 let workInProgressHook = null; // 指向当前正在处理的那个hook // 针对App这个组件,我们需要一种数据结构来记录App内所使用的hook都有哪些,以及记录App函数本身。这种结构我们就命名为fiber const fiber = { stateNode: App, // 对函组件来说,stateNode就是函数本身 memorizedState: null // 链表结构。用来记录App里所使用的hook的。 } // 使用 setNum是会更新组件的, 那么我们也需要一种可以更新组件的方法。这个方法就叫做 schedule function schedule () { // 每次执行更新组件时,都需要从头开始执行各个useState,而fiber.memorizedState记录着链表的起点。即workInProgressHook重置为hook链表的起点 workInProgressHook = fiber.memorizedState; // 执行 App() const app = fiber.stateNode(); // 执行完 App函数了,意味着初次渲染已经结束了,这时候标志位该改变了。 isMount = false; return app; }
useState 究竟怎么保持住之前的状态的? 如果多次调用 setNum 这类更新状态的函数,该怎么处理这些函数呢? 如果这个 useState 执行完了,怎么知道下一个 hook 该去哪里找呢?
// 计算新状态,返回改变状态的方法 function useState(initialState) { // 声明一个hook对象,hook对象里将有三个属性,分别用来记录一些东西,这些东西跟我们上述的三个疑问相关 // 1. memorizedState, 记录着state的初始状态 (疑问1相关) // 2. queue, queue.pending 也是个链表,像上面所说,setNum是可能被调用多次的,这里的链表,就是记录这些setNum。 (疑问2相关) // 3. next, 链表结构,表示在App函数中所使用的下一个useState (疑问3相关) let hook; if (isMount) { // 首次渲染,也就是第一次进入到本useState内部,每一个useState对应一个自己的hook对象,所以这时候本useState还没有自己的的hook数据结构,创建一个 hook = { memorizedState: initialState, queue: { pending: null // 此时还是null的,当我们以后调用setNum时,这里才会被改变 }, next: null } // 虽然现在是在首次渲染阶段,但是,却不一定是进入的第一个useState,需要判断 if (!fiber.memorizedState) { // 这时候才是首次渲染的第一个useState. 将当前hook赋值给fiber.memorizedState fiber.memorizedState = hook; } else { // 首次渲染进入的第2、3、4...N 个useState // 前面我们提到过,workInProgressHook的用处是,记录当前正在处理的hook (即useState),当进入第N(N>1)个useState时,workInProgressHook已经存在了,并且指向了上一个hook // 这时候我们需要把本hook,添加到这个链表的结尾 workInProgressHook.next = hook; } // workInProgressHook指向当前的hook workInProgressHook = hook; } else { // 非首次渲染的更新阶段 // 只要不是首次渲染,workInProgressHook所在的这条记录hook顺序的链表肯定已经建立好了。而且 fiber.memorizedState 记录着这条链表的起点。 // 组件更新,也就是至少经历了一次schedule方法,在schedule方法里,有两个步骤: // 1. workInProgressHook = fiber.memorizedState,将workInProgressHook置为hook链表的起点。初次渲染阶段建立好了hook链表,所以更新时,workInProgressHook肯定是存在的 // 2. 执行App函数,意味着App函数里所有的hook也会被重新执行一遍 hook = workInProgressHook; // 更新阶段此时的hook,是初次渲染时已经建立好的hook,取出来即可。 所以,这就是为什么不能在条件语句中使用React hook。 // 将workInProgressHook往后移动一位,下次进来时的workInProgressHook就是下一个当前的hook workInProgressHook = workInProgressHook.next; } // 上述都是在建立、操作hook链表,useState还要处理state。 let state = hook.memorizedState; // 可能是传参的初始值,也可能是记录的上一个状态值。新的状态,都是在上一个状态的基础上处理的。 if (hook.queue.pending) { let firstUpdate = hook.queue.pending.next; // hook.queue.pending是个环装链表,记录着多次调用setNum的顺序,并且指向着链表的最后一个,那么hook.queue.pending.next就指向了第一个 do { const action = firstUpdate.action; state = action(state); // 所以,多次调用setNum,state是这么被计算出来的 firstUpdate.next = firstUpdate.next } while (firstUpdate !== hook.queue.pending.next) // 一直处理action,直到回到环状链表第一位,说明已经完全处理了 hook.queue.pending = null; } hook.memorizedState = state; // 这就是useState能保持住过去的state的原因 return [state, dispatchAction.bind(null, hook.queue)] }
建立 hook 的链表。将所有使用过的 hook 有序连接在一起,并通过移动指针,使链表里记录的 hook 和当前真正被处理的 hook 能够一一对应。 处理 state。在上一个 state 的基础上,通过 hook.queue.pending 链表来不断调用 action 函数,直到计算出最新的 state。
function dispatchAction(queue, action) { // 每次dispatchAction触发的更新,都是用一个update对象来表述 const update = { action, next: null // 记录多次调用该dispatchAction的顺序的链表 } if (queue.pending === null) { // 说明此时,是这个hook的第一次调用dispatchAction // 建立一个环状链表 update.next = update; } else { // 非第一调用dispatchAction // 将当前的update的下一个update指向queue.pending.next update.next = queue.pending.next; // 将当前update添加到queue.pending链表的最后一位 queue.pending.next = update; } queue.pending = update; // 把每次dispatchAction 都把update赋值给queue.pending, queue.pending会在下一次dispatchAction中被使用,用来代表上一个update,从而建立起链表 // 每次dispatchAction都触发更新 schedule(); }
上面这段代码里,7 -18 行不太好理解,我来简单解释一下。
假设我们调用了 3 次setNum函数,产生了 3 个 update, A、B、C。
当产生第一个 update A 时:
A:此时 queue.pending === null,
执行 update.next = update, 即 A.next = A;
然后 queue.pending = A;
建立 A -> A 的环状链表
建立 B -> A -> B 的环状链表
建立起 C -> A -> B -> C 环状链表
let isMount = true; let workInProgressHook = null; const fiber = { stateNode: App, memorizedState: null } function schedule () { workInProgressHook = fiber.memorizedState; const app = fiber.stateNode(); isMount = false; return app; } function useState(initialState) { let hook; if (isMount) { hook = { memorizedState: initialState, queue: { pending: null }, next: null } if (!fiber.memorizedState) { fiber.memorizedState = hook; } else { workInProgressHook.next = hook; } workInProgressHook = hook; } else { hook = workInProgressHook; workInProgressHook = workInProgressHook.next; } let state = hook.memorizedState; if (hook.queue.pending) { let firstUpdate = hook.queue.pending.next do { const action = firstUpdate.action; state = action(state); firstUpdate.next = firstUpdate.next } while (firstUpdate !== hook.queue.pending.next) hook.queue.pending = null; } hook.memorizedState = state; return [state, dispatchAction.bind(null, hook.queue)] } function dispatchAction(queue, action) { const update = { action, next: null } if (queue.pending === null) { update.next = update; } else { update.next = queue.pending.next; queue.pending.next = update; } queue.pending = update; schedule(); } function App () { const [num, setNum] = useState(0); const [age, setAge] = useState(10); console.log(isMount ? '初次渲染' : '更新'); console.log('num:', num); console.log('age:', age); const clickNum = () => { setNum(num => num + 1); // setNum(num => num + 1); // 是可能调用多次的 } const clickAge = () => { setAge(age => age + 3); // setNum(num => num + 1); // 是可能调用多次的 } return { clickNum, clickAge } } window.App = schedule();
由于我们是每次更新都调用了 schedule,所以 hook.queue.pending只要存在就会被执行,然后将 hook.queue.pending = null, 所以在我们的简略版 useState 里,queue.pending 所建立的环状链表没有被使用到。而在真实的 React 中,batchedUpdates会将多次 dispatchAction执行完后,再触发一次更新。这时候就需要环状链表了。
useState 究竟怎么保持住之前的状态?
如果我多次调用 setNum 这类 dispatch 函数,该怎么处理这些函数呢?
如果这个 useState 执行完了,下一个 hook 该去哪里找呢?
React hook 的理念
class Box extends React.components { componentDidMount () { // fetch data } componentWillReceiveProps (props, nextProps) { if (nextProps.id !== props.id) { // this.setState } } }
function Box () { useEffect(() => { // fetch data }, []) useEffect(() => { // setState }, [id]) }
function App() { const [count, setCount] = useState(0) const handleWindowResize = () => { // 把count输出 console.log(`count is ${count}`) } useEffect(() => { // 让resize事件触发handleResize window.addEventListener('resize', handleWindowResize) return () => window.removeEventListener('resize', handleWindowResize) }, []) return ( <div className="App"> <button onClick={() => setCount(count + 1)}>+</button> <h1>{count}</h1> </div> ); }
class App extends Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleWindowResize = this.handleWindowResize.bind(this); this.handleClick = this.handleClick.bind(this); } handleWindowResize() { console.log(`count is ${this.state.count}`); } handleClick() { this.setState({ count: this.state.count + 1 }); } componentDidMount() { window.addEventListener("resize", this.handleWindowResize); } componentWillUnmount () { window.removeEventListener('resize', this.handleWindowResize) } render() { const { count } = this.state; return ( <div className="App"> <button onClick={this.handleClick}>+</button> <h1>{count}</h1> </div> ); } }
在组件之间复用状态逻辑很难 复杂组件变得难以理解 难以理解的 class
React hook 的意义
import React from "react"; function Count({ count, add, minus }) { return ( <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> <div>You clicked {count} times</div> <button onClick={add} title={"add"} style={{ minHeight: 20, minWidth: 100 }} > +1 </button> <button onClick={minus} title={"minus"} style={{ minHeight: 20, minWidth: 100 }} > -1 </button> </div> ); } const countNumber = (initNumber) => (WrappedComponent) => class CountNumber extends React.Component { state = { count: initNumber }; add = () => this.setState({ count: this.state.count + 1 }); minus = () => this.setState({ count: this.state.count - 1 }); render() { return ( <WrappedComponent {...this.props} count={this.state.count} add={this.add.bind(this)} minus={this.minus.bind(this)} /> ); } }; export default countNumber(0)(Count);
因为我们想让子组件重新渲染的方式有限,要么高阶组件 setState,要么 forceUpdate,而这类方法都是 React 组件内的,无法独立于 React 组件使用,所以add\minus 这种业务逻辑和展示的 UI 逻辑,不得不粘合在一起。 使用 HOC 时,我们往往是多个 HOC 嵌套使用的。而 HOC 遵循透传与自身无关的 props 的约定,导致最终到达我们的组件时,有太多与组件并不太相关的 props,调试也相当复杂。我们没有一种很好的方法来解决多层 HOC 嵌套所带来的麻烦。
// 业务逻辑拆分到这里了 import { useState } from "react"; function useCounter() { const [count, setCount] = useState(0); const add = () => setCount((count) => count + 1); const minus = () => setCount((count) => count - 1); return { count, add, minus }; } export default useCounter
// 纯UI展示组件 import React from "react"; import useCounter from "./counterHook"; function Count() { const { count, add, minus } = useCounter(); return ( <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> <div>You clicked {count} times</div> <button onClick={add} title={"add"} style={{ minHeight: 20, minWidth: 100 }} > +1 </button> <button onClick={minus} title={"minus"} style={{ minHeight: 20, minWidth: 100 }} > -1 </button> </div> ); } export default Count;
function Count() { const { count, add, minus } = useCounter(); const { loading } = useLoading(); return loading ? ( <div>loading...please wait...</div> ) : ( <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> ... </div> ); } export default Count;
可以设置计数器的初始值、每次加减值、最大值最小值、精度 可以通过返回的方法,直接获得超出最大最小值时按钮变灰无法点击等等效果。 可以通过返回的方法,直接获取中间输入框只能输入数字,不能输入文字等等功能。
function HookUsage() { const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = useNumberInput({ step: 0.01, defaultValue: 1.53, min: 1, max: 6, precision: 2, }) const inc = getIncrementButtonProps() const dec = getDecrementButtonProps() const input = getInputProps() return ( <HStack maxW='320px'> <Button {...inc}>+</Button> <Input {...input} /> <Button {...dec}>-</Button> </HStack> ) }
Table, Thead, Tbody, Tfoot, Tr, Th, Td, TableCaption, TableContainer,
React hook 的局限
被强制的顺序
复杂的useEffct
function App () { let varibaleCannotReRender; // 普通变量,改变它并不会触发组件重新渲染 useEffect(() => { // some code }, [varibaleCannotReRender]) // 比如在一次点击事件中改变了varibaleCannotReRender varibaleCannotReRender = '123' }
function App() { const [num, setNum] = useState(0); let b = 1; useEffect(() => { console.log('effefct', b); }, [b]); const click = () => { b = Math.random(); set((num) => num + 1); }; return <div onClick={click}>App {get}</div>; }
函数的纯粹性
// 把这种 function YourComponent () { const [num, setNum] = useState(0); return <span>{num}</span> } // 理解成这种形式,使用了useState,React就自动给你生成AutoContainer包裹你的函数。这样你的组件仍可以看成是纯函数。 function AutoContainer () { const [num, setNum] = useState(0); return <YourComponent num={num} /> } function YourComponent (props) { return <span>{props.num}</span> }
写在最后