“Preact” line by line analysis of hooks source code

Time:2019-11-20

Preface

What is preact? React’s 3KB lightweight solution has the same es6api

Although preact and react have the same API, the differences in their internal implementation mechanism are still huge. But this does not prevent us from reading and learning the source code of preact. To put it another way, at the beginning of this year, one of my buddies @ Xiaohan met a Daniel from Facebook in an interview with a company in Beijing. The Daniel from Facebook also recommended him to read and learn the source code of preact.

Hooks are not magic, and their design has nothing to do with react (Dan Abramov). It’s the same in preact, so even if you haven’t read the source code of preact or react, you can understand the implementation of hooks.

I hope that the following sharing can give you some inspiration to understand the implementation behind hooks.

On the rules of hooks

The rules for using hooks in react are as follows. We can see that the use of hooks is highly dependent on the execution order. After reading the source code, we will know why there are two rules for using hooks.

  1. ✅ use hook only at the top level. Do not call hook in loops, conditions, or nested functions.
  2. Do not call Hook in the ordinary JavaScript function.

Analysis of hooks source code

getHookState

getHookStateFunction, will be in theCurrent componentOn the instance of__hooksAttribute.__hooksAs an object,__hooksObject_listAttribute usagearrayAll types ofhooks(useState, useEffect…………)Results of the execution of, return values, etc. because_listAttribute is used to store state in the form of array, so the execution order of each hooks is particularly important.

function getHookState(index) {
  if (options._hook) options._hook(currentComponent);
  //Check the component to see if it has the "hooks" property. If not, proactively mount an empty "hooks" object
  const hooks =
    currentComponent.__hooks ||
    (currentComponent.__hooks = {
      _List: [], // / list stores the status of all hooks
      _Pendingeffects: [], // / state of useeffect stored in pendingeffects
      _Pendinglayouteffects: [], // / state of uselayouteffects stored in pendinglayouteffects
      _handles: []
    });
  //According to index. Determine whether there is a corresponding state in the "hooks." list array.
  //If not, an empty state is actively added.
  if (index >= hooks._list.length) {
    hooks._list.push({});
  }
  //Returns the status corresponding to the index in the "hooks." list array
  return hooks._list[index];
}

Some key global variables to be used

staygetHookStateIn, we used global variablescurrentComponent。 variablecurrentComponentPoints to an instance of the current component. How do we get the reference of the current component instance? Combined with the source code of hooks and preact, it is found that when preactdiffThe virtual node vnode of the current component will be passed to the options. Render function, so that we can get the instance of the current component smoothly.

//Execution order pointer of current hooks
let currentIndex;

//Instance of the current component
let currentComponent;

let oldBeforeRender = options._render;

// vnode is
options._render = vnode => {
  if (oldBeforeRender) oldBeforeRender(vnode);
  //Instance of current component
  currentComponent = vnode._component;
  //Reset the index. The hooks state list of each component is accumulated from 0
  currentIndex = 0;

  if (currentComponent.__hooks) {
    currentComponent.__hooks._pendingEffects = handleEffects(
      currentComponent.__hooks._pendingEffects
    );
  }
};
//The omitted diff method
function diff() {
  let tmp, c;

  // ...

  //Mount an instance of the current component on vnode
  newVNode._component = c = new Component(newProps, cctx);

  // ...

  //Pass vnode to the options. Render function, so that we can get an instance of the current component
  if ((tmp = options._render)) tmp(newVNode);
}

useState && useReducer

useState

useStateBased onuseReducerEncapsulation. See below for detailsuseReducer

//Usestate accepts an initial value of initialstate, initializing state
function useState(initialState) {
  return useReducer(invokeOrReturn, initialState);
}
invokeOrReturn

invokeOrReturnIt’s a simple tool function. I won’t elaborate here.

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

useReducer

useReducerThree parameters are accepted.reducerResponsible for handlingdispatchSponsoredactioninitialStateyesstateInitial value of the state,initIs a function of inerting the initial value.useReducerReturn[state, dispatch]The content of the format.

function useReducer(reducer, initialState, init) {
  //The currentindex is incremented by one, creating a new state, which will be stored in the currentcomponent. \\\\\\\\\
  const hookState = getHookState(currentIndex++);

  if (!hookState._component) {
    //State stores references to the current component
    hookState._component = currentComponent;

    hookState._value = [
      //Return initialstate if the third parameter ` init 'is not specified
      //If the third parameter is specified, it returns the initialstate processed by the function of inerting the initial value

      //'usestate' is based on the encapsulation of 'usereducer'.
      //In 'usestate', hookstate. U value [0], by default, returns initialstate directly
      !init ? invokeOrReturn(null, initialState) : init(initialState),

      //Hookstate. U value [1], accept an 'action', {type: 'XX'}
      //Because 'usestate' is based on the encapsulation of 'usereducer', the action parameter may also be a new state value, or the state update function as a parameter
      action => {
        //Returns the new status value
        const nextValue = reducer(hookState._value[0], action);
        //Update status with new status value
        if (hookState._value[0] !== nextValue) {
          hookState._value[0] = nextValue;
          //Call setstate of component and perform diff operation again (in preact, the actual DOM node will be updated synchronously during diff process)
          hookState._component.setState({});
        }
      }
    ];
  }

  //For usereduer, return [state, dispath]
  //For usestate, return [state, setstate]
  return hookState._value;
}

⭐️useEffect

useEffectLet’s do side effects in function components. Event binding, data request, dynamic DOM modification.useEffectIt will be executed after each react rendering. Whether it is the first time to mount, or update. Useeffect can return a function that will be executed when react is cleared. Whenever this effect is executed, the previous effect is cleared. Cleanup is also performed when the component is uninstalled.

function useEffect(callback, args) {
  //The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
  const state = getHookState(currentIndex++);

  //Argschanged function, which checks whether the dependency of useeffect has changed.
  //If there is a change, argschanged returns true, and the callback of useeffect will be executed again.
  //If there is no change, argschanged returns false and does not perform a callback
  //In the first rendering, if state. Args is equal to undefined, argschanged directly returns true
  if (argsChanged(state._args, args)) {

    state._value = callback;
    //Save the last dependency in useeffect's state and use it for comparison the next time
    state._args = args;

    //Store the state of useeffect in hooks. Pending effects
    currentComponent.__hooks._pendingEffects.push(state);

    //Add the component of callback that needs to execute useeffect to the array of afterpainteffects and save it temporarily
    //Because we need to wait for the rendering to finish, and execute the callback of useeffect
    afterPaint(currentComponent);
  }
}

argsChanged

argsChangedIs a simple utility function to compare the differences between two arrays. Returns false if each item in the array is equal, and true if one item is not equal. The main purpose is to compare the dependence of hooks such as useeffect and usememo.

function argsChanged(oldArgs, newArgs) {
  return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

afterPaint

afterPaintFunction that will need to be executeduseEffectThe component of the callback, push to the globalafterPaintEffectsArray.

let afterPaintEffects = [];

let afterPaint = () => {};

if (typeof window !== "undefined") {
  let prevRaf = options.requestAnimationFrame;
  afterPaint = component => {  
    if (
      //The "afterpaintqueued" attribute ensures that each component can only be pushed once into the afterpainteffects
      (!component._afterPaintQueued &&
        (component._afterPaintQueued = true) &&
        //Afterpainteffects. Push (component) = = 1, ensuring that 'saferaf' will only be executed once before emptying
        //Add component to the afterpainteffects array
        afterPaintEffects.push(component) === 1) ||
      prevRaf !== options.requestAnimationFrame
    ) {
      prevRaf = options.requestAnimationFrame;
      //Execute saferaf (flusafterpainteffects)
      (options.requestAnimationFrame || safeRaf)(flushAfterPaintEffects);
    }
  };
}

safeRaf

safeRafWill open arequestAnimationFrame, it will callflushAfterPaintEffects, handle the callback of useeffect.

const RAF_TIMEOUT = 100;

function safeRaf(callback) {
  const done = () => {
    clearTimeout(timeout);
    cancelAnimationFrame(raf);
    setTimeout(callback);
  };
  const timeout = setTimeout(done, RAF_TIMEOUT);
  //The diff process is synchronous, and the requestanimationframe will be executed after the diff is completed (after the macro task is completed)
  const raf = requestAnimationFrame(done);
}

flushAfterPaintEffects

flushAfterPaintEffectsResponsible for handlingafterPaintEffectsAll components in the array

function flushAfterPaintEffects() {
  //Loop through all components to be processed in the afterpainteffects array
  afterPaintEffects.some(component => {
    component._afterPaintQueued = false;
    if (component._parentDom) {
      //Use handleeffects to clear the state of all useeffects in currentcomponent. Hooks. Pending effects
      //Handleeffects performs the logic of clearing and executing the effect
      //Finally, handleeffects returns an empty array, which resets the component. Hooks. Pending effects
      component.__hooks._pendingEffects = handleEffects(
        component.__hooks._pendingEffects
      );
    }
  });
  //Empty afterpainteffects
  afterPaintEffects = [];
}

handleEffects

Clear and execute theuseEffect

function handleEffects(effects) {
  //Clear effect
  effects.forEach(invokeCleanup);
  //Perform all effects
  effects.forEach(invokeEffect);
  return [];
}
invokeCleanup
//Perform clear effect
function invokeCleanup(hook) {
  if (hook._cleanup) hook._cleanup();
}
invokeEffect
function invokeEffect(hook) {
  const result = hook._value();
  //If the return value of the callback of useeffect is a function
  //The function is recorded to the "cleanup" property of useeffect
  if (typeof result === "function") {
    hook._cleanup = result;
  }
}

useMemo && useCallback

useMemoA remembered value is returned.useCallbackA remembered callback function is returned.useMemoThe memorized value is recalculated when the dependent array changes.useCallbackIt will return a new function when the dependent array changes.

useMemo

function useMemo(callback, args) {
  //The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
  const state = getHookState(currentIndex++);
  //Determine whether the dependent array changes
  //If there is a change, the callback will be executed again, and the new return value will be returned
  //Otherwise, return the last return value
    if (argsChanged(state._args, args)) {
        state._args = args;
    state._callback = callback;
    //State. U valuerecord the last return value (for usecallback, record the last callback)
        return state._value = callback();
  }
  //Returns the return value of callback
    return state._value;
}

useCallback

useCallbackBased onuseMemoEncapsulation. Only when the dependent array changes,useCallbackA new function will be returned, otherwise the first incoming call will always be returned.


function useCallback(callback, args) {
    return useMemo(() => callback, args);
}

useRef

useRefIt’s also based onuseMemoEncapsulation. But the difference is that the dependency array passes in an empty array, which means that every timeuseRefWill be recalculated.


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

Application of useref

⭐️Because useref is recalculated every time, we can use the feature to avoid the side effects of closure

//Old values will be printed out
function Bar () {
  const [ count, setCount ] = useState(0)

  const showMessage = () => {
    console.log(`count: ${count}`)
  }

  setTimeout(() => {
    //The printed output is still '0', forming a closure
    showMessage()
  }, 2000)

  setTimout(() => {
    setCount((prevCount) => {
      return prevCount + 1
    })
  }, 1000)

  return <div/>
}


//Using useref will print out the new value
function Bar () {
  const count = useRef(0)

  const showMessage = () => {
    console.log(`count: ${count.current}`)
  }

  setTimeout(() => {
    //The new value '1' is printed, and the latest value is obtained by count.current
    showMessage()
  }, 2000)

  setTimout(() => {
    count.current += 1 
  }, 1000)

  return <div/>
}

useLayoutEffect

useEffecT will be executed after the diff algorithm finishes rendering the dom. AnduseEffectThe difference is,useLayoutEffectIt will be completed in diff algorithmAfter DOM update, before browser drawingAt all times.useLayoutEffectHow to do it? Similar to the method of getting the current component, preact inserts aoptions.diffedHook.

function useLayoutEffect(callback, args) {
  //The currentindex is incremented by 1, adding a new status to the currentcomponent. Hooks. List
  const state = getHookState(currentIndex++);
  //If the array is dependent, the update will be skipped if there is no change
  //If the array is dependent, the parameter change performs a callback
  if (argsChanged(state._args, args)) {
    state._value = callback;
    //Record the last dependency array
    state._args = args;
    currentComponent.__hooks._pendingLayoutEffects.push(state);
  }
}
//Options.diffed will update the browser before the diff algorithm finishes redrawing
options.diffed = vnode => {
  if (oldAfterDiff) oldAfterDiff(vnode);

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

  const hooks = c.__hooks;
  if (hooks) {
    hooks._handles = bindHandles(hooks._handles);
    //Execute the callback of uselayouteffects of the component
    hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
  }
};
//The omitted diff method
function diff() {
  let tmp, c;

  // ...

  // ...

  //Before browser drawing, after the diff algorithm is updated, execute the callback of uselayouteeffect
  if (tmp = options.diffed) tmp(newVNode);

  //Return to the updated Dom and redraw
  return newVNode._dom;
}

useImperativeHandle

useImperativeHandleYou can customize the instance values exposed to the parent component.useImperativeHandleShould andforwardRefUse together. So let’s first look at preact.forwardRefThe specific implementation of.

forwardRef

Forwardref creates a react component that accepts the ref attribute, but forwards the ref to the component’s child nodes. We ref access the element instance on the child node.

How to use forwardref
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
))

const ref = React.createRef()

//Component accepts ref attribute, but forwards ref to < button >
<FancyButton ref={ref}>Click me!</FancyButton>
The source code of forwardref in preact
//FN is the rendering function, taking (props, ref) as the parameter
function forwardRef(fn) {
  function Forwarded(props) {
    //Props.ref is the ref on the component created by forwardref
    let ref = props.ref;
    delete props.ref;
    //Call the render function, render the component, and forward ref to the render function
    return fn(props, ref);
  }
  Forwarded.prototype.isReactComponent = true;
  Forwarded._forwarded = true;
  Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')';
  return Forwarded;
}

useImperativeHandle && bindHandles

function useImperativeHandle(ref, createHandle, args) {
  //// currentindex increases by 1, adding a new status to currentcomponent. \\\\\\\\\
  const state = getHookState(currentIndex++);
  //Determine if dependency has changed
  if (argsChanged(state._args, args)) {
    //Save the last dependency in useeffect's state and use it for comparison the next time
    state._args = args;
    //Add the state of useimperatoryhandle to the hooks. Handles array
    //Ref is the ref forwarded by forwardref
    //The return value of CreateHandle is the custom value exposed by useimperatoryhandle to the parent component
    currentComponent.__hooks._handles.push({ ref, createHandle });
  }
}
// options.diffed calls bindHandles and handles __hooks._handles.
function bindHandles(handles) {
  handles.some(handle => {
    if (handle.ref) {
      //Replace the current of forward ref
      //The replacement is the return value of the second parameter of useimperativehandle
      handle.ref.current = handle.createHandle();
    }
  });
  return [];
}

Take an example

function Bar(props, ref) {
  useImperativeHandle(ref, () => ({
    hello: () => {
      alert('Hello')
    }
  }));
  return null
}

Bar = forwardRef(Bar)

function App() {
  const ref = useRef('')

  setTimeout(() => {
    //Useimperatoryhandle modifies the current value of ref
    //The current value is the return value of the second parameter of useimperatoryhandle
    //So we can call the Hello method exposed by useimperativehandle
    ref.current.hello()
  }, 3000)

  return <Bar ref={ref}/>
}

Recommended reading

  • React hooks: not magic, just arrays
  • How Are Function Components Different from Classes?