本节是Hook专题,将从 preact 借鉴 Hook 的底层原理实现,虽然实际上 preact 与 react 的 实现有所差异,但是胜在简单,了解了解思路逻辑也是可以的嘛。
Hooks 目前react内置了13种hooks
import { useCallback, useMemo, useContext, useEffect, useLayoutEffect, useImperativeHandle, useDebugValue, useReducer, useRef, useState, useResponder, useTransition, useDeferredValue, } from './ReactHooks' ; import {withSuspenseConfig} from './ReactBatchConfig' ;if (exposeConcurrentModeAPIs ) { React.useTransition = useTransition; React.useDeferredValue = useDeferredValue; React.SuspenseList = REACT_SUSPENSE_LIST_TYPE; React.unstable_withSuspenseConfig = withSuspenseConfig; } if (enableFlareAPI) { React.unstable_useResponder = useResponder; React.unstable_createResponder = createResponder; }
最后3个 Hook 尚处于 unstable ,需要等到支持conCurrentMode
,这里就不去赘述。
export function useState <S >(initialState: (( ) => S ) | S ) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } export function useEffect ( create: ( ) => (( ) => void ) | void , inputs : Array <mixed > | void | null , ) { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, inputs); } export function useRef <T >(initialValue: T ): {current: T} { const dispatcher = resolveDispatcher(); return dispatcher.useRef(initialValue); }
从最常用的useState、useEffect、useRef源码,可以看到几乎都和 resolveDispatcher
函数有关。
function resolveDispatcher ( ) { const dispatcher = ReactCurrentDispatcher.current; invariant( dispatcher !== null , 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.' , ); return dispatcher; }
都知道 Hooks
的三条铁则,这些方法只会在拿到节点实例的时候触发执行,为了适配多平台ReactCurrentDispatcher
实际上需要等到 react-dom
渲染的时候才能拿到。
const ReactCurrentDispatcher = { current: (null : null | Dispatcher), };
光看这些得不到什么比较有效的信息,但本质上是将节点实例返回后调用该节点实例上的对应方法。
原理探究 function Foo ( ) { const [str, setStr] = useState('' ); const change = useCallback((e )=> { setStr(e.target.value) },[]) useEffect(() => { console .log('effect' ) return () => { console .log('effect clean' ) }; },[Math .random()]) useLayoutEffect(() => { console .log('layoutEffect' ) return () => { console .log('layoutEffect clean' ) }; }, [str]) return ( <input value={str} onChange={change} /> ) }
一个简单的Hook组件,可能会有个疑问,Hooks
是针对 Function Component 设计的Api,从而赋予 Function Component 拥有与类组件同样保存状态的能力。为什么不会被实例化还能够拥有状态,是怎么做到的?
其实Hook都依赖了闭包,而hook之间依靠单向链表的方式串联,从而拥有了“状态”,这也是之所以为什么Hooks必须在函数作用域的最顶层声明且不能嵌套在块级作用域内,如果在某个循环或者是表达式内跳过执行,那么上一次的Hook“链表”和本次update的链表某个指针指向错误,将会得到意料之外的结果。
可以借鉴下preact
的实现,与React不同,preact使用的是下标索引。
import { options } from 'preact' ;let currentIndex; let currentComponent; let afterPaintEffects = []; let oldBeforeRender = options._render;let oldAfterDiff = options.diffed;let oldCommit = options._commit;let oldBeforeUnmount = options.unmount;function getHookState (index ) { if (options._hook) options._hook(currentComponent); const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], }); if (index >= hooks._list.length) { hooks._list.push({}); } return hooks._list[index]; }
通过 getHookState
收集管理Effect,即便没有实例化其本质上是函数每次都会重新执行。通过比较依赖值结果来决定逻辑更新,从这点上看getHookState
是一个组件的核心管理器。需要注意的是 _pendingEffect
放入的是不阻塞页面渲染的 effect 操作,也就是useEffect。
export interface ComponentHooks { _list: HookState[]; _pendingEffects: EffectHookState[]; } export interface Component extends PreactComponent<any, any> { __hooks?: ComponentHooks; }
Hook组件与类组件差不多,只不过多了一个__hooks
属性 —— hooks管理器。
useState 与 useReducer 匆匆一瞥:
export function useState (initialState ) { return useReducer(invokeOrReturn, initialState); } export function useReducer (reducer, initialState, init ) { const hookState = getHookState(currentIndex++); if (!hookState._component) { hookState._component = currentComponent; hookState._value = [ !init ? invokeOrReturn(undefined , initialState) : init(initialState), action => { const nextValue = reducer(hookState._value[0 ], action); if (hookState._value[0 ] !== nextValue) { hookState._value[0 ] = nextValue; hookState._component.setState({}); } } ]; } return hookState._value; }
在 preact 里 useState 与useReducer是一码事。也可以使用useState定义useReducer。
function useMyReducer (reducer, initialState, init ) { const compatible = init ? init(initialState) : initialState; const [state, setState] = useState(compatible); function dispatch (action ) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; }
在Foo组件effect收集阶段,useState调用useReducer传入加工函数invokeOrReturn
作为reducer传入。
function invokeOrReturn (arg, f ) { return typeof f === 'function' ? f(arg) : f; }
通过getHookState
在当前组件申明一个新的hooks,放入currentComponent.__hooks._list
然后将其返回。hookState暂时只是个空对象,当它没有关联组件时需要对其进行当前组件的关联。
export function useReducer (reducer, initialState, init ) { const hookState = getHookState(currentIndex++); if (!hookState._component) { hookState._component = currentComponent; hookState._value = [ !init ? invokeOrReturn(undefined , initialState) : init(initialState), action => { const nextValue = reducer(hookState._value[0 ], action); if (hookState._value[0 ] !== nextValue) { hookState._value[0 ] = nextValue; hookState._component.setState({}); } } ]; } return hookState._value; } export interface ReducerHookState { _value?: any; _component?: Component; }
hookState._value 返回的即是平常所用的 const [str, setStr] = useState('');
,值与更新器。 hookState._component 就是一个简单的无状态组件,但是React底层仍然是通过调用setState触发enqueueRender进行diff更新。
这些后面再写…因为确实很难简短描述。
useEffect 与 useLayoutEffect function argsChanged (oldArgs, newArgs ) { return !oldArgs || newArgs.some((arg, index ) => arg !== oldArgs[index]); } export function useEffect (callback, args ) { const state = getHookState(currentIndex++); if (argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent.__hooks._pendingEffects.push(state); } } export function useLayoutEffect (callback, args ) { const state = getHookState(currentIndex++); if (argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent._renderCallbacks.push(state); } } export interface EffectHookState { _value?: Effect; _args?: any[]; _cleanup?: Cleanup; }
useEffect 与 useLayoutEffect 唯一不同的是在于推入的队列以及执行的时机,前面讲到过,__hooks._pendingEffects
队列执行的时机是下一帧绘制前执行(本次render后,下次render前),不阻塞本次的浏览器渲染。而 _renderCallbacks
则在组件commit钩子内执行
组件render的流程是怎样的?还有是怎么进行比对和派发更新的。
在 Function Component中 除去 vnode 阶段外,组件自身有四个钩子阶段,也就是 render=>diffed=>commit=>unmount
options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode._component; currentIndex = 0 ; if (currentComponent.__hooks) { currentComponent.__hooks._pendingEffects.forEach(invokeCleanup); currentComponent.__hooks._pendingEffects.forEach(invokeEffect); currentComponent.__hooks._pendingEffects = []; } }; options.diffed = vnode => { if (oldAfterDiff) oldAfterDiff(vnode); const c = vnode._component; if (!c) return ; const hooks = c.__hooks; if (hooks) { if (hooks._pendingEffects.length) { afterPaint(afterPaintEffects.push(c)); } } }; options._commit = (vnode, commitQueue ) => { commitQueue.some(component => { component._renderCallbacks.forEach(invokeCleanup); component._renderCallbacks = component._renderCallbacks.filter(cb => cb._value ? invokeEffect(cb) : true ); }); if (oldCommit) oldCommit(vnode, commitQueue); }; options.unmount = vnode => { if (oldBeforeUnmount) oldBeforeUnmount(vnode); const c = vnode._component; if (!c) return ; const hooks = c.__hooks; if (hooks) { hooks._list.forEach(hook => hook._cleanup && hook._cleanup()); } }; function invokeCleanup (hook ) { if (hook._cleanup) hook._cleanup(); } function invokeEffect (hook ) { const result = hook._value(); if (typeof result === 'function' ) hook._cleanup = result; }
最后有两个函数,invokeCleanup
和 invokeEffect
用来执行清理函数和回调函数.
前面三个钩子在render 函数内被同步调用。
export function render (vnode, parentDom, replaceNode ) { if (options._root) options._root(vnode, parentDom); let isHydrating = replaceNode === IS_HYDRATE; let oldVNode = isHydrating ? null : (replaceNode && replaceNode._children) || parentDom._children; vnode = createElement(Fragment, null , [vnode]); let commitQueue = []; diff( parentDom, ((isHydrating ? parentDom : replaceNode || parentDom)._children = vnode), oldVNode || EMPTY_OBJ, EMPTY_OBJ, parentDom.ownerSVGElement !== undefined , replaceNode && !isHydrating ? [replaceNode] : oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), commitQueue, replaceNode || EMPTY_OBJ, isHydrating ); commitRoot(commitQueue, vnode); }
具体的功能不用涉及,首先进行diff,diff 负责执行生命周期类方法以及调用_render
和 diffed
方法。
_render 负责将 currentComponent
指向 vnode._component
并执行 _pendingEffects
队列。
diffed 执行 afterPaint(afterPaintEffects.push(c))
会把带有 _pendingEffects
推入 afterPaintEffects
队列,然后 afterPaint
调用 afterNextFrame(flushAfterPaintEffects)
执行effect 保证其在下一帧前调用.
function afterPaint (newQueueLength ) { if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) { prevRaf = options.requestAnimationFrame; (prevRaf || afterNextFrame)(flushAfterPaintEffects); } } function afterNextFrame (callback ) { const done = () => { clearTimeout(timeout); cancelAnimationFrame(raf); setTimeout(callback); }; const timeout = setTimeout(done, RAF_TIMEOUT); let raf; if (typeof window !== 'undefined' ) { raf = requestAnimationFrame(done); } } function flushAfterPaintEffects ( ) { afterPaintEffects.some(component => { if (component._parentDom) { component.__hooks._pendingEffects.forEach(invokeCleanup); component.__hooks._pendingEffects.forEach(invokeEffect); component.__hooks._pendingEffects = []; } }); afterPaintEffects = []; }
得到diff后的vnode之后,还不能进行渲染。
export function commitRoot (commitQueue, root ) { if (options._commit) options._commit(root, commitQueue); commitQueue.some(c => { try { commitQueue = c._renderCallbacks; c._renderCallbacks = []; commitQueue.some(cb => { cb.call(c); }); } catch (e) { options._catchError(e, c._vnode); } }); }
最后一个阶段在diffChildren 删除vnode之前执行.
useImperativeHandle 在官方例子里,useImperativeHandle
用于获取子组件实例方法.因为自定义组件会过滤ref所以通常要与 forwardRef
组合搭配.
const FancyInput = forwardRef( (props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } ) function App ( ) { const childrenRef = useRef() return ( <div> <FancyInput ref={childrenRef}/> <button onClick={() => childrenRef.focus()}>click</button> </ div> ) }
其原理是获取到父组件的ref后将实例方法对象传入.
export function useImperativeHandle (ref, createHandle, args ) { useLayoutEffect( () => { if (typeof ref === 'function' ) ref(createHandle()); else if (ref) ref.current = createHandle(); }, args == null ? args : args.concat(ref) ); }
useMemo 与 useCallback useCallback 是 useMemo的函数版本,其原理实现相同.通过比较依赖的变化返回新值.
export function useMemo (callback, args ) { const state = getHookState(currentIndex++); if (argsChanged(state._args, args)) { state._args = args; state._callback = callback; return (state._value = callback()); } return state._value; } export function useCallback (callback, args ) { return useMemo(() => callback, args); }
useRef useRef也是对于useMemo的变种.
export function useRef (initialValue ) { return useMemo(() => ({ current : initialValue }), []); }
createContext export function createContext (defaultValue ) { const ctx = {}; const context = { _id: '__cC' + i++, _defaultValue: defaultValue, Consumer(props, context) { return props.children(context); }, Provider(props) { if (!this .getChildContext) { const subs = []; this .getChildContext = () => { ctx[context._id] = this ; return ctx; }; this .shouldComponentUpdate = _props => { if (props.value !== _props.value) { subs.some(c => { c.context = _props.value; enqueueRender(c); }); } }; this .sub = c => { subs.push(c); let old = c.componentWillUnmount; c.componentWillUnmount = () => { subs.splice(subs.indexOf(c), 1 ); old && old.call(c); }; }; } return props.children; } }; context.Consumer.contextType = context; return context; } export function useContext (context ) { const provider = currentComponent.context[context._id]; if (!provider) return context._defaultValue; const state = getHookState(currentIndex++); if (state._value == null ) { state._value = true ; provider.sub(currentComponent); } return provider.props.value; }
通过订阅收发的模式生产和消费数据.
后话 本文的目的是研究Hooks原理与机制,实际上 preact 与 react 其实有很多地方不一样,其底层如children和ref的处理机制受限;children只能是数组,react则可以是任何数据;ref的获取时机;事件系统直接绑定在元素上而非基于冒泡;由于体积较小diff算法过于简单;setState的时机被推迟;生态问题…
不过作为一个只有3kb的库,确实不能对其要求太高.