当谈论 React hook,我们究竟说的是什么?
 
                    
这个标题很大,但是落点很小,只是我,一个开发者在学习和使用 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函数本身。这种结构我们就命名为fiberconst fiber = {stateNode: App, // 对函组件来说,stateNode就是函数本身memorizedState: null // 链表结构。用来记录App里所使用的hook的。}// 使用 setNum是会更新组件的, 那么我们也需要一种可以更新组件的方法。这个方法就叫做 schedulefunction 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.memorizedStatefiber.memorizedState = hook;} else {// 首次渲染进入的第2、3、4...N 个useState// 前面我们提到过,workInProgressHook的用处是,记录当前正在处理的hook (即useState),当进入第N(N>1)个useState时,workInProgressHook已经存在了,并且指向了上一个hook// 这时候我们需要把本hook,添加到这个链表的结尾workInProgressHook.next = hook;}// workInProgressHook指向当前的hookworkInProgressHook = 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就是下一个当前的hookworkInProgressHook = 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.nextupdate.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.nextdo {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事件触发handleResizewindow.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><buttononClick={add}title={"add"}style={{ minHeight: 20, minWidth: 100 }}>+1</button><buttononClick={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><buttononClick={add}title={"add"}style={{ minHeight: 20, minWidth: 100 }}>+1</button><buttononClick={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])// 比如在一次点击事件中改变了varibaleCannotReRendervaribaleCannotReRender = '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>}
写在最后
参考文档:
快 来 找 又 小 拍 

推 荐 阅 读 


设为星标

更新不错过




设为星标

更新不错过
[广告]赞助链接:
                        关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
                        让资讯触达的更精准有趣:https://www.0xu.cn/
                    
 关注KnowSafe微信公众号
            关注KnowSafe微信公众号随时掌握互联网精彩
- Unity重大漏洞!微软多款最新大作受影响:建议卸载
- 苹果iOS 18.0.1更新发布:修复iPhone 16系列触屏失灵等Bug
- Linux 的成功,为什么会带来“写一个操作系统不难,难的是生态构建”的错觉?
- 这些人用华为手机的镜头造了一个梦
- 00 后博士获聘南大特任副研究员,曾 4 岁上小学,14 岁考入南大!
- AI 考高数得分 81,网友:AI 模型也免不了“内卷”!
- 把 GPL 视作“病毒”?请停止污名化 GPL !
- 在Z|北京雪诺诚招C++开发、Java开发、性能测试、解决方案、售前等工程师
- 网站被入侵、攻击,相关企业要负责任吗?
- Linux 内核支持 Rust;中科院计划每半年升级一次 RISC-V 芯片;Python 3.10.1 发布 | 开源日报
- MySSL的使用
- 北上广,是程序员最好的归宿?
            赞助链接
        
    
 
                 
             
             
            
 
        
 
        
