This article is the author’s own technical precipitation and is not intended for readers, so the notes are not as detailed as previous articles. However, this article is complete enough for act15, including the mount phase, the update phase (including diff), and the patching of the real DOM. There is a link at the end of the video for those interested.
1. Implementation principles of React15
1.1. Mount stage
1. Mount virtual DOM elements to generate corresponding component class instances (text component class instances, generic component class instances, synthetic component class instances) according to their types (text component type nodes, native label type nodes, component type nodes)
- These component class instances have a number of property methods, including mount methods for the current node (virtual DOM), update methods, current node properties, and so on
2. Call the component class instance’s getMarkup method (getMarkup) to generate the corresponding node markup (markup: HTML string) return
-
For file component class instances: directly create the corresponding text node tag return
-
For a generic component class instance (the virtual DOM type is a native tag), create a dom node tag based on the node (node: virtual DOM) type, traverse the props of the node, add the props attribute to the node tag, and return the current node tag. The following is the processing procedure for the props
-
For events (in props) : add to document using event delegate
-
For styles (in props) : Create a corresponding style tag based on the style key-value to add to the node tag
-
For className (in props) : Create a class tag to add to the node tag
-
For children (in props) : Iterate over all child nodes, recursively create and get all child node tokens, and add them to the current node token
-
For other properties (in props) : Create a key=value token to add to the current node token
-
-
For synthetic (virtual dom types for the function component | | class components) component class instances:
-
Create an instance of the class component and call the instance render method (passed in props), which returns the render element that is the virtual DOM element to render.
- In the case of a function component, it passes in props directly to execute the function component, whose return value is the virtual DOM that will eventually be rendered
-
Render element component class instance (render element component class instance)
-
Executes if the current class component instance has the componentWillMount method
-
Call the fetch tag method of the Render element component class instance to get the Render element tag
-
If a component class instance has a componentDidMount method, add a subscription to execute that method. This subscription is triggered when all components are mounted to the real DOM, i.e., when the component is mounted, execute the componentDidMount method for all components (if present).
-
Returns the Render element tag
-
3, get the tag of the root node (root node: root virtual DOM) (the sub tag of the root node has been added to the root node recursively), add it to the real DOM, and publish all subscriptions to executing the componentDidMount method.
React.render(<App/>.document.getElementById('root'));
//
: root node
// document.getelementById ('root') : The real DOM to which the root node tag is added
Copy the code
1.2. Update phase
If the new and old root nodes are of different types, a new node will be created to replace the old node. If the new and old root nodes are of the same type, the new and old root nodes will continue to be compared (setState will be called with the root node as the class component to trigger the update description).
ShouldComponentUpdate (); shouldComponentUpdate (); shouldComponentUpdate (); shouldComponentUpdate ()
Render element (” new RENDER element “); render element (” virtual DOM “);
3, compare the class component old Render element (old Render element: the virtual DOM returned by last render), judge whether the old and new render element types are the same, if not, directly create the new RENDER element corresponding to the DOM node to replace the original node
4, the old node is updated by passing the new Render element to the update method of the component class instance of the old Render element
Update the props of the real DOM corresponding to the old node (old node: old Render element)
-
Traverse the props of the old node:
-
The props attribute on the old node that exists on the old node but not in the new node is removed directly from the real DOM corresponding to the old node
-
Cancel all event delegates in the real DOM corresponding to the old node
-
-
Traverse the props of the new node:
-
For events (in the props of the new node) : add to the document using an event delegate
-
For styles (in the new node props) : Compare the old and new node styles and update the old node to the style in the real DOM
-
For className (in the new node props) : Add or overwrite the className property of the new node to the class property of the real DOM corresponding to the old node (if the property already exists in the real DOM, the original property will be overwritten)
-
For other properties (in props of the new node) : Add or overwrite directly to the real DOM corresponding to the old node
-
For children (props in new node) : update separately
-
6, compare the children of the new and old nodes
-
6.1, traverse the children of the new node, find the corresponding key or the old child of the corresponding position in the old node according to the key of the new child (if it exists) or the position of the new child (that is, the index of the child in children). To collect an updated collection of children component class instances, do the following
-
Create an updated children component class instance collection before traversal, initially empty, to hold new and old child component instances updated or created new Child component instances
-
If there is a corresponding old child, the update method of the component class instance of the new child is given to the old child to complete the update of the real DOM corresponding to the old child, and the updated component class instance of the old child is added to the updated children component class instance collection
-
If no corresponding child exists, the component class instance of the new child is directly created and added to the updated children component class instance collection
-
When the traversal is complete, the updated Children component class instance collection will hold every child component class instance corresponding to the new Children element that is new (either through an update to the old Child component class instance or through the creation of a new Child component class instance directly)
-
-
At this point, the real DOM update corresponding to part of the old child has been completed. Note that the update of this part of the old child is limited to the old child with the same key or position as the new child. The rest of the update includes moves, inserts, and deletes:
-
The real DOM corresponding to the incorrectly moved but updated old child: For example, if the location index of the old child is 100 and the location index of the new child is 1, and the key of the old and new child is the same, then the real DOM corresponding to the old child, i.e. the real DOM with index 100 (the location index of the same level), will be updated. The actual DOM node with index 100 also needs to be moved to index 1
-
Insert the real DOM corresponding to the new child: that is, for the new child, the old child does not have the same key or position in the old children. In this case, a new child node needs to be created and inserted into the real DOM
-
Delete the real DOM corresponding to the old child: That is, if no new child has the same key or position in the new children, the real DOM node corresponding to the old child needs to be deleted
-
-
6.2. Use the _mountIndex&&lastIndex algorithm to collect node insertion and deletion operations
-
Several concepts were declared before the collection:
-
LastIndex: The last node to determine its position, starting with 0
-
_mountIndex: the index of the position of each old child in the old children (or the dom node index of each old child in the real DOM)
-
NextIndex: Index of the position of each new child in the new children
-
-
6.2.1, traverse the new children to find the nodes that need to be moved, deleted and inserted and collected
-
If there is an old child with the same key or position as the new child traversed, then check whether the new child is the same as the old child. And the real DOM node corresponding to the old child has been updated, but the location may be wrong)
-
If different:
-
The real DOM node corresponding to the old child needs to be deleted. The location index of the real DOM node is the _mountIndex of the old child
-
Insert the real DOM corresponding to the new child: the position index in the real DOM is the position index of the new child in the new children (i.e. the lastIndex of the new node).
-
-
_mountIndex = _mountIndex;
-
If _mountIndex<lastIndex, then the actual DOM node corresponding to the old child needs to be moved from the old child._mountIndex position to the new child.nextIndex position, and the lastIndex is updated: LastIndex = Max(old child._mountIndex,lastIndex)
-
If _mountIndex<lastIndex, no operation is performed
-
-
-
-
6.2.2, traverse old children to find the nodes to be deleted and collect them:
- If the current old child does not have a new child with the same key or position in the new children, the dom node corresponding to the old child needs to be deleted. The deletion position is the old child._mountIndex
-
1.3 patch stage
Diff operations in the update stage have collected the differences between the old and new DOM trees, namely, moving node operations, inserting node operations and deleting node operations. At this point, we will update the real DOM tree based on these differences, assuming that these differences are stored in patchQueue:
-
1. Delete all dom nodes that need to be moved or deleted from patchQueue from the real DOM according to their fromIndex:
-
1.1. The difference operation records the fromIndex and toIndex of the real DOM node to be moved by the movement operation, that is, the fromIndex position in the real DOM node of the current level is moved to the toIndex position
-
1.2. The difference operation records the fromIndex of the real DOM node to be deleted by the move operation, that is, the fromIndex is deleted from the position of the real DOM node at the current level
-
In fact, in addition to fromIndex and lastIndex position, each difference operation also records other attributes, such as the real DOM node to be operated on, its parent node, operation name (delete, move or insert) and other attributes, which is convenient for us to update the real DOM
-
-
2. Insert all the real DOM nodes to be moved and the real DOM nodes to be inserted into the patchQueue to the toIndex position in the real DOM node of the current hierarchy
-
3. Complete the real DOM tree update
2, the code
Create a basic react application with create-react-app and install jquery.
2.1. Dependencies in file directories and package.json
Since my code is not platform hosted, if you want to get my code, you can do as follows:
-
Create a react app with create-react-app, install jquery, and run your React app
-
2, delete all files in public folder except index. HTML (only div tags with id root are retained)
-
3, delete all files in SRC except index.js
-
4. Create the corresponding JS file in SRC as shown above (copy the file content directly from 1.2 to 1.7).
2.2, the index. Js
import React from './react'
class Todos extends React.Component {
constructor(props) {
super(props)
this.state = { lists: [].text: ' ' }
}
onChange = event= > {
this.setState({ text: event.target.value })
}
handleClick = () = > {
let text = this.state.text
this.setState({
lists: [...this.state.lists, text], text: ' '
})
}
onDel = (index) = > {
this.setState({
lists: [...this.state.lists.slice(0, index), ... this.state.lists.slice(index +1)]})}render() {
let lists = this.state.lists.map((item, index) = > {
return React.createElement('li', {}, item, React.createElement('button', {
onClick: () = > {
this.onDel(index)
}
}, 'X'))})let input = React.createElement('input', { onKeyup: this.onChange, value: this.state.text })
let button = React.createElement('button', { onClick: this.handleClick }, 'add')
return React.createElement('div', {}, input, button,
// React.createElement('ul', {}, ... lists). lists ) } }let element = React.createElement(Todos, { name: 'counter' })
React.render(element, document.getElementById('root'));
Copy the code
2.3, the react. Js
import $ from 'jquery'
import { createUnit } from './unit'
import { createElement } from './element'
import { Component } from './component'
let React = {
render,
createElement,
Component
}
function render(element, container) {
let unit = createUnit(element)
let markUp = unit.getMarkUp('0');
$(container).html(markUp)
$(document).trigger('mounted')}export default React
Copy the code
2.4, element. Js
class Element {
constructor(type, props) {
this.type = type
this.props = props
}
}
function createElement(type, props, ... children) {
props.children = children
return new Element(type, props)
}
export {
Element,
createElement
}
Copy the code
2.5, component. Js
class Component {
constructor(props) {
this.props = props
}
setState(partialState) {
this._currentUnit.update(null, partialState)
}
}
export {
Component
}
Copy the code
2.6,type.js
export default {
MOVE: 'MOVE'./ / move
INSERT: 'INSERT'./ / insert
REMOVE: 'REMOVE' / / delete
}
Copy the code
2.7, unit. Js
import { Element, createElement } from './element'
import $ from 'jquery'
import types from './types'
let diffQueue = []// Difference queue
let updateDepth = 0 // Update level (DOM level)
class Unit {
constructor(element) {
this._currentElement = element
}
getMarkUp() {
throw new Error('This method cannot be called')}}class TextUnit extends Unit {
getMarkUp(reactid) {
this._reactid = reactid
return `<span data-reactid=${reactid}>The ${this._currentElement}</span>`
}
update(nextElement) {
if (this._currentElement ! == nextElement) {this._currentElement = nextElement
$(`[data-reactid="The ${this._reactid}"] `).html(this._currentElement)
}
}
}
class NativeUnit extends Unit {
getMarkUp(reactid) {
this._reactid = reactid
const { type, props } = this._currentElement
let tagStart = ` <${type} data-reactid="The ${this._reactid}"`
let childString = ' '
let tagEnd = ` < /${type}> `
this._renderedChildrenUnits = []
for (let propName in props) {
if (/^on[A-Z]/.test(propName)) {
let eventName = propName.slice(2).toLowerCase()
$(document).delegate(`[data-reactid="The ${this._reactid}"] `.`${eventName}.The ${this._reactid}`, props[propName])
}
else if (propName === 'style') {
let styleObj = props[propName]
let styles = Object.entries(styleObj).map(([attr, value]) = > {
return `${attr.replace(/[A-Z]/g, m => ` -${m.toLowerCase()}`)} : ${value}`
}).join('; ')
tagStart += `style="${styles}"`
}
else if (propName === 'className') {
tagStart += `${propName}="${props[propName]}"`
}
else if (propName === 'children') {
const children = props[propName]
children.forEach((child, index) = > {
const childUnit = createUnit(child)
childUnit._mountIndex = index // Every childUnit has a _mountIndex that points to its position in the parent node
this._renderedChildrenUnits.push(childUnit)
const childMarkUp = childUnit.getMarkUp(`The ${this._reactid}.${index}`)
childString += childMarkUp
})
} else {
tagStart += ` ${propName}=${props[propName]} `}}return tagStart + '>' + childString + tagEnd
}
update(nextElement) {
let oldProps = this._currentElement.props
let newProps = nextElement.props
/ / update the props
this.updateDOMProperties(oldProps, newProps)
// Update children (compare old and new children, find differences, modify dom)
this.updateDOMChildren(nextElement.props.children)
}
updateDOMChildren(newChildrenElements) {
updateDepth++
this.diff(diffQueue, newChildrenElements)
updateDepth--
// updateDepth When traversal is complete, the value is restored to 0, and patches can be installed
if (updateDepth === 0) {
this.patch(diffQueue)
diffQueue = []
}
}
patch(diffQueue) {
let deleteChildren = []
let deleteMap = {}
for (let i = 0; i < diffQueue.length; i++) {
let difference = diffQueue[i]
if (difference.type === types.MOVE || difference.type === types.REMOVE) {
let fromIndex = difference.fromIndex
let oldChild = $(difference.parentNode.children().get(fromIndex))
if(! deleteMap[difference.parentId]) { deleteMap[difference.parentId] = {} } deleteMap[difference.parentId][fromIndex] = oldChild deleteChildren.push(oldChild) } } $.each(deleteChildren,(idx, item) = > $(item).remove())
for (let i = 0; i < diffQueue.length; i++) {
let difference = diffQueue[i]
switch (difference.type) {
case types.INSERT:
this.insertChildAt(difference.parentNode, difference.toIndex, $(difference.markUp))
break;
case types.MOVE:
this.insertChildAt(difference.parentNode, difference.toIndex, deleteMap[difference.parentId][difference.fromIndex])
break;
default: break; }}}insertChildAt(parentNode, index, newNode) {
let oldChild = parentNode.children().get(index)
oldChild ? newNode.insertBefore(oldChild) : newNode.appendTo(parentNode)
}
diff(diffQueue, newChildrenElements) {
Generate the old key-childrenUnit map
let oldChildrenUnitMap = this.getOldChildrenMap(this._renderedChildrenUnits)
// Generate an updated array of newChildrenUnits (pass the new children element to the corresponding old childrenUnit by key, or create a new childrenUnit if no old childrenUnit exists)
let { newChildrenUnitMap, newChildrenUnits } = this.getNewChildren(oldChildrenUnitMap, newChildrenElements)
let lastIndex = 0 // The index of the last position determined
// Walk through the new node (newChildUnit), according to _mountIndex,lastIndex to find the inserted, moved node
for (let i = 0; i < newChildrenUnits.length; i++) {
let newUnit = newChildrenUnits[i]
letnewKey = newChildrenUnits[i]? ._currentElement? .props? .key ?? i.toString()let oldChildUnit = oldChildrenUnitMap[newKey]
if (oldChildUnit === newUnit) { // If the new node is the same as the old node, the old node is reused
if (oldChildUnit._mountIndex < lastIndex) {
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="The ${this._reactid}"] `),
type: types.MOVE,
fromIndex: oldChildUnit._mountIndex,
toIndex: i
})
}
lastIndex = Math.max(oldChildUnit._mountIndex, lastIndex)
} else {
if (oldChildUnit) {
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="The ${this._reactid}"] `),
type: types.REMOVE,
fromIndex: oldChildUnit._mountIndex,
})
this._renderedChildrenUnits = this._renderedChildrenUnits.filter(item= >item ! == oldChildUnit) $(document).undelegate(`.${oldChildUnit._reactid}`)
}
diffQueue.push({
parentId: this._reactid,
parentNode: $(`[data-reactid="The ${this._reactid}"] `),
type: types.INSERT,
toIndex: i,
markUp: newUnit.getMarkUp(`The ${this._reactid}.${i}`)
})
}
newUnit._mountIndex = i
}
// Walk through the old node (oldChildUnit) to find the old node to delete
for (let oldKey in oldChildrenUnitMap) {
let oldChild = oldChildrenUnitMap[oldKey]
if(! newChildrenUnitMap.hasOwnProperty(oldKey)) { diffQueue.push({parentId: this._reactid,
parentNode: $(`[data-reactid="The ${this._reactid}"] `),
type: types.REMOVE,
fromIndex: oldChild._mountIndex,
})
this._renderedChildrenUnits = this._renderedChildrenUnits.filter(item= >item ! == oldChild) $(document).undelegate(`.${oldChild._reactid}`)}}}getNewChildren(oldChildrenUnitMap, newChildrenElements) {
let newChildrenUnits = []
let newChildrenUnitMap = {}
newChildrenElements.forEach((newElement, index) = > {
letnewKey = newElement? .props? .key ?? index.toString()let oldUnit = oldChildrenUnitMap[newKey]
letoldElement = oldUnit? ._currentElementif (shouldDeepCompare(oldElement, newElement)) {
oldUnit.update(newElement)
newChildrenUnits.push(oldUnit)
newChildrenUnitMap[newKey] = oldUnit
} else {
let nextUnit = createUnit(newElement)
newChildrenUnits.push(nextUnit)
newChildrenUnitMap[newKey] = nextUnit
this._renderedChildrenUnits[index] = nextUnit
}
})
return { newChildrenUnitMap, newChildrenUnits }
}
getOldChildrenMap(childrenUnit = []) {
let map = {}
for (let i = 0; i < childrenUnit.length; i++) {
letkey = childrenUnit[i]? ._currentElement? .props? .key || i.toString() map[key] = childrenUnit[i] }return map
}
updateDOMProperties(oldProps, newProps) {
let propName
// Go over the old props
for (propName in oldProps) {
// If the old props property does not exist in the new props, remove the old props property from the DOM
if(! newProps.hasOwnProperty(propName)) { $(`[data-reactid="The ${this._reactid}"] `).removeAttr(propName)
}
// Cancel the event delegate in the old props property
if (/^on[A-Z]/.test(propName)) {
$(document).undelegate(`.The ${this._reactid}`)}}// Traverse the new props
for (propName in newProps) {
// children are not processed
if (propName === 'children') {
continue
}
// Add the new props event to the real DOM
else if (/^on[A-Z]/.test(propName)) {
let eventName = propName.slice(2).toLowerCase()
$(document).delegate(`[data-reactid="The ${this._reactid}"] `.`${eventName}.The ${this._reactid}`, newProps[propName])
}
// Add the class attribute to the real DOM
else if (propName === 'className') {$(`[data-reactid="The ${this._reactid}"] `).attr('class', newProps[propName])
}
// Add style attributes to the real DOM
else if (propName === 'style') {
let styleObj = newProps[propName]
Object.entries(styleObj).map(([attr, value]) = >{$(`[data-reactid="The ${this._reactid}"] `).css(attr, value)
})
}
// Add additional attributes to the real DOM Chinese
else{$(`[data-reactid="The ${this._reactid}"] `).prop(propName, newProps[propName])
}
}
}
}
class CompositeUnit extends Unit {
// This handles component update operations
update(nextElement, partialState) {
// this._currentelement updates to the newly passed virtual Dom element (if any)
this._currentElement = nextElement || this._currentElement
State = nextState; // Update the current component state (component instance state is immutable, so update the component instance state directly).
let nextState = Object.assign(this._componentInstance.state, partialState)
// Update the current component props, that is, the props of the newly passed virtual DOM element
let nextProps = this._currentElement.props
// If the component method shouldComponentUpdate (passed nextProps, nextState) returns false, it is not updated
if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
return
}
// Get the unit of the previous render element
let preRenderedUnitInstance = this._renderedUnitInstance
// Get the virtual DOM of the previous Render element
let preRenderedElement = preRenderedUnitInstance._currentElement
// Get the virtual DOM of the current render element
let nextRenderedElement = this._componentInstance.render()
// If the old and new elements have the same type, continue to compare, if not, directly kill the old element, create a new element
if (shouldDeepCompare(preRenderedElement, nextRenderedElement)) {
/ / if can deep comparison, the update operation to the previous render element unit (preRenderedUnitInstance. Update) for processing
preRenderedUnitInstance.update(nextRenderedElement)
this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
} else {
this._renderedUnitInstance = createUnit(nextRenderedElement)
let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._reactid)
$(`[data-reactid="The ${this._reactid}"] `).replaceWith(nextMarkUp)
}
}
getMarkUp(reactid) {
this._reactid = reactid
let { type: component, props } = this._currentElement
let componentInstance = this._componentInstance = new component(props)
// make the component's currentUnit equal to the currentUnit
componentInstance._currentUnit = this
// If there is a component that will render the function to execute
componentInstance.componentWillMount && componentInstance.componentWillMount()
// Call the component render method to get the element to render
let renderedElement = componentInstance.render();
// Get the unit of the render element
let renderedUnitInstance = this._renderedUnitInstance = createUnit(renderedElement)
// Get its HTML markup with renderedUnit
let renderedMarkUp = renderedUnitInstance.getMarkUp(this._reactid)
// If the component instance has the componentDidMount method, add the method subscription, which is triggered after the component is mounted to the real DOM
$(document).on('mounted'.() = > componentInstance.componentDidMount && componentInstance.componentDidMount())
return renderedMarkUp
}
}
function createUnit(element) {
if (typeof element === 'string' || typeof element === 'number') {
return new TextUnit(element)
}
if (element instanceof Element && typeof element.type === 'string') {
return new NativeUnit(element)
}
if (element instanceof Element && typeof element.type === 'function') {
return new CompositeUnit(element)
}
}
// Determine whether the two elements are of the same type
function shouldDeepCompare(oldElement, newElement) {
if(oldElement ! = =null&& newElement ! = =null) {
let oldType = typeof oldElement
let newType = typeof newElement
if ((oldType === 'string' || oldType === 'number') && newType === 'string' || newType === 'number') {
return true
}
if (oldElement instanceof Element && newElement instanceof Element) {
return oldElement.type === newElement.type
}
}
return false
}
export {
createUnit
}
Copy the code
Thanks for reference:
- Mount Everest architecture from zero handwriting React framework 课时 1
- Mount Everest architecture from zero handwriting React framework 课时 2
- Mount Everest architecture from zero handwriting React framework 课时 3
- Mount Everest architecture from zero handwriting React framework 课时 4
- Mount Everest architecture from zero handwriting React framework class 5
- Mount Everest architecture from zero handwriting React framework 课时 6