React framework Luy is a React framework that uses exactly the same things as React. My goal was to create a framework exactly like React.


Project address: Foveluy/Luy


React 16 (before React 16) architecture · Issue #9 · Foveluy/Luy


Directory:

  • Luy (pre-React 16) architecture
  • twocreateElement: The beginning of everything
    • document.createElement
    • React.createElement
    • React.createElement function
  • Building the Virtual DOM
    • mountComponent
    • componentDidCatch
    • Mount Other nodes
  • Component updates
    • updateComponent
    • Trigger the React event
    • React 16 Features
  • That’s the end of this article



Luy (pre-React 16) architecture


This article mainly explains, clarifications and reviews the architecture before Luy, in order to facilitate the comparison between the post-refactoring and pre-refactoring, the content is more, as a review of myself.



twocreateElement: The beginning of everything


document.createElement


A few years ago, React didn’t actually use the Document. createElement API to create DOM nodes. Instead, React used innerHTML to create DOM nodes.


The reason for doing this is because document.createElement is much faster than innerHTML. This change gives React an even bigger performance boost.


On the official blog, we can see the official answer.


Using document.createElement is also faster in modern browsers and fixes a number of edge cases >related to SVG elements and running multiple copies of React on the same page.


React.createElement


If it weren’t for the public library authors, I think business programmers would rarely use this API to do things, and instead, people would use JSX instead. React created a syntactic sugar called JSX to replace the createElement call.

The Babel plugin that converts JSX to React. CreateElement is called:


"transform-react-jsx",
{
  "pragma": "React.createElement"
}Copy the code



Pragma is the function we converted JSX to, and react. createElement is its default value. If you change it to:


"transform-react-jsx",
{
   "pragma": "dom"
}Copy the code

Then the corresponding JSX will become:

<div>1</div>
        |
        |
        v
dom('div',{},1)Copy the code



React.createElement function


createElement(type, config, … Children), the main function of this function is to construct a Vnode. All DOM nodes will be corresponding to each Vnode. Whether you are a virtual DOM node, or a virtual component, or a virtual stateless component, they will be unified into a Vnode by Luy.


After this function completes, it returns the Vnode.

function Vnode(type, props, key, Ref) {this.owner = currentowner. cur // This is used to bind the object's type this.props = props // attribute this.key = Key // key for diff this.ref = ref // ref}Copy the code


However, the Vnode has been changed to a Fiber structure since React 16. Many of its properties are different, but the meaning remains the same: it is a virtual DOM node.


Building the Virtual DOM


Building the virtual DOM is actually done through the render function under the Luy/vdom.js code. This function is reactdom.render, which we often use. One of the secrets of this function is that it only updates, not reloads, nodes that are already bound. The reason for this is that

  • In order to implement the server-side rendering waterflooding process
  • Use redux

The code is very simple, just make a judgment. For the same DOM node, run render twice, first mount and second update.


if (typeNumber(container) ! == 8) { throw new Error('Target container is not a DOM element.') } const UniqueKey = container.UniqueKey if (container.UniqueKey) {// Already rendered const oldVnode = containerMap[UniqueKey] const rootVnode = update(oldVnode, Vnode, Container) runException() return vnode._instance} else {// Vnode.istop = true container.UniqueKey = mountIndexAdd() containerMap[container.UniqueKey] = Vnode renderByLuy(Vnode, container, false, Vnode.context, Vnode.owner) runException() return Vnode._instance }Copy the code



Begin to build

The process of starting to build the virtual DOM is essentially a tree traversal. The easiest way to do this is recursively. In Luy, this is at a pace like this:


RenderByLuy () | | | | primary node document. The createElement method () | / v/virtual component (stateful components, stateless components) based on node information - > mountComponent () -- - > MountChild () render child node ----> Continue traversing \ \ text node mountTextComponent() according to child nodeCopy the code


In fact, whenever you have a tree structure, it’s a traversal, and you can recursively solve the problem.


mountComponent


The mountComponent function does a little too much

  1. Create the virtual component New action to get an instance
  2. The instance determines if there is a Render method, and if there is no render method, this is a stateless component
  3. Passing the context (actually, this is no longer done in the new version)
  4. runinstance.componentWillMountDeclare periodic function
  5. runrenderFunction. In the latest React 16, all lifecycle functions are called by acatchErrorThis function implements acomponentdidcatchThis function. The implementation of this function is extremely difficult, as I’ll say in a moment.
  6. renderBefore the operation, we need to record the current instance, which is for us to implement ref. The process of constructing ref is also very difficult and complicated, in order to ensure that eachrenderThe ref created by the function is the same as the instance binding of the current component, and we must record the instance as soon as it is generated.renderWe’ll delete it right after that.
  7. renderThe delta function is going to produce and it’s going to produce a bunch of delta functionsVnodeAfter we get these VNodes, we’ll go backrenderByLuyFor recursive creation and insertion.
  8. Determine whether or notPortal nodeBecause theportalNodes need to bubble up the virtual DOM, so you need to dynamically create a node as a placeholder here.
  9. runcomponentDidMount, were alsocatchErrorThe parcel
  10. Check if there are setState operations in all previous steps, and if so, execute all setState operations at once.



componentDidCatch


What makes this function so difficult is that it not only needs to implement Javascript’s catch function, but also needs to simulate the error stack and the number of lines in the error file (dev). If you’ve ever used componentDidCatch, you know that hanging components wrapped around this component, they’re going to go back based on the tree that the user is mounting.

To do this, and ensure the order is correct, it is very difficult and lengthy, see the source code to know, when we capture all the mistakes will under the border (error) node is deleted, each error nodes, will deal with only one error, if error bounds out his error, then will be up to the boundary of the above error node for processing.

To implement such a complex logic, Luy abstracts out a runException function. The runException call is also very secretive and does not run until it is mounted or updated.



Mount Other nodes

The mountNativeElement and mountTextComponent both do the same thing, installing the real DOM and the literal node.



Component updates

Component updates are the essence of React. These guys have been doing this for so long, they’ve been doing this. React implements an extremely complex set of database-like transactions for high-performance updates.


In simple theory, all update operations are packed into an array within a single transaction, and when the transaction ends (all callback functions have been executed), the update is done once. This is called Debounce, delay.


The consequence of this delay is that setState appears to be asynchronous, but the asynchrony is not really asynchronous, but rather a Nexttick-like callback that focuses all tasks at the end of an event loop.

In luy/component.js, we see the setState function:


If (this.lifecycle === com.create) {// Component mount period} else {// Component update period if (this.lifecycle === com.updating) {return} if (this lifeCycle. = = = Com MOUNTTING) {/ / componentDidMount calls setState enclosing stateMergeQueue. Push (1) the return} the if (this. LifeCycle = = = Com. CATCHING) {/ / componentDidMount calls setState enclosing stateMergeQueue. Push (1) the return} the if (options.async === true) {// call let dirty = options.dirtyComponent[this._uniqueId] if (! Dirty) {options.dirtyComponent[this._uniqueId] = this} return} // Not called in lifecycle, possibly asynchronously called this.updatecomponent ()}Copy the code

This function mostly returns, not updates. Only in some asynchronous cases do updates occur after the transaction has been removed.


updateComponent


This function is the core of the update, so the point that triggers this function is in two:

  1. Not called in the life cycle, possibly asynchronous (setTimeout, etc.)
  2. This is checked after each event is triggereddirtyComponentAfter each event is triggered, the user may set setState when the user setssetStateIn the future, do two things: put all the setState content in a queue and mark the virtual component as dirty
  3. After the event callback is complete, checkdirtyComponentFor each of those dirty elements, and then for each of those dirty elementsupdateComponentOperation.
  4. When all the updates are complete,DirtyComponent = {}// Clear

This code is available in luy/ mapprops.js


Trigger the React event


React events are triggered in a very strange way because it implements its own event triggering system. In fact, the principle is very simple, all the events registered in a unified document


function addEvent(domNode, fn, eventName) {
  if (domNode.addEventListener) {
    domNode.addEventListener(eventName, fn, false)
  } else if (domNode.attachEvent) {
    domNode.attachEvent('on' + eventName, fn)
  }
}Copy the code



We can see here that React does a compatible set of attachEvent for the more stupid IE


And the path that’s triggered is like this, because you’re registered with a document, like a click, it’s going to trigger an event wherever you click, right


Register events to document, Back off function is dispatchEvent click trigger | | v | v document will generate an event object | v through the target of the event object (dom) back out a path path | v Later, the large loop triggers all callback functions on triggerEventByPathCopy the code



Event tracebacks rely heavily on the parent property of the real DOM, so you must be very familiar with the DOM.

All of this code can be seen in luy/ mapprops.js


updateChildren


This algorithm has always been a bit of a loser in React. In Luy, I used another well-known virtual DOM algorithm that wasn’t the fastest, but was the best to understand.


For the child nodes of the same layer, SNabbDOM mainly has deletion and creation operations. Meanwhile, it achieves the purpose of maximizing reuse of existing nodes by shifting them. Four indexes need to be maintained, which are as follows: OldStartIdx => oldEndIdx => old tail index newStartIdx => new head index newEndIdx => new tail index Then start to compare the old and new child node groups one by one until each child node group is traversed. There are 5 comparison strategies: OldStartVnode and newStartVnode are compared. If they are similar, patch will be performed, and then the old and new header indexes will be moved back. OldEndVnode and newEndVnode are compared. Then oldStartVnode and newEndVnode are moved forward for comparison. If they are similar, patch will be performed to shift the old node to the last and the new node is [1,2,3,4,5]. If this judgment is lacking, Insert 5->1,1->2,2->3,3->4,4->5 [5,1,2,3,4] -> [1,5,2,3,4] -> [1,2,5,3,4] -> [1,2,3,5,4] -> [1,2,3,5,4] -> [1,2,3,4,5] OldEndVnode compares newStartVnode to oldEndVnode and moves it to the left. If all the above cases fail, we can only reuse nodes with the same key. First we create a key-index mapping with createKeyToOldIdx. If the new node does not exist in the old node, we insert it in front of the old header indexer, and then the new header index backwards. If the new node in the existing in the old node group, find the corresponding first old node, then patch, and sets the corresponding node in the old node group to undefined, representatives have traversal, no longer traversal, otherwise there may be the problem of repeated injection, before finally nodes will shift to the old head index, a new index head backward After traversing, /** ** @param parentElm parent node * @param oldCh Array of old nodes * @param newCh Array of new nodes * @param insertedVnodeQueue */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) {// Diff update the old and new indexed nodes, Thus achieve the effect of multiplexing nodes patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); OldStartVnode = oldCh[++oldStartIdx]; NewStartVnode = newCh[++newStartIdx]; } // If the old tail index is similar to the new tail index, PatchVnode (oldEndVnode, newEndVnode) else if (sameVnode(oldEndVnode, newEndVnode)) { insertedVnodeQueue); // oldEndVnode = oldCh[--oldEndIdx]; NewEndVnode = newCh[--newEndIdx]; newEndVnode = newCh[--newEndIdx]; } // if the old indexed node is similar to the new indexed node, it can be used again by moving it, such as the old node is [5,1,2,3,4] and the new node is [1,2,3,4,5]. Insert 5->1,1->2,2->3,3->4,4->5 / / will appear - > 5,1,2,3,4 】 【,5,2,3,4 【 1 】 - > - > 1,2,5,3,4 】 【,2,3,5,4 [1] - > [1, 2, 3, 4, 5] / / a total of 4 times the operation, if with this judgment, Else if (sameVnode(oldStartVnode, newEndVnode)) {// Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; Else if (sameVnode(oldEndVnode, newStartVnode)) {Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } // If none of the above criteria is true, then we need the key-index table to maximize reuse. Else {// If there is no old key-index table, then we need the key-index table to maximize reuse. If (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); // Find the location of the new node in the old node group idxInOld = oldKeyToIdx[newStartvNode.key]; // If the new node does not exist in the old node, we insert it in front of the old header index node, If (isUndef(idxInOld)) {// New Element api.insertbefore (parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; ElmToMove = oldCh[idxInOld]; elmToMove = oldCh[idxInOld]; // Update patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); OldCh [idxInOld] = undefined; oldCh[idxInOld] = undefined; InsertBefore (parentElm, elmtomove.elm, oldStartvNode.elm); NewStartVnode = newCh[++newStartIdx]; }} // If the old header index is greater than the old tail index, the old node group has been traversed. If (oldStartIdx > oldEndIdx) {before = isUndef(newCh[newEndIdx+1])? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } // If the new node group is traversed first, then the remaining nodes in the old node group are not needed. Else if (newStartIdx > newEndIdx) {removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }}Copy the code



The code above shows why React requires a key, and that key cannot be the root cause of an index.


React 16 Features

  1. Returns arbitrary nodes, such as arrays, strings, etc. This one isn’t too difficult to implement, it just takes a few judgments
  2. The difficulty of creating createPortal is the bubble of the virtual DOM, which requires the creation of an empty node in the virtual DOM
  3. ComponentDidcatch is the most difficult function to implement. Fiber is implemented, but react 15 is not implemented.


That’s the end of this article

  • This article is a bit of a review of my Luy architecture. The points are very scattered, so I have gathered them together for future review.
  • Luy has recently been reconfigured to study the Fiber architecture of React. The study of the Fiber architecture is extremely useful for front-end performance optimization
  • If you want to write react yourself, look at the previous article. I’ve fixed the example


Founder made Luy 1 Founder made Luy 2