本文通过对preact
的hook
源码分析,理解和掌握react/preact
的hook
用法以及一些常见的问题。虽然react
和preact
的实现上有一定的差异,但是对于hook
的表现来说,是基本一致的。对于 preact的
hook`分析,我们很容易旧记住 hook 的使用和防止踩一些误区
preact hook 作为一个单独的包preact/hook
引入的,它的总代码包含注释区区 300 行。
在阅读本文之前,先带着几个问题阅读:
1、函数组件是无状态的,那么为什么 hook 让它变成了有状态呢?
2、为什么 hook 不能放在 条件语句里面
3、为什么不能在普通函数执行 hook
基础 前面提到,hook
在preact
中是通过preact/hook
内一个模块单独引入的。这个模块中有两个重要的模块内的全局变量:1、currentIndex
:用于记录当前函数组件正在使用的 hook 的顺序(下面会提到)。2、currentComponent
。用于记录当前渲染对应的组件。
preact hook
的实现对于原有的 preact
是几乎零入侵。它通过暴露在preact.options
中的几个钩子函数在preact
的相应初始/更新时候执行相应的hook
逻辑。这几个钩子分别是_render
=> 组件的render方法
=>diffed
=>_commit
=>umount
\_render
位置 。执行组件的 render 方法之前执行,用于执行_pendingEffects
(_pendingEffects
是不阻塞页面渲染的 effect 操作,在下一帧绘制前执行)的清理操作和执行未执行的。这个钩子还有一个很重要的作用就是让 hook 拿到当前正在执行的render
的组件实例options
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 = []; } };
结合_render
在 preact 的执行时机,可以知道,在这个钩子函数里是进行每次 render 的初始化操作。包括执行/清理上次未处理完的 effect、初始化 hook 下标为 0、取得当前 render 的组件实例。
diffed
位置 。 vnode 的 diff 完成之后,将当前的_pendingEffects
推进执行队列,让它在下一帧绘制前执行,不阻塞本次的浏览器渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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)); } } };
\_commit
位置 。初始或者更新 render 结束之后执行_renderCallbacks
,在这个\_commit
中只执行 hook 的回调,如useLayoutEffect
。(_renderCallbacks
是指在preact
中指每次 render 后,同步执行的操作回调列表,例如setState
的第二个参数 cb、或者一些render
后的生命周期函数、或者forceUpdate
的回调)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 options._commit = (vnode, commitQueue ) => { if (options._commit ) options._commit (root, commitQueue); commitQueue.some (component => { component._renderCallbacks .forEach (invokeCleanup); component._renderCallbacks = component._renderCallbacks .filter (cb => cb._value ? invokeEffect (cb) : true ); }); if (oldCommit) oldCommit (vnode, commitQueue); };
unmount
。 组件的卸载之后执行effect
的清理操作
1 2 3 4 5 6 7 8 9 10 11 12 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 ()); } };
对于组件来说加入的 hook 只是在 preact 的组件基础上增加一个__hook 属性。在 preact 的内部实现中,无论是函数组件还是 class 组件, 都是实例化成 PreactComponent,如下数据结构
1 2 3 4 5 6 7 8 export interface Component extends PreactComponent <any , any > { __hooks?: { _list : HookState []; _pendingEffects : EffectHookState []; }; }
对于问题 1 的回答,通过上面的分析,我们知道,hook
最终是挂在组件的__hooks
属性上的,因此,每次渲染的时候只要去读取函数组件本身的属性就能获取上次渲染的状态了,就能实现了函数组件的状态。这里关键在于getHookState
这个函数。这个函数也是整个preact
hook
中非常重要的
1 2 3 4 5 6 7 8 9 10 11 12 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]; }
这个函数是在组件每次执行useXxx
的时候,首先执行这一步获取 hook 的状态的(以useEffect
为例子)。所有的hook
都是使用这个函数先获取自身 hook 状态
1 2 3 4 5 export function useEffect (callback, args ) { const state = getHookState (currentIndex++); }
这个currentIndex
在每一次的render
过程中是从 0 开始的,每执行一次useXxx
后加一。每个hook
在多次render
中对于记录前一次的执行状态正是通过currentComponent.__hooks
中的顺序决定。所以如果处于条件语句,如果某一次条件不成立,导致那个useXxx
没有执行,这个后面的 hook 的顺序就发生错乱并导致 bug。
例如
1 2 3 4 5 6 7 8 const Component = ( ) => { const [state1, setState1] = useState (); if (condition) { const [state2, setState2] = useState (); } const [state3, setState3] = useState (); };
第一次渲染后,__hooks = [hook1,hook2,hook3]
。 第二次渲染,由于const [state2, setState2] = useState();
被跳过,通过currentIndex
取到的const [state3, setState3] = useState();
其实是hook2
。就可能有问题。所以,这就是问题 2,为什么 hook 不能放到条件语句中。
经过上面一些分析,也知道问题 3 为什么 hook 不能用在普通函数了。因为 hook 都依赖了 hook 内的全局变量currentIndex
和currentComponent
。而普通函数并不会执行options.render
钩子重置currentIndex
和设置currentComponent
,当普通函数执行 hook 的时候,currentIndex
为上一个执行 hook 组件的实例的下标,currentComponent
为上一个执行 hook 组件的实例。因此直接就有问题了。
hook 分析 虽然 preact 中的 hook 有很多,数据结构来说只有 3 种HookState
结构,所有的 hook 都是在这 3 种的基础上实现的。这 3 种分别是
EffectHookState
(useLayoutEffect
useEffect
useImperativeHandle
)
1 2 3 4 5 6 7 8 export interface EffectHookState { _value?: Effect ; _args?: any []; _cleanup?: Cleanup ; }
MemoHookState
(**useMemo
** useRef
useCallback
)
1 2 3 4 5 6 7 8 export interface MemoHookState { _value?: any ; _args?: any []; _callback?: () => any ; }
ReducerHookState
(useReducer
useState
``)
1 2 3 4 export interface ReducerHookState { _value?: any ; _component?: Component ; }
MemoHookState MemoHook
是一类用来和性能优化有关的 hook
useMemo 作用:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
1 2 3 4 5 6 const Component = props => { const result = calculate (props.xx ); return <div > {result}</div > ; };
默认情况下,每次Component
渲染都会执行calculate
的计算操作,如果calculate
是一个大计算量的函数,这里会有造成性能下降,这里就可以使用useMemo
来进行优化了。这样如果calculate
依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。要注意的是calculate
对外部依赖的值都需要传进依赖项数组,否则当部分值变化是,useMemo
却还是旧的值可能会产生 bug。
1 2 3 4 5 6 const Component = props => { const result = useMemo (() => calculate (props.xx ), [props.xx ]); return <div > {result}</div > ; };
useMemo
源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 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 ; }
useMemo
的实现逻辑不复杂,判断依赖项是否改变,改变后执行callback
函数返回值。值得一提的是,依赖项比较只是普通的===
比较,如果依赖的是引用类型,并且直接改变改引用类型上的属性,将不会执行callback
。
useCallback 作用:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memorized 版本,该回调函数仅在某个依赖项改变时才会更新
假设有这样一段代码
1 2 3 4 5 6 const Component = props => { const [number, setNumber] = useState (0 ); const handle = ( ) => console .log (number); return <button onClick ={handle} > 按钮</button > ; };
对于每次的渲染,都是新的 handle,因此 diff 都会失效,都会有一个创建一个新的函数,并且绑定新的事件代理的过程。当使用useCallback
后则会解决这个问题
1 2 3 4 5 6 7 const Component = props => { const [number, setNumber] = useState (0 ); const handle = useCallback (() => () => console .log (number), [number]); return <button onClick ={handle} > 按钮</button > ; };
有一个坑点是,[number]
是不能省略的,如果省略的话,每次打印的log
永远是number
的初始值 0
1 2 3 4 5 6 7 const Component = props => { const [number, setNumber] = useState (0 ); const handle = useCallback (() => () => console .log (number), []); return <button onClick ={handle} > 按钮</button > ; };
至于为什么这样,结合useMomo
的实现分析。useCallback
是在useMemo
的基础上实现的,只是它不执行这个 callback,而是返回这个 callback,用于执行。
1 2 3 4 function useCallback (callback, args ) { return useMemo (() => callback, args); }
我们想象一下,每次的函数组件执行,都是一个全新的过程。而我们的 callback 只是挂在MemoHook
的_value
字段上,当依赖没有改变的时候,我们执行的callback
永远是创建的那个时刻那次渲染的形成的闭包函数。而那个时刻的number
就是初次的渲染值。
1 2 3 4 5 6 7 8 9 10 11 const Component = props => { const [number, setNumber] = useState (0 ); const handle = useCallback ( () => () => console .log (number), [] ); return <button onClick ={handle} > 按钮</button > ; };
useMemo
和useCallback
对于性能优化很好用,但是并不是必须的。因为对于大多数的函数来说,一方面创建/调用消耗并不大,而记录依赖项是需要一个遍历数组的对比操作,这个也是需要消耗的。因此并不需要无脑useMemo
和useCallback
,而是在一些刚好的地方使用才行
useRef 作用:useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。就是在函数组件中替代React.createRef
的功能或者类似于this.xxx
的功能。在整个周期中,ref 值是不变的
用法一:
1 2 3 4 5 6 7 8 9 10 11 12 13 const Component = props => { const [number, setNumber] = useState (0 ); const inputRef = useRef (null ) const focus = useCallback ( () => inputRef.focus (), [] ); return <div > <input ref ={inputRef} > <button onClick ={focus} > 按钮</button > </div > ; };
用法二:类似于this
1 2 3 4 5 6 7 8 9 10 11 12 13 const Component = props => { const [number, setNumber] = useState (0 ); const inputRef = useRef (null ) const focus = useCallback ( () => inputRef.current .focus (), [] ); return <div > <input ref ={node => inputRef.current = node}> <button onClick ={focus} > 按钮</button > </div > ; };
之所以能这么用,在diff
过程中于applyRef
这个函数,react
也是类似。 (diff
中,通过applyRef
将dom对象挂到对应的ref
上)
1 2 3 4 5 6 7 8 export function applyRef (ref, value, vnode ) { try { if (typeof ref == "function" ) ref (value); else ref.current = value; } catch (e) { options._catchError (e, vnode); } }
查看useRef
的源码。
1 2 3 function useRef (initialValue ) { return useMemo (() => ({ current : initialValue }), []); }
可见 就是初始化的时候创建一个{current:initialValue}
,不依赖任何数据,需要手动赋值修改
ReducerHookState useReducer useReducer
和使用redux
非常像。
用法:
1 2 3 4 const [state, dispatch] = useReducer (reducer, initialState, init);
计数器的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const initialState = 0 ;function reducer (state, action ) { switch (action.type ) { case "increment" : return { number : state.number + 1 }; case "decrement" : return { number : state.number - 1 }; default : return state; } } function init (initialState ) { return { number : initialState }; } function Counter ( ) { const [state, dispatch] = useReducer (reducer, initialState, init); return ( <div > {state.number} <button onClick ={() => dispatch({ type: "increment" })}>+</button > <button onClick ={() => dispatch({ type: "decrement" })}>-</button > </div > ); }
对于熟悉redux
的同学来说,一眼明了。后面提到的useState
旧是基于useReducer
实现的。
源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 ; }
更新state
就是调用 demo 的dispatch
,也就是通过reducer(preState,action)
计算出下次的state
赋值给_value
。然后调用组件的setState
方法进行组件的diff
和相应更新操作(这里是preact
和react
不太一样的一个地方,preact 的函数组件在内部和 class 组件一样使用 component 实现的)。
useState useState
大概是 hook 中最常用的了。类似于 class 组件中的 state 状态值。
用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Component = ( ) => { const [number, setNumber] = useState (0 ); const [index, setIndex] = useIndex (0 ); return ( <div > {/* setXxx可以传入回调或者直接设置值**/} <button onClick ={() => setNumber(number => number + 1)}> 更新number </button > {number} // <button onClick ={() => setIndex(index + 1)}>更新index</button > {index} </div > ); };
上文已经提到过,useState
是通过useReducer
实现的。
1 2 3 4 5 6 7 8 9 10 11 export function useState (initialState ) { return useReducer (invokeOrReturn, initialState); }
只要我们给useReduecr
的reducer
参数传invokeOrReturn
函数即可实现useState
。回顾下useState
和useReducer
的用法
1 2 3 4 5 6 7 const [index, setIndex] = useIndex (0 );setIndex (index => index + 1 );setIndex (1 );const [state, dispatch] = useReducer (reducer, initialState);dispatch ({ type : "some type" });
1、对于setState
直接传值的情况。reducer
(invokeOrReturn
)函数,直接返回入参即可
1 2 const nextValue = reducer (hookState._value [0 ], action);
2、对于setState
直接参数的情况的情况。
1 2 const nextValue = reducer (hookState._value [0 ], action);
可见,useState
其实只是传特定reducer
的useReducer
一种实现。
EffectHookState
useEffect
和 useLayoutEffect
这两个 hook 的用法完全一致,都是在 render 过程中执行一些副作用的操作,可来实现以往 class 组件中一些生命周期的操作。区别 在于,useEffect
的 callback 执行是在本次渲染结束之后,下次渲染之前执行。 useLayoutEffect
则是在本次会在浏览器 layout 之后,painting 之前执行,是同步的。
用法。传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function useLayoutEffect (effect: EffectCallback, inputs?: Inputs ): void ;export function useEffect (effect: EffectCallback, inputs?: Inputs ): void ;
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function LayoutEffect ( ) { const [color, setColor] = useState ("red" ); useLayoutEffect (() => { alert (color); }, [color]); useEffect (() => { alert (color); }, [color]); return ( <> <div id ="myDiv" style ={{ background: color }}> 颜色 </div > <button onClick ={() => setColor("red")}>红</button > <button onClick ={() => setColor("yellow")}>黄</button > <button onClick ={() => setColor("blue")}>蓝</button > </> ); }
从 demo 可以看出,每次改变颜色,useLayoutEffect
的回调触发时机是在页面改变颜色之前,而useEffect
的回调触发时机是页面改变颜色之后。它们的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 function useEffect (callback, args ) { const state = getHookState (currentIndex++); if (argsChanged (state._args , args)) { state._value = callback; state._args = args; currentComponent.__hooks ._pendingEffects .push (state); } }
它们的实现几乎一模一样,唯一的区别是useLayoutEffect
的回调进的是_renderCallbacks
数组,而useEffect
的回调进的是_pendingEffects
。
前面已经做过一些分析,_renderCallbacks
是在\_commit
钩子中执行的,在这里执行上次renderCallbacks
的effect
的清理函数和执行本次的renderCallbacks
。\_commit
则是在preact
的commitRoot
中被调用,即每次 render 后同步调用(顾名思义 renderCallback 就是 render 后的回调,此时 DOM 已经更新完,浏览器还没有 paint 新一帧,上图所示的 layout 后 paint 前)因此 demo 中我们在这里alert
会阻塞浏览器的 paint,这个时候看不到颜色的变化。
而_pendingEffects
则是本次重绘之后,下次重绘之前执行。在 hook 中的调用关系如下
1、 options.differed
钩子中(即组件 diff 完成后),执行afterPaint(afterPaintEffects.push(c))
将含有_pendingEffects
的组件推进全局的afterPaintEffects
队列
2、afterPaint
中执行执行afterNextFrame(flushAfterPaintEffects)
。在下一帧 重绘之前,执行flushAfterPaintEffects
。同时,如果 100ms 内,当前帧的 requestAnimationFrame 没有结束(例如窗口不可见的情况),则直接执行flushAfterPaintEffects
。flushAfterPaintEffects
函数执行队列内所有组件的上一次的_pendingEffects
的清理函数和执行本次的_pendingEffects
。
几个关键函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function flushAfterPaintEffects ( ) { afterPaintEffects.some (component => { if (component._parentDom ) { component.__hooks ._pendingEffects .forEach (invokeCleanup); component.__hooks ._pendingEffects .forEach (invokeEffect); component.__hooks ._pendingEffects = []; } }); afterPaintEffects = []; } 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 ); const raf = requestAnimationFrame (done); }
useImperativeHandle useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function FancyInput (props, ref ) { const inputRef = useRef (); useImperativeHandle (ref, () => ({ focus : () => { inputRef.current .focus (); } })); return <input ref ={inputRef} ... /> ; } FancyInput = forwardRef (FancyInput );function App ( ){ const ref = useRef () return <div > <FancyInput ref ={ref}/ > <button onClick ={() => ref.focus()}>click</button > </div > }
默认情况下,函数组件是没有ref
属性,通过forwardRef(FancyInput)
后,父组件就可以往子函数组件传递ref
属性了。useImperativeHandle
的作用就是控制父组件不能在拿到子组件的ref
后为所欲为。如上,父组件拿到FancyInput
后,只能执行focus
,即子组件决定对外暴露的 ref 接口,class
组件是无法做到的。
1 2 3 4 5 6 7 8 9 function useImperativeHandle (ref, createHandle, args ) { useLayoutEffect ( () => { if (typeof ref === "function" ) ref (createHandle ()); else if (ref) ref.current = createHandle (); }, args == null ? args : args.concat (ref) ); }
useImperativeHandle
的实现也是一目了然,因为这种是涉及到 dom 更新后的同步修改,所以是用useLayoutEffect
实现的。从实现可看出,useImperativeHandle
也能接收依赖项数组的
createContext 接收一个 context 对象(Preact.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop 决定。当组件上层最近的<MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
使用 context 最大的好处就是避免了深层组件嵌套时,需要一层层往下通过 props 传值。使用 createContext 可以非常方便的使用 context 而不用再写繁琐的Consumer
react context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const context = Preact .createContext (null );const Component = ( ) => { const { xx } = useContext (context); return <div > </div > ; }; const App = ( ) => { return ( <Context.Provider value ={{ xx: xx }}> <Component > </Component > </Context.Provider > ); };
useContext
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ; }
可以看出,useContext
会在初始化的时候,当前组件对应的Context.Provider
会把该组件加入订阅回调(provider.sub(currentComponent)
),当 Provider value 变化时,在 Provider 的shouldComponentUpdate
周期中执行组件的 render。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Provider (props) { 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); }; }; }
总结: preact
和react
在源码实现上有一定差异,但是通过对 preact hook 源码的学习,对于理解 hook 的很多观念和思想是非常有帮助的。
最后附上带了注释的 hook 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 import { options } from "preact" ;/\*_ @type {number} _/ let currentIndex;/\*_ @type {import('./i nternal').Component} _/ let currentComponent; /\*_ @type {Array<import(' ./internal').Component>} _/ let afterPaintEffects = []; let oldBeforeRender = options.\_render; options.\_render = vnode => { // render 钩子函数 if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode.\_component; currentIndex = 0; if (currentComponent.**hooks) { // 执行清理操作 currentComponent.**hooks.\_pendingEffects.forEach(invokeCleanup); // 执行 effect currentComponent.**hooks.\_pendingEffects.forEach(invokeEffect); currentComponent.**hooks.\_pendingEffects = []; } }; // \_pendingEffects paint 后调用 // \_renderCallbacks render 后同步调用 // render(执行 render 方法之前) -> diffed(diff 结束) -> \_commit(初始或者更新生命周期结束之后) -> unmount(卸载) let oldAfterDiff = options.diffed; 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)); } } }; let oldCommit = options.\_commit; options.\_commit = (vnode, commitQueue) => { commitQueue.some(component => { component.\_renderCallbacks.forEach(invokeCleanup); // \_renderCallbacks 有可能是 setState 的第二个参数这种的、或者生命周期、或者 forceUpdate 的回调。 // 通过\_value 判断是 hook 的回调 component.\_renderCallbacks = component.\_renderCallbacks.filter(cb => cb.\_value ? invokeEffect(cb) : true ); }); if (oldCommit) oldCommit(vnode, commitQueue); }; let oldBeforeUnmount = options.unmount; 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()); } }; /\*\* - Get a hook' s state from the currentComponent- @param {number} index The index of the hook to get - @returns {import ('./internal' ).HookState } _/ 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];} /\*\* - @param {import ('./index' ).StateUpdater <any>} initialState \*/ export function useState (initialState ) { return useReducer (invokeOrReturn, initialState); } /\*\* - @param {import ('./index' ).Reducer <any, any>} reducer - @param {import ('./index' ).StateUpdater <any>} initialState - @param {(initialState: any ) => void } [init] - @returns {[ any, (state: any ) => void ]} _/ export function useReducer (reducer, initialState, init ) { /\*\* @type {import('./i nternal').ReducerHookState} _/ 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; } /\*\* - @param {import(' ./internal').Effect} callback - @param {any[]} args _/ export function useEffect(callback, args) { /\*\* @type {import(' ./internal').EffectHookState} _/ const state = getHookState(currentIndex++); if (argsChanged(state.\_args, args)) { state.\_value = callback; state.\_args = args; currentComponent.__hooks._pendingEffects.push(state); } } /\*\* - @param {import(' ./internal').Effect} callback - @param {any[]} args _/ export function useLayoutEffect(callback, args) { /\*\* @type {import(' ./internal').EffectHookState} _/ const state = getHookState(currentIndex++); if (argsChanged(state.\_args, args)) { state.\_value = callback; state.\_args = args; currentComponent._renderCallbacks.push(state); } } export function useRef(initialValue) { return useMemo(() => ({ current: initialValue }), []); } /\*\* - @param {object} ref - @param {() => object} createHandle - @param {any[]} args \*/ 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) ); } /\*\* - @param {() => any} callback - @param {any[]} args _/ export function useMemo(callback, args) { /\*\* @type {import(' ./internal').MemoHookState} _/ const state = getHookState(currentIndex++); if (argsChanged(state.\_args, args)) { state.\_args = args; state.\_callback = callback; return (state.\_value = callback()); } return state.\_value; } /\*\* - @param {() => void} callback - @param {any[]} args \*/ export function useCallback(callback, args) { return useMemo(() => callback, args); } /\*\* - @param {import(' ./internal').PreactContext} context \*/ export function useContext(context) { const provider = currentComponent.context[context._id]; if (!provider) return context.\_defaultValue; const state = getHookState(currentIndex++); // This is probably not safe to convert to "!" if (state.\_value == null) { state.\_value = true; provider.sub(currentComponent); } return provider.props.value; } /\*\* - Display a custom label for a custom hook for the devtools panel - @type {<T>(value: T, cb?: (value: T) => string | number) => void} \*/ export function useDebugValue(value, formatter) { if (options.useDebugValue) { options.useDebugValue(formatter ? formatter(value) : value); } } // Note: if someone used Component.debounce = requestAnimationFrame, // then effects will ALWAYS run on the NEXT frame instead of the current one, incurring a ~16ms delay. // Perhaps this is not such a big deal. /\*\* - Schedule afterPaintEffects flush after the browser paints - @type {(newQueueLength: number) => void} _/ /_ istanbul ignore next \*/ let afterPaint = () => {}; /\*\* - 绘制之后执行回调 \*/ function flushAfterPaintEffects() { afterPaintEffects.some(component => { if (component.\_parentDom) { // 清理上一次的 Effect component.**hooks.\_pendingEffects.forEach(invokeCleanup); // 执行当前 effect component.**hooks.\_pendingEffects.forEach(invokeEffect); component.\_\_hooks.\_pendingEffects = []; } }); afterPaintEffects = []; } const RAF_TIMEOUT = 100; /\*\* - 希望在下一帧 重绘之前,执行 callback。同时,如果 100ms 内,当前帧没有结束(例如窗口不可见的情况),则直接执行 callback \*/ function afterNextFrame(callback) { const done = () => { clearTimeout(timeout); cancelAnimationFrame(raf); setTimeout(callback); }; const timeout = setTimeout(done, RAF_TIMEOUT); const raf = requestAnimationFrame(done); } /_ istanbul ignore else _/ if (typeof window !== "undefined") { let prevRaf = options.requestAnimationFrame; afterPaint = newQueueLength => { if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) { prevRaf = options.requestAnimationFrame; // 执行下一帧结束后,清空 useEffect的回调 (prevRaf || afterNextFrame)(flushAfterPaintEffects); } }; } /\*\* - 执行清理 effect 操作。 - @param {import(' ./internal').EffectHookState} hook \*/ function invokeCleanup(hook) { if (hook.\_cleanup) hook.\_cleanup(); } /\*\* - 执行 effect hook 的 cb,并将清理函数赋值给\_cleanup - Invoke a Hook' s effect- @param {import ('./internal' ).EffectHookState } hook \*/ function invokeEffect (hook ) { const result = hook.\_value (); if (typeof result === "function" ) hook.\_cleanup = result; } /\*\* - 判断两个数组是否变化 - @param {any[]} oldArgs - @param {any[]} newArgs \*/ function argsChanged (oldArgs, newArgs ) { return !oldArgs || newArgs.some ((arg, index ) => arg !== oldArgs[index]); } /\*\* - 执行或者返回 \*/ function invokeOrReturn (arg, f ) { return typeof f === "function" ? f (arg) : f; }