Virtual DOM and Diff algorithm
1. What exactly is JSX
React: JSX, JSX, JSX It’s an extension of JavaScript syntax that React uses to describe what the user interface looks like. Although it looks a lot like HTML, it is JavaScript. Before the React code executes, Babel compiles JSX to the React API.
<div className="container">
<h3>Hello React</h3>
<p>React is great </p>
</div>
Copy the code
React.createElement(
"div",
{
className: "container"
},
React.createElement("h3", null, "Hello React"),
React.createElement("p", null, "React is great")
);
Copy the code
In comparison, the JSX syntax was created to make it easier for React developers to write user interface code.
Babel REPL
2. DOM operation problems
Manipulating the DOM with JavaScript is essential in modern Web applications, but unfortunately it is much slower than most other JavaScript operations.
Most JavaScript frameworks make this slow operation worse by updating the DOM far more than they have to.
For example, if you have a list of ten items and you only change the first item in the list, most JavaScript frameworks will rebuild the entire list, which is ten times more work than is necessary.
React introduced something called the Virtual DOM to solve this problem. The Virtual DOM was invented to improve the efficiency of JavaScript in manipulating DOM objects.
3. What is Virtual DOM
In React, each DOM object has a Virtual DOM object, which is a JavaScript representation of the DOM object. In fact, JavaScript objects are used to describe information about the DOM object, such as what type the DOM object is. What properties does it have, what child elements does it have.
A Virtual DOM object can be thought of as a copy of a DOM object, but it cannot be displayed directly on the screen.
<div className="container">
<h3>Hello React</h3>
<p>React is great </p>
</div>
Copy the code
{
type: "div",
props: { className: "container" },
children: [
{
type: "h3",
props: null,
children: [
{
type: "text",
props: {
textContent: "Hello React"
}
}
]
},
{
type: "p",
props: null,
children: [
{
type: "text",
props: {
textContent: "React is great"
}
}
]
}
]
}
Copy the code
4. How does Virtual DOM improve efficiency
Pinpoint the DOM objects that have changed and update only the parts that have changed.
After React creates a DOM object for the first time, it creates a Virtual DOM object for each DOM object. React updates all Virtual DOM objects before DOM objects are updated. React then compares the updated Virtual DOM to the pre-updated Virtual DOM to find out what has changed. React updates the changed part to the real DOM object. React updates only the necessary part.
Updates and comparisons of Virtual DOM objects occur only in memory and do not render anything in the view, so the performance cost for this part is negligible.
<div id="container">
<p>Hello React</p>
</div>
Copy the code
<div id="container">
<p>Hello Angular</p>
</div>
Copy the code
const before = {
type: "div",
props: { id: "container" },
children: [
{
type: "p",
props: null,
children: [
{ type: "text", props: { textContent: "Hello React" } }
]
}
]
}
Copy the code
const after = {
type: "div",
props: { id: "container" },
children: [
{
type: "p",
props: null,
children: [
{ type: "text", props: { textContent: "Hello Angular" } }
]
}
]
}
Copy the code
5. Create a Virtual DOM
Before the React code executes, JSX is converted by Babel to a call to the react. createElement method, which passes in the element type, element attributes, and element children. The createElement method returns the constructed Virtual DOM object.
{
type: "div",
props: null,
children: [{type: "text", props: {textContent: "Hello"}}]
}
Copy the code
/ * * * create Virtual DOM * @ param * @ {string} type type param {object | null} props attribute * @ param {createElement method []} children child elements * @return {object} Virtual DOM */ function createElement (type, props, ... children) { return { type, props, children } }Copy the code
All child elements begin with the third argument to the createElement method. Children puts all the children elements into the children array.
Const virtualDOM = (<div className="container"> <h1> hello Tiny React</h1> <h2> </div> <h3> This will be changed) < / h3 > {= = 1 & 2 & < div > if 2 and 1 is equal to render the current content < / div >} {2 = = 2 && < div > < / div > 2} < span > this is a paragraph < / span > < button onClick = {() = > </button> <h3> this will be deleted </h3> 2, 3 </div>) console.log(virtualDOM)Copy the code
Through the above code test, we found that there are some problems in the returned Virtual DOM. The first problem is that the text node is directly put into the array
The expectation is that the text node should look like this
children: [
{
type: "text",
props: {
textContent: "React is great"
}
}
]
Copy the code
The following code is used to reconstruct the Virtual DOM.
// Make a copy of the original children and do not operate on the original array
constchildElements = [].concat(... children).map(child= > {
// Determine if child is an object type
if (child instanceof Object) {
// Do not need to do anything directly to return
return child
} else {
// Call the createElement method manually to convert the text to a Virtual DOM if it is not an object or text
return createElement("text", { textContent: child })
}
})
return {
type,
props,
children: childElements
}
Copy the code
By observing the returned Virtual DOM, the text node has been converted to the Virtual DOM of object type, but booleans are also converted as text nodes. In JSX, if the Virtual DOM is converted to Boolean or NULL, Should not be updated into the real DOM, so the next thing to do is clear the Virtual DOM of booleans and NULL.
Const childElements = [].concat(...) const childElements = [].concat(... Children). Reduce ((result, child) => {// Determine the type of the child element by eliminating null true false if (child! = null && child ! = false && child ! = true) { if (child instanceof Object) { result.push(child) } else { result.push(createElement("text", { textContent: Child}))}} return result}, [])Copy the code
In the React component, you can get the child elements through props. Children, so you also need to store the child elements in the props object.
return {
type,
props: Object.assign({ children: childElements }, props),
children: childElements
}
Copy the code
6. Render the Virtual DOM object as a DOM object
You can update a Virtual DOM object to a real DOM object by calling the Render method.
Before the update, it is necessary to determine whether there is an old Virtual DOM. If there is, it is necessary to compare the differences. If not, the Virtual DOM can be directly converted into a DOM object.
For now, we only consider the case where the old Virtual DOM does not exist, that is, we update the Virtual DOM object directly to the real DOM object.
// render.js export default function render(virtualDOM, container, OldDOM = container. FirstChild) {diff(virtualDOM, container, oldDOM)}Copy the code
// diff.js import mountElement from "./mountElement" export default function diff(virtualDOM, container, OldDOM) {// Check whether oldDOM exists if (! OldDOM) {// Convert the Virtual DOM to real DOM mountElement(virtualDOM, container)}}Copy the code
The Component VS Native Element class of the Virtual DOM needs to be determined before the virtual DOM transformation.
Different types need to do different processing if it is a Native Element directly converted.
If it’s a component, you also need to get the component instance object and use the component instance object to get the virtual DOM that the component returns and then convert it.
For now, only the Native Element is considered.
// mountElement.js import mountNativeElement from "./mountNativeElement" export default function MountElement (virtualDOM, container) {// Convert NativeElement mountNativeElement(virtualDOM, container) by calling mountNativeElement. container) }Copy the code
// mountNativeElement.js
import createDOMElement from "./createDOMElement"
export default function mountNativeElement(virtualDOM, container) {
const newElement = createDOMElement(virtualDOM)
container.appendChild(newElement)
}
Copy the code
// createDOMElement.js import mountElement from "./mountElement" import updateElementNode from "./updateElementNode" Export default function createDOMElement(virtualDOM) {let newElement = null if (virtualdom.type === "text") {// Create a text node NewElement = document. CreateTextNode (virtualDOM. Props. TextContent)} else {newElement = / / create the element node Document.createelement (virtualdom.type) // Update element attribute updateElementNode(newElement, VirtualDOM)} / / recursive rendering node virtualDOM children. ForEach (child = > {/ / because uncertain elements of the stator is NativeElement or Component invocation MountElement (child, newElement)}) return newElement}Copy the code
7. Add attributes to the element node
If (virtualdom.type === "text") {// Create a text node and set the content of the node newElement = Document. CreateTextNode (virtualDOM. Props. TextContent)} else {/ / based on Virtual DOM newElement = the type attribute value to create DOM elements Document.createelement (virtualdom.type) // Set the attribute for the element updateElementNode(newElement, virtualDOM)}Copy the code
export default function updateElementNode(element, VirtualDOM) {// Get the property object in the virtualDOM object to be parsed const newProps = virtualdom.props // Put the property names in the property object into an array and loop through the array Object. Keys (newProps).foreach (propName => {const newPropsValue = newProps[propName] // Whether the property name starts with on is an event property onClick -> click if (propName.slice(0, 2) === "on") { const eventName = propName.toLowerCase().slice(2) element.addEventListener(eventName, NewPropsValue) / / if the property name is the value or checked to add in the form of []} else if (propName = = = "value" | | propName = = = "checked") { Element [propName] = newPropsValue // Remove children because it is a child and not an attribute} else if (propName! If (propName === "className") {if (propName === "children") {if (propName === "className") { Element.setattribute ("class", newPropsValue)} else {// Common element.setAttribute(propName, newPropsValue)}})}Copy the code
8. Render components
8.1 Functional Components
The first thing to make clear before rendering a component is that its Virtual DOM type value is a function, and this is true for both function components and class components.
// Original component const Heart = () => <span>♥ </span>Copy the code
<Heart />
Copy the code
// Component Virtual DOM {type: f function() {}, props: {} children: []}Copy the code
When rendering components, first distinguish Component from Native Element. If it is a Native Element, you can directly start rendering; if it is a Component, special processing.
// mountElement.js export default function mountElement(virtualDOM, Container) {// Both class components and function components are essentially functions. // If the Virtual DOM type attribute is a function, the current Virtual DOM is the component if (isFunction(virtualDOM)) {// If the component calls the mountComponent method to render the component mountComponent(virtualDOM, container) } else { mountNativeElement(virtualDOM, Container)}} // Whether Virtual DOM is a function type export function isFunction(virtualDOM) {return virtualDOM && typeof virtualDOM.type === "function" }Copy the code
The mountComponent method separates function components and types and handles them separately.
// mountComponent.js import mountNativeElement from "./mountNativeElement" export default function mountComponent(virtualDOM, Let nextVirtualDOM = null // Distinguish between functional components and class components if (isFunctionalComponent(virtualDOM)) {// Function components call the buildFunctionalComponent method to handle function components nextVirtualDOM = If (isFunction(nextVirtualDOM)) {// buildFunctionalComponent(virtualDOM)} else {// buildFunctionalComponent(virtualDOM) If it is a component, continue calling mountComponent to dissect mountComponent(nextVirtualDOM, Container)} else {// If it is a Navtive Element render mountNativeElement(nextVirtualDOM, Container)}} // Whether the Virtual DOM is a functional component // There are two conditions: 1. The type attribute of the Virtual DOM is a function. 2. Export function isFunctionalComponent(virtualDOM) {const type = virtualDOM && virtualDOM.type return ( type && isFunction(virtualDOM) && ! (type.prototype && type.prototype.render))} function buildFunctionalComponent(virtualDOM) When the component function is called, pass the props property of the Virtual DOM object to the component function so that the component can get data from the props property return virtualDOM && virtualDOM.type(virtualDOM.props || {}) }Copy the code
8.2 class components
The class component itself is also a Virtual DOM, and you can determine whether the component to be rendered is a class component or a function component using the type attribute value in the Virtual DOM.
After determining the current component to render as a class component, it is necessary to instantiate the class component to obtain the class component instance object, and invoke the Render method in the class component through the class component instance object to obtain the Virtual DOM to be rendered by the component.
Class components need to inherit from the Component parent class. The subclass needs to pass its props attribute to the Component parent class using the super method. The parent class mounts the props attribute as its parent class. The advantage of this is that when the props is updated, the parent class can help the subclass update the view based on the updated props.
Suppose the following code is the class component we want to render:
Class Alert extends TinyReact.Com constructor(props) {constructor(props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com Props is just an argument to the constructor function. // The advantage of passing props to the parent class is that the parent class helps update the props update the component view when the props changes. title: "default title" } } render() { return ( <div> <h2>{this.state.title}</h2> <p>{this.props.message}</p> </div> ) } } TinyReact.render(<Alert message="Hello React" />, root)Copy the code
// component.js implements export default class Component {constructor(props) {this.props = props}}Copy the code
The mountComponent method calls buildStatefulComponent to get the Virtual DOM that the class component will render
// mountComponent.js export default function mountComponent(virtualDOM, Container) {let nextVirtualDOM = null // Distinguishes between functional components and class components if (isFunctionalComponent(virtualDOM)) {// Function component nextVirtualDOM = BuildFunctionalComponent (virtualDOM)} else {// nextVirtualDOM = buildStatefulComponent(virtualDOM) If (isFunction(nextVirtualDOM)) {mountComponent(nextVirtualDOM, container) } else { mountNativeElement(nextVirtualDOM, Function buildStatefulComponent(virtualDOM) {// Instantiate the class component to get the class component instance object and pass the props property to the class component const Component = new virtualdom.type (virtualdom.props) // Call the render method in the class component to get the VirtualDOM to render const nextVirtualDOM = Component.render () // Return nextVirtualDOM}Copy the code
9. Virtual DOM comparison
When making Virtual DOM comparison, the updated Virtual DOM and the pre-updated Virtual DOM need to be used. The updated Virtual DOM can be transferred through the render method at present. The question now is how to obtain the Virtual DOM before the update.
For the Virtual DOM before the update, the corresponding object is actually the actual DOM object that has been displayed on the page. Since this is the case, we can add the Virtual DOM to the properties of the real DOM object when we create it. Before the Virtual DOM comparison is performed, the corresponding Virtual DOM object can be obtained from the real DOM object, which is actually obtained from the third parameter of the Render method, container.firstChild.
Add the corresponding Virtual DOM object when you create the real DOM object
// mountElement.js import mountElement from "./mountElement" export default function mountNativeElement(virtualDOM, Container) {// Mount the Virtual DOM to the properties of the real DOM object to get its Virtual DOM newElement._virtualDOM = virtualDOM}Copy the code
9.1 The Same Virtual DOM Type
Virtual DOM has the same type. If it is an element node, it compares whether the attribute of the element node changes; if it is a text node, it compares whether the content of the text node changes
To achieve comparison, we need to obtain the corresponding Virtual DOM object from the existing DOM object.
// diff.js // Get the unupdated VirtualDOM const oldVirtualDOM = oldDOM && olddom._virtualdomCopy the code
Check whether oldVirtualDOM exists. If so, continue to check whether the VirtualDOM type to be compared is the same. If the VirtualDOM type is the same, check whether the node type is text. SetAttributeForElement is called if it’s an element node comparison
// diff.js else if (oldVirtualDOM && virtualdom.type === oldVirtualdom.type) {if (virtualdom.type === "text") {// Text node UpdateTextNode (virtualDOM, oldVirtualDOM, OldDOM)} else {// Whether an element node changes its attributes setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)}Copy the code
The updateTextNode method is used to compare whether the content of the text node has changed. If so, the content in the real DOM object is updated. Since the real DOM object has changed, the latest Virtual DOM is synchronized to the real DOM object.
The function updateTextNode (virtualDOM oldVirtualDOM, oldDOM) {/ / if the text node content is different if (virtualDOM. Props. TextContent! = = oldVirtualDOM. Props. TextContent) {/ / update the contents of the real DOM object oldDOM textContent = virtualDOM. Props. TextContent} / real DOM/synchronization VirtualDOM olddom._virtualdom = virtualDOM}Copy the code
The setAttributeForElement method is used to set/update the element node attributes
The method is to obtain the props properties of the Virtual DOM after the update and the props properties of the Virtual DOM before the update, and then loop through the properties of the new Virtual DOM to check whether the properties of the new Virtual DOM have changed. If something changes, you need to update the changed value to the real DOM object
Recycle the Virtual DOM object before the update and check whether there are deleted attributes in the new Virtual DOM by comparison. If there are deleted attributes, the corresponding attributes in the DOM object should also be deleted
// updateNodeElement.js export default function updateNodeElement( newElement, virtualDOM, OldVirtualDOM = {}) {/ / get the corresponding property object const newProps = virtualDOM. Props | | {} const oldProps = oldVirtualDOM. Props | | {} Object. Keys (newProps). ForEach (propName => {// Get the property const newPropsValue = newProps[propName] const oldPropsValue = oldProps[propName] if (newPropsValue ! OnClick -> click if (propname. slice(0, 2) === "on") {// eventName const eventName = propName.tolowerCase ().slice(2) // Add event to element newElement.addEventListener(eventName, NewPropsValue) / / remove the existing event handler if (oldPropsValue) {newElement. RemoveEventListener (eventName, oldPropsValue) } } else if (propName === "value" || propName === "checked") { newElement[propName] = newPropsValue } else if (propName ! == "children") { if (propName === "className") { newElement.setAttribute("class", newPropsValue) } else { newElement.setAttribute(propName, ForEach (propName => {const newPropsValue =)}}}}) // Determine if the property is deleted object.keys (oldProps) newProps[propName] const oldPropsValue = oldProps[propName] if (! NewPropsValue) {// Property removed if (propname.slice (0, 2) === "on") { const eventName = propName.toLowerCase().slice(2) newElement.removeEventListener(eventName, oldPropsValue) } else if (propName ! == "children") { newElement.removeAttribute(propName) } } }) }Copy the code
The above comparison is only for the topmost element. After the comparison of the topmost element is completed, we also need to recursively compare the child elements
Else if (oldVirtualDOM && Virtualdom.type === oldVirtualdom.type) {// Compare the child elements of the VirtualDOM recursively virtualDOM.children.forEach((child, i) => { diff(child, oldDOM, oldDOM.childNodes[i]) }) }Copy the code
9.2 Different Virtual DOM Types
When the element node type of comparison is different, there is no need to continue the comparison. The new Virtual DOM is directly used to create the DOM object, and the new DOM object is directly used to replace the old DOM object. In this case, the component is stripped and processed separately.
// diff.js else if (// If the Virtual DOM type is different virtualdom.type! == oldVirtualdom.type && // And the VirtualDOM is not a component because the component handles typeof virtualdom.type separately! Const newDOMElement = createDOMElement(virtualDOM) // Use the created real DOM element Replace the old DOM elements oldDOM. ParentNode. ReplaceChild (newDOMElement oldDOM)}Copy the code
9.3 Deleting a Node
Deleting a node occurs after a node is updated and occurs to all children of the same parent node.
If the number of old node objects exceeds the number of new VirtualDOM nodes after a node update is complete, a node needs to be deleted.
// Let oldChildNodes = olddom.childNodes // If the number of old nodes is greater than the length of new nodes to render if (oldChildNodes. Length > virtualDOM.children.length) { for ( let i = oldChildNodes.length - 1; i > virtualDOM.children.length - 1; i-- ) { oldDOM.removeChild(oldChildNodes[i]) } }Copy the code
9.4 Class Component Status Updates
The following code is the class component to update its state. There is a default title state in the state object of the class component. Click the Change Title button to call the handleChange method. Call the this.setState method in the handleChange method to change the status value of the title.
class Alert extends TinyReact.Component { constructor(props) { super(props) this.state = { title: HandleChange = this.handlechange.bind (this)} HandleChange () {// Call the setState method in the parent class to change the state this.setState({title: "changed title" }) } render() { return ( <div> <h2>{this.state.title}</h2> <p>{this.props.message}</p> <button onClick={this.handleChange}>change title</button> </div> ) } }Copy the code
The setState method is defined in the Component parent class. This method changes the state of a subclass to create a new state object.
// Component.js export default class Component { constructor(props) { this.props = props } setState (state) { // This. state = object. assign({}, this.state, state)}}Copy the code
Subclasses can now call the parent class’s setState method to change the state value, and call the Render method to update the component view when the component’s state object changes.
Before updating the component, compare the updated Virtual DOM object with the unupdated Virtual DOM to find the updated part to achieve the purpose of DOM minimization.
In the setState method, the updated Virtual DOM can be obtained by calling this.render method. Since the setState method is called by the subclass, and this refers to the subclass, the render method of the subclass is called here.
Module.js setState(state) {// component.js setState(state) {// setState is called by a subclass. This.state, state) let virtualDOM = this.render()}Copy the code
To achieve comparison, we also need to obtain the Virtual DOM before updating. According to previous experience, we can obtain the corresponding Virtual DOM object from the DOM object. The DOM object before updating is actually the DOM object displayed in the page now. As long as we can get this DOM object, we can get its corresponding Virtual DOM object.
How do I get the DOM object in the page? The DOM object on the page is mounted to the page using the mountNativeElement method, so we can save the DOM object in the Component class by calling the Component class method in this method. When a subclass calls the setState method, it calls another method in the setState method to retrieve the previously saved DOM object.
// component.js // getDOM() {this._dom = DOM} // getDOM() {return this._dom}Copy the code
Next we need to look at how to call setDOM from the mountNativeElement method. To call setDOM, we need to get the instance object of the class, So the question is how to get an instance of the class in the mountNativeElement method. This class is not Component, because we’re not instantiating Component directly in our code. We’re instantiating a subclass of Component. So you can call the setDOM method in an instance object of a subclass.
The mountNativeElement method receives the latest Virtual DOM object. If the Virtual DOM object is generated by the class component, the class instance object must be obtained before the Virtual DOM object is generated. It then calls the Render method below the instance object to fetch it. At that point we can add the class component instance object to the properties of the Virtual DOM object, which will eventually be passed to the mountNativeElement method, Now that the mountNativeElement method has the component instance object, we can call the setDOM method.
Add the Component property to the Virtual DOM object in the buildClassComponent method, whose value is the instance object of the class component.
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type(virtualDOM.props)
const nextVirtualDOM = component.render()
nextVirtualDOM.component = component
return nextVirtualDOM
}
Copy the code
The component instance object is obtained in the mountNativeElement method, and the DOM object is saved through the instance call setDOM method, which is convenient to obtain its Virtual DOM object through it during comparison
export default function mountNativeElement(virtualDOM, Container) {/ / get the component instance objects const component = virtualDOM.com ponent / / if the component instance objects exist if (component) {/ / save the DOM object component.setDOM(newElement) } }Copy the code
You can then call the getDOM method in the setState method to get the DOM object
setState(state) { this.state = Object.assign({}, this.state, Let oldDOM = this.getdom ()} let oldDOM = this.getdom ()}Copy the code
Now both the pre-updated and post-updated Virtual DOM objects are retrieved, and the parent container object of the real DOM object is retrieved, because it is used when the diff method is called for comparison
setState(state) { this.state = Object.assign({}, this.state, State) let virtualDOM = this.render() let oldDOM = this.getdom () let container = olddom.parentNode }Copy the code
Then we can call the diff method to compare, which will update the DOM object according to the logic we wrote earlier, and we can see the effect on the page
setState(state) { this.state = Object.assign({}, this.state, State) let virtualDOM = this.render() let oldDOM = this.getdom () let container = olddom.parentNode diff(virtualDOM, container, oldDOM) }Copy the code
9.5 Component Updates
Determine whether the Virtual DOM to be updated is a component in the diff method.
If the component to be updated is the same as the unupdated component, it does not need to update the component if the component is not the same, and directly calls the mountElement method to add the Virtual DOM returned by the component to the page.
If it is the same component, we update the component, passing the latest props to the component, calling the render method of the component to get the latest Virtual DOM object returned by the component, and passing the Virtual DOM object to the diff method. Let the diff method find the differences and update them to the real DOM object.
Different component lifecycle functions are called at different stages of updating a component.
Check whether the Virtual DOM to be updated is a component in the diff method. If it is a component, the diffComponent method is added to handle it
Else if (typeof virtualdom.type === "function") {// virtualDOM object can be used to obtain the latest props of the component. The component instance object to be updated can be used to call the lifecycle function of the component to update the props property of the component to get the latest Virtual DOM // 3) The DOM image to be updated needs to be modified on the existing DOM object when updating the component 4) If the component to be updated is not the same as the old component, you need to display the Virtual DOM returned by the component directly on the page. In this case, you need to use Container as the parent container diffComponent(virtualDOM, oldComponent, oldDOM, container) }Copy the code
Check the diffComponent method to see if the component to be updated is the same as the original component
// diffComponent.js export default function diffComponent(virtualDOM, oldComponent, oldDOM, Container) {// Check whether the updated component and the unupdated component are the same component by checking whether they use the same constructor. If (isSameComponent(virtualDOM, OldComponent)) {// do component updates for the same component} else {// Not the same component directly displays component content on the page}} // The updated component constructor // Function isSameComponent(virtualDOM, oldComponent) { return oldComponent && virtualDOM.type === oldComponent.constructor }Copy the code
If the component is not the same, you do not need to update the component. Instead, the content of the component is displayed on the page instead of the original content
// DiffComponent.js else {// Instead of the same component directly displaying the component content on the page // oldDOM is added to the mountElement method to insert the DOM object before inserting it into the page Otherwise, both old and new DOM objects will be displayed in the page mountElement(virtualDOM, container, oldDOM)}Copy the code
Delete the old DOM object in the mountNativeElement method
// mountNavtiveElement.js
export default function mountNativeElement(virtualDOM, container, oldDOM) {
// If the old DOM object exists, delete it
if (oldDOM) {
unmount(oldDOM)
}
}
Copy the code
// unmount.js
export default function unmount(node) {
node.remove()
}
Copy the code
If it is the same component, the component update operation needs to be performed and the component lifecycle function needs to be called
Add a lifecycle function to the Component class and override it if subclasses want to use it
Component.js export default class Component {componentWillMount() {} componentDidMount() {} componentDidMount() {} componentWillReceiveProps(nextProps) {} shouldComponentUpdate(nextProps, nextState) { return nextProps ! = this.props || nextState ! = this.state } componentWillUpdate(nextProps, nextState) {} componentDidUpdate(prevProps, preState) {} componentWillUnmount() {} }Copy the code
Create a new updateComponent method to update the component action and call it after if is established
// diffComponent.js if (isSameComponent(virtualDOM, oldComponent)) { oldComponent, oldDOM, container) }Copy the code
Call the component’s lifecycle function in the updateComponent method, update the component to get the latest Virtual DOM, and finally call the diff method to update it
import diff from "./diff" export default function updateComponent( virtualDOM, oldComponent, oldDOM, Container) {/ / oldComponent.com ponentWillReceiveProps lifecycle function (virtualDOM. Props) if (/ / call shouldComponentUpdate Life cycle function to judge whether or not to perform the update operation oldComponent. ShouldComponentUpdate (virtualDOM. Props)) {/ / will not update props to save a let prevProps = OldComponent.com ponentWillUpdate oldComponent. Props / / life cycle function (virtualDOM. Props) / / update props attribute updateProps methods defined in the component Component type oldComponent.updateprops (virtualdom.props) // Call render to get the latest Virtual DOM const because the Component props has been updated NextVirtualDOM = oldComponent.render() // Mount the component instance object to the VirtualDOM nextVirtualdom.component.ponent = oldComponent // Call the diff method update view diff (nextVirtualDOM, container, oldDOM) / / oldComponent.com ponentDidUpdate lifecycle function (prevProps)}}Copy the code
// Component.js
export default class Component {
updateProps(props) {
this.props = props
}
}
Copy the code
The ref attribute
Adding a ref attribute to a node can get the DOM object of that node. For example, in the DemoRef class, the ref attribute is added to the input element to get the INPUT DOM element object, and to get what the user typed in the text box when a button is clicked
class DemoRef extends TinyReact.Component { handle() { let value = this.input.value console.log(value) } render() { return ( <div> <input type="text" ref={input => (this.input = input)} /> <button OnClick ={this.handle.bind(this)}> button </button> </div>)}}Copy the code
The implementation idea is to judge whether the ref attribute exists in the Virtual DOM object of the node when it is created. If so, the method stored in the REF attribute is called and the created DOM object is passed to the REF method as a parameter. This allows you to take the element object and store it as a component property when rendering the component node.
// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
virtualDOM.props.ref(newElement)
}
Copy the code
You can also add a ref attribute to a class component to get an instance object of the component. For example, in the following code, we render the Alert component in the DemoRef component and add the ref attribute to the Alert component. The purpose is to get the Alert component instance object in the DemoRef component.
class DemoRef extends TinyReact.Component {
handle() {
let value = this.input.value
console.log(value)
console.log(this.alert)
}
componentDidMount() {
console.log("componentDidMount")
}
render() {
return (
<div>
<input type="text" ref={input => (this.input = input)} />
<button onClick={this.handle.bind(this)}>按钮</button>
<Alert ref={alert => (this.alert = alert)} />
</div>
)
}
}
Copy the code
In the mountComponent method, if the class component is being processed, obtain the component instance object from the Virtual DOM object returned by the class component, and check whether the ref attribute exists in the props attribute of the component instance object. If it does, the ref method is called and the component instance object is passed to it.
Mountcomponent.js let Component = null if (isFunctionalComponent(virtualDOM)) {} else {// Class component nextVirtualDOM = BuildStatefulComponent (virtualDOM) / / access component instance object component = nextVirtualDOM.com ponent} / / if the component instance objects exist if (component) { If (component.props && component.props. Ref) {// Call the ref method and pass the component instance object component.props.ref(component) } }Copy the code
The code goes there and deals with the lifecycle functions that the component has mounted
/ / if the component instance objects exist if (component) {component.com ponentDidMount ()}Copy the code
11. The key attribute
React renders list data by adding a key attribute to the rendered list element. The key attribute is the unique identifier of the data and helps React identify which data has been modified or deleted, thus minimizing DOM operations.
The key attribute does not need to be globally unique, but must be unique between siblings under the same parent node.
That is, the key attribute is used to compare children of the same type under the same parent node.
11.1 Node Comparison
The idea is that when two elements are compared, if they are of the same type, we loop through the child element of the old DOM object to see if it has a key attribute. If so, we store the DOM object of this child element in a JavaScript object. Then loop the child element of the Virtual DOM object to be rendered. In the loop process, obtain the key attribute of this child element, and then use this key attribute to search the DOM object in the JavaScript object. If it can be found, it means that the element already exists and does not need to be re-rendered. If the element is not found by the key attribute, it is new and needs to be rendered.
// diff.js else if (oldVirtualDOM && virtualdom.type === oldVirtualdom.type) {// Let the elements that have the key attribute in the keyedElements object keyedElements = {} for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) { let domElement = oldDOM.childNodes[i] if (domElement.nodeType === 1) { let key = domElement.getAttribute("key") if (key) { keyedElements[key] = domElement } } } }Copy the code
Keys (keyedElements).length === 0 // If no element with key is found According to the index to compare the if (hasNoKey) {/ / recursive contrast Virtual virtualDOM DOM child element. Children. ForEach ((child, I) = > {diff (child, oldDOM. OldDOM..childnodes [I])})} else {/ / use the key attribute compare elements virtualDOM. Children. The forEach ((child, I) => {// get the key attribute of the element to be compared. Let key = child.props. Key if (key) {// Find the corresponding DOM element in the existing DOM element DomElement = keyedElements[key] // If the element is found, it already exists and does not need to be re-rendered If (oldDOM. ChildNodes [I] &&) {oldDOM. ChildNodes [I] && oldDOM.childNodes[i] ! Olddom.childnodes [I] is the current location. // domElement is placed in the current location oldDOM.insertBefore(domElement, oldDOM.childNodes[i]) } } else { mountElement(child, oldDOM, oldDOM.childNodes[i]) } } }) }Copy the code
// mountNativeElement.js if (oldDOM) { container.insertBefore(newElement, OldDOM)} else {// Place the converted DOM object in the page container.appendChild(newElement)}Copy the code
11.2 Node Uninstallation
In the process of comparing nodes, if the number of old nodes is more than the number of new nodes to be rendered, it indicates that some nodes have been deleted. Continue to check whether there are elements in the keyedElements object. If there are no elements, use the index method to delete them; if there are, use the key attribute method to delete them.
Implementation approach is recycling old node, in the process of recycling old node to obtain the old node corresponding key attributes, and then according to the key attribute to find the old node in the new node, if found that this node has not been deleted, if not found, that node is deleted, calling the method uninstall uninstall node.
// Let oldChildNodes = olddom.childNodes // If the number of old nodes is greater than the length of new nodes to render if (oldChildNodes. Length > virtualDOM.children.length) { if (hasNoKey) { for ( let i = oldChildNodes.length - 1; i >= virtualDOM.children.length; i-- ) { oldDOM.removeChild(oldChildNodes[i]) } } else { for (let i = 0; i < oldChildNodes.length; i++) { let oldChild = oldChildNodes[i] let oldChildKey = oldChild._virtualDOM.props.key let found = false for (let n = 0; n < virtualDOM.children.length; n++) { if (oldChildKey === virtualDOM.children[n].props.key) { found = true break } } if (! found) { unmount(oldChild) i-- } } } }Copy the code
Uninstalling a node does not mean simply deleting the node. There are several situations that need to be considered
- If the node to be deleted is a text node, you can delete it directly
- If the node to be deleted is generated by the component, you need to call the component unload lifecycle function
- If the node to be deleted contains nodes generated by other components, you need to call the other component’s unload lifecycle function
- If the node you want to remove has a REF attribute on it, you also need to remove the DOM node object passed to the component through the REF attribute
- If an event exists on the node to be deleted, you need to delete the event handler corresponding to the event
Export default function unmount(dom) {// Obtain the virtualDOM object corresponding to the node const virtualDOM = dom._virtualdom // Text if to delete the node (virtualdom.type === "text") {// Directly remove the node dom.remove() // prevent the program from running down return} // check whether the node is generated by the component let Component = Ponent / / virtualDOM.com if generated by the component if (component) {/ / component uninstall lifecycle function called component.com ponentWillUnmount ()} / / if the node has a ref attribute Delete the DOM object passed to the component by calling the ref method again if (virtualdom.props && virtualdom.props. Ref) {virtualdom.props Object.keys(virtualDOM.props).forEach(propName => { if (propName.slice(0, 2) === "on") { const eventName = propName.toLowerCase().slice(2) const eventHandler = virtualDOM.props[propName] Dom.removeeventlistener (eventName, eventHandler)}}) if (dom.childNodes.length > 0) {for (let I = 0; i < dom.childNodes.length; i++) { unmount(dom.childNodes[i]) i-- } } dom.remove() }Copy the code