This article analyzes preact hook source code to understand and master react/ Preact hook usage and some common problems. React and Preact are implemented differently, but the performance of hooks is basically the same. With Preact’s hook analysis, it’s easy to remember the use of hooks and avoid some pitfalls

Preact Hook was introduced as a separate package, Preact/Hook, whose total code contains comments of just 300 lines.

Before you read this article, read it with a few questions:

A function component is stateless, so why do hooks make it stateful?

2. Why can’t hook be used in conditional statements

3. Why can’t I hook a normal function

basis

As mentioned earlier, hooks are introduced separately in Preact through a module within Preact/Hook. There are two important global variables in this module: currentIndex: Used to record the order in which the current function component is using the hooks (mentioned below). 2, currentComponent. Used to record the component corresponding to the current rendering.

The implementation of Preact Hook is virtually non-invasive to preact. It performs the corresponding hook logic at the corresponding initialization/update of preact through several hook functions exposed in preact.options. The hooks are _render=>diffed=>_commit=>umount

  • \_renderlocation. Executed before executing the render method of the component for execution_pendingEffects(_pendingEffectsIs the effect operation that does not block page rendering, executed before the next frame is drawn) and executes the unexecuted. Another important function of this hook is to let the hook get the current executionrenderComponent instance of
options._render = vnode= > {
  // render hook function
  if (oldBeforeRender) oldBeforeRender(vnode);

  currentComponent = vnode._component;
  currentIndex = 0;

  if (currentComponent.__hooks) {
    // Perform cleanup operations
    currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
    / / execution effectcurrentComponent.__hooks._pendingEffects.forEach(invokeEffect); currentComponent.__hooks._pendingEffects = []; }};Copy the code

Given the timing of _render in preact, the hook function initializes each render. This includes executing/cleaning up the last unfinished effect, initializing hook subscript 0, and getting the component instance of current Render.

  • diffedlocation. After the vNode diff is complete, the current_pendingEffectsAdvance the execution queue so that it executes before the next frame is drawn without blocking the browser rendering.
options.diffed = vnode= > {
  if (oldAfterDiff) oldAfterDiff(vnode);

  const c = vnode._component;
  if(! c)return;

  const hooks = c.__hooks;
  if (hooks) {
    UseEffect is entered into the _pendingEffects queue
    if (hooks._pendingEffects.length) {
      // afterPaint indicates that a frame is drawn before the next frame beginsafterPaint(afterPaintEffects.push(c)); }}};Copy the code
  • \_commitlocation. Execute after initial or update render ends_renderCallbacksAnd, in the\_commitOnly a hook callback is performed in theuseLayoutEffect. (_renderCallbacksRefers to thepreactMiddle finger list of action callbacks executed synchronously after each render, e.gsetStateThe second argument to cb, or somerenderAfter the lifecycle function, orforceUpdate“).
options._commit = (vnode, commitQueue) = > {
  commitQueue.some(component= > {
    // Execute the cleanup function for the last _renderCallbacks
    component._renderCallbacks.forEach(invokeCleanup);
    // _renderCallbacks can be a callback to the second parameter of setState, life cycle, or forceUpdate.
    // The callback that is defined as hook by _value is executed here
    component._renderCallbacks = component._renderCallbacks.filter(cb= >
      cb._value ? invokeEffect(cb) : true
    );
  });

  if (oldCommit) oldCommit(vnode, commitQueue);
};
Copy the code
  • unmount. Executes after the component is unloadedeffectCleanup operation of
options.unmount = vnode= > {
  if (oldBeforeUnmount) oldBeforeUnmount(vnode);

  const c = vnode._component;
  if(! c)return;

  const hooks = c.__hooks;
  if (hooks) {
    This cleanup is the effect class hook cleanup function, which is the return value of each of our effect callbacks
    hooks._list.forEach(hook= >hook._cleanup && hook._cleanup()); }};Copy the code

Adding a hook to a component simply adds a __hook attribute to preact’s component. In the internal implementation of Preact, both function and class components are instantiated as PreactComponent, with the following data structure

export interface Component extendsPreactComponent<any, any> { __hooks? : {// Hook storage for each component
    _list: HookState[];
    / / useLayoutEffect useEffect, etc
    _pendingEffects: EffectHookState[];
  };
}
Copy the code

In response to question 1, we know that hooks are linked to the __hooks attribute of the component. Therefore, every time a function component is rendered, it can obtain the state of the function component by reading its own attribute. The key here is the getHookState function. This function is also very important throughout Preact Hook

function getHookState(index) {
  if (options._hook) options._hook(currentComponent);
  const hooks =
    currentComponent.__hooks ||
    (currentComponent.__hooks = { _list: []._pendingEffects: []});// Create an empty hook during initialization
  if (index >= hooks._list.length) {
    hooks._list.push({});
  }
  return hooks._list[index];
}
Copy the code

This function first performs this step to get the hook state each time the component executes useXxx (useEffect as an example). All hooks use this function to get their own hook state first

export function useEffect(callback, args) {
  //....
  const state = getHookState(currentIndex++);
  / /...
}
Copy the code

The currentIndex starts at 0 during each render session and is incremented by 1 after each useXxx execution. The state of each hook’s execution in multiple render is determined by the order in currentComponent.__hooks. So if you are in a conditional statement, if one of the conditions is not true and that useXxx is not executed, the order of the following hooks will be out of order and cause a bug.

For example,

const Component = (a)= > {
  const [state1, setState1] = useState();
  // Suppose condition is rendered true the first time and false the second time
  if (condition) {
    const [state2, setState2] = useState();
  }
  const [state3, setState3] = useState();
};
Copy the code

After the first rendering, __hooks = [hook1,hook2,hook3]. Second render, because const [state2, setState2] = useState(); Const [state3, setState3] = useState(); It’s hook2. There might be a problem. So, that’s question 2, why you can’t put a hook in a conditional statement.

After some analysis above, we also know why hook can not be used in ordinary functions. Because hooks rely on global variables currentIndex and currentComponent within hooks. The options.render hook is not used to reset currentIndex and set currentComponent. When a normal hook is executed, currentIndex is the subscript of the last instance that executed the hook. CurrentComponent is the last instance that executed the hook component. So there’s an immediate problem.

Hook analysis

Although there are many hooks in Preact, there are only three HookState structures for data structures, and all hooks are implemented on top of these three. These three are

  • EffectHookState (useLayoutEffect useEffect useImperativeHandle)
export interface EffectHookState {
  // Effect hook callback function_value? : Effect;/ / dependencies_args? :any[];
  // Effect hook cleanup function, the return value of _value_cleanup? : Cleanup; }Copy the code
  • MemoHookState (useMemo useRef useCallback)
export interface MemoHookState {
  // Return value of useMemo_value? :any;
  // The previous dependency array_args? :any[];
  // Callback passed by useMemo_callback? :(a)= > any;
}
Copy the code
  • ReducerHookState (useReducer useState` `)
export interfaceReducerHookState { _value? :any; _component? : Component; }Copy the code
  • useContextThis is a special one

MemoHookState

MemoHook is a class of hooks related to performance optimization

useMemo

What it does: Pass in the create function and dependency array as arguments to useMemo, which recalculates memoized values only when a dependency changes. This optimization helps avoid costly calculations every time you render

/ / case
const Component = props= > {
  // Suppose calculate is a costly operation
  const result = calculate(props.xx);
  return <div>{result}</div>;
};
Copy the code

By default, every Component render performs a calculate of Calculate. If calculate is a computation-heavy function, there is a performance penalty and this can be optimized using useMemo. This way, if the value of the calculate dependency does not change, you do not need to execute this function, but instead take its cached value. Note that all calculate values for external dependencies need to be passed into the dependency array, otherwise useMemo may be buggy if some values change.

/ / case
const Component = props= > {
  // The calculate function will be re-executed only if the props. Xx value changes
  const result = useMemo((a)= > calculate(props.xx), [props.xx]);
  return <div>{result}</div>;
};
Copy the code

UseMemo source code analysis

function useMemo(callback, args) {
  // State is the MemoHookState type
  const state = getHookState(currentIndex++);
  // Determine whether the dependency has changed
  if (argsChanged(state._args, args)) {
    // Store the dependent data values
    state._args = args;
    state._callback = callback;
    // Callback returns the value after the change.
    return (state._value = callback());
  }
  return state._value;
}
Copy the code

The implementation logic of useMemo is not complicated. It checks whether the dependency changes and executes the callback function to return the value after the change. It is worth noting that the dependency comparison is just plain === comparison, and if the dependency is on a reference type and the attribute on the modified reference type is directly changed, the callback will not be executed.

useCallback

UseCallback returns a Memorized version of the callback function that is updated only when a dependency changes. UseCallback returns a memorized version of the callback function that is updated only when a dependency changes

So let’s say I have some code that looks like this

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  const handle = (a)= > console.log(number);
  return <button onClick={handle}>button</button>;
};
Copy the code

For each rendering, it is a new handle, so the diff is inactivated, and there is a process of creating a new function and binding a new event broker. This problem is fixed when useCallback is used

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  // If number remains unchanged, handle is the same value each time
  const handle = useCallback((a)= >() = >console.log(number), [number]);
  return <button onClick={handle}>button</button>;
};
Copy the code

One pitfall is that [number] cannot be omitted; if omitted, the log printed each time will always be the initial value of number 0

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  // Always print 0 here
  const handle = useCallback((a)= >() = >console.log(number), []);
  return <button onClick={handle}>button</button>;
};
Copy the code

As for why this is the case, combined with the implementation analysis of useMomo. UseCallback is implemented on top of useMemo, except that instead of executing the callback, it returns the callback for execution.

function useCallback(callback, args) {
  // Return the callback directly instead of executing it
  return useMemo((a)= > callback, args);
}
Copy the code

Imagine that each time a function component is executed, it is a completely new process. Our callback is just hanging on the _value field of MemoHook, and when the dependency does not change, the callback we execute is always the closure of the render generated at the moment of creation. The number at that moment is the first render value.

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  // Always print 0 here
  const handle = useCallback(
    (a)= > /** Later on, our handle will not execute this callback, but the callback of the previous record */() = >console.log(number),
    []
  );
  return <button onClick={handle}>button</button>;
};
Copy the code

UseMemo and useCallback are great for performance tuning, but not necessary. Because for most functions, on the one hand, the creation/call cost is not very high, and recording the dependency requires a comparison operation to traverse the number of sets, which is also expensive. So you don’t need mindless useMemo and useCallback, but just use it in the right places

useRef

UseRef returns a mutable ref object whose current property is initialized as the parameter passed in. Replace react. createRef or something like this. XXX in a function component. The ref value is constant throughout the period

Usage:

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  const inputRef = useRef(null)
  const focus = useCallback(
    (a)= >inputRef.focus(),
    []
  );
  return<div>
  <input ref={inputRef}>
   <button onClick={focus}>button</button>
  </div>;
};
Copy the code

Usage two: Similar to this

/ / case
const Component = props= > {
  const [number, setNumber] = useState(0);
  const inputRef = useRef(null)
  const focus = useCallback(
    (a)= >inputRef.focus(),
    []
  );
  return<div>
  <input ref={node= > inputRef.current = node}>
   <button onClick={focus}>button</button>
  </div>;
};
Copy the code

And the reason why it works is because applyRef, react is similar.

export function applyRef(ref, value, vnode) {
  try {
    if (typeof ref == "function") ref(value);
    else ref.current = value;
  } catch(e) { options._catchError(e, vnode); }}Copy the code

View the useRef source code.

function useRef(initialValue) {
  return useMemo((a)= > ({ current: initialValue }), []);
}
Copy the code

InitialValue {current:initialValue} is not dependent on any data and needs to be manually assigned

ReducerHookState

useReducer

UseReducer is very similar to using Redux.

Usage:

// Reducer is the reducer function of normal redux
// initialState State of the initialization
// init a function to lazily calculate the initial value of state
const [state, dispatch] = useReducer(reducer, initialState, init);
Copy the code

An example of a counter.

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:
      returnstate; }}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>
  );
}
Copy the code

For those of you familiar with Redux, it’s a no-brainer. UseState, mentioned later, used to be implemented based on useReducer.

Source code analysis

export function useReducer(reducer, initialState, init) {
  const hookState = getHookState(currentIndex++);
  // The ReducerHookState data structure analyzed earlier has two attributes
  // _value Specifies the current state value
  // _component corresponds to the component instance

  if(! hookState._component) {// Initialization process
    // The reference to component needs to be recorded because the setState update is needed later
    hookState._component = currentComponent;

    hookState._value = [
      // init is the lazy initialization function mentioned earlier. If init is passed in, the initial value is the result of init calculation
      // invokeOrReturn when init is not passed. This is simply returning the initialization value
      /*** ** ' 'js * invokeOrReturn * if f is a function, return f(arg) * if f is not a function, Return f * function invokeOrReturn(arg, f) {return typeof f === "function"? f(arg) : f; } * ` ` ` * /! init ? invokeOrReturn(undefined, initialState) : init(initialState),

      action => {
        The reducer function calculates the next state
        const nextValue = reducer(hookState._value[0], action);
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue;
          // setState starts the next updatehookState._component.setState({}); }}]; }// Return the current state
  return hookState._value;
}
Copy the code

Update state is to call the dispatch of demo, that is, calculate the next state value assigned to _value by reducer(preState,action). The component’s setState method is then called to diff the component and update it accordingly. (Here’s where Preact and React differ. Preact’s function components are internally implemented using Component as well as class components.)

useState

UseState is probably the most commonly used hook. Similar to the state state value in a class component.

usage

const Component = (a)= > {
  const [number, setNumber] = useState(0);
  const [index, setIndex] = useIndex(0);
  return (
    <div>{/* setXxx can pass in a callback or set the value directly **/}<button onClick={()= >SetNumber (number => number + 1)}> Update number</button>
      {number}
      //
      <button onClick={()= >SetIndex (index + 1)}> Update index</button>
      {index}
    </div>
  );
};
Copy the code

As mentioned above, useState is implemented through useReducer.

export function useState(initialState) {
  /*** * * ```js * function invokeOrReturn(arg, f) { return typeof f === "function" ? f(arg) : f; } * ` ` ` * /
  return useReducer(invokeOrReturn, initialState);
}
Copy the code

UseState can be realized by passing an invokeOrReturn function to the reducer parameter of useReduecr. Review the use of useState and useReducer

const [index, setIndex] = useIndex(0);
setIndex(index= > index + 1);
// or
setIndex(1);
//-----
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "some type" });
Copy the code

1. For the case of setState directly passing the value. Reducer (invokeOrReturn) function, directly return the input parameter

Reducer (hookstate. _value[0], action) specifies a reducer(hookstate. _value[0], action)
const nextValue = reducer(hookState._value[0], action);
Copy the code

2. In the case of the setState direct parameter.

Reducer (hookstate._value [0], action) reducer(hookstate._value [0])
const nextValue = reducer(hookState._value[0], action);
Copy the code

It can be seen that useState is actually just an implementation of the useReducer uploaded to a specific Reducer.

EffectHookState

  • useEffect å’Œ useLayoutEffect

Both hooks are used in exactly the same way, performing side effects in the render process to implement life cycle operations in previous class components. The difference is that useEffect callback is executed after this rendering and before the next. UseLayoutEffect is executed synchronously after the browser layout and before the painting.

Usage. Pass a callback function and an array of dependencies. When the array’s dependencies change, the callback is executed again.

/** * receives a function that contains the necessary side effects to read layout from the DOM and synchronize the operations in re-render * 'useLayoutEffect'. Use 'useEffect' to avoid blocking view updates * * @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 ===). */
export function useLayoutEffect(effect: EffectCallback, inputs? : Inputs) :void;
/** * receives a function that contains some necessary side effect code. * Side effects are executed after the browser draws, * * @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 ===). */
export function useEffect(effect: EffectCallback, inputs? : Inputs) :void;
Copy the code

demo

function LayoutEffect() {
  const [color, setColor] = useState("red");
  useLayoutEffect((a)= > {
    alert(color);
  }, [color]);
  useEffect((a)= > {
    alert(color);
  }, [color]);
  return (
    <>
      <div id="myDiv" style={{ background: color}} >color</div>
      <button onClick={()= >SetColor (" red ")} > red</button>
      <button onClick={()= >SetColor (" yellow ")} > yellow</button>
      <button onClick={()= >SetColor (" blue ")} > blue</button>
    </>
  );
}
Copy the code

As you can see from the demo, useLayoutEffect’s callback is triggered before the page changes color, while useEffect’s callback is triggered after the page changes color. Their implementation is as follows

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); }}Copy the code

The implementation is almost identical, the only difference being that useLayoutEffect calls back to _renderCallbacks and useEffect calls back to _pendingEffects.

As we’ve already done some analysis, _renderCallbacks is executed in the \_commit hook, where the last renderCallbacks effect cleanup function is executed and the renderCallbacks renderCallbacks are executed. RenderCallback is called after render. The DOM has been updated and the browser has not painted a new frame. RenderCallback is called after render. So in the demo, alert blocks the browser paint, and you can’t see the color change.

And _pendingEffects is after this redraw and before the next one. The call relationship in hook is as follows

1, Options. differed in the hook (i.e. after component diff was done), AfterPaint (afterPaintEffects. Push (c)) pushes components with _pendingEffects into the global afterPaintEffects queue

AfterNextFrame (flushafterpaint ects) FlushAfterPaintEffects is executed before redrawing the next frame. Also, if the requestAnimationFrame for the current frame does not end within 100ms (for example if the window is not visible), flushAfterPaintEffects is directly executed. The flushAfterPaintEffects function executes the last _pendingEffects cleanup function and the current _pendingEffects cleanup function for all components in the queue.

Several key functions

Execute the cleanup function of the last '_pendingEffects' and execute the cleanup function of the current' _pendingEffects' for all components in the queue. * /
function flushAfterPaintEffects() {
  afterPaintEffects.some(component= > {
    if (component._parentDom) {
      // Clean up the last _pendingEffects
      component.__hooks._pendingEffects.forEach(invokeCleanup);
      // Execute current _pendingEffectscomponent.__hooks._pendingEffects.forEach(invokeEffect); component.__hooks._pendingEffects = []; }});/ / empty afterPaintEffects
  afterPaintEffects = [];
}

/** * Preact diff is synchronous and is a macro task. NewQueueLength === 1 ensures that afterNextFrame(flushafterPaint ects) in afterPaint is executed only once. Because will call n macro task afterPaint ended, will perform flushAfterPaintEffects once all components containing pendingEffect * * / callback
afterPaint = newQueueLength= > {
  if (newQueueLength === 1|| prevRaf ! == options.requestAnimationFrame) { prevRaf = options.requestAnimationFrame;// Clear useEffect callback after the next frame(prevRaf || afterNextFrame)(flushAfterPaintEffects); }};/** * callback is expected to be executed before the next frame is redrawn. Also, if the requestAnimationFrame for the current frame does not end within 100ms (such as if the window is not visible), then callback */ is executed directly
function afterNextFrame(callback) {
  const done = (a)= > {
    clearTimeout(timeout);
    cancelAnimationFrame(raf);
    setTimeout(callback);
  };
  const timeout = setTimeout(done, RAF_TIMEOUT);
  const raf = requestAnimationFrame(done);
}
Copy the code

useImperativeHandle

UseImperativeHandle allows you to customize the instance value exposed to the parent component when using a ref. In most cases, imperative code like ref should be avoided. UseImperativeHandle should be together with the forwardRef

function FancyInput(props, ref) {
  const inputRef = useRef();
  // The first argument is the parent component ref
  // The second argument is return, which will be used as the value of the parent component's ref current property
  useImperativeHandle(ref, () => ({
    focus: (a)= >{ 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> }Copy the code

By default, a function component does not have the ref property. The parent can pass the ref property to the parent after the forwardRef(FancyInput) function. UseImperativeHandle is used to prevent the parent component from doing whatever it wants when it gets a ref from the child component. As shown above, once the parent component gets the FancyInput, it can only perform focus, which is the ref interface that the child component decides to expose.

function useImperativeHandle(ref, createHandle, args) {
  useLayoutEffect(
    (a)= > {
      if (typeof ref === "function") ref(createHandle());
      else if (ref) ref.current = createHandle();
    },
    args == null ? args : args.concat(ref)
  );
}
Copy the code

The implementation of useImperativeHandle is also clear at a glance, because it involves synchronous modification after DOM update, so it is freely realized with useLayoutEffect. UseImperativeHandle can also accept an array of dependencies

createContext

Receives a context object (the return value of preact.createcontext) and returns the current value of that context. The current context value is determined by the < MyContext.provider > value prop of the upper-layer component closest to the current component. When the most recent

update is made to the component’s upper layer, the Hook triggers a rerender and uses the latest context value passed to MyContext Provider.

The biggest advantage of using context is that it avoids the need to pass values down through props when deep components are nested. Using createContext makes it very easy to use context without having to write a Consumer

const context = Preact.createContext(null);

const Component = (a)= > {
  // Every time context. Provider value={{xx:xx}} changes, Component rerenders
  const { xx } = useContext(context);
  return <div></div>;
};

const App = (a)= > {
  return (
    <Context.Provider value={{ xx: xx}} >
      <Component></Component>
    </Context.Provider>
  );
};
Copy the code

UseContext implementation

function useContext(context) {
  // The context property of each 'preact' component holds a Provider reference to the current global context. Each context has a unique ID
  // Get the Context Provider that the current component belongs to
  const provider = currentComponent.context[context._id];
  if(! provider)return context._defaultValue;
  const state = getHookState(currentIndex++);
  if (state._value == null) {
    // Change the value of the current component subscription Provider during initialization
    // Re-render the current component when the value of the Provider changes
    state._value = true;
    provider.sub(currentComponent);
  }
  return provider.props.value;
}
Copy the code

When useContext is initialized, the context. Provider of the currentComponent adds the component to the subscription callback (provider.sub(currentComponent)). When the Provider value changes, Execute the render of the component in the Provider’s shouldComponentUpdate cycle.

/ /...
// Provider part source code
    Provider(props) {
      //....
      // The part executed when initializing the Provider
        this.shouldComponentUpdate = _props= > {
          if(props.value ! == _props.value) { subs.some(c= > {
              c.context = _props.value;
              // Execute render for sub subscription callback componentenqueueRender(c); }); }};this.sub = c= > {
          subs.push(c);
          let old = c.componentWillUnmount;
          c.componentWillUnmount = (a)= > {
            // When the component is uninstalled, it is removed from the subscription callback component list
            subs.splice(subs.indexOf(c), 1);
            old && old.call(c);
          };
        };
      }
 //....

Copy the code

Summary: Preact and React have some differences in source code implementation, but through the study of preact Hook source code, it is very helpful to understand many concepts and ideas of hook.


Finally, attach the annotated hook source code

Begin to see

import { options } from "preact";

/*_ @type {number} _/ let currentIndex;

/*_ @type {import('./internal').Component} _/ let currentComponent;

/*_ @type {Array<import('./internal').Component>} _/ let afterPaintEffects = [];

let oldBeforeRender = options._render; Options. _render = vnode => {// render hook function if (oldBeforeRender) oldBeforeRender(vnode);

currentComponent = vnode._component; currentIndex = 0;

**hooks._pendingEffects. ForEach (invokeCleanup); // Execute effect currentComponent.** links._pendingEffects. ForEach (invokeEffect); currentComponent.**hooks._pendingEffects = []; }};

Render (before render) -> Diffed (end of diff) -> Diffed (end of diff) -> Diffed (end of diff) -> _commit(after the initial or update life cycle ends) -> unmount (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 can be a callback to the second parameter of setState, life cycle, or forceUpdate. 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); // Largely Inspired by: // _ github.com/michael-kle... / / _ github.com/michael-kle... // Other implementations to look at: // _ codesandbox.io/s/mnox05qp8 const hooks = currentComponent.**hooks || (currentComponent.**hooks = { _list: [], _pendingEffects: [] });

If (index >= links._list.length) {links._list.push ({}); } return hooks._list[index]; }

/ * *

  • @param {import('./index').StateUpdater} initialState */ export function useState(initialState) { return useReducer(invokeOrReturn, initialState); }

/ * *

  • @param {import('./index').Reducer<any, any>} reducer

  • @param {import('./index').StateUpdater} initialState

  • @param {(initialState: any) => void} [init]

  • @returns {[ any, (state: any) => void ]} _/ export function useReducer(reducer, initialState, init) { /** @type {import('./internal').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({}); } } ];Copy the code

    }

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);
    Copy the code

    }}

/ * *

  • @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);
    Copy the code

    }}

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 {(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) {// ** links._pendingEffects. ForEach (invokeCleanup); // Execute the current effect component.** links._pendingEffects. ForEach (invokeEffect); component.__hooks._pendingEffects = []; }}); afterPaintEffects = []; }

const RAF_TIMEOUT = 100;

/ * *

  • The callback is expected to be executed before the next frame is redrawn. Also, if the current frame does not end within 100ms (such as if the window is not visible), 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;

/ / execution after the next frame, empty useEffect callback (prevRaf | | afterNextFrame) (flushAfterPaintEffects); }Copy the code

};
}

/ * *

  • Perform the clean effect operation.
  • @param {import('./internal').EffectHookState} hook */ function invokeCleanup(hook) { if (hook._cleanup) hook._cleanup(); }

/ * *

  • Execute effect Hook cb and assign the cleanup function to _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;
    }

/ * *

  • Check whether the two arrays have changed
  • @param {any[]} oldArgs
  • @param {any[]} newArgs */ function argsChanged(oldArgs, newArgs) { return ! oldArgs || newArgs.some((arg, index) => arg ! == oldArgs[index]); }

/ * *

  • Return */ function invokeOrReturn(arg, f) {return typeof f === "function"? f(arg) : f; }

Copy the code