The last review
React: Build a Virtual DOM from scratch
In the last video we achieved the first render from JSX=>Hyperscript=>VDOM=>DOM. Today we will look at how to update the DOM when the data changes.
Rewrite the view ()
function view(count) {
const r = [...Array(count).keys()]
return <ul id="filmList" className={`list-${count % 3}`}>
{ r.map(n => <li>item {(count * n).toString()}</li>) }
</ul>
}
Our view function takes one argument, count, and the variable r represents an array from 0 to count-1. If count=3, r=[0, 1, 2]. Ul className has three possible values: list-0, list-1, list-2. The number of li depends on count.
Rewrite the render ()
function render(el) { const initialCount = 0 el.appendChild(createElement(view(initialCount))) setTimeout(() => tick(el, initialCount), 1000) } function tick(el, count) { const patches = diff(view(count + 1), view(count)) patch(el, patches) if(count > 5) { return } setTimeout(() => tick(el, count + 1), 1000) }
There are two changes to the render function. The first is to call view() and pass count=0. Second, write a timer and 1 second regret to execute the tick function. The tick function takes two arguments, el for the node element and count for the current count.
The tick function does these things in turn:
- Call the diff function to compare the old and new VDOMs and get the patches that need to be modified based on the differences between the two
- Put the patch patch on the real DOM
- When the counter is less than or equal to 5, increase the count by 1 and continue to the next tick
- When the counter is greater than 5, stop
Next, we will implement diff function and patch function.
Let’s list the differences between the old VDOM and the new VDOM. Declare a few constants at the beginning of the index.js file.
Const CREATE = 'CREATE' // Add a new node const REMOVE = 'REMOVE' // Remove the original node const REPLACE = 'REPLACE' // Replace the original node const UPDATE = 'UPDATE' // Check if properties or child nodes have changed const SET_PROP = 'SET_PROP' // Add or replace properties const REMOVE_PROP = 'REMOVE PROP' // Remove properties
diff()
function diff(newNode, oldNode) { if (! oldNode) { return { type: CREATE, newNode } } if (! newNode) { return { type: REMOVE } } if (changed(newNode, oldNode)) { return { type: REPLACE, newNode } } if (newNode.type) { return { type: UPDATE, props: diffProps(newNode, oldNode), children: diffChildren(newNode, oldNode) } } }
- If the old node does not exist, the patches object we return will be of type new node;
- If the new node does not exist, the node is deleted.
- If both exist, call the changed function to determine if they changed.
- If both exist and () returns false, determine if the newNode is VDOM (if type exists, newNode is either an empty node or a string if type does not exist). If the new node is a VDOM, a patch object of type Update is returned, with diffProps and diffChildren operations performed on props and children respectively.
Let’s take a look at the changed, diffProps, and diffChildren functions once again.
changed()
function changed(node1, node2) { return typeof(node1) ! == typeof(node2) || typeof(node1) === 'string' && node1 ! == node2 || node1.type ! == node2.type }
It’s easy to check for changes in the old and new VDOM,
- First of all, if the data types are different, it must have changed.
- Secondly, if both types are plain text, then directly compare whether they are equal or not;
- Finally, compare whether the two types are equal.
diffProps()
function diffProps(newNode, oldNode) { let patches = [] let props = Object.assign({}, newNode.props, oldNode.props) Object.keys(props).forEach(key => { const newVal = newNode.props[key] const oldVal = oldNode.props[key] if (! newVal) { patches.push({type: REMOVE_PROP, key, value: oldVal}) } if (! oldVal || newVal ! == oldVal) { patches.push({ type: SET_PROP, key, value: newVal}) } }) return patches }
Compare the changes in the attributes of the old and new VDOM and return the corresponding patches.
- First we use the most likely principle and combine all the attributes of the old and new VDOM to assign them to a new variable, props
- Iterate through all Keys of the props variable, comparing the old and new VDOM values for this KEY in turn
- If the new value does not exist, the attribute is deleted
- If the old value does not exist, or if the new value is different, then we need to reset the property
diffChildren()
function diffChildren(newNode, oldNode) {
let patches = []
const maximumLength = Math.max(
newNode.children.length,
oldNode.children.length
)
for(let i = 0; i < maximumLength; i++) {
patches[i] = diff(
newNode.children[i],
oldNode.children[i]
)
}
return patches
}
Similarly, the principle of maximum likelihood is adopted to take the longest value of children of the new and old VDOM as the length of traversal children. Then compare each child of the old and new VDOM under the same INDEX in turn.
It is important to note here that for simplicity, we have not introduced the concept of key, we are directly comparing the child under the same index. So let’s say that a list ul has 5 entries, li1, li2, li3, li4, li5; If we delete this first term, the new one becomes li2, li3, li4, li5. So when we have diffChildren, we compare Li1 with Li2, and so on. In this case, instead of removing li1, li2, li3, li4, and li5, we get the diff conclusion that li1 is replaced, li2 is replaced, li3 is replaced, li4 is replaced, li5 is deleted. So when React asks you to render a list, you have to add a Key.
As of now, we have the patch we need. Next, we need to Patch Patch into DOM.
patch()
function patch(parent, patches, index = 0) { if (! patches) { return } const el = parent.childNodes[index] switch (patches.type) { case CREATE: { const { newNode } = patches const newEl = createElement(newNode) parent.appendChild(newEl) break } case REMOVE: { parent.removeChild(el) break } case REPLACE: { const {newNode} = patches const newEl = createElement(newNode) return parent.replaceChild(newEl, el) break } case UPDATE: { const {props, children} = patches patchProps(el, props) for(let i = 0; i < children.length; i++) { patch(el, children[i], i) } } } }
- First, when the patches are not present, the patches return directly without any operation
- Select the node being processed using ChildNodes and Index and assign the value EL
- Begin to determine the type of patch
- When the type is Create, a new node is generated and append to the root node
- When the type is REMOVE, the current node EL is directly deleted
- When the type is Replace, a new node is generated and the original node is replaced
- When the type is UPDATE, we need special handling
- Call patchProps to render the patch we got earlier with diffProps to the node
- Traverse the patch list obtained by diffChildren before, and then recursively call patch in turn
Finally, we add patchProps
patchProps
function patchProps(parent, patches) {
patches.forEach(patch => {
const { type, key, value } = patch
if (type === 'SET_PROP') {
setProp(parent, key, value)
}
if (type === 'REMOVE_PROP') {
removeProp(parent, key, value)
}
})
}
function removeProp(target, name, value) { //@
if (name === 'className') {
return target.removeAttribute('class')
}
target.removeAttribute(name)
}
I don’t have to explain this, but the code is pretty straightforward, and we defined the setProp function in the last video. In this way, we have completed the entire process of data updates leading to DOM updates. NPM run compile after opening the browser to see the result, you should see a background color in different changes, while the list of items in the gradually increasing list.
End and spend
At this point, our VDOM is complete. I don’t know if you have the answers to the questions I raised at the beginning of this series. If you have an answer, please share your thoughts in the comments section of this post. Complete and accurate analysis will receive a special reward from me. 😁 😁 😁 😁