Writing in the front

In this article, we will learn how to implement the React SSR API based on the following three questions:

  • How does the React component become an HTML string?
  • How are these strings concatenated and streamed?
  • What exactly did Hydrate do?

How does the React component become an HTML string?

Enter a React component:

class MyComponent extends React.Component { constructor() { super(); this.state = { title: 'Welcome to React SSR! '}; } handleClick() { alert('clicked'); } render() { return ( <div> <h1 className="site-title" onClick={this.handleClick}>{this.state.title} Hello There! </h1> </div> ); }}Copy the code

The ReactDOMServer. RenderToString () after processing output HTML string:

'<div data-reactroot=""><h1 class="site-title">Welcome to React SSR! <! -- --> Hello There! </h1></div>'Copy the code

What happened in between?

First, you create the component instance, execute Render and the life cycle before that, and finally map the DOM elements to HTML strings

Creating a component instance

inst = new Component(element.props, publicContext, updater);
Copy the code

The external updater is injected with the third parameter updater, which is used to intercept operations such as setState:

var updater = { isMounted: function (publicInstance) { return false; }, enqueueForceUpdate: function (publicInstance) { if (queue === null) { warnNoop(publicInstance, 'forceUpdate'); return null; } }, enqueueReplaceState: function (publicInstance, completeState) { replace = true; queue = [completeState]; }, enqueueSetState: function (publicInstance, currentPartialState) { if (queue === null) { warnNoop(publicInstance, 'setState'); return null; } queue.push(currentPartialState); }};Copy the code

This way of intercepting status updates is faster than the previous scheme to maintain the virtual DOM:

In React 16, though, the core team rewrote the server renderer from scratch, and it doesn’t do any vDOM work at all. This means it can be much, much faster.

(What’s New With Server-side Rendering in React 16)

The part that replaces the React built-in updater is in the constructor of the react.componentbase class:

function Component(props, context, updater) {
  this.props = props;
  this.context = context; // If a component has string refs, we will assign a different object later.

  this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the
  // renderer.

  this.updater = updater || ReactNoopUpdateQueue;
}
Copy the code

Rendering component

After getting the initial data (inst.state), execute the component lifecycle functions in turn:

// getDerivedStateFromProps var partialState = Component.getDerivedStateFromProps.call(null, element.props, inst.state); inst.state = _assign({}, inst.state, partialState); // componentWillMount if (typeof Component.getDerivedStateFromProps ! == 'function') { inst.componentWillMount(); } // UNSAFE_componentWillMount if (typeof inst.UNSAFE_componentWillMount === 'function' && typeof Component.getDerivedStateFromProps ! == 'function') { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for any component with the new gDSFP. inst.UNSAFE_componentWillMount(); }Copy the code

Pay attention to the old and new life cycle of a mutex relationship, priority getDerivedStateFromProps, if there is no will implement componentWillMount/UNSAFE_componentWillMount, special, if the two old life cycle function exist at the same time, Both functions are executed in the above order

Next to render, but before that, should first check the updater queue, because componentWillMount/UNSAFE_componentWillMount could create a status update:

if (queue.length) { var nextState = oldReplace ? oldQueue[0] : inst.state; for (var i = oldReplace ? 1:0; i < oldQueue.length; i++) { var partial = oldQueue[i]; var _partialState = typeof partial === 'function' ? partial.call(inst, nextState, element.props, publicContext) : partial; nextState = _assign({}, nextState, _partialState); } inst.state = nextState; }Copy the code

Next to render:

child = inst.render();
Copy the code

And recursively do the same for the child component (processChild) :

while (React.isValidElement(child)) { // Safe because we just checked it's an element. var element = child; var Component = element.type; if (typeof Component ! == 'function') { break; } processChild(element, Component); }Copy the code

Until we encounter a native DOM element (the component type is not function), we “render” the DOM element into a string and print:

if (typeof elementType === 'string') {
  return this.renderDOM(nextElement, context, parentNamespace);
}
Copy the code

“Render” DOM elements

In particular, the props of the controlled component are preprocessed:

// input props = _assign({ type: undefined }, props, { defaultChecked: undefined, defaultValue: undefined, value: props.value ! = null ? props.value : props.defaultValue, checked: props.checked ! = null ? props.checked : props.defaultChecked }); // textarea props = _assign({}, props, { value: undefined, children: '' + initialValue }); // select props = _assign({}, props, { value: undefined }); // option props = _assign({ selected: undefined, children: undefined }, props, { selected: selected, children: optionChildren });Copy the code

To begin formally concatenating strings, create an open label:

Var out = createOpenTagMarkup(element.type, tag, props, namespace, this.makeStaticMarkup, this.stack.length === 1); function createOpenTagMarkup(tagVerbatim, tagLowercase, props, namespace, makeStaticMarkup, isRootElement) { var ret = '<' + tagVerbatim; for (var propKey in props) { var propValue = props[propKey]; // serialize style if (propKey === style) {propValue = createMarkupForStyles(propValue); } // Create the markup attribute var markup = null; markup = createMarkupForProperty(propKey, propValue); (markup) {ret += "+ markup; }} // renderToStaticMarkup() returns a clean HTML tag if (makeStaticMarkup) {return ret; } // renderToString() adds an extra react attribute to the root element data-reactroot="" if (isRootElement) {ret += "+ createMarkupForRoot(); } return ret; }Copy the code

Create a closed tag:

Var footer = ''; if (omittedCloseTags.hasOwnProperty(tag)) { out += '/>'; } else { out += '>'; footer = '</' + element.type + '>'; }Copy the code

And handles child nodes:

/ / text node child, directly to open label var innerMarkup = getNonChildrenInnerMarkup (props); if (innerMarkup ! = null) { out += innerMarkup; } else { children = toArray(props.children); Var frame = {domNamespace: getChildNamespace(parentNamespace, element.type), type: tag, children: children, childIndex: 0, context: context, footer: footer }; this.stack.push(frame); return out;Copy the code

Note that the complete HTML fragment has not yet been rendered (the child nodes have not been rolled out of HTML, so the closed tag cannot be spelled), but the open tag section is fully determined and ready to be printed to the client

2. How are these strings concatenated and streamed?

Render one node at a time until there are no more pending render tasks in the stack:

function read(bytes) { try { var out = ['']; while (out[0].length < bytes) { if (this.stack.length === 0) { break; } var frame = this.stack[this.stack.length - 1]; If (frame.childIndex >= frame.children.length) {var footer = frame.footer; // The current node (rendering task) pushes this.stack.pop(); [this. SuspenseDepth] += footer; continue; } childIndex+ 1 var Child = frame.children[frame.childindex ++]; var outBuffer = ''; OutBuffer += this.render(child, frame.context, frame.domNamespace); } catch (err) { /*... */ } out[this.suspenseDepth] += outBuffer; } return out[0]; } finally { /*... * /}}Copy the code

This fine-grained task scheduling makes it possible to combine and send tasks by streaming. It is similar to React Fiber scheduling mechanism, which is also for small tasks. Fiber scheduling is based on time, while SSR scheduling is based on workload (while (out[0].length < bytes)).

Output block by block at a given target amount of work (bytes) is the basic nature of a stream:

A stream is a collection of data, much like an array or a string. But instead of accessing all the data at once, a stream sends/receives bits and pieces (chunk-like)

The producer’s production mode is already fully compliant with the characteristics of streams, so it only needs to be wrapped as a Readable Stream:

function ReactMarkupReadableStream(element, makeStaticMarkup, options) { var _this; / / create a Readable Stream _this = _Readable. Call (this, {}) | | this; _this.partialRenderer = new ReactDOMServerRenderer(Element, makeStaticMarkup, options); return _this; } var _proto = ReactMarkupReadableStream.prototype; // Override the _read() method, Each read. Specify the size of the string _proto _read = function _read (size) {try {this. Push (this) partialRenderer) read (size)); } catch (err) { this.destroy(err); }};Copy the code

Surprisingly simple:

function renderToNodeStream(element, options) {
  return new ReactMarkupReadableStream(element, false, options);
}
Copy the code

P.S. For non-streaming apis, read(Infinity) once:

function renderToString(element, options) { var renderer = new ReactDOMServerRenderer(element, false, options); try { var markup = renderer.read(Infinity); return markup; } finally { renderer.destroy(); }}Copy the code

What exactly has Hydrate done?

After the component is pumped into the server and “rendered” into HTML, it can render meaningful content directly on the client side, but does not have interactive behavior because the above server rendering process does not handle properties such as onClick (in fact, it deliberately ignores these properties) :

function shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) { if (name.length > 2 && (name[0] === 'o' || name[0] === 'O') && (name[1] === 'n' || name[1] === 'N')) { return true; }}Copy the code

There is also no life cycle after render, and the component is not fully “rendered”. Therefore, the other part of rendering still needs to be done on the client side, and this process is Hydrate

Difference between Hydrate and Render

Hydrate () has exactly the same function signature as Render () and both render components on specified container nodes:

ReactDOM.hydrate(element, container[, callback])
ReactDOM.render(element, container[, callback])
Copy the code

But unlike Render () starting from scratch, Hydrate () happens on top of server render products, so the biggest difference is that the Hydrate process reuses DOM nodes that are already rendered on the server

Node overcommitment Policy

In Hydrate mode, the component rendering process is also divided into two phases:

  • First phase (Render/Reconciliation) : Find existing reusable nodes and attach them to the stateNode of the Fiber node

  • Phase 2 (COMMIT) : diffHydratedProperties determines whether an existing node needs to be updated by looking at whether the Attributes on the DOM node are consistent with the props

That is, you find an existing DOM node that is “potentially reusable” for hydratable at the corresponding location, write it down as a render result, and then try to reuse it during the COMMIT phase

Select existing nodes as follows:

Function updateHostRoot(current, workInProgress, renderRoot) renderLanes) { var root = workInProgress.stateNode; // In hydrate mode, Find the first available child node from the container if (root.hydrate && enterHydrationState(workInProgress)) {var child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); workInProgress.child = child; } } function enterHydrationState(fiber) { var parentInstance = fiber.stateNode.containerInfo; NextHydratableInstance = getFirstHydratableChild(parentInstance); nexThyDratAblechild (parentInstance); hydrationParentFiber = fiber; isHydrating = true; return true; }Copy the code

The selection criteria are element node (nodeType 1) or text node (nodeType 3) :

Function getNextHydratable(node) {for (; node ! = null; node = node.nextSibling) { var nodeType = node.nodeType; if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) { break; } } return node; }Copy the code

After the pre-selected node is rendered to the native HostComponent, the pre-selected node will be attached to the stateNode of the Fiber node:

Function updateHostComponent(current, workInProgress, RenderLanes) {if (current = = = null) {/ / try to reuse existing nodes of primary tryToClaimNextHydratableInstance (workInProgress); }} function tryToClaimNextHydratableInstance (fiber) {/ / remove the primary node var nextInstance = nextHydratableInstance; // Try to reuse tryHydrate(fiber, nextInstance); }Copy the code

Take the element node as an example (text nodes are similar) :

function tryHydrate(fiber, nextInstance) { var type = fiber.type; Var instance = canHydrateInstance(nextInstance, type); // If (instance!) {// If (instance!) {// If (instance! == null) { fiber.stateNode = instance; return true; }}Copy the code

Note that the attributes are not checked for an exact match, but are considered reusable as long as the tag names of element nodes are the same (e.g., div, h1) :

function canHydrateInstance(instance, type, props) { if (instance.nodeType ! == ELEMENT_NODE || type.toLowerCase() ! == instance.nodeName.toLowerCase()) { return null; } return instance; }Copy the code

Attribute consistency checks are performed at the end of the first stage (completeWork), and attribute value correction actually occurs in the second stage:

function completeWork(current, workInProgress, renderLanes) { var _wasHydrated = popHydrationState(workInProgress); / / if there is a match if the existing nodes (_wasHydrated) {/ / check if need to update the attributes (prepareToHydrateHostInstance (workInProgress, RootContainerInstance, currentHostContext) {// Start markUpdate(workInProgress); Else {var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress); appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance; if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) { markUpdate(workInProgress); }}}Copy the code

A consistency check is to check whether the attributes on a DOM node are consistent with the component props.

  • Warning and error correction for different values of text child nodes (use client state to correct server rendering results)
  • otherstyle,classValues such as different warning, not error correction
  • Warning about redundant attributes on DOM nodes

In other words, errors will be automatically corrected only when there is a difference in the content of the text child nodes. For the difference in the number of attributes and values, only a warning is thrown, not corrected. Therefore, in the development stage, we must pay attention to the warning of the rendering result mismatch

P.S. see diffHydratedProperties for details, which has a large amount of code and will not be expanded here

Component rendering process

Like Render, Hydrate executes a full life cycle (including a pre-life cycle that was executed on the server side) :

Var instance = new ctor(props, context); // Execute the pre-lifecycle function //... getDerivedStateFromProps // ... componentWillMount // ... UNSAFE_componentWillMount // render nextChildren = instance.render(); // componentDidMount instance.componentDidMount();Copy the code

So, in terms of client render performance alone, hydrate is about the same amount of work as Render, save creating DOM nodes, setting initial property values, etc

At this point, React SSR’s underlying implementations are all out in the open

The resources

It is good to have gained and confused

Follow the “front end back” wechat official account and you will receive a series of “heart original” high-quality technical articles on topics including but not limited to front-end, Node.js and server-side technologies

This article was originally published on ayqy.net. The original link is www.ayqy.net/blog/react-…