Last month Ant Financial announced a new framework, Remax, for developing applets using the real, complete React.
For React developers, ‘Learn once, write anywhere’ is a similar development experience to ReactNative. For small applications, it’s a completely new development experience.
Taro claims to be a “React like” development solution, but it is implemented using static compilation, as mentioned by Byryu in his article “Remax – Building Small Programs with the Real React” :
The so-called static compilation, is the use of tools to analyze the code syntax, the JSX part and the logical part of the extraction, respectively generate small program template and Page definition.
This solution is complicated to implement, and React does not exist at runtime.
By comparison, Remax’s solution is much simpler, and it’s nothing more than the new React renderer.
Since Remax has just been released, the core code is relatively simple. If you are interested, you can go to Github to see the contributions and try out the custom Renderer at CodeSandbox: Edit React-custom -renderer article seems to be a long one. Step by step: 🦖
The article Outlines
- Some basic concepts about React
- Customize the React renderer
- HostConfig renderer ADAPTS
- The host components
- Mirror tree construction and operation
- Node updates
- Side effect submission
- HostConfig execution process summary
- Sync to the renderer
- conclusion
- Further reading
Some basic concepts about React
To create a React custom renderer, you need to understand the basic principles of React rendering. So before diving into this article, make sure you understand the following basic concepts:
1. Element
We can create an Element using JSX or react. createElement to describe the view node we want to create. Such as:
<button class='button button-blue'>
<b>
OK!
</b>
</button>
Copy the code
JSX will be escaped as:
React.createElement(
"button",
{ class: 'button button-blue' },
React.createElement("b".null."OK!"))Copy the code
React.createElement eventually builds an object that looks like this:
{
type: 'button'.props: {
className: 'button button-blue'.children: {
type: 'b'.props: {
children: 'OK! '}}}}Copy the code
That is, an Element is a generic object that describes the node types, props, and children created by the user. These Elements are combined to form a tree describing the user view
2. Component
Can be thought of as the type of Element, which comes in two types:
-
Host Component: a ‘built-in’ Component provided by the rendering platform, such as a DOM node under the ReactDOM platform, such as div, SPAN… These component types are strings
-
Composite Component: Composite Component, which is a user-defined unit of Component encapsulation. Usually contains custom logic, state, and an output Element tree. Compound types can be classes or functions
const DeleteAccount = (a)= > (
<div>
<p>Are you sure?</p>
<DangerButton>Yep</DangerButton>
<Button color='blue'>Cancel</Button>
</div>
);
Copy the code
3. Instance
When React starts rendering an Element, it creates an ‘instance’ of it based on the component type, such as a class component, which is instantiated by calling the new operator. This instance will be referenced until Element is removed from the Element Tree.
First render: React instantiates a MyButton instance, calls the mount associated lifecycle method, and executes the Render method to recursively render the child
render(<MyButton>foo</MyButton>, container)
Copy the code
Update: Because the component type does not change, React is no longer instantiated. This is a ‘node update’ and React executes update-related lifecycle methods, such as shouldComponentUpdate. If an update is required, the Render method is executed again
render(<MyButton>bar</MyButton>, container)
Copy the code
Uninstall: The component type is different and the old MyButton has been replaced. The instance of MyButton will be destroyed, and React will perform unmount related lifecycle methods, such as componentWillUnmount
render(<button>bar</button>, container)
Copy the code
4. Reconciler & Renderer
The relationship between the Reconciler and the Renderer can be clearly traced in the following figure.
The Reconciler is responsible for maintaining the VirtualDOM tree, and the Diff/Fiber algorithm is implemented internally to decide when and what to update
The Renderer is responsible for platform-specific rendering, providing hosting components, handling events, and so on. ReactDOM, for example, is a renderer responsible for DOM node rendering and DOM event handling.
With The React architecture, the update process is divided into two phases.
- React identifies nodes that need to be updated. This phase can be interrupted, for example when there are higher-priority events to deal with.
- The Commit Phase executes all the ** Effects ** calculated in the previous Phase that need to be addressed at once. This phase must be executed synchronously and without interruption
If bounded by render, life cycle functions can be divided into two stages:
- Coordinate phase
constructor
componentWillMount
abandonedcomponentWillReceiveProps
abandonedstatic getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate
abandonedrender
getSnapshotBeforeUpdate()
- The commit phase
componentDidMount
componentDidUpdate
componentWillUnmount
Don’t understand? This may be a bit hard for you to read, but it is recommended to read some articles on the basic principles of React.
At present, most of the core work of React has been completed in Reconciler. Fortunately, the architecture and module division of React are still fairly clear, and React has also exposed some libraries, which greatly simplifies the difficulty of developing Renderer. Let’s go!
Customize the React renderer
React officially exposes some libraries for developers to extend custom renderers:
- The React-Reconciler – this is the react coordinator, the heart of React. We mainly use it to develop renderers.
- Some APIS for scheduler – Cooperative scheduler. This article will not use
It should be noted that these packages are experimental and the apis may be unstable. In addition, there is no detailed documentation, you need to look at the source code or other renderer implementation; This article and the articles in the extended reading are also good learning materials.
Creating a custom renderer takes just two steps:
The first step: implement the host configuration, which is a number of adapter methods and configuration items that the React-Reconciler requires from the host. These configuration items define how to create a node instance, build a node tree, commit, update, and so on. These configuration items are described in detail below
const Reconciler = require('react-reconciler');
const HostConfig = {
/ /... Implement adapter methods and configuration items
};
Copy the code
Step 2: Implement the render function, similar to the reactdom.render () method
// Create a Reconciler instance and pass HostConfig to the Reconciler
const MyRenderer = Reconciler(HostConfig);
/** * Rendered (
, container, () => console.log(' Rendered ')) */
export function render(element, container, callback) {
// Create root container
if(! container._rootContainer) { container._rootContainer = ReactReconcilerInst.createContainer(container,false);
}
// Update the root container
return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback);
}
Copy the code
The Container is both the mounting target of the React component tree (e.g., ReactDOM is normally mounted to the #root element, which is a container) and the Fiber root node of the component tree. The root node is the entry point to the entire component tree, which will be used to hold some information and manage updates and rendering of all the nodes.
For more details on the Fiber architecture, see these articles:
- React Fiber Architecture and Source Code
- React Fiber for those of you who have the ability to read Lin Clark’s talk
HostConfig renderer ADAPTS
HostConfig supports a wide range of parameters, the full list can be found here. Here are some of the parameters that custom renderers must provide:
interface HostConfig {
/** * Used to share some context information */
// Get the context information of the root container, called only once at the root node
getRootHostContext(rootContainerInstance: Container): HostContext;
// Get the context information of the child nodes, called each time a node is traversed
getChildHostContext(parentHostContext: HostContext, type: Type, rootContainerInstance: Container): HostContext;
/** * Node instance creation */
// Create a normal node instance, such as the Element type of the DOM
createInstance(type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle,): Instance;
// Create a Text node, such as the DOM Text type
createTextInstance(text: string, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle): TextInstance;
// Decide whether to process child nodes/child text nodes. Return true if you do not want to create one. For example, if dangerouslySetInnerHTML is used in ReactDOM, the child node is ignored
shouldSetTextContent(type: Type, props: Props): boolean;
/** * Node tree construction */
// This is called to add child nodes if the node is in the * unmounted * state
appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void;
// ** The following are all effects, which are executed in the commit phase **
// Add child nodesappendChild? (parentInstance: Instance,child: Instance | TextInstance): void;
// Add child node to container node (root node)appendChildToContainer? (container: Container,child: Instance | TextInstance): void;
// Insert child nodesinsertBefore? (parentInstance: Instance,child: Instance | TextInstance, beforeChild: Instance | TextInstance): void;
// Insert child node into container node (root node)insertInContainerBefore? (container: Container,child: Instance | TextInstance, beforeChild: Instance | TextInstance,): void;
// Delete the child noderemoveChild? (parentInstance: Instance,child: Instance | TextInstance): void;
// Remove child nodes from the container node (root node)removeChildFromContainer? (container: Container,child: Instance | TextInstance): void;
/** * The node mounts */
// Called when all child nodes are initialized (all appendInitialChild ends), and if true is returned, commitMount will be fired
// ReactDOM implements the autofocus functionality of form elements through this attribute and commitMount configuration
finalizeInitialChildren(parentInstance: Instance, type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext): boolean;
// In conjunction with finalizeInitialChildren, commitRoot executes after resetAfterCommit, that is, after the component tree is renderedcommitMount? (instance: Instance,type: Type, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
/** * Node update */
// Prepare a node update. If nothing is returned, there is no update, and commitUpdate is not called
prepareUpdate(instance: Instance, type: Type, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: HostContext,): null | UpdatePayload;
// ** The following are all effects, which are executed in the commit phase **
// Text node submissioncommitTextUpdate? (textInstance: TextInstance,oldText: string, newText: string): void;
// Common node commitcommitUpdate? (instance: Instance,updatePayload: UpdatePayload, type: Type, oldProps: Props, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
// Reset the normal node text content. This needs to be used with shouldSetTextContent(if true),resetTextContent? (instance: Instance):void;
/** * submit */
// it is called before the commit is started. For example, here you can save some state and restore the state after the commit is complete. For example, ReactDOM stores the focus state of the current element, which is restored after the commit
// After prepareForCommit, Effects will be executed.
prepareForCommit(containerInfo: Container): void;
PrepareForCommit, which is executed after the commit is complete
resetAfterCommit(containerInfo: Container): void;
/** ** */
This function is used by Reconciler to calculate the current time, such as the time remaining in a task
// Performance. Now is preferred in ReactDOM. Date.now is preferred in common scenarios
now(): number;
// Custom timer
setTimeout(handler: (. args: any[]) = > void.timeout: number): TimeoutHandle | NoTimeout;
// Cancel the timer
clearTimeout(handle: TimeoutHandle | NoTimeout): void;
// represents an empty timer, as shown in the signature of 👆clearTimeout
noTimeout: NoTimeout;
/ /? Function of the unknown
shouldDeprioritizeSubtree(type: Type, props: Props): boolean;
/ / abandoned
scheduleDeferredCallback(callback: (a)= >any, options? : {timeout: number }): any;
/ / abandoned
cancelDeferredCallback(callbackID: any): void;
/** * Function enabled */
// Enable node modification, usually the renderer is enabled, otherwise the node cannot be updated
supportsMutation: boolean;
// Enable persistence?
supportsPersistence: boolean;
// Enable Hydrate, usually used for server rendering
supportsHydration: boolean;
/** * miscellaneous */
// Get the publicly available node instance, that is, the node information you want to expose to the user. The user can get this object through ref. Custom renderers generally return as is, unless you want to selectively expose information to the user
getPublicInstance(instance: Instance | TextInstance): PublicInstance;
/ /... There are a lot of parameters, but we won't go into them because we don't normally use them in renderers
}
Copy the code
If you divide the interface into two phases of Fiber, it looks like this:
Coordinate phase | To submit | The commit phase | Submit completed |
---|---|---|---|
createInstance | prepareCommit | appendChild | resetAfterCommit |
createTextInstance | appendChildToContainer | commitMount | |
shouldSetTextContent | insertBefore | ||
appendInitialChild | insertInContainerBefore | ||
finalizeInitialChildren | removeChild | ||
prepareUpdate | removeChildFromContainer | ||
commitTextUpdate | |||
commitUpdate | |||
resetTextContent |
From the above interface definition, we can know that HostConfig configuration is rich, involving node operation, mount, update, scheduling, and various life cycle hooks, which can control various behaviors of the renderer.
A little loopy? That’s ok, you don’t need to know all the parameters yet, but we’ll explain them bit by bit below. You can come back to this one last time.
The host components
React has two Component types: Host Component and CompositeComponent. Host components are provided by the platform. For example, the ReactDOM platform provides div, SPAN, H1… These components are usually strings that are rendered directly as view nodes under the platform.
Composite components, also known as custom components, are used to combine other composite components with host components, usually classes or functions.
The renderer does not need to care about the processing of the composite components; what is handed to the renderer is a tree of host components.
Of course, there are many applets specific host components defined in Remax, for example, we can use them like this:
function MyComp() {
return <view><text>hello world</text></view>
}
Copy the code
The Reconciler calls createInstance and createTextInstance of HostConfig to create instances of the host component, so the custom renderer must implement both methods. See how Remax does it:
const HostConfig = {
// Create a host component instance
createInstance(type: string, newProps: any, container: Container) {
const id = generate();
// Preprocessing props, remax does some special processing for the event type props
const props = processProps(newProps, container, id);
return new VNode({
id,
type,
props,
container,
});
},
// Create a host component text node instance
createTextInstance(text: string, container: Container) {
const id = generate();
const node = new VNode({
id,
type: TYPE_TEXT,
props: null,
container,
});
node.text = text;
return node;
},
// Determine whether child nodes need to be processed. If true is returned, it is not created and the entire sub-tree of components is ignored.
// Some scenarios do not require the creation of text nodes, but are digested internally by the parent node.
For example, in ReactDOM, if dangerouslySetInnerHTML is set for a node, its children should be ignored,
ShouldSetTextContent should return true
shouldSetTextContent(type, nextProps) {
return false}}Copy the code
In the ReactDOM, the above two methods create the host component (the DOM node) through Document. createElement and Document. createTextNode, respectively.
Above is the micro channel small program architecture diagram (image source: together take off the coat of small program – micro channel small program architecture analysis).
Because the applet isolates the renderer process from the logic process. Remax is run on the logical process, in the logical process can not carry out the actual rendering, can only pass the update instruction to the rendering process through setData, and then parse rendering.
So Remax chose to create a Mirror Tree in the logical process and then synchronize it to the rendering process, as shown below:
The above VNode is the virtual node in the image tree. It is mainly used to store some node information without any special processing. Its structure is as follows:
export default class VNode {
id: number; // A unique node ID
container: Container;
children: VNode[]; / / child nodes
mounted = false; // Whether the node has been mounted
type: string | symbol; // Node typeprops? : any;// Props of the node
parent: VNode | null = null; // Parent node referencetext? : string;// If it is a text node, the text content is saved here
path(): Path // The path of the node. After synchronization to the renderer process, restore to the tree via path
// Child node operation
appendChild(node: VNode, immediately: boolean)
removeChild(node: VNode, immediately: boolean)
insertBefore(newNode: VNode, referenceNode: VNode, immediately: boolean)
update() // Triggers synchronization to the renderer
toJSON(): string
}
Copy the code
The full VNode code can be seen here
Mirror tree construction and operation
To build a complete node tree you need to implement HostConfig’s appendChild, insertBefore, removeChild, etc. These methods are easy to understand and therefore don’t need much explanation.
const HostConfig = {
// ...
// Node modification is supported
// Some statically rendered scenes, such as PDF files, can be closed
// When closed, just implement appendInitiaChild
supportsMutation: true.// Used to add child nodes during initialization (first time)
appendInitialChild: (parent: VNode, child: VNode) = > {
parent.appendChild(child, false);
},
// Add child nodes
appendChild(parent: VNode, child: VNode) {
parent.appendChild(child, false);
},
// Insert child nodes
insertBefore(parent: VNode, child: VNode, beforeChild: VNode) {
parent.insertBefore(child, beforeChild, false);
},
// Delete a node
removeChild(parent: VNode, child: VNode) {
parent.removeChild(child, false);
},
// Add nodes to container nodes. In general we do not need to make a special distinction with appendChild
appendChildToContainer(container: any, child: VNode) {
container.appendChild(child);
child.mounted = true;
},
// Insert node to container node
insertInContainerBefore(container: any, child: VNode, beforeChild: VNode) {
container.insertBefore(child, beforeChild);
},
// Remove the node from the container node
removeChildFromContainer(container: any, child: VNode) { container.removeChild(child); }},Copy the code
Node updates
In the previous section, we looked at updates at the tree structure level. Updates are also required when node attributes change or text content changes. We can handle such updates with the following HostConfig configuration:
const HostConfig = {
/** * Update related */
// This is the same as shouldComponentUpdate for the React component if the props are not changed
// If ** returns' null ', the node is not updated, and commitUpdate is not called
prepareUpdate(node: VNode, type: string, oldProps: any, newProps: any) {
oldProps = processProps(oldProps, node.container, node.id);
newProps = processProps(newProps, node.container, node.id);
if(! shallowequal(newProps, oldProps)) {return true;
}
return null;
},
// Update the node
commitUpdate(
node: VNode,
updatePayload: any,
type: string,
oldProps: any,
newProps: any
) {
node.props = processProps(newProps, node.container, node.id);
node.update();
},
// Update the text node
commitTextUpdate(node: VNode, oldText: string, newText: string) {
if(oldText ! == newText) { node.text = newText;// Update the nodenode.update(); }}},Copy the code
Ok, this is also easier to understand. For ordinary node updates, prepareUpdate is first called to determine whether to update, and if non-empty data is returned, the Reconciler puts the node into the Effects chain, and commitUpdate is called during the commit phase to perform the update. Text node updates call commitTextUpdate without further delay.
Side effect submission
The concept of two phases of the React update is very important, and this is also reflected in HostConfig:
const HostConfig = {
It is time for me to submit, "Reconciler said." What do you do before you submit it, right here
For example, ReactDOM stores the selected and focused states of the current DOM document and disables event handling. DOM updates can break these states
prepareForCommit: (a)= > {},
"I have submitted it," Reconciler said
// ReactDOM restores the selected and focused state of the DOM document before the submission
resetAfterCommit: (a)= > {},
// Called in the coordination phase when a node is' created '. If there are child nodes, it is called after all child nodes appendInitialChild has completed
// Returns a Boolean value indicating whether to call commitMount after the commit is complete. In layman's terms, this is to tell the Reconciler that there is something to be done after the current node is "mounted.
// ReactDOM uses this hook to process nodes with autofoucs attributes, automatically retrieving focus on commitMount
finalizeInitialChildren: (a)= > false.The Reconciler is used in conjunction with finalizeInitialChildren. If the former Reconciler returns true, the commitMount of the corresponding nodes is executed after the Reconciler is completed
commitMount: (a)= >{},}Copy the code
Aggregate all the hooks mentioned above and divide them by update phase and application target. Their distribution looks like this:
So when should Remax commit an ‘update’ to the rendering process? The answer is all methods called in the commit phase.
The commit phase is intended to perform a variety of side effects, such as view updates, remote method requests, subscriptions… So Remax also collects update instructions at this stage and pushes them to the renderer in the next loop.
HostConfig execution process summary
To review the flow of custom renderer method calls, first take a look at the mounted flow:
Suppose our component structure looks like this:
const container = new Container()
const MyComp = (a)= > {
return (
<div>
<span>hello world</span>
</div>
)
}
render(
<div className="root">
<MyComp />
<span>--custom renderer</span>
</div>,
container,
() => {
console.log("rendered")},Copy the code
The React component tree looks like this (left), but for the renderer, the tree looks like this (right). Custom components are at the React level, and the renderer only needs to care about the view structure that ultimately needs to be rendered. In other words, the renderer only cares about the host component:
Mounting will go through the following process:
From the flowchart above, you can clearly see when each hook is called.
Similarly, let’s look at the process of node update. Let’s modify the above program slightly so that it triggers periodic updates:
const MyComp = (a)= > {
const [count, setCount] = useState(1)
const isEven = count % 2= = =0
useEffect((a)= > {
const timer = setInterval((a)= > {
// Increment the counter
setCount(c= > c + 1)},10000)
return (a)= > clearInterval(timer)
}, [])
return (
<div className="mycomp" style={{ color: isEven ? "red" : "blue}} ">
{isEven ? <div>even</div> : null}
<span className="foo">hello world {count}</span>
</div>)}Copy the code
Here is the update process:
When MyComp’s count changes from 1 to 2, MyComp is rerendered with a new div node (red dotted box) and Hello World 1 becomes Hello World 2.
The new DIV node creation process is the same as when it was mounted, except that instead of being inserted immediately into the parent node, it is placed in the Effect linked list first and executed at commit time.
Similarly, updates to the Hello World {count} text node and Props updates to other nodes are placed in the effects list and submitted at the last minute. As above, insertBefore, commitTextUpdate, commitUpdate.
The other key is the prepareUpdate hook, where you can tell the Reconciler whether a node needs to be updated, and if it does, return a non-null value so that the commitUpdate is fired.
Sync to the renderer
The React custom renderer is more or less there, and platform-specific things come next. Remax currently does this by passing the update instruction to the renderer process via the setData method of the Page object in the applet. The rendering process restores the update instruction to the tree through WXS mechanism. Finally, the tree is rendered recursively through the template mechanism.
The overall structure is as follows:
Let’s take a look at how the logical process pushes the update command:
// Manage updates on the root container
export default class Container {
// ...
// Trigger the update
requestUpdate(
path: Path,
start: number,
deleteCount: number,
immediately: boolean, ... items: RawNode[] ) {const update: SpliceUpdate = {
path, // Update the tree path of the node
start, // Update the index of the node in children
deleteCount,
items, // Information about the current node
};
if (immediately) {
this.updateQueue.push(update);
this.applyUpdate();
} else {
// Put it into the update queue to collect the update instruction
if (this.updateQueue.length === 0) {
setTimeout((a)= > this.applyUpdate());
}
this.updateQueue.push(update);
}
}
applyUpdate() {
const action = {
type: 'splice'.payload: this.updateQueue.map(update= > ({
path: stringPath(update.path),
start: update.start,
deleteCount: update.deleteCount,
item: update.items[0],})),};// Notify the renderer with setData
this.context.setData({ action });
this.updateQueue = []; }}Copy the code
The logic is pretty clear, push the node that needs to be updated (including node path, node information) into the update queue, and then trigger setData notification to the renderer process.
In the rendering process, WXS mechanism should be used to restore update instructions to the rendering tree:
/ / render tree
var tree = {
root: {
children: [].}};// Apply the directive to the render tree
function reduce(action) {
switch (action.type) {
case 'splice':
for (var i = 0; i < action.payload.length; i += 1) {
var value = get(tree, action.payload[i].path);
if (action.payload[i].item) {
value.splice(
action.payload[i].start,
action.payload[i].deleteCount,
action.payload[i].item
);
} else {
value.splice(action.payload[i].start, action.payload[i].deleteCount);
}
set(tree, action.payload[i].path, value);
}
return tree;
default:
returntree; }}Copy the code
OK, to start rendering, Remax takes the form of a template for rendering:
<wxs src=".. /.. /helper.wxs" module="helper" />
<import src=".. /.. /base.wxml"/>
<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />
Copy the code
Remax generates a template for each component type, dynamically ‘recursively’ rendering the entire tree:
<template name="REMAX_TPL"> <block wx:for="{{tree.root.children}}" wx:key="{{id}}"> <template is="REMAX_TPL_1_CONTAINER" data="{{i: item}}" /> </block> </template> <wxs module="_h"> module.exports = { v: function(value) { return value ! == undefined ? value : ''; }}; </wxs> <% for (var i = 1; i <= depth; i++) { %> <%var id = i; %> <% for (let component of components) { %> <%- include('./component.ejs', { props: component.props, id: component.id, templateId: id, }) %> <% } %> <template name="REMAX_TPL_<%=id%>_plain-text" data="{{i: i}}"> <block>{{i.text}}</block> </template> <template name="REMAX_TPL_<%=id%>_CONTAINER" data="{{i: i}}"> <template is="{{'REMAX_TPL_<%=id%>_' + i.type}}" data="{{i: i}}" /> </template> <% } %>Copy the code
Limited to the rendering mechanism of applets, the following factors may affect rendering performance:
- The process of IPC. Update instructions are notified to the renderer process via IPC, and frequent updates may affect performance. This problem also exists in ReactNative, which involves communication between Native and JS engines. That’s where the miniprogram comes in
WXS
This type of scenario is used to deal with complex view interaction problems, such as animation. In the futureRemax
You need to think about that, too Reconciler
This layer has Diff toRendering process
You might have to do it again and again, right?- For template-based scenarios, will local updates result in page-level re-rendering? How does the performance compare to the native custom components of applets?
conclusion
This article uses Remax as an example to show how a React custom renderer works. For Remax, it is still in the development stage, and many features are not perfect yet. As for the performance, the author is not good to comment, you can see the initial benchmark given by the official. Students with ability can participate in code contribution or Issue discussion.
Finally, I would like to thank Bian Liu for his review and suggestions.
Further reading
- Remax – Build applets using real React
- What is React Fiber
- React Fiber architecture and source code
- Hello World Custom React Renderer – Shailesh – Medium
- ⚛️👆 Part 1/3 – Beginners Guide to Custom React Renderers. How to build your own renderer from scratch? This series is great
- Enigma WXS, Uni-App How to use it to dramatically improve performance
- Uni-app doubles the performance of wechat
- Talk about the running mechanism of small program