You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * 接收一个包含一些必要副作用代码的函数,这个函数需要从DOM中读取layout和同步re-render * `useLayoutEffect` 里面的操作将在DOM变化之后,浏览器绘制之前 执行 * 尽量使用`useEffect`避免阻塞视图更新 * * @param effect Imperative function that can return a cleanup function * @param inputs If present, effect will only activate if the values in the list change (using ===). */exportfunctionuseLayoutEffect(effect: EffectCallback,inputs?: Inputs): void;/** * 接收一个包含一些必要副作用代码的函数。 * 副作用函数会在浏览器绘制后执行,不会阻塞渲染 * * @param effect Imperative function that can return a cleanup function * @param inputs If present, effect will only activate if the values in the list change (using ===). */exportfunctionuseEffect(effect: EffectCallback,inputs?: Inputs): void;
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
结合
_render
在 preact 的执行时机,可以知道,在这个钩子函数里是进行每次 render 的初始化操作。包括执行/清理上次未处理完的 effect、初始化 hook 下标为 0、取得当前 render 的组件实例。diffed
位置。 vnode 的 diff 完成之后,将当前的_pendingEffects
推进执行队列,让它在下一帧绘制前执行,不阻塞本次的浏览器渲染。\_commit
位置。初始或者更新 render 结束之后执行_renderCallbacks
,在这个\_commit
中只执行 hook 的回调,如useLayoutEffect
。(_renderCallbacks
是指在preact
中指每次 render 后,同步执行的操作回调列表,例如setState
的第二个参数 cb、或者一些render
后的生命周期函数、或者forceUpdate
的回调)。unmount
。 组件的卸载之后执行effect
的清理操作对于组件来说加入的 hook 只是在 preact 的组件基础上增加一个__hook 属性。在 preact 的内部实现中,无论是函数组件还是 class 组件, 都是实例化成 PreactComponent,如下数据结构
对于问题 1 的回答,通过上面的分析,我们知道,
hook
最终是挂在组件的__hooks
属性上的,因此,每次渲染的时候只要去读取函数组件本身的属性就能获取上次渲染的状态了,就能实现了函数组件的状态。这里关键在于getHookState
这个函数。这个函数也是整个preact
hook
中非常重要的这个函数是在组件每次执行
useXxx
的时候,首先执行这一步获取 hook 的状态的(以useEffect
为例子)。所有的hook
都是使用这个函数先获取自身 hook 状态这个
currentIndex
在每一次的render
过程中是从 0 开始的,每执行一次useXxx
后加一。每个hook
在多次render
中对于记录前一次的执行状态正是通过currentComponent.__hooks
中的顺序决定。所以如果处于条件语句,如果某一次条件不成立,导致那个useXxx
没有执行,这个后面的 hook 的顺序就发生错乱并导致 bug。例如
第一次渲染后,
__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
)MemoHookState
(useMemo
useRef
useCallback
)ReducerHookState
(useReducer
useState
``)useContext
这个比较特殊MemoHookState
MemoHook
是一类用来和性能优化有关的 hookuseMemo
作用:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
默认情况下,每次
Component
渲染都会执行calculate
的计算操作,如果calculate
是一个大计算量的函数,这里会有造成性能下降,这里就可以使用useMemo
来进行优化了。这样如果calculate
依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。要注意的是calculate
对外部依赖的值都需要传进依赖项数组,否则当部分值变化是,useMemo
却还是旧的值可能会产生 bug。useMemo
源码分析useMemo
的实现逻辑不复杂,判断依赖项是否改变,改变后执行callback
函数返回值。值得一提的是,依赖项比较只是普通的===
比较,如果依赖的是引用类型,并且直接改变改引用类型上的属性,将不会执行callback
。useCallback
作用:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memorized 版本,该回调函数仅在某个依赖项改变时才会更新
假设有这样一段代码
对于每次的渲染,都是新的 handle,因此 diff 都会失效,都会有一个创建一个新的函数,并且绑定新的事件代理的过程。当使用
useCallback
后则会解决这个问题有一个坑点是,
[number]
是不能省略的,如果省略的话,每次打印的log
永远是number
的初始值 0至于为什么这样,结合
useMomo
的实现分析。useCallback
是在useMemo
的基础上实现的,只是它不执行这个 callback,而是返回这个 callback,用于执行。我们想象一下,每次的函数组件执行,都是一个全新的过程。而我们的 callback 只是挂在
MemoHook
的_value
字段上,当依赖没有改变的时候,我们执行的callback
永远是创建的那个时刻那次渲染的形成的闭包函数。而那个时刻的number
就是初次的渲染值。useMemo
和useCallback
对于性能优化很好用,但是并不是必须的。因为对于大多数的函数来说,一方面创建/调用消耗并不大,而记录依赖项是需要一个遍历数组的对比操作,这个也是需要消耗的。因此并不需要无脑useMemo
和useCallback
,而是在一些刚好的地方使用才行useRef
作用:useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。就是在函数组件中替代
React.createRef
的功能或者类似于this.xxx
的功能。在整个周期中,ref 值是不变的用法一:
用法二:类似于
this
之所以能这么用,在
diff
过程中于applyRef
这个函数,react
也是类似。(
diff
中,通过applyRef
将dom对象挂到对应的ref
上)查看
useRef
的源码。可见 就是初始化的时候创建一个
{current:initialValue}
,不依赖任何数据,需要手动赋值修改ReducerHookState
useReducer
useReducer
和使用redux
非常像。用法:
计数器的例子。
对于熟悉
redux
的同学来说,一眼明了。后面提到的useState
旧是基于useReducer
实现的。源码分析
更新
state
就是调用 demo 的dispatch
,也就是通过reducer(preState,action)
计算出下次的state
赋值给_value
。然后调用组件的setState
方法进行组件的diff
和相应更新操作(这里是preact
和react
不太一样的一个地方,preact 的函数组件在内部和 class 组件一样使用 component 实现的)。useState
useState
大概是 hook 中最常用的了。类似于 class 组件中的 state 状态值。用法
上文已经提到过,
useState
是通过useReducer
实现的。只要我们给
useReduecr
的reducer
参数传invokeOrReturn
函数即可实现useState
。回顾下useState
和useReducer
的用法1、对于
setState
直接传值的情况。reducer
(invokeOrReturn
)函数,直接返回入参即可2、对于
setState
直接参数的情况的情况。可见,
useState
其实只是传特定reducer
的useReducer
一种实现。EffectHookState
useEffect
和useLayoutEffect
这两个 hook 的用法完全一致,都是在 render 过程中执行一些副作用的操作,可来实现以往 class 组件中一些生命周期的操作。区别在于,
useEffect
的 callback 执行是在本次渲染结束之后,下次渲染之前执行。useLayoutEffect
则是在本次会在浏览器 layout 之后,painting 之前执行,是同步的。用法。传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。
demo
从 demo 可以看出,每次改变颜色,
useLayoutEffect
的回调触发时机是在页面改变颜色之前,而useEffect
的回调触发时机是页面改变颜色之后。它们的实现如下它们的实现几乎一模一样,唯一的区别是
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
。几个关键函数
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起
默认情况下,函数组件是没有ref
属性,通过forwardRef(FancyInput)
后,父组件就可以往子函数组件传递ref
属性了。useImperativeHandle
的作用就是控制父组件不能在拿到子组件的ref
后为所欲为。如上,父组件拿到FancyInput
后,只能执行focus
,即子组件决定对外暴露的 ref 接口,class
组件是无法做到的。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
useContext
实现可以看出,
useContext
会在初始化的时候,当前组件对应的Context.Provider
会把该组件加入订阅回调(provider.sub(currentComponent)
),当 Provider value 变化时,在 Provider 的shouldComponentUpdate
周期中执行组件的 render。总结:
preact
和react
在源码实现上有一定差异,但是通过对 preact hook 源码的学习,对于理解 hook 的很多观念和思想是非常有帮助的。最后附上带了注释的 hook 源码
The text was updated successfully, but these errors were encountered: