Analysis of Remax principle

Preface

The weak native development experience of wechat applets is bound to have a small program development framework to enhance the development experience. Facing the fragmentation of small program platform, the framework is bound to shoulder the challenge of cross platform transplantation. The framework of the open source community can be roughly divided into two categories: compile time and runtime. The former is based on the selected DSL, through translation, to generate code that fully conforms to the standard of small programs. In essence, it is a simulation of the underlying DSL. It is inevitable that BUG is hard to trace and development is too limited. The runtime mode does not simulate or modify the underlying DSL. The framework implements the custom renderer through the adaptation layer, and keeps the flexibility of the underlying DSL as much as possible. This article introduces Remax of runtime mode. I personally understand that if there is any mistake, please comment and point out.

Render VNode

In a narrow sense, Virtual DOM is a JavaScript object with a specific data structure. Through the mapping mechanism, JavaScript objects can be transformed into real DOM nodes. The process is called rendering, and the instance is called renderer. Rendering VNode in the host environment and keeping the data synchronized are the core of Virtual DOM library.

The simple VNode is as follows:

{
  "type": "ul",
  "props": {
    "class": "ant-list-items"
  },
  "children": ["I'm happy when unhappy!"]
}
Copy code

The source code to VNode needs to be compiled with babel, and the runtime needs to provide JSX functions. When the function runs, it generates Virtual Node, which is oriented to different host environments and has different VNode structures.

Put aside the complex update logic and components, give priority to how to render VNode statically in browser like environment. The JSX function is simplified as follows:

function h(type, props, ...children) {
  return {
    type,
    children,
    props: props || {},
  };
}
Copy code

In the browser environment, JavaScript can directly create DOM nodes. The rendering code is as follows:

function render(vnode) {
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }

  const props = Object.entries(vnode.props);
  const element = document.createElement(vnode.type);

  for (const [key, value] of props) {
    element.setAttribute(key, value);
  }

  vnode.children.forEach((node) => {
    element.appendChild(render(node));
  });

  return element;
}
Copy code

The implementation uses recursion to complete the restore from VNode to dom. If there are many restrictions on the target environment, do not support direct DOM creation, only template rendering, how to deal with it?

Here, we take handlerbars as an example to illustrate that recursion in handlerbars requires declaring Partial as a multiplex template and calling Partial in Children traversal. With special attention, the formability of the template engine expression is limited, the logic derived property is pre computed, and the JSX function is adjusted as follows:

function h(type, props, ...children) {
  return {
    type,
    children,
    props: props = {},    
    // Self closing element, used by template engine
    isVoidElement: htmlVoidElements.includes(type),
  };
}
Copy code

The implementation is as follows:

{{!-- vtree transparent transmission --}}
{{> template this}}

{{#* inline "template"}}
  {{!-- HTMLElement --}}
  {{#if type}}
    {{!-- Self closing tag nothing children --}}
    {{!-- Non key value pairs are not considered temporarily property --}}
    {{#if isVoidElement}}
      <{{type}} {{#each props}} {{@key}}="{{this}}" {{/each}}/>
    {{else}}
      <{{type}} {{#each props}} {{@key}}="{{this}}" {{/each}}>
        {{#each children as |node|}}
            {{> template node}}
        {{/each}}
      </{{type}}>        
    {{/if}}
  {{!-- TextNode --}}
  {{else}}
    {{this}}
  {{/if}}  
{{/inline}}
Copy code

Compared with creating DOM directly, the implementation of template engine is much more complicated, which is limited by the environment. If the host environment is more demanding, type cannot be dynamically populated, only dynamic parts are supported, and how to deal with it...

See the details of VNode in the case gist.github.com/huang-xiao-...

If the type cannot be filled dynamically, only the Partial including the type can be predefined. According to the type, the predefined Partial can be selected to save the nation by the curve. If the Element set is limited, the predefined method can be fully used, as follows:

{{!-- vtree transparent transmission --}}
{{> template this}}

{{#* inline "template"}}
  {{!-- HTMLElement --}}
  {{#if type}}
    {{!-- Dynamic Partial --}}
    {{> (lookup . 'type') }}
  {{!-- TextNode --}}
  {{else}}
    {{this}}
  {{/if}}  
{{/inline}}

{{!-- node transparent transmission --}}
{{!-- Do not consider non key value pairs property --}}
{{#* inline "div"}}
<div {{#each props}} {{@key}}="{{this}}" {{/each}}>
    {{#each children as |node|}}
        {{> template node}}
    {{/each}}
</div>
{{~/inline}}

{{#* inline "p"}}
<p {{#each props}} {{@key}}="{{this}}" {{/each}}>
    {{#each children as |node|}}
        {{> template node}}
    {{/each}}
</p>
{{~/inline}}

{{#* inline "ul"}}
<ul {{#each props}} {{@key}}="{{this}}" {{/each}}>
    {{#each children as |node|}}
        {{> template node}}
    {{/each}}
</ul>
{{~/inline}}

{{#* inline "li"}}
<li {{#each props}} {{@key}}="{{this}}" {{/each}}>
    {{#each children as |node|}}
        {{> template node}}
    {{/each}}
</li>
{{~/inline}}

{{#* inline "img"}}
<img {{#each props}} {{@key}}="{{this}}" {{/each}} />
{{~/inline}}
Copy code

The pre-defined mode also causes the template to expand rapidly. If the host environment is more demanding, property must be explicitly bound one by one. Traversal cannot be used, logical judgment cannot be made, and how to deal with it...

A simple and crude solution is to bind all the attributes of DOM nodes in full. In the browser environment, property is complex, and the feasibility approaches to 0 infinitely. In the small program environment, element types are relatively less, element property is relatively simplified, and full binding is acceptable.

The view document is abbreviated as follows:

The very simple template implementation is as follows:

{{!-- vtree transparent transmission --}}
{{> template this}}

{{!-- Non key value pairs are not considered temporarily property --}}
{{#* inline "template"}}
  {{!-- HTMLElement --}}
  {{#if type}}
    {{!-- Dynamic Partial --}}
    {{> (lookup . 'type') }}
  {{!-- TextNode --}}
  {{else}}
    {{this}}
  {{/if}}  
{{/inline}}


{{!-- node transparent transmission --}}
{{#* inline "view"}}
<view
  class="{{props.[class]}}"
  hover-class="{{props.[hover-class]}}"
  hover-stop-propagation="{{props.[hover-stop-propagation]}}"
  hover-start-time="{{props.[hover-start-time]}}"
  hover-stay-time="{{props.[hover-stay-time]}}"
>
  {{#each children as |node|}}
    {{> template node}}
  {{/each}}
</view>
{{~/inline}}

{{#* inline "image"}}
<image
  src="{{props.[src]}}"
  mode="{{props.[mode]}}"
  webp="{{props.[webp]}}"
  lazy-load="{{props.[lazy-load]}}"
  show-menu-by-longpress="{{props.[show-menu-by-longpress]}}"
  binderror="{{props.[binderror]}}"
  bindload="{{props.[bindload]}}"
  class="{{props.[class]}}"
/>
{{~/inline}}
Copy code

The rendering result is as follows:

<view
  class="ant-list-items"
  hover-class=""
  hover-stop-propagation=""
  hover-start-time=""
  hover-stay-time=""
>
  <view
    class=""
    hover-class=""
    hover-stop-propagation=""
    hover-start-time=""
    hover-stay-time=""
  >
    <view
      class="ant-comment"
      hover-class=""
      hover-stop-propagation=""
      hover-start-time=""
      hover-stay-time=""
    >
      <view
        class="ant-comment-inner"
        hover-class=""
        hover-stop-propagation=""
        hover-start-time=""
        hover-stay-time=""
      >
      </view>
    </view>
  </view>
</view>
Copy code

There are a large number of null values in the rendering results, and the influence of the string based template engine is not significant. However, in the applet environment, whether the corresponding components are compatible with null values needs to be measured in practice. Moreover, a large number of null values are also a challenge to the performance of the applet itself. Whether there is a more refined operation mode can not affect the property Reduce unnecessary binding under the restriction of any logical judgment?

Refer to the previous preset element partial enumeration method to enumerate the possible combinations of properties. Use the corresponding hash value as the partial name to hash the props when generating VNode to reference the preset property partial. The JSX function is adjusted as follows:

function h(type, props, ...children) {
  return {
    type,
    children,
    props: props || {},
    // props hash calculation
    hash:
      props == null ? type : `${type}_${Object.keys(props).sort().join("_")}`,
  };
}
Copy code

The template implementation is as follows:

{{!-- vtree transparent transmission --}}
{{> template this}}

{{!-- Non key value pairs are not considered temporarily property --}}
{{#* inline "template"}}
  {{!-- HTMLElement --}}
  {{#if type}}
    {{!-- Dynamic Partial --}}
    {{> (lookup . 'hash') }}
  {{!-- TextNode --}}
  {{else}}
    {{this}}
  {{/if}}  
{{/inline}}

{{#* inline "view"}}
<view>
  {{#each children as |node|}}
    {{> template node}}
  {{/each}}
</view>
{{~/inline}}

{{#* inline "view_class"}}
<view class="{{props.[class]}}">
  {{#each children as |node|}}
    {{> template node}}
  {{/each}}
</view>
{{~/inline}}

{{#* inline "view_class_hover-class"}}
<view class="{{props.[class]}}" hover-class="{{props.[hover-class]}}">
  {{#each children as |node|}}
    {{> template node}}
  {{/each}}
</view>
{{~/inline}}

{{#* inline "image_src"}}
<image src="{{props.[src]}}"/>
{{~/inline}}
Copy code

The following problems arise: the elements with less property can be exhausted, and the number of element combinations with more property can explode directly in situ. The scheme still has no practical significance and needs to be optimized.

If you scan the source code in advance and judge the properties declared in the source code, you can remove a large number of unreferenced properties as the benchmark for optimization and adjustment. The options are as follows:

  1. All the scanned attributes of the same element are bound as a complete set, and the method of binding one by one with full amount needs to deal with null value compatibility;
  2. All scanned attributes of the same element are bound as a whole set, and different reuse templates are declared based on combination exhaustion;
  3. Different usage scenarios of the same element declare different reuse templates;

Scheme 2 is excluded first, and the number of templates declared must be greater than or equal to scheme 3, so only the battle of scheme 1 and scheme 3 is left. In essence, the pre scanning scheme is to simplify the set of preset template s before entering the host environment, which has some flavor of static translation. Once static translation is encountered, certain compromises must be made in the development process to minimize the difficulty of translation. Explicit binding is recommended as follows:

import * as React from 'react';
import { View, Text, Image } from 'remax/wechat';
import styles from './index.module.css';

export default () => {
  return (
    <View className={styles.app}>
      <View className={styles.header}>
        <Image
          src="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*OGyZSI087zkAAAAAAAAAAABkARQnAQ"
          className={styles.logo}
          alt="logo"
        />
        <View className={styles.text}>
          //Edit < text classname = {styles. Path} > Src / pages / index / index. JS < / text > start
        </View>
      </View>
    </View>
  );
};
Copy code

Implicit binding is used as less as possible, for example:

import * as React from 'react';
import { View, Text, Image } from 'remax/wechat';
import styles from './index.module.css';

export default () => {
  const parentViewProps = {
    className: styles.header
  };
  const imageProps = {
    src: "https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*OGyZSI087zkAAAAAAAAAAABkARQnAQ",
    className: styles.logo,
    alt: "logo"
  }

  return (
    <View {...parentViewProps}>
      <Image {...imageProps} />
      <View className={styles.text}>
        //Edit < text classname = {styles. Path} > Src / pages / index / index. JS < / text > start
      </View>
  </View>
  );
};
Copy code

The case here is not a big challenge to static translation, but more complex scenarios are likely to have unexpected problems, which need to be used carefully. Source code scanning, using babel tool chain, case scanning results are as follows:

// Option 1
{
  View: Set { 'class' },
  Image: Set { 'src', 'class', 'alt' },
  Text: Set { 'class' }
}
Copy code
// Option 3
Set { 'View_class', 'Image_alt_class_src', 'Text_class' }
Copy code

In this case, no matter which scheme, it will not have a great impact on the volume of the generated applet template. The final generated template will not be described in detail and can be implemented by yourself. Based on the static template engine handlebars, it is only used as a demonstration. After feasibility verification, it still needs to be verified in the applet environment. Simple and crude applet templates are migrated as follows:

<template is="{{vnode.type}}" data="{{vnode}}" />

<template name="view">
  <view class="{{vnode.props.class}}">
    <block wx:for="{{vnode.children}}" wx:key="id">
      <template is="{{item.type}}" data="{{vnode: item}}" />
    </block>
  </view>
</template>

<template name="image">
  <image class="{{vnode.props.class}}" src="{{vnode.props.src}}"></image>
</template>

<template name="text">
  <text class="{{vnode.props.class}}">
    {{vnode.children[0]}}
  </text>
</template>
Copy code

However, on the other side of the mountain, it is still a mountain, and the page is not presented as expected. The console prompts as follows:

Looking back, I found that the applet template does not support recursion...

So far, the template engine must support recursion, and the applet template does not support recursion, so it can only support recursion through simulation. Dawu shadow separation: repeat the declaration of the element template, add the increasing sequence number to distinguish, and the specific implementation needs foot support, as follows:

module.exports = {
  tid: function (type, ancestor) {
    var items = ancestor.split(",");
    var depth = 0;

    for (var i = 0; i < items.length; i++) {
      if (type === items[i]) {
        depth = depth + 1;
      }
    }

    return depth === 0 ? type : type + "_" + depth;
  },
};
Copy code
<wxs module="h">
<!-- Code see -->
</wxs>

<!-- Start template -->
<template is="{{vnode.type}}" data="{{vnode: vnode, ancestor: vnode.type}}" />

<template name="view">
  <view class="{{vnode.props.class}}">
    <block wx:for="{{vnode.children}}" wx:key="id">
      <template is="{{h.tid(item.type, ancestor)}}" data="{{vnode: item, ancestor: ancestor + ',view'}}" />
    </block>
  </view>
</template>

<template name="view_1">
  <view class="{{vnode.props.class}}">
    <block wx:for="{{vnode.children}}" wx:key="id">
      <template is="{{h.tid(item.type, ancestor)}}" data="{{vnode: item, ancestor: ancestor + ',view'}}" />
    </block>
  </view>
</template>

<template name="view_2">
  <view class="{{vnode.props.class}}">
    <block wx:for="{{vnode.children}}" wx:key="id">
      <template is="{{h.tid(item.type, ancestor)}}" data="{{vnode: item, ancestor: ancestor + ',view'}}" />
    </block>
  </view>
</template>

<template name="image">
  <image class="{{vnode.props.class}}" src="{{vnode.props.src}}"></image>
</template>

<template name="text">
  <text class="{{vnode.props.class}}">
    {{vnode.children}}
  </text>
</template>
Copy code

In the actual construction process, the nesting depth of different elements is unknown, so template generation and optimization are dealt with to a considerable extent, and the part of react reconciler is relatively less difficult to understand.

Remax renderer

In the previous large chapter with handlebars as an example, various host environment restrictions are added, which is the helpless of WeChat applet and the core of Remax. The bottom layer of REMAX is react, which is platform independent, and supports cross platform through the renderer. React DOM is responsible for rendering vnode as a real DOM node, react native is responsible for rendering vnode as a native UI, react three fiber is responsible for rendering vnode as a three-dimensional figure, so REMAX is responsible for rendering vnode why can we complete the linchpin of vnode to a small program?

The structure of the applet is simplified as follows:

The logical layer is separated from the view layer. The view layer template has solved the conversion from vnode tree to applet element, and also the problem of data transmission. The data management mechanism of the applet, which is triggered by setData, and the state management mechanism of React, which is triggered by setState, requires coordination at the framework level to convert the generated virtual tree into the data of the applet. In essence, the renderer implemented by Remax is to render vnode into vnode.

As a framework, Remax certainly shields many details. If you are interested, please refer to react-reconciler Learn more.

Remax myth

The implementation mechanism of remax is different from that of translation framework. The author understands as follows:

  • remax can seamlessly use redux and mobx state management libraries without additional encapsulation layers
  • remax performance is relatively lossy compared with static template. Whether it affects normal application needs to be analyzed in detail
  • Event monitoring at the App and Page levels, distributed to the components in the React application by proxy
  • The applet is in multi page form. remax currently simulates multi page form (🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔)

Reference link

Tags: React less Javascript Handlebars

Posted on Mon, 20 Apr 2020 01:29:43 -0400 by Angry Lettuce