作者:京东零售 郑炳懿
前言
React Hooks是React16.8 引入的一个新特性,它允许函数组件中使用state和其他 React 特性,而不必使用类组件。Hooks是一个非常重要的概念,因为它们提供了更简单、更易于理解的React开发体验。
React Hooks的核心源码主要包括两个部分:React内部的Hook管理器和一系列预置的Hook函数。
首先,让我们看一下React内部的Hook管理器。这个管理器是React内部的一个重要机制,它负责管理组件中的所有Hook,并确保它们在组件渲染期间以正确的顺序调用。
内部Hook管理器
示例:
1const Hook = { 2 queue: [], 3 current: null, 4}; 5 6function useState(initialState) { 7 const state = Hook.current[Hook.queue.length]; 8 if (!state) { 9 Hook.queue.push({ 10 state: typeof initialState === 'function' ? initialState() : initialState, 11 setState(value) { 12 this.state = value; 13 render(); 14 }, 15 }); 16 } 17 return [state.state, state.setState.bind(state)]; 18} 19 20function useHook(callback) { 21 Hook.current = { 22 __proto__: Hook.current, 23 }; 24 try { 25 callback(); 26 } finally { 27 Hook.current = Hook.current.__proto__; 28 } 29} 30 31function render() { 32 useHook(() => { 33 const [count, setCount] = useState(0); 34 console.log('count:', count); 35 setTimeout(() => { 36 setCount(count + 1); 37 }, 1000); 38 }); 39} 40 41render(); 42
在这个示例中,Hook对象有两个重要属性:queue和current。queue存储组件中所有Hook的状态和更新函数,current存储当前正在渲染的组件的Hook链表。useState和useHook函数则分别负责创建新的Hook状态和在组件中使用Hook。
预置 Hook 函数
useState Hook
以下是useState Hook的实现示例:
1function useState(initialState) { 2 const hook = updateWorkInProgressHook(); 3 if (!hook.memoizedState) { 4 hook.memoizedState = [ 5 typeof initialState === 'function' ? initialState() : initialState, 6 action => { 7 hook.queue.pending = true; 8 hook.queue.dispatch = action; 9 scheduleWork(); 10 }, 11 ]; 12 } 13 return hook.memoizedState; 14} 15
上述代码实现了useState Hook,其主要作用是返回一个state和更新函数的数组,state 初始值为initialState。
在这个实现中,updateWorkInProgressHook()函数用来获取当前正在执行的函数组件的 fiber 对象并判断是否存在对应的hook。它的实现如下:
1function updateWorkInProgressHook() { 2 const fiber = getWorkInProgressFiber(); 3 let hook = fiber.memoizedState; 4 if (hook) { 5 fiber.memoizedState = hook.next; 6 hook.next = null; 7 } else { 8 hook = { 9 memoizedState: null, 10 queue: { 11 pending: null, 12 dispatch: null, 13 last: null, 14 }, 15 next: null, 16 }; 17 } 18 workInProgressHook = hook; 19 return hook; 20} 21
getWorkInProgressFiber()函数用来获取当前正在执行的函数组件的fiber对象,workInProgressHook则用来存储当前正在执行的hook对象。在函数组件中,每一个useState调用都会创建一个新的 hook 对象,并将其添加到fiber对象的hooks链表中。这个hooks链表是通过fiber对象的memoizedState属性来维护的。
我们还需要注意到在useState Hook的实现中,每一个hook对象都包含了一个queue对象,用来存储待更新的状态以及更新函数。scheduleWork()函数则用来通知React调度器有任务需要执行。
在React的源码中,useState函数实际上是一个叫做useStateImpl的内部函数。
下面是useStateImpl的源码:
1function useStateImpl<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] { 2 const dispatcher = resolveDispatcher(); 3 return dispatcher.useState(initialState); 4} 5
可以看到,useStateImpl函数的作用就是获取当前的dispatcher并调用它的useState方法,返回一个数组,第一个元素是状态的值,第二个元素是一个dispatch函数,用来更新状态。这里的resolveDispatcher函数用来获取当前的dispatcher,其实现如下:
1function resolveDispatcher(): Dispatcher { 2 const dispatcher = currentlyRenderingFiber?.dispatcher; 3 if (dispatcher === undefined) { 4 throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)'); 5 } 6 return dispatcher; 7} 8
resolveDispatcher函数首先尝试获取当前正在渲染的fiber对象的dispatcher属性,如果获取不到则说
明当前不在组件的渲染过程中,就会抛出一个错误。
最后,我们来看一下useState方法在具体的dispatcher实现中是如何实现的。我们以useReducer的
dispatcher为例,它的实现如下:
1export function useReducer<S, A>( 2 reducer: (prevState: S, action: A) => S, 3 initialState: S, 4 initialAction?: A, 5): [S, Dispatch<A>] { 6 const [dispatch, currentState] = updateReducer<S, A>( 7 reducer, 8 // $FlowFixMe: Flow doesn't like mixed types 9 [initialState, initialAction], 10 // $FlowFixMe: Flow doesn't like mixed types 11 reducer === basicStateReducer ? basicStateReducer : updateStateReducer, 12 ); 13 return [currentState, dispatch]; 14} 15
可以看到,useReducer方法实际上是调用了一个叫做updateReducer的函数,返回了一个包含当前状态和dispatch函数的数组。updateReducer的实现比较复杂,涉及到了很多细节,这里不再展开介绍。
useEffect Hook
useEffect是React中常用的一个Hook函数,用于在组件中执行副作用操作,例如访问远程数据、添加/移除事件监听器、手动操作DOM等等。useEffect的核心功能是在组件的渲染过程结束之后异步执行回调函数,它的实现方式涉及到 React 中的异步渲染机制。
以下是useEffect Hook的实现示例:
1function useEffect(callback, dependencies) { 2 // 通过调用 useLayoutEffect 或者 useEffect 方法来获取当前的渲染批次 3 const batch = useContext(BatchContext); 4 5 // 根据当前的渲染批次判断是否需要执行回调函数 6 if (shouldFireEffect(batch, dependencies)) { 7 callback(); 8 } 9 10 // 在组件被卸载时清除当前 effect 的状态信息 11 return () => clearEffect(batch); 12} 13
在这个示例中,useEffect接收两个参数:回调函数和依赖项数组。当依赖项数组中的任何一个值发生变化时,
React会在下一次渲染时重新执行useEffect中传入的回调函数。
useEffect函数的实现方式主要依赖于React中的异步渲染机制。当一个组件需要重新渲染时,React会将所有的state更新操作加入到一个队列中,在当前渲染批次结束之后再异步执行这些更新操作,从而避免在同一个渲染批次中连续执行多次更新操作。
在useEffect函数中,我们通过调用useContext(BatchContext)方法来获取当前的渲染批次,并根据shouldFireEffect方法判断是否需要执行回调函数。在回调函数执行完毕后,我们需要通过clearEffect方法来清除当前effect的状态信息,避免对后续的渲染批次产生影响。
总结
总的来说,React Hooks的实现原理并不复杂,它主要依赖于React内部的fiber数据结构和调度系统,通过这些机制来实现对组件状态的管理和更新。Hooks能够让我们在函数组件中使用状态和其他React特性,使得函数组件的功能可以和类组件媲美。
除了useState、useEffect等hook,React还有useContext等常用的Hook。它们的实现原理也基本相似,都是利用fiber架构来实现状态管理和生命周期钩子等功能。
以上是hook简单实现示例,它们并不是React中实际使用的代码,但是可以帮助我们更好地理解hook的核心实现方式。
