在 React 的设计哲学中,简而言之,可以这样理解:后台提供数据,前端实现函数,设计流程看似合理,但在函数内部,会遇到各种细节问题。这时,Hooks 的引入解决了这些问题。自定义 Hook 使得逻辑与 UI 分离,实现复用。但在使用 Hooks 时,我们可能会有许多疑问。带着这些问题,让我们思考如何自己实现一个简单的 React Hook。Hook API 的实现中,可以使用作为钩子函数的第二个数组项返回的 setter 函数,该函数控制由 Hook 管理的状态。就像模拟代码所示,useState 接受一个初始值,返回一个 setter 和初始值给使用者。官方的实现是怎样的呢?useState() 的执行等于 dispatcher.useState(initialState)。在这里引入了 dispatcher。ReactCurrentDispatcher.current 初始化时为 null。通过判断是否为空,判断组件是否首次渲染或更新,分别调用不同的 hooks 对象,即 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate。初始化时,找到 HooksDispatcherOnMount.useState,即 mountState。最终获取的是由 mountWorkInProgressHook 创建的 Hook 对象的 memoizedState 属性。初始状态可以接收一个函数,返回的 state 为函数的返回值。回到我们自己实现的简单版本的 useState。运行后发现 num 并不会变化,为什么呢?我们没有存储状态,每次渲染 Counter 组件时,状态都是新重置的。因此,我们想到,闭包可以将状态提取出来,保存在 useState 外面。这是 Hooks 的第一个核心原理。useEffect 的实现中,初始化调用的是 HooksDispatcherOnMount.mountEffect。和 useState 初始化类似,是从 mountWorkInProgressHook 创建一个 Hook 对象,然后将 hook 的 memoizedState 保存当前 hook 信息。pushEffect 里面做了什么?看到这里,可以解开 hooks 中外一个关键词 next 链表。next 指向下一个 Hook 对象。初始化时,首先创建一个 effect,如果组件是首次渲染,创建 componentUpdateQueue,即 currentlyRenderingFiber 的 updateQueue。然后将 effect 放入 updateQueue 中。由 fiber 节点和指针 nextEffect 构成的单链表结构,包括第一个节点 firstEffect 和最后一个节点 lastEffect,从而实现一个循环链表。这里可以解释为什么在学习 hooks 时,被提醒不要在循环、条件判断或子函数中调用 Hook?因为一旦在条件语句中声明 Hook,在下一次函数组件更新时,链表结构与上一次的结构不一致,如果涉及到读取状态等操作,会引发异常。接下来,尝试实现一个简单的版本 of useEffect。主要工作是对依赖的变化进行对比,这时我们可以解答一个问题:为什么第二个参数是空数组,相当于 componentDidMount?因为依赖一直不变,回调不会二次执行。到现在为止,我们已实现了一个乞丐版的 useState 和 useEffect,但存在只能使用一次的问题。因为只有一个 _state 和一个 _deps。换个思路,如果一个变量无法满足场景,我们可以将值都存在一个对象里,并且这个对象需要有调用的先后顺序,官方的思路是链表,这里我们用数组来实现。初始化时,按照 useState 和 useEffect 的顺序,将 state 和 deps 等按顺序塞入 memoizedState 数组中。更新时,按照顺序从 memoizedState 中取出上次记录的值。初始化的流程可以参考下图。每次后续渲染时,光标都会被重置,这些值只是从每个数组中读取。每个 setter 都有一个对其光标位置的引用,通过触发对 any 的调用,setter 将更改状态数组中该位置的状态值。同样,useEffect 的情况也类似,通过 cursor 拿到对应的依赖列表,对数组里面的每一项做比较判断是否需要调用回调函数,理论上,回调也会被缓存在一个链表队列上。至此,我们已经基本了解了一些常规问题。以上内容从初始化和组件更新渲染,大致了解了 React Hooks 的原理和内部运行机制,这对我们在工作中更好地使用 React Hooks 非常有帮助。虽然使用数组基本实现了一个简单的 Hook,了解了 Hook 的原理,但在 React 中,实现方式存在一些差异。React 使用类似单链表的形式代替数组。通过 next 按顺序串联所有 Hook。React 会生成一棵组件树,树中的每个节点对应一个组件,Hook 的数据作为组件的信息存储在这些节点上,伴随组件一起诞生,一起消亡。当然,基本的 Hook 语法错误都可以通过插件协助,例如 eslint-plugin-react-hooks。