React’s code base is already quite large, and with v16’s Fiber refactoring, it’s easy for beginners to get bogged down in detail, which makes them feel like they’re awesome, and lose confidence and wonder if they should continue working on the front end. Try to regain some confidence in this post.

Preact, a shortened version of React, is very small but has all the guts. If you want to learn the basics of React, check out the source code for Preact, which is the purpose of this article.

There are many excellent articles about the React principle. This article is a summary of the old wine in a new bottle, which will pave the way for the following articles.

The length of the article is long and the reading time is about 20min, which is mainly occupied by codes. In addition, the flow chart is also drawn to facilitate the understanding of codes.

Note: This code is based on Preact V10, and features such as SVG, replaceNode and context are omitted


  • Virtual-DOM
  • Starting from the createElement method
  • The realization of the Component
  • The diff algorithm
    • diffChildren
    • diff
    • diffElementNodes
    • diffProps
  • The realization of the Hooks
    • useState
    • useEffect
  • Technology map
  • extension


Virtual-DOM

Virtual-dom is essentially a tree of objects, nothing special, that eventually maps to graphic objects. The core of Virtual-DOM is its diff algorithm.

You can imagine that there is a DOM mapper, as the name implies. The job of this “DOM mapper” is to map the Virtual-DOM object tree to the DOM of the browser page, but only to improve the DOM ‘performance ‘. Instead of rendering the entire Virtual-dom tree in full every time, it supports receiving two virtual-DOM object trees (one before and one after the update) and using the diff algorithm to calculate the differences between the two virtual-dom trees. Then apply only those differences to the actual DOM tree, reducing the cost of DOM changes.

Virtual-dom is a bit of a challenge, but it’s faster than React. Why? . Never leave the scene to judge a technology. React was so popular on the web that some people thought virtual-dom was bad and JQuery was too weak.

In terms of performance, however awesome the framework is, it needs to manipulate the native DOM, and it’s not necessarily as’ refined ‘as you would use JQuery to manipulate the DOM manually. Improper use of the frame can also result in a small state modification, resulting in a rendering avalanche (extensive re-rendering); Similarly, although JQuery can refine DOM operations, unreasonable DOM update strategies may also become performance bottlenecks for applications. So it all depends on how you use it.

So why virtual-dom?

My personal understanding is to liberate productivity. As hardware gets better and web applications get more complex, productivity needs to keep up. While manual manipulation of the DOM is possible to achieve greater performance and flexibility, it is too inefficient for most developers, and we can afford to sacrifice a bit of performance for greater development efficiency.

Therefore, the greater significance of Virtual-DOM lies in the change of development mode: Declarative and data-driven, developers do not need to care about the details of DOM manipulation (property manipulation, event binding, DOM node changes), which means that the application development mode is changed to View = F (state), which is a great boost to the liberation of productivity.

Of course, Virtual-dom is neither the only nor the first such solution. Template-based implementations, such as AngularJS and Vue1. X, can also make this transition. That might be better performance than their virtual-dom counterparts, plus the virtual-DOM is more thoroughly abstracted in the rendering layer, no longer coupled to the DOM itself, such as rendering as ReactNative, PDF, terminal UI, etc.




Starting from the createElement method

Many people equate JSX to virtual-DOM, but there is no direct relationship between the two. We know that JSX is just a syntactic sugar.

For example, < a href = “/” > < span > Home < / span > < / a > will eventually converted to h (‘ a ‘, {href: ‘/’}, h (” span “, null, ‘Home’)) in this form, h is JSX Element factory method.

H under React is react. createElement. Most virtual-dom frameworks use h. h as an alias for createElement. The Vue ecosystem also uses this convention. .

JSX factories can be configured using @jsx annotations or the Babel configuration item:

/** * @jsx h */
render(<div>hello jsx</div>, el);
Copy the code

This article isn’t a primer on React or Preact, so check out the official tutorial for more.

Now look at createElement, which simply constructs an object (VNode):

// ⚛️type Specifies the type of the node, including DOM elements (string) and custom components, and fragments, which, if null, represent text nodes
export function createElement(type, props, children) {
  props.children = children;
  // ⚛️ apply defaultProps
  if(type ! =null&& type.defaultProps ! =null)
    for (let i in type.defaultProps)
      if (props[i] === undefined) props[i] = type.defaultProps[i];
  let ref = props.ref;
  let key = props.key;
  // ...
  // ⚛️ build the VNode object
  return createVNode(type, props, key, ref);
}

export function createVNode(type, props, key, ref) {
  return { type, props, key, ref, / *... Ignore some of the built-in fields */ constructor: undefined };
}
Copy the code

With JSX and components, you can construct a complex tree of objects:

render(
  <div className="container">
    <SideBar />
    <Body />
  </div>,
  root,
);
Copy the code




The realization of the Component

Components are the soul of a view framework, just as functions are to functional languages and classes are to object-oriented languages, complex applications cannot be formed without components.

Component-based thinking recommends divide-and-conquer an application, breaking down and combining components at different levels to simplify application development and maintenance and make it easier to understand. Technically, a component is a custom element type that can declare its inputs (props), have its own lifecycle and state and methods, and ultimately output a tree of Virtual-DOM objects as a branch of the application Virtual-DOM tree.

Preact’s custom components are implemented based on the Component class. The most basic component is state maintenance, which is implemented through setState:

function Component(props, context) {}

/ / ⚛ ️ setState implementation
Component.prototype.setState = function(update, callback) {
  // Clone the State for the next rendering, _nextState is used in some lifecycle mode (shouldComponentUpdate)
  let s = (this._nextState ! = =this.state && this._nextState) ||
    (this._nextState = assign({}, this.state));

  / / state update
  if (typeofupdate ! = ='function' || (update = update(s, this.props)))
    assign(s, update);

  if (this._vnode) { / / mounted
    // Push the render callback queue and call it in batches after rendering is complete
    if (callback) this._renderCallbacks.push(callback);
    // Put it into the asynchronous scheduling queue
    enqueueRender(this); }};Copy the code


EnqueueRender puts components in an asynchronous batch queue, which merges frequent setState calls, and the implementation is simple:

let q = [];
// Asynchronous scheduler, used to execute a callback asynchronously
const defer = typeof Promise= ='function'
    ? Promise.prototype.then.bind(Promise.resolve()) // micro task
    : setTimeout; // Call back to setTimeout

function enqueueRender(c) {
  // There is no need to push components already in the queue repeatedly
  if(! c._dirty && (c._dirty =true) && q.push(c) === 1)
    defer(process); // When the queue changes from empty to non-empty, scheduling starts
}

// Empty the queue in batches and call Component's forceUpdate
function process() {
  let p;
  // Sort the queue from lower level components first?
  q.sort((a, b) = > b._depth - a._depth);
  while ((p = q.pop()))
    if (p._dirty) p.forceUpdate(false); // false does not force updates, that is, shouldComponentUpdate should not be ignored
}
Copy the code


Ok, the above code shows that setState is essentially a component re-rendering call to forceUpdate. Dig a little deeper into the forceUpdate implementation.

Instead of looking at diff as a black box, it is a DOM mapper that takes in two VNode trees and a DOM mount point that can create, remove, or update components and DOM elements during alignment. Trigger the corresponding lifecycle method.

Component.prototype.forceUpdate = function(callback) { // callback places the rendered callback
  let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;

  if (parentDom) { // It has been mounted
    constforce = callback ! = =false;
    let mounts = [];
    // Call diff to re-render and virtual-dom the current component
    // ⚛️ ignore these parameters for a moment and think of diff as a black box, which is a DOM mapper,
    dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
    if(dom ! =null&& dom.parentNode ! == parentDom) parentDom.appendChild(dom); commitRoot(mounts, vnode); }if (callback) callback();
};
Copy the code


The render method implements the same implementation as forceUpdate, calling the diff algorithm to perform DOM updates, but specifying a DOM container externally:

/ / simplified version
export function render(vnode, parentDom) {
  vnode = createElement(Fragment, null, [vnode]);
  parentDom.childNodes.forEach(i= > i.remove())
  let mounts = [];
  diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
  commitRoot(mounts, vnode);
}
Copy the code


Comb through the above process:

Other features of the component, such as initialization and lifecycle functions, are not seen so far. These features are defined in the diff function, which is called during component mounting or updating. Diff will be covered in the next section




The diff algorithm

As you can see, the createElement and Component logic is thin, and the main logic is concentrated in the diff function. React calls this process Reconciliation and Differantiate in Preact.

To simplify the implementation of Preact, diff and DOM are mixed together, but the logic is clear:


├─ ├─ SRC /diff ├── ├─ SRC/Diff ├── ├─ props.js #Copy the code




Before diving into the diff program, let’s take a look at the basic object structure to understand the program flow. Take a look at the appearance of a VNode:

type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;

interface VNode<P = {}> {
  // Node type. The built-in DOM element is of type string, while custom components are of type Component. Function components in Preact are just special Component types
  type: string | ComponentFactory<P> | null;
  props: P & { children: ComponentChildren } | string | number | null;
  key: Key
  ref: Ref<any> | null;

  /** * Internal cache information */
  // VNode child node
  _children: Array<VNode> | null;
  // The associated DOM node, the first child of the Fragment
  _dom: PreactElement | Text | null;
  // Fragment, or the component returns the last DOM child of the Fragment,
  _lastDomChild: PreactElement | Text | null;
  / / Component instance
  _component: Component | null;
}
Copy the code




diffChildren

We’ll start with the simplest one, which already guessed that diffChildren is used to compare two VNode lists.

As shown in the figure above, a variable oldDOM representing the current insertion position needs to be maintained, starting with the first element of the DOM childrenNode and then pointing to the next sibling of newDOM each time an update is inserted or newDOM is inserted.

As we traverse the newChildren list, we try to find the old VNode with the same key and diff with it. If the new VNode and the old VNode are not in the same position, this requires them to be moved; For newly added DOM, if the oldDOM insertion position is already at the end, it is appended directly to the parent node; otherwise, it is inserted before oldDOM.

Unmount the unused vnodes from the old VNode list.

Take a closer look at the source code:

export function diffChildren(
  parentDom,         //Children's parent DOM element, newParentVNode,//VNode oldParentVNode, the new parent of children,//The virtualized "old" vnodes compare the virtualized "children" with the virtualized "children".//Store component instances that were mounted during the comparison, and after the comparison, trigger the componentDidMount life cycle function ancestorComponent for those components,//The immediate father of children'components'Render (= render)VNodeComponent instance ofoldDom, // Currently mountedDOMfordiffChildrenSpeaking,oldDomStart by pointing to the first child node.{
  let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
  let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
  // ...

  // ⚛️ iterate through new children
  for (i = 0; i < newChildren.length; i++) {
    childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // Normalize the VNode
    if (childVNode == null) continue
    // ⚛️ find if there is a corresponding element in oldChildren. If there is an element in oldChildren, remove it from oldChildren by setting it to undefined
    // Keep null if not found
    oldVNode = oldChildren[i];
    for (j = 0; j < oldChildrenLength; j++) {
      oldVNode = oldChildren[j];
      if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
        oldChildren[j] = undefined;
        break;
      }
      oldVNode = null; // No old node was found, indicating a new node
    }
    // ⚛️ to recursively compare vNodes
    newDom = diff(parentDom, childVNode, oldVNode, mounts, ancestorComponent, null, oldDom);
    // Vnode is not unloaded by diff
    if(newDom ! =null) {
      if(childVNode._lastDomChild ! =null) {
        // ⚛️ The current VNode type is Fragment
        // Only vnodes with fragments or components returning fragments will have a non-null _lastDomChild from the DOM tree at the end of the Fragment:
        //  
        // <> <> 👈 Fragment type, diff will recursively compare its children, so in the end we just need to point newDom to the last child after comparison
        // a <- diff -> b
        // b a ----+
        / / < / a > < / a > \
        // 
      
x
👈oldDom will point to this
// newDom = childVNode._lastDomChild; } else if (oldVNode == null|| newDom ! = oldDom || newDom.parentNode ==null) { // ⚛️ newDom does not match the current oldDom, try to add or modify the location outer: if (oldDom == null|| oldDom.parentNode ! == parentDom) {// ⚛️oldDom points to the end, i.e. there are no more elements, just insert; This is where the first rendering will be called parentDom.appendChild(newDom); } else { // This is an optimization measure, remove will not affect the normal program. Ignore this code for clarity // Try to find oldChildLength/2 elements backwards. If you find oldChildLength, you do not need to call insertBefore. This code reduces the frequency of insertBefore calls for (sibDom = oldDom, j = 0; (sibDom = sibDom.nextSibling) && j < oldChildrenLength; j += 2) { if (sibDom == newDom) break outer; } // ⚛️insertBefore() moves newDom before oldDomparentDom.insertBefore(newDom, oldDom); }}// ⚛️ In other cases, newDom === oldDOM is not handled ⚛️ oldDom points to the next DOM nodeoldDom = newDom.nextSibling; }}// ⚛️ offloads elements not set to undefined for (i = oldChildrenLength; i--; ) if(oldChildren[i] ! =null) unmount(oldChildren[i], ancestorComponent); } Copy the code


To understand how diffChilrend is called:




Summarize the flow chart






diff

The diff function is used to compare two vNodes. The diff function is verbose, but there is no complicated logic in it, mainly some custom component lifecycle processing. So start with the flow chart, if you’re not interested in the code you can skip it.




Source code parsing:

export function diff(
  parentDom,         //The parent DOM node, newVNode,//New VNode oldVNode,//Old VNode mounts,//AncestorComponent will be batchprocessed after diff,//The immediate parent component force,//ShouldComponentUpdate oldDom is ignored if it is true,//The DOM node currently mounted) {
  / /...
  try {
    outer: if (oldVNode.type === Fragment || newType === Fragment) {
      // the Fragment type is ⚛️ and diffChildren is used for comparison
      diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);

      // ⚛️ Record the start and end DOM of the Fragment
      let i = newVNode._children.length;
      if (i && (tmp = newVNode._children[0]) != null) {
        newVNode._dom = tmp._dom;
        while (i--) {
          tmp = newVNode._children[i];
          if (newVNode._lastDomChild = tmp && (tmp._lastDomChild || tmp._dom))
            break; }}}else if (typeof newType === 'function') {
      // ⚛️ Customize the component type
      if (oldVNode._component) {
        // ⚛️ ️ The component instance already exists
        c = newVNode._component = oldVNode._component;
        newVNode._dom = oldVNode._dom;
      } else {
        // ⚛️ initializes the component instance
        if (newType.prototype && newType.prototype.render) {
          // ⚛️ class component
          newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
        } else {
          // ⚛️ function component
          newVNode._component = c = new Component(newVNode.props, cctx);
          c.constructor = newType;
          c.render = doRender;
        }
        c._ancestorComponent = ancestorComponent;
        c.props = newVNode.props;
        if(! c.state) c.state = {}; isNew = c._dirty =true;
        c._renderCallbacks = [];
      }

      c._vnode = newVNode;
      if (c._nextState == null) c._nextState = c.state;

      // ⚛️getDerivedStateFromProps lifecycle method
      if(newType.getDerivedStateFromProps ! =null)
        assign(c._nextState == c.state
            ? (c._nextState = assign({}, c._nextState)) // Lazy copy
            : c._nextState,
          newType.getDerivedStateFromProps(newVNode.props, c._nextState),
        );

      if (isNew) {
        // ⚛️ calls some pre-mount lifecycle methods
        / / ⚛ ️ componentWillMount
        if (newType.getDerivedStateFromProps == null&& c.componentWillMount ! =null) c.componentWillMount();

        / / ⚛ ️ componentDidMount
        Components are pushed into the mounts array and called in batches after the entire component tree diff is complete. They are called in the commitRoot method
        // Called in first out (stack) order, i.e. ComponentDidMount is called first for the child component
        if(c.componentDidMount ! =null) mounts.push(c);
      } else {
        // ⚛️ calls some of the life-cycle methods related to re-rendering
        / / ⚛ ️ componentWillReceiveProps
        if (newType.getDerivedStateFromProps == null && force == null&& c.componentWillReceiveProps ! =null)
          c.componentWillReceiveProps(newVNode.props, cctx);

        / / ⚛ ️ shouldComponentUpdate
        if(! force && c.shouldComponentUpdate ! =null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx) === false) {
          // shouldComponentUpdate returns false to cancel the render update
          c.props = newVNode.props;
          c.state = c._nextState;
          c._dirty = false;
          newVNode._lastDomChild = oldVNode._lastDomChild;
          break outer;
        }

        / / ⚛ ️ componentWillUpdate
        if(c.componentWillUpdate ! =null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
      }

      // ⚛️ At this point the props and state have been identified, and the props and state have been cached and updated to prepare the rendering
      oldProps = c.props;
      oldState = c.state;
      c.props = newVNode.props;
      c.state = c._nextState;
      let prev = c._prevVNode || null;
      c._dirty = false;

      / / ⚛ ️ rendering
      let vnode = (c._prevVNode = coerceToVNode(c.render(c.props, c.state)));

      / / ⚛ ️ getSnapshotBeforeUpdate
      if(! isNew && c.getSnapshotBeforeUpdate ! =null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);

      // ⚛️ component level, which affects the update priority
      c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
      // ⚛️ recursively diff rendering results
      c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);

      if(vnode ! =null) {
        newVNode._lastDomChild = vnode._lastDomChild;
      }
      c._parentDom = parentDom;
      // ⚛️ apply ref
      if ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
      // ⚛️ calls renderCallbacks, the setState callback
      while ((tmp = c._renderCallbacks.pop())) tmp.call(c);

      / / ⚛ ️ componentDidUpdate
      if(! isNew && oldProps ! =null&& c.componentDidUpdate ! =null) c.componentDidUpdate(oldProps, oldState, snapshot);
    } else {
      // ⚛️ compares two DOM elements
      newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, mounts, ancestorComponent);

      if ((tmp = newVNode.ref) && oldVNode.ref !== tmp) applyRef(tmp, newVNode._dom, ancestorComponent);
    }
  } catch (e) {
    // ⚛️ catches render errors and passes them to the parent component's didCatch lifecycle method
    catchErrorInComponent(e, ancestorComponent);
  }

  return newVNode._dom;
}
Copy the code




diffElementNodes

Comparing two DOM elements is very simple:

function diffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
  // ...
  // ⚛️ create a DOM node
  if (dom == null) {
    if (newVNode.type === null) {
      // ⚛️ text node, with no attributes and children, returns directly
      return document.createTextNode(newProps);
    }
    dom = document.createElement(newVNode.type);
  }

  if (newVNode.type === null) {
    // ⚛️ text node update
    if(oldProps ! == newProps) dom.data = newProps; }else {
    if(newVNode ! == oldVNode) {// newVNode ! == oldVNode this indicates a static node
      let oldProps = oldVNode.props || EMPTY_OBJ;
      let newProps = newVNode.props;

      // ⚛️ dangerouslySetInnerHTML processing
      let oldHtml = oldProps.dangerouslySetInnerHTML;
      let newHtml = newProps.dangerouslySetInnerHTML;
      if (newHtml || oldHtml)
        if(! newHtml || ! oldHtml || newHtml.__html ! = oldHtml.__html) dom.innerHTML = (newHtml && newHtml.__html) ||' ';

      // ⚛️ recurses over child elements
      diffChildren(dom, newVNode, oldVNode, context, mounts, ancestorComponent, EMPTY_OBJ);
      // ⚛️ to recursively compare DOM attributesdiffProps(dom, newProps, oldProps, isSvg); }}return dom;
}
Copy the code




diffProps

The diffProps is used to update the attributes of a DOM element

export function diffProps(dom, newProps, oldProps, isSvg) {
  let i;
  const keys = Object.keys(newProps).sort();
  // ⚛️ compare and set properties
  for (i = 0; i < keys.length; i++) {
    const k = keys[i];
    if(k ! = ='children'&& k ! = ='key'&& (! oldProps || (k ==='value' || k === 'checked'? dom : oldProps)[k] ! == newProps[k])) setProperty(dom, k, newProps[k], oldProps[k], isSvg); }// ⚛️ clears properties
  for (i in oldProps)
    if(i ! = ='children'&& i ! = ='key' && !(i in newProps))
      setProperty(dom, i, null, oldProps[i], isSvg);
}
Copy the code


The diffProps implementation is relatively simple, which is to traverse whether the property has changed, and if it has changed, set the property through setProperty. For failed props, setProperty is also null. The slightly more complicated one is setProperty. This involves handling events, converting names, and so on:

function setProperty(dom, name, value, oldValue, isSvg) {
  if (name === 'style') {
    // ⚛️ style Settings
    const set = assign(assign({}, oldValue), value);
    for (let i in set) {
      // The style attribute has not changed
      if ((value || EMPTY_OBJ)[i] === (oldValue || EMPTY_OBJ)[i]) continue;
      dom.style.setProperty(
        i[0= = =The '-' && i[1= = =The '-' ? i : i.replace(CAMEL_REG, '- $&'),
        value && i in value
          ? typeof set[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false
            ? set[i] + 'px'
            : set[i]
          : ' './ / to empty); }}else if (name[0= = ='o' && name[1= = ='n') {
    // ⚛️ Event binding
    letuseCapture = name ! == (name = name.replace(/Capture$/.' '));
    let nameLower = name.toLowerCase();
    name = (nameLower in dom ? nameLower : name).slice(2);
    if (value) {
      // ⚛️ adds the event for the first time. Note that eventProxy is the event handler
      // Preact collects all event handlers into dom._Listeners and distributes them collectively
      // function eventProxy(e) {
      // return this._listeners[e.type](options.event ? options.event(e) : e);
      // }
      if(! oldValue) dom.addEventListener(name, eventProxy, useCapture); }else {
      // Remove the event
      dom.removeEventListener(name, eventProxy, useCapture);
    }
    // Save the event queue
    (dom._listeners || (dom._listeners = {}))[name] = value;
  } else if(name ! = ='list'&& name ! = ='tagName' && name in dom) {
    // ⚛️DOM object properties
    dom[name] = value == null ? ' ' : value;
  } else if (
    typeofvalue ! = ='function'&& name ! = ='dangerouslySetInnerHTML'
  ) {
    // ⚛️DOM element attributes
    if (value == null || value === false) {
      dom.removeAttribute(name);
    } else{ dom.setAttribute(name, value); }}}Copy the code

This is the end of the Diff algorithm. The logic is not particularly complex, of course, Preact is an extremely simplified framework. React is much more complex, especially after the React Fiber refactoring. You can also think of Preact as a historical review of React and be interested in learning more about the latest React architecture.




The realization of the Hooks

React16.8 officially introduced hooks, which bring a new way of developing the React component that makes code simpler. React Hooks: Not Magic, Just Arrays This article has revealed the basic implementation of hooks, which are simply arrays based. Preact also implements the hooks mechanism, which is only a few hundred lines, so let’s get a feel for it.

The hooks function itself is not integrated within the Preact code base, but is imported via Preact /hooks

import { h } from 'preact';
import { useEffect } from 'preact/hooks';
function Foo() {
  useEffect((a)= > {
    console.log('mounted'); } []);return <div>hello hooks</div>;
}
Copy the code


So how does Preact extend the diff algorithm to implement hooks? In fact, Preact provides options objects to extend Preact diff. Options are similar to Preact lifecycle hooks that are called during diff (I’ve omitted the above code for brevity). Such as:

export function diff(/ *... * /) {
  // ...
  // ⚛️ start diff
  if ((tmp = options.diff)) tmp(newVNode);

  try {
    outer: if (oldVNode.type === Fragment || newType === Fragment) {
      // Fragment diff
    } else if (typeof newType === 'function') {
      // Custom component diff
      // ⚛️ start rendering
      if ((tmp = options.render)) tmp(newVNode);
      try {
        // ..
        c.render(c.props, c.state, c.context),
      } catch (e) {
        // ⚛️ catch an exception
        if ((tmp = options.catchRender) && tmp(e, c)) return;
        throwe; }}else {
      // DOM element diff
    }
    / / ⚛ ️ diff
    if ((tmp = options.diffed)) tmp(newVNode);
  } catch (e) {
    catchErrorInComponent(e, ancestorComponent);
  }
  return newVNode._dom;
}
// ...
Copy the code


useState

Start with the most commonly used useState:

export function useState(initialState) {
  // ⚛️OK is just an array, no Magic, each hooks call increments currenIndex, fetching state from the current component
  const hookState = getHookState(currentIndex++);

  // ⚛️ initializes
  if(! hookState._component) { hookState._component = currentComponent;// The current component instance
    hookState._value = [
      // ⚛️state, initialize state
      typeof initialState === 'function' ? initialState() : initialState,
      / / ⚛ ️ dispatch
      value => {
        const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
        if (hookState._value[0] !== nextValue) {
          // ⚛️ saves the state and calls setState to force an update
          hookState._value[0] = nextValue; hookState._component.setState({}); }},]; }return hookState._value; // [state, dispatch]
}
Copy the code


As you can see from the code, the key is the implementation of getHookState

import { options } from 'preact';

let currentIndex; // Save the index of the current hook
let currentComponent;

// ⚛️render hook, called before the component starts rendering
// Because Preact is rendered recursively and synchronously, and Javascript is single-threaded, it is safe to reference the component instance currently being rendered
options.render = vnode= > {
  currentComponent = vnode._component; // Save the component currently being rendered
  currentIndex = 0;                    // Index is reset to 0 when rendering starts

  // useEffect can be understood
  UseEffect clears the Effect that was not processed in the last rendering (useEffect). This happens only during a quick re-rendering, and is normally processed in an asynchronous queue
  if(currentComponent.__hooks) { currentComponent.__hooks._pendingEffects = handleEffects( currentComponent.__hooks._pendingEffects, ); }};/ / ⚛ ️ no magic! Is just an array, and the state is stored in the component instance's _list array
function getHookState(index) {
  // Get or initialize the list
  const hooks = currentComponent.__hooks ||
    (currentComponent.__hooks = {
      _list: [].// Put state
      _pendingEffects: [],        // Place the effect to be processed, saved by useEffect
      _pendingLayoutEffects: [],  // Place the layoutEffect to be processed and save it with useLayoutEffect
    });

  // Create a state
  if (index >= hooks._list.length) {
    hooks._list.push({});
  }

  return hooks._list[index];
}
Copy the code


The general process is as follows:




useEffect

See also useEffect and useLayoutEffect. UseEffect is similar to useLayouteEffect except that it is triggered at a different time. UseEffect draws the trigger after it has finished rendering. UseLayoutEffect triggers when diff is complete:

export function useEffect(callback, args) {
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    // ⚛️ Status changes
    state._value = callback;
    state._args = args;
    currentComponent.__hooks._pendingEffects.push(state); // ⚛️ advance _pendingEffects queueafterPaint(currentComponent); }}export function useLayoutEffect(callback, args) {
  const state = getHookState(currentIndex++);
  if (argsChanged(state._args, args)) {
    // ⚛️ Status changes
    state._value = callback;
    state._args = args;
    currentComponent.__hooks._pendingLayoutEffects.push(state); // ⚛️ advance _pendingLayoutEffects queues}}Copy the code


UseEffect is placed in an asynchronous queue and is scheduled by requestAnimationFrame to batch process:

// This is an asynchronous queue similar to the one above
afterPaint = component= > {
  if(! component._afterPaintQueued &&// Avoid duplicate push-in of components
    (component._afterPaintQueued = true) &&
    afterPaintEffects.push(component) === 1 // Start scheduling
  )
    requestAnimationFrame(scheduleFlushAfterPaint);  // Scheduled by requestAnimationFrame
};

function scheduleFlushAfterPaint() {
  setTimeout(flushAfterPaintEffects);
}

function flushAfterPaintEffects() {
  afterPaintEffects.some(component= > {
    component._afterPaintQueued = false;
    if (component._parentDom)
      // Clear the _pendingEffects queue
      component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
  });
  afterPaintEffects = [];
}

function handleEffects(effects) {
  // Clear before calling effect
  effects.forEach(invokeCleanup); // Call cleanup
  effects.forEach(invokeEffect);  // Call effect again
  return [];
}

function invokeCleanup(hook) {
  if (hook._cleanup) hook._cleanup();
}

function invokeEffect(hook) {
  const result = hook._value();
  if (typeof result === 'function') hook._cleanup = result;
}
Copy the code


Let’s see how LayoutEffect is triggered. It’s very simple. It’s triggered after diff is done.

options.diffed = vnode= > {
  const c = vnode._component;
  if(! c)return;
  const hooks = c.__hooks;
  if(hooks) { hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects); }};Copy the code

👌 hooks Basic principles, finally, let’s use a graph to sum up.

Technology map

It’s a long article, mostly with too much code, and I don’t like reading it myself, so I didn’t expect readers to see it. After the article thinks way to improve improve again. Thank you for reading this far.

The main character of this issue is itself a small but beautiful view framework, with no other technology stack. Here are some other small and beautiful libraries from Preact author developit.

  • Workerize elegantly executes and invokes programs in webWorker
  • Microbundle zero-configuration library packaging tool
  • Greenlet is similar to workerize, which executes a single asynchronous function in a Webworker, whereas workerize is a module
  • Mitt EventEmitter 200 byte
  • DLV securely accesses deeply nested object properties, similar to loDash’s GET method
  • Snarkdown 1KB Markdown Parser
  • Unistore Redux state container supports React and Preact
  • Stockroom supports a status manager in webWorker

extension

  • Preact: Into the void 0
  • React Virtual DOM vs Incremental DOM vs Ember’s Glimmer: Fight