Originally published in Zhihu column: zhuanlan.zhihu.com/ne-fe

Due to the React Licence problem some time ago, the team actively explored alternative solutions for React. Considering the possible mobile terminal business in the future, the team aimed to find an alternative product with low migration cost and small volume. After much exploration, Preact came to our attention. I have lost a lot of hair and gained a lot of thinking since I came into contact with Preact. Here I would like to introduce the implementation of Preact to you and share my own thinking.

What is Preact

Preact, a lightweight 3KB alternative to React, has the same ES6 API. If that’s too vague, I can say a few more words. Preact = Performance + React Preact = Performance + React The graph below shows the performance of different frameworks in the scenario of long list initialization, and you can see that Preact does perform well.

High performance, light weight and instant production are at the core of Preact’s focus. Based on these themes, Preact focuses on the core functions of React, implementing a set of simple and predictable diff algorithms that make it one of the fastest virtual DOM frameworks. Preact-compat also provides compatibility guarantees, allowing Preact to seamlessly work with a large number of components in the React ecosystem. It also adds a lot of functionality that Preact didn’t implement.

Long list initialization time comparison

Preact workflow

After a brief introduction to Preact’s past and present life, here’s how Preact works, which consists of five modules:

  • component
  • H function
  • render
  • The diff algorithm
  • Recycling mechanism

The flow process is shown below.

The first is the component defined by us. At the beginning of rendering, h function will be entered to generate the corresponding Virtual node (if it is written by JSX, a step of transcoding will be required before). Each VNode contains information about its own node and its child nodes, which are connected to form a virtual DOM tree. Based on the generated VNode, the Render module will control the flow based on the current DOM tree and prepare for subsequent diff operations. Preact maintains only a new virtual DOM tree. During the diff process, it restores the old virtual DOM tree based on the DOM tree and compares the two. In the process of comparison, the patch operation of DOM tree is carried out in real time, and the new DOM tree is finally generated. At the same time, components and nodes unloaded during diFF will not be deleted directly, but will be cached in the recycle pool respectively. When another component or node of the same type is built again, the element with the same name can be found in the recycle pool for modification, avoiding the cost of building from zero.

Preact workflow flowchart


Component

Key words: Hook, linkState, batch update

For those of you who have experience in React development, the concept of Component will be familiar. We will not explain it too much, but we will introduce some of the new features Preact adds to the Component level.

Hook function

In addition to the basic lifecycle functions, Preact provides three hook functions that allow users to perform unified operations at a specified point in time.

  • afterMount
  • afterUpdate
  • beforeUnmount

linkState

LinkState is targeted at scenarios where you bind this in the Render method for callbacks to user operations, creating a function closure locally for each rendering, which is inefficient and forces the garbage collector to do a lot of unnecessary work. The ideal application scenarios of linkState are as follows.

export default class App extends Component {
  constructor() {
    super(a);this.state = {
      text: 'initial'
    }
  }

  handleChange = e= > {
    this.setState({
      text: e.target.value
    })
  }

  render({desc}, {text}} {
    return (
      <div>
        <input value={text} onChange={this.linkState('text', 'target.value')} >
        <div>{text}</div>
      </div>)}}Copy the code

However, the implementation of linkState… Create a closure for each callback during component initialization, bind this, and create an instance property to cache the bound callback function so that it does not need to bind again when render is performed. The actual effect is equivalent to binding in the component’s constructor. Embarrassingly, linkState only implements setState operation internally, and does not support custom parameters, so the usage scenarios are relatively limited.

/ / linkState source code
// Cache callback
linkState(key, eventPath) {
  let c = this._linkedStates || (this._linkedStates = {});
  return c[key+eventPath] || (c[key+eventPath] = createLinkedState(this, key, eventPath));
}

// The closure is created when the callback is first registered
export function createLinkedState(component, key, eventPath) {
  let path = key.split('. ');
  return function(e) {
    let t = e && e.target || this,
      state = {},
      obj = state,
      v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? (t.type.match(/^che|rad/)? t.checked : t.value) : e, i =0;
    for(; i<path.length- 1; i++) { obj = obj[path[i]] || (obj[path[i]] = ! i && component.state[path[i]] || {}); } obj[path[i]] = v; component.setState(state); }; }Copy the code

Batch update

Preact implements a batch update of components. Each time an update is performed to the properties of the props, the properties are updated immediately, but rendering operations based on the new state or props are pushed into an update queue. The actions in the queue are executed at the end of the current event loop or at the beginning of the next event loop. Multiple updates of the same component state are not queued repeatedly. As shown in the figure below, the _dirty value is true after the property is updated and before the component is rendered, so subsequent property updates before the component is rendered will not re-enqueue the component.

// Update queue source code
export function enqueueRender(component) {
  if(! component._dirty && (component._dirty =true) && items.push(component)==1) { (options.debounceRendering || defer)(rerender); }}Copy the code

H function

Key words: node merging

The h function acts like react. CreateElement and is used to generate a virtual node. The input format is as follows, and the three parameters are node type, node attribute, and child element.

h('a', { href: '/', h{'span'.null.'Home'}})Copy the code

Node merge

In the process of generating vnodes, h function will merge simple adjacent nodes to reduce the number of nodes and reduce the diff burden. Take a look at the following example.

import { h, Component } from 'preact';
const innerinnerchildren = [['innerchild2', 'innerchild3'], 'innerchild4'];
const innerchildren = [
  <div>
    {innerinnerchildren}
  </div>,
  <span>desc</span>
]

export default class App extends Component {
  render() {
    return (
      <div>
        {innerchildren}
      </div>
    )
  }
}Copy the code

Render

Key words: process control, DIFF preparation

First of all, the Render module refers to the process of inserting vNodes into the DOM tree. However, some of the work of these operations is carried out by the DIff module, so the Render module is actually more responsible for the process control and the pre-diff work.

Process control

The so-called flow control is divided into two parts: the judgment of node type, whether it is a custom component or a native DOM node, and the judgment of rendering type, whether it is the first rendering or update operation. Depending on the situation, specify different rendering paths and implement the corresponding lifecycle methods, hook functions and render logic.

The Diff to prepare

As mentioned earlier, Preact only maintains a new virtual DOM tree in memory that contains the updated content. Another tree representing the old virtual DOM tree that has been updated is actually restored from the DOM tree. Meanwhile, the UPDATE operation of the DOM tree is also performed during the comparison process, while the patch operation is performed. To ensure that the above operations are not messy, some custom attributes need to be added to the DOM nodes to record the state before generating/updating the DOM tree.

// Create a custom property record
export function renderComponent(component, opts, mountAll, isChild) {
  if (component._disable) return;

  let skip, rendered,
    props = component.props,
    state = component.state,
    context = component.context,
    previousProps = component.prevProps || props,
    previousState = component.prevState || state,
    previousContext = component.prevContext || context,
    isUpdate = component.base,
    nextBase = component.nextBase,
    initialBase = isUpdate || nextBase,
    initialChildComponent = component._component,
    inst, cbase;Copy the code

The Diff algorithm

Keywords: DOM dependency, Disconnected or Not, DocumentFragment

The diff process is mainly divided into two stages. The first stage is to establish the corresponding relationship between virual node and DOM node, and the second stage is to compare the two and update DOM node.

  • In actual execution, the starting point for the diff operation is the comparison between the root node of the Update component and the vNode that represents its next state. This step the corresponding relationship between the two is clear, and the next, you need to in child elements to determine the corresponding relationship of the two, the specific method is to first of all child nodes of the same key value pairs, then the same type of node pairs, finally has not been matched vnode as a newly added node, while the fate of the single dom node is recycled.
  • After entering the update stage, it will classify and process according to the type of virtual node and the situation of reference nodes in the DOM tree. Patch operation will be carried out in real time in the process of DIFF, and finally generate new DOM nodes, and then recurse the child nodes.


    The flow chart of the Diff

DOM rely on

After the introduction, I believe you have a certain understanding of the virtual DOM implementation of Preact, so I will not repeat it here. The advantage of this implementation is that it can always truly reflect the situation of the previous virtual DOM tree, but the disadvantage is the risk of memory leakage.

Disconnected or Not

  • What does Disconnected mean

As we all know, when we implement the appendChild, removeChild operation on a node in the DOM tree, each time the reflow of the page is triggered, this is an expensive behavior. Therefore, when we have to perform a series of operations, we can take such optimization means, first create a node, after the append operation of all the child nodes on this node, and then use this node as the root node subtree append or replace into the DOM tree. The entire subtree is updated with a single reflow trigger, which is called disconnected.

In contrast, when a node is created, it is immediately inserted into the DOM tree and then the child node operation continues, it is called Connected.

  • Go ahead to Preact

With that premise clarified, the implementation of Preact, Disconnected or Connected, is a siege. Despite the author’s claims that Preact rendered differently, the truth is that it was not always true. Let’s start with a simple case where the value of a TextNode is changed or an old node is replaced with a TextNode. All Preact does is create a TextNode or modify the nodeValue of the previous TextNode. While it is pointless to dwell on this scenario, it is necessary to explain the diff process in order to complete it. Get to the point. Let’s do the first example. To illustrate, let’s use a slightly more extreme example.

In this example, you can see that after text is entered, there is an update of the div subtree to the section subtree. To describe the extreme case, the child nodes are the same before and after the update.

// a placeholder subtree differs only from the root node
import { h, Component } from 'preact';

export default class App extends Component {
  constructor() {
    super(a);this.state = {
      text: ' '
    }
  }

  handlechang = e= > {
    this.setState({
      text: e.target.value
    })
  }

  render({desc}, { text }) {
    return (
      <div>
        <input value={text} onChange={this.handlechang}/>
        {text ? <section key='placeholder'> 
          <h2>placeholder</h2>  
        </section>: <div key='placeholder'>
          <h2>placeholder</h2>  
        </div>}
      </div>)}}Copy the code

Let’s look at the detailed flow of diff operations for this scenario.

// Idiff logic for native DOM
let out = dom,  / / comment 1
  nodeName = String(vnode.nodeName),
  prevSvgMode = isSvgMode,
  vchildren = vnode.children;

isSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode;

if(! dom) {/ / comment 2
  out = createNode(nodeName, isSvgMode);
}
else if(! isNamedNode(dom, nodeName)) {/ / comment 3
  out = createNode(nodeName, isSvgMode);
  while (dom.firstChild) out.appendChild(dom.firstChild);
  if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  recollectNodeTree(dom);
}

// The child node recurses...else if(vchildren && vchildren.length || fc) { innerDiffNode(out, vchildren, context, mountAll); }...Copy the code

Whether the elements participating in diff are custom components or native DOM, they are all compared in DOM form after layer upon layer of deconstruction. So we just need to focus on the native DOM’s diff logic.

The DOM represents the node in the DOM tree, i.e. the node to be updated, and the vNode is the virtual node to be rendered. In example 1, the start of diff is the outermost div, which is the DOM variable of the first round, so the judgment at comments 2 and 3 is false. Recursive diff operations are then performed on the children of the OUT node and the corresponding children of the VNode.

So here’s the first problem: render operations always start with connected.

if (vlen) {
  for (let i=0; i<vlen; i++) {
    vchild = vchildren[i];
    child = null;

    let key = vchild.key;
    // The same key value matches
    if(key! =null) {
      if (keyedLen && key in keyed) {
    child = keyed[key];
    keyed[key] = undefined; keyedLen--; }}// Same nodeName matches
    else if(! child && min<childrenLen) {for (j=min; j<childrenLen; j++) {
    c = children[j];
    if (c && isSameNodeType(c, vchild)) {
      child = c;
      children[j] = undefined;
      if (j===childrenLen- 1) childrenLen--;
          if (j===min) min++;
      break; }}}// When vnode is a section node, the DOM tree has neither the same key node nor the same nodeName node, so it is nullchild = idiff(child, vchild, context, mountAll); ...Copy the code

The correspondence between child nodes is established on the basis of either the same key value or the same nodeName. It can be known that the relation between section and div does not meet the above two situations. So when we re-enter the idiff method, at comment 2, a new section node is assigned to out because the DOM doesn’t exist. So when we re-diff the child element, out is a new node that doesn’t contain any child elements. All children of the section diff object are null, which means that all children of the section are created (with or without a key), even though they look exactly like old DOM nodes… So to summarize, in the case of example 1, all the children of the section are created, not reused, but the operation is disconnected.

What if I add the same key to both of them?

// The component structure is the same; the only difference is that the subtree in the placeholder contains the same key
import { h, Component } from 'preact';

export default class App extends Component {
  constructor() {
    super(a);this.state = {
      text: ' '
    }
  }

  handlechang = e= > {
    this.setState({
      text: e.target.value
    })
  }


  render({desc}, { text }) {
    return (
      <div>
    <input value={text} onChange={this.handlechang}/>
        {text ? <section key='placeholder'> 
          <h2>placeholder</h2>  
        </section>: <div key='placeholder'>
          <h2>placeholder</h2>  
        </div>}
      </div>)}}Copy the code

Because they have the same key value, they can be paired successfully when the corresponding relationship between VNode and DOM is determined and the diff link is entered. However, a replace operation causes all subsequent operations to become connected. The good news is that the same child nodes are being reused.

// Native DOM diff logic
// THE DOM node, div, exists and is of a different type from the vnode type section
else if(! isNamedNode(dom, nodeName)) { out = createNode(nodeName, isSvgMode);while (dom.firstChild) out.appendChild(dom.firstChild);
  if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  recollectNodeTree(dom);
}Copy the code

DocumentFragment

In addition to the disconnected method described above, you can insert a series of nodes into the DOM at once using a DocumentFragment. When the DocumentFragment node is inserted into the document tree, all of its descendants are inserted instead of the DocumentFragment itself. This makes the DocumentFragment a useful placeholder for temporarily storing nodes that are inserted into the document at once. The same question was posed to the author on Github, and the author said that he had tried to reduce reflow using The DocumentFragment method, but the result was surprising.

The figure above shows the performance comparison diagram of the test case prepared by the author. The horizontal coordinate is Operation per second. The higher the value is, the higher the execution efficiency is. You can see that DocumentFragement performs worse in either connected or disconnected situations. The exact reason remains to be seen. BenchMark the original link.

Recycling mechanism

Key words: Recovery pool &Enhanced Mount

Recovery pool &Enhanced Mount

When a node is removed from the DOM, the node is not removed directly, but is stored in two separate recycle pools based on the node type (component or node) after some cleanup logic is performed. At the time of executing each Mount operation, create methods will be looking for the same type in recycling pool node, once you find the same node, it will be as to update the reference node to the diff algorithm, this again later in the process of comparing, a node from the recycling pool will be as a prototype patch renovation, to create new nodes. This is equivalent to changing Mount to Update to avoid the extra overhead of building from zero.

Reality often turns out to be less than a fairy tale, and recycling eventually stumbles. [Bug Mc-10868] – Recovery mechanism can cause nodes to be mistakenly reused in some cases… So, like an inflamed appendix, the recovery mechanism may soon be out of sight.

conclusion

This article focuses on the work flow of Preact and some details of its various modules, hoping to achieve the role of a brick to attract more people to participate in the community communication. Friends who are interested in the content of the article are welcome to contact me at any time. If the online communication is not smooth, you can send your resume to [email protected]. I can think of the most romantic thing is to collect dribs and drabs of laughter with you all the way, leave to later, sit on the station, slowly chat.