详细 preact hook 源码逐行解析

Posted 前端大全

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细 preact hook 源码逐行解析相关的知识,希望对你有一定的参考价值。

(给前端大全加星标,提升前端技能

https://juejin.im/post/5dc8f360e51d45782a445204

本文通过对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=>diffed=>commit=>umount

_render位置。执行组件的 render 方法之前执行,用于执行pendingEffects(pendingEffects是不阻塞页面渲染的 effect 操作,在下一帧绘制前执行)的清理操作和执行未执行的。这个钩子还有一个很重要的作用就是让 hook 拿到当前正在执行的render的组件实例

 
   
   
 
  1. options._render = vnode => {

  2. // render 钩子函数

  3. if (oldBeforeRender) oldBeforeRender(vnode);


  4. currentComponent = vnode._component;

  5. currentIndex = 0;


  6. if (currentComponent.__hooks) {

  7. // 执行清理操作

  8. currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);

  9. // 执行effect

  10. currentComponent.__hooks._pendingEffects.forEach(invokeEffect);

  11. currentComponent.__hooks._pendingEffects = [];

  12. }

  13. };

结合_render在 preact 的执行时机,可以知道,在这个钩子函数里是进行每次 render 的初始化操作。包括执行/清理上次未处理完的 effect、初始化 hook 下标为 0、取得当前 render 的组件实例。

diffed位置。vnode 的 diff 完成之后,将当前的_pendingEffects推进执行队列,让它在下一帧绘制前执行,不阻塞本次的浏览器渲染。

 
   
   
 
  1. options.diffed = vnode => {

  2. if (oldAfterDiff) oldAfterDiff(vnode);


  3. const c = vnode._component;

  4. if (!c) return;


  5. const hooks = c.__hooks;

  6. if (hooks) {

  7. // 下面会提到useEffect就是进入_pendingEffects队列

  8. if (hooks._pendingEffects.length) {

  9. // afterPaint 表示本次帧绘制完,下一帧开始前执行

  10. afterPaint(afterPaintEffects.push(c));

  11. }

  12. }

  13. };

_commit位置。初始或者更新 render 结束之后执行,在这个_commit中只执行 hook 的回调,如useLayoutEffect。(renderCallbacks是指在中指每次 render 后,同步执行的操作回调列表,例如setState的第二个参数 cb、或者一些render后的生命周期函数、或者forceUpdate的回调)。

 
   
   
 
  1. options._commit = (vnode, commitQueue) => {

  2. commitQueue.some(component => {

  3. // 执行上次的_renderCallbacks的清理函数

  4. component._renderCallbacks.forEach(invokeCleanup);

  5. // _renderCallbacks有可能是setState的第二个参数这种的、或者生命周期、或者forceUpdate的回调。

  6. // 通过_value判断是hook的回调则在此出执行

  7. component._renderCallbacks = component._renderCallbacks.filter(cb =>

  8. cb._value ? invokeEffect(cb) : true

  9. );

  10. });


  11. if (oldCommit) oldCommit(vnode, commitQueue);

  12. };

unmount。组件的卸载之后执行effect的清理操作

 
   
   
 
  1. options.unmount = vnode => {

  2. if (oldBeforeUnmount) oldBeforeUnmount(vnode);


  3. const c = vnode._component;

  4. if (!c) return;


  5. const hooks = c.__hooks;

  6. if (hooks) {

  7. // _cleanup 是effect类hook的清理函数,也就是我们每个effect的callback 的返回值函数

  8. hooks._list.forEach(hook => hook._cleanup && hook._cleanup());

  9. }

  10. };

对于组件来说加入的 hook 只是在 preact 的组件基础上增加一个__hook 属性。在 preact 的内部实现中,无论是函数组件还是 class 组件, 都是实例化成 PreactComponent,如下数据结构

 
   
   
 
  1. export interface Component extends PreactComponent<any, any> {

  2. __hooks?: {

  3. // 每个组件的hook存储

  4. _list: HookState[];

  5. // useLayoutEffect useEffect 等

  6. _pendingEffects: EffectHookState[];

  7. };

  8. }

对于问题 1 的回答,通过上面的分析,我们知道,hook最终是挂在组件的__hooks属性上的,因此,每次渲染的时候只要去读取函数组件本身的属性就能获取上次渲染的状态了,就能实现了函数组件的状态。这里关键在于getHookState这个函数。这个函数也是整个preact hook中非常重要的

 
   
   
 
  1. function getHookState(index) {

  2. if (options._hook) options._hook(currentComponent);

  3. const hooks =

  4. currentComponent.__hooks ||

  5. (currentComponent.__hooks = { _list: [], _pendingEffects: [] });


  6. // 初始化的时候,创建一个空的hook

  7. if (index >= hooks._list.length) {

  8. hooks._list.push({});

  9. }

  10. return hooks._list[index];

  11. }

这个函数是在组件每次执行useXxx的时候,首先执行这一步获取 hook 的状态的(以useEffect为例子)。所有的hook都是使用这个函数先获取自身 hook 状态

 
   
   
 
  1. export function useEffect(callback, args) {

  2. //....

  3. const state = getHookState(currentIndex++);

  4. //.....

  5. }

这个currentIndex在每一次的render过程中是从 0 开始的,每执行一次useXxx后加一。每个hook在多次render中对于记录前一次的执行状态正是通过currentComponent.__hooks中的顺序决定。所以如果处于条件语句,如果某一次条件不成立,导致那个useXxx没有执行,这个后面的 hook 的顺序就发生错乱并导致 bug。

例如

 
   
   
 
  1. const Component = () => {

  2. const [state1, setState1] = useState();

  3. // 假设condition第一次渲染为true,第二次渲染为false

  4. if (condition) {

  5. const [state2, setState2] = useState();

  6. }

  7. const [state3, setState3] = useState();

  8. };

第一次渲染后,__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. export interface EffectHookState {

  2. // effect hook的回调函数

  3. _value?: Effect;

  4. // 依赖项

  5. _args?: any[];

  6. // effect hook的清理函数,_value的返回值

  7. _cleanup?: Cleanup;

  8. }

MemoHookState (useMemo useRef useCallback)

 
   
   
 
  1. export interface MemoHookState {

  2. // useMemo的返回值

  3. _value?: any;

  4. // 前一次的依赖数组

  5. _args?: any[];

  6. //useMemo传入的callback

  7. _callback?: () => any;

  8. }

ReducerHookState (useReducer useState ``)

 
   
   
 
  1. export interface ReducerHookState {

  2. _value?: any;

  3. _component?: Component;

  4. }

useContext 这个比较特殊

MemoHookState

MemoHook是一类用来和性能优化有关的 hook

useMemo

作用:把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. // 假设calculate是个消耗很多的计算操作

  4. const result = calculate(props.xx);

  5. return <div>{result}</div>;

  6. };

默认情况下,每次Component渲染都会执行calculate的计算操作,如果calculate是一个大计算量的函数,这里会有造成性能下降,这里就可以使用useMemo来进行优化了。这样如果calculate依赖的值没有变化,就不需要执行这个函数,而是取它的缓存值。要注意的是calculate对外部依赖的值都需要传进依赖项数组,否则当部分值变化是,useMemo却还是旧的值可能会产生 bug。

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. // 这样子,只会在props.xx值改变时才重新执行calculate函数,达到了优化的目的

  4. const result = useMemo(() => calculate(props.xx), [props.xx]);

  5. return <div>{result}</div>;

  6. };

useMemo源码分析

 
   
   
 
  1. function useMemo(callback, args) {

  2. // state是MemoHookState类型

  3. const state = getHookState(currentIndex++);

  4. // 判断依赖项是否改变

  5. if (argsChanged(state._args, args)) {

  6. // 存储本次依赖的数据值

  7. state._args = args;

  8. state._callback = callback;

  9. // 改变后执行`callback`函数返回值。

  10. return (state._value = callback());

  11. }

  12. return state._value;

  13. }

useMemo的实现逻辑不复杂,判断依赖项是否改变,改变后执行callback函数返回值。值得一提的是,依赖项比较只是普通的===比较,如果依赖的是引用类型,并且直接改变改引用类型上的属性,将不会执行callback。

useCallback

作用:接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memorized 版本,该回调函数仅在某个依赖项改变时才会更新

假设有这样一段代码

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. const handle = () => console.log(number);

  5. return <button onClick={handle}>按钮</button>;

  6. };

对于每次的渲染,都是新的 handle,因此 diff 都会失效,都会有一个创建一个新的函数,并且绑定新的事件代理的过程。当使用useCallback后则会解决这个问题

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. // 这里,如果number不变的情况下,每次的handle是同一个值

  5. const handle = useCallback(() => () => console.log(number), [number]);

  6. return <button onClick={handle}>按钮</button>;

  7. };

有一个坑点是,[number]是不能省略的,如果省略的话,每次打印的log永远是number的初始值 0

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. // 这里永远打印0

  5. const handle = useCallback(() => () => console.log(number), []);

  6. return <button onClick={handle}>按钮</button>;

  7. };

至于为什么这样,结合useMomo的实现分析。useCallback是在useMemo的基础上实现的,只是它不执行这个 callback,而是返回这个 callback,用于执行。

 
   
   
 
  1. function useCallback(callback, args) {

  2. // 直接返回这个callback,而不是执行

  3. return useMemo(() => callback, args);

  4. }

我们想象一下,每次的函数组件执行,都是一个全新的过程。而我们的 callback 只是挂在MemoHook的_value字段上,当依赖没有改变的时候,我们执行的callback永远是创建的那个时刻那次渲染的形成的闭包函数。而那个时刻的number就是初次的渲染值。

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. // 这里永远打印0

  5. const handle = useCallback(

  6. () => /** 到了后面的时候,我们的handle并不是执行这次的callback,而是上次的那个记录的callback*/ () =>

  7. console.log(number),

  8. []

  9. );

  10. return <button onClick={handle}>按钮</button>;

  11. };

useMemo和useCallback对于性能优化很好用,但是并不是必须的。因为对于大多数的函数来说,一方面创建/调用消耗并不大,而记录依赖项是需要一个遍历数组的对比操作,这个也是需要消耗的。因此并不需要无脑useMemo和useCallback,而是在一些刚好的地方使用才行

useRef

作用:useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。就是在函数组件中替代React.createRef的功能或者类似于this.xxx的功能。在整个周期中,ref 值是不变的

用法一:

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. const inputRef = useRef(null)

  5. const focus = useCallback(

  6. () =>inputRef.focus(),

  7. []

  8. );

  9. return<div>

  10. <input ref={inputRef}>

  11. <button onClick={focus}>按钮</button>

  12. </div>;

  13. };

用法二:类似于this

 
   
   
 
  1. // 例子

  2. const Component = props => {

  3. const [number, setNumber] = useState(0);

  4. const inputRef = useRef(null)

  5. const focus = useCallback(

  6. () =>inputRef.focus(),

  7. []

  8. );

  9. return<div>

  10. <input ref={node => inputRef.current = node}>

  11. <button onClick={focus}>按钮</button>

  12. </div>;

  13. };

之所以能这么用,在于applyRef这个函数,react也是类似。

 
   
   
 
  1. export function applyRef(ref, value, vnode) {

  2. try {

  3. if (typeof ref == "function") ref(value);

  4. else ref.current = value;

  5. } catch (e) {

  6. options._catchError(e, vnode);

  7. }

  8. }

查看useRef的源码。

 
   
   
 
  1. function useRef(initialValue) {

  2. return useMemo(() => ({ current: initialValue }), []);

  3. }

可见 就是初始化的时候创建一个{current:initialValue},不依赖任何数据,需要手动赋值修改

ReducerHookState

useReducer

useReducer和使用redux非常像。

用法:

 
   
   
 
  1. // reducer就是平时redux那种reducer函数

  2. // initialState 初始化的state状态

  3. // init 一个函数用于惰性计算state初始值

  4. const [state, dispatch] = useReducer(reducer, initialState, init);

计数器的例子。

 
   
   
 
  1. const initialState = 0;

  2. function reducer(state, action) {

  3. switch (action.type) {

  4. case "increment":

  5. return { number: state.number + 1 };

  6. case "decrement":

  7. return { number: state.number - 1 };

  8. default:

  9. return state;

  10. }

  11. }

  12. function init(initialState) {

  13. return { number: initialState };

  14. }

  15. function Counter() {

  16. const [state, dispatch] = useReducer(reducer, initialState, init);

  17. return (

  18. <div>

  19. {state.number}

  20. <button onClick={() => dispatch({ type: "increment" })}>+</button>

  21. <button onClick={() => dispatch({ type: "decrement" })}>-</button>

  22. </div>

  23. );

  24. }

对于熟悉redux的同学来说,一眼明了。后面提到的useState旧是基于useReducer实现的。

源码分析

 
   
   
 
  1. export function useReducer(reducer, initialState, init) {

  2. const hookState = getHookState(currentIndex++);

  3. // 前面分析过ReducerHookState的数据结构,有两个属性

  4. // _value 当前的state值

  5. // _component 对应的组件实例


  6. if (!hookState._component) {

  7. // 初始化过程

  8. // 因为后面需要用到setState更新,所以需要记录component的引用

  9. hookState._component = currentComponent;


  10. hookState._value = [

  11. // init是前面提到的惰性初始化函数,传入了init则初始值是init的计算结果

  12. // 没传init的时候是invokeOrReturn。这里就是直接返回初始化值

  13. /***

  14. *

  15. * ```js

  16. * invokeOrReturn 很精髓

  17. * 参数f为函数,返回 f(arg)

  18. * 参数f非函数,返回f

  19. * function invokeOrReturn(arg, f) {

  20. return typeof f === "function" ? f(arg) : f;

  21. }

  22. * ```

  23. */

  24. !init ? invokeOrReturn(undefined, initialState) : init(initialState),


  25. action => {

  26. // reducer函数计算出下次的state的值

  27. const nextValue = reducer(hookState._value[0], action);

  28. if (hookState._value[0] !== nextValue) {

  29. hookState._value[0] = nextValue;

  30. // setState开始进行下一轮更新

  31. hookState._component.setState({});

  32. }

  33. }

  34. ];

  35. }


  36. // 返回当前的state

  37. return hookState._value;

  38. }

更新state就是调用 demo 的dispatch,也就是通过reducer(preState,action)计算出下次的state赋值给_value。然后调用组件的setState方法进行组件的diff和相应更新操作(这里是preact和react不太一样的一个地方,preact 的函数组件在内部和 class 组件一样使用 component 实现的)。

useState

useState大概是 hook 中最常用的了。类似于 class 组件中的 state 状态值。

用法

 
   
   
 
  1. const Component = () => {

  2. const [number, setNumber] = useState(0);

  3. const [index, setIndex] = useIndex(0);

  4. return (

  5. <div>

  6. {/* setXxx可以传入回调或者直接设置值**/}

  7. <button onClick={() => setNumber(number => number + 1)}>

  8. 更新number

  9. </button>

  10. {number}

  11. //

  12. <button onClick={() => setIndex(index + 1)}>更新index</button>

  13. {index}

  14. </div>

  15. );

  16. };

上文已经提到过,useState是通过useReducer实现的。

 
   
   
 
  1. export function useState(initialState) {

  2. /***

  3. *

  4. * ```js

  5. * function invokeOrReturn(arg, f) {

  6. return typeof f === "function" ? f(arg) : f;

  7. }

  8. * ```

  9. */

  10. return useReducer(invokeOrReturn, initialState);

  11. }

只要我们给useReduecr的reducer参数传invokeOrReturn函数即可实现useState。回顾下useState和useReducer的用法

 
   
   
 
  1. const [index, setIndex] = useIndex(0);

  2. setIndex(index => index + 1);

  3. // or

  4. setIndex(1);

  5. //-----

  6. const [state, dispatch] = useReducer(reducer, initialState);

  7. dispatch({ type: "some type" });

对于setState直接传值的情况。reducer(invokeOrReturn)函数,直接返回入参即可

 
   
   
 
  1. // action非函数,reducer(hookState._value[0], action)结果为action

  2. const nextValue = reducer(hookState._value[0], action);

对于setState直接参数的情况的情况。

 
   
   
 
  1. // action为函数,reducer(hookState._value[0], action)结果为action(hookState._value[0])

  2. const nextValue = reducer(hookState._value[0], action);

可见,useState其实只是传特定reducer的useReducer一种实现。

EffectHookState

useEffect 和 useLayoutEffect

这两个 hook 的用法完全一致,都是在 render 过程中执行一些副作用的操作,可来实现以往 class 组件中一些生命周期的操作。区别在于,useEffect 的 callback 执行是在本次渲染结束之后,下次渲染之前执行。useLayoutEffect则是在本次会在浏览器 layout 之后,painting 之前执行,是同步的。

用法。传递一个回调函数和一个依赖数组,数组的依赖参数变化时,重新执行回调。

 
   
   
 
  1. /**

  2. * 接收一个包含一些必要副作用代码的函数,这个函数需要从DOM中读取layout和同步re-render

  3. * `useLayoutEffect` 里面的操作将在DOM变化之后,浏览器绘制之前 执行

  4. * 尽量使用`useEffect`避免阻塞视图更新

  5. *

  6. * @param effect Imperative function that can return a cleanup function

  7. * @param inputs If present, effect will only activate if the values in the list change (using ===).

  8. */

  9. export function useLayoutEffect(effect: EffectCallback, inputs?: Inputs): void;

  10. /**

  11. * 接收一个包含一些必要副作用代码的函数。

  12. * 副作用函数会在浏览器绘制后执行,不会阻塞渲染

  13. *

  14. * @param effect Imperative function that can return a cleanup function

  15. * @param inputs If present, effect will only activate if the values in the list change (using ===).

  16. */

  17. export function useEffect(effect: EffectCallback, inputs?: Inputs): void;

demo
 
   
   
 
  1. function LayoutEffect() {

  2. const [color, setColor] = useState("red");

  3. useLayoutEffect(() => {

  4. alert(color);

  5. }, [color]);

  6. useEffect(() => {

  7. alert(color);

  8. }, [color]);

  9. return (

  10. <>

  11. <div id="myDiv" style={{ background: color }}>

  12. 颜色

  13. </div>

  14. <button onClick={() => setColor("red")}>红</button>

  15. <button onClick={() => setColor("yellow")}>黄</button>

  16. <button onClick={() => setColor("blue")}>蓝</button>

  17. </>

  18. );

  19. }

从 demo 可以看出,每次改变颜色,useLayoutEffect的回调触发时机是在页面改变颜色之前,而useEffect的回调触发时机是页面改变颜色之后。它们的实现如下

 
   
   
 
  1. export function useLayoutEffect(callback, args) {

  2. const state = getHookState(currentIndex++);

  3. if (argsChanged(state._args, args)) {

  4. state._value = callback;

  5. state._args = args;


  6. currentComponent._renderCallbacks.push(state);

  7. }

  8. }


  9. export function useEffect(callback, args) {

  10. const state = getHookState(currentIndex++);

  11. if (argsChanged(state._args, args)) {

  12. state._value = callback;

  13. state._args = args;


  14. currentComponent.__hooks._pendingEffects.push(state);

  15. }

  16. }

它们的实现几乎一模一样,唯一的区别是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. * 执行队列内所有组件的上一次的`_pendingEffects`的清理函数和执行本次的`_pendingEffects`。

  4. */

  5. function flushAfterPaintEffects() {

  6. afterPaintEffects.some(component => {

  7. if (component._parentDom) {

  8. // 清理上一次的_pendingEffects

  9. component.__hooks._pendingEffects.forEach(invokeCleanup);

  10. // 执行当前_pendingEffects

  11. component.__hooks._pendingEffects.forEach(invokeEffect);

  12. component.__hooks._pendingEffects = [];

  13. }

  14. });

  15. // 清空afterPaintEffects

  16. afterPaintEffects = [];

  17. }


  18. /**

  19. *preact的diff是同步的,是宏任务。

  20. newQueueLength === 1 保证了afterPaint内的afterNextFrame(flushAfterPaintEffects)只执行一遍。因为会调用n次宏任务的afterPaint结束后,才会执行flushAfterPaintEffects一次将所有含有pendingEffect的组件进行回调进行

  21. * */

  22. afterPaint = newQueueLength => {

  23. if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {

  24. prevRaf = options.requestAnimationFrame;


  25. // 执行下一帧结束后,清空 useEffect的回调

  26. (prevRaf || afterNextFrame)(flushAfterPaintEffects);

  27. }

  28. };

  29. /**

  30. * 希望在下一帧 重绘之前,执行callback。同时,如果100ms内,当前帧的requestAnimationFrame没有结束(例如窗口不可见的情况),则直接执行callback

  31. */

  32. function afterNextFrame(callback) {

  33. const done = () => {

  34. clearTimeout(timeout);

  35. cancelAnimationFrame(raf);

  36. setTimeout(callback);

  37. };

  38. const timeout = setTimeout(done, RAF_TIMEOUT);

  39. const raf = requestAnimationFrame(done);

  40. }

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起

 
   
   
 
  1. function FancyInput(props, ref) {

  2. const inputRef = useRef();

  3. // 第一个参数是 父组件 ref

  4. // 第二个参数是返回,返回的对象会作为父组件 ref current 属性的值

  5. useImperativeHandle(ref, () => ({

  6. focus: () => {

  7. inputRef.current.focus();

  8. }

  9. }));

  10. return <input ref={inputRef} ... />;

  11. }

  12. FancyInput = forwardRef(FancyInput);


  13. function App(){

  14. const ref = useRef()


  15. return <div>

  16. <FancyInput ref={ref}/>

  17. <button onClick={()=>ref.focus()}>click</button>

  18. </div>

  19. }

默认情况下,函数组件是没有ref属性,通过forwardRef(FancyInput)后,父组件就可以往子函数组件传递ref属性了。useImperativeHandle的作用就是控制父组件不能在拿到子组件的ref后为所欲为。如上,父组件拿到FancyInput后,只能执行focus,即子组件决定对外暴露的 ref 接口。

 
   
   
 
  1. function useImperativeHandle(ref, createHandle, args) {

  2. useLayoutEffect(

  3. () => {

  4. if (typeof ref === "function") ref(createHandle());

  5. else if (ref) ref.current = createHandle();

  6. },

  7. args == null ? args : args.concat(ref)

  8. );

  9. }

useImperativeHandle的实现也是一目了然,因为这种是涉及到 dom 更新后的同步修改,所以自如是用useLayoutEffect实现的。从实现可看出,useImperativeHandle也能接收依赖项数组的

createContext

接收一个 context 对象(Preact.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

使用 context 最大的好处就是避免了深层组件嵌套时,需要一层层往下通过 props 传值。使用 createContext 可以非常方便的使用 context 而不用再写繁琐的Consumer

 
   
   
 
  1. const context = Preact.createContext(null);


  2. const Component = () => {

  3. // 每当Context.Provider value={{xx:xx}}变化时,Component都会重新渲染

  4. const { xx } = useContext(context);

  5. return <div></div>;

  6. };


  7. const App = () => {

  8. return (

  9. <Context.Provider value={{ xx: xx }}>

  10. <Component></Component>

  11. </Context.Provider>

  12. );

  13. };

useContext实现

 
   
   
 
  1. function useContext(context) {

  2. // 每个`preact`组件的context属性都保存着当前全局context的Provider引用,不同的context都有一个唯一id

  3. // 获取当前组件 所属的Context Provider

  4. const provider = currentComponent.context[context._id];

  5. if (!provider) return context._defaultValue;

  6. const state = getHookState(currentIndex++);

  7. if (state._value == null) {

  8. // 初始化的时候将当前 组件订阅 Provider的value变化

  9. // 当Provider的value变化时,重新渲染当前组件

  10. state._value = true;

  11. provider.sub(currentComponent);

  12. }

  13. return provider.props.value;

  14. }

可以看出,useContext会在初始化的时候,当前组件对应的Context.Provider会把该组件加入订阅回调(provider.sub(currentComponent)),当 Provider value 变化时,在 Provider 的shouldComponentUpdate周期中执行组件的 render。

 
   
   
 
  1. //.....

  2. // Provider部分源码

  3. Provider(props) {

  4. //....

  5. // 初始化Provider的时候执行的部分

  6. this.shouldComponentUpdate = _props => {

  7. if (props.value !== _props.value) {

  8. subs.some(c => {

  9. c.context = _props.value;

  10. // 执行sub订阅回调组件的render

  11. enqueueRender(c);

  12. });

  13. }

  14. };

  15. this.sub = c => {

  16. subs.push(c);

  17. let old = c.componentWillUnmount;

  18. c.componentWillUnmount = () => {

  19. // 组件卸载的时候,从订阅回调组件列表中移除

  20. subs.splice(subs.indexOf(c), 1);

  21. old && old.call(c);

  22. };

  23. };

  24. }

  25. //....

总结:preact和react在源码实现上有一定差异,但是通过对 preact hook 源码的学习,对于理解 hook 的很多观念和思想是非常有帮助的。



推荐阅读

(点击标题可跳转阅读)






觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

好文章,我在看❤️


以上是关于详细 preact hook 源码逐行解析的主要内容,如果未能解决你的问题,请参考以下文章

从Preact中了解React组件和hooks基本原理

preact 源码学习:JSX解析与DOM渲染

YOLOv5源码逐行超详细注释与解读——项目目录结构解析

preact 源码学习系列之二:组件的渲染与更新

从Preact中了解React组件和hooks基本原理

React 的轻量化替代方案Preact