The introduction
In the last article, I talked about the principle of Vue3 responsiveness to Vue3 made an introduction to the implementation principle of Vue3 responsiveness. Presumably, we have a certain understanding of how to use Proxy to achieve data Proxy in Vue3, and how to achieve data responsiveness. Today, we are again advanced. Let’s see how it relates to the View layer and implement a Low version of Vue3.
Implementation approach
First of all, we should know that whether Vue or React, their overall implementation idea is to first convert the template template or JSX code we wrote into a virtual node, and then after a series of logical processing, finally through the render method hanging on the specified node, rendering the real DOM.
So, our first step is to implement the Render method, which converts the virtual DOM node into a real DOM node and renders it on the page.
Rendering of virtual nodes
To implement the Render method, you first have to have a virtual DOM, and here we’ll use a classic counter as an example.
// 计数器虚拟节点
const vnode = {
tag: 'div',
props: {
style: {
textAlign: 'center'
}
},
children: [
{
tag: 'h1',
props: {
style : {
color: 'red'
}
},
children: '计数器'
},
{
tag: 'button',
props: {
onClick: () => alert('Congratulations!')
},
children: 'Click Me!'
}
]
}
Copy the code
As a result, we can confirm that the Render method has two fixed parameters: the virtual node vnode and the container to render, using #app as an example.
render
methods
Export function render (vnode, container) {patch(null, vnode, container); }Copy the code
Render just does the initialization of the parameters to receive, refer to the source code, we also build a patch method to do the rendering.
patch
methods
Function patch (n1, n2, Tag === 'string'){// mountElement mountElement(n2,container); }else if(typeof n2.tag === 'object'){// If component}}Copy the code
Patch method is not only used for initial rendering, but also for subsequent update operations. Therefore, three parameters are required, namely, old node N1, new node N2, and container. In addition, we need to consider the two forms of tag and component, and need to make a separate judgment. Let’s start with the simple tag.
mountElment
methods
The mountElment method mounts common elements, and at its core is recursion.
Pass: Dom operations are frequently used in mount elements, so you need to put these common utility methods in a runtime-dom.js file.
Function mountElement (vnode, container) {const {tag, props, children} = vnode; Let el = (vnode.el = nodeops.createElement (tag)); // If (props) {for (let key in props) {nodeOps. HostPatchProps (el, key, {}, Props [key])}} // children isArray if (array.isarray (children)) {mountChildren(children, El)} else {/ / string nodeOps hostSetElementText (el, children); } // Insert node nodeOps. Insert (el, container)}Copy the code
To handle multiple children, let’s define a mountChildren method for recursive mount.
mountChildren
methods
Function mountChildren (children, container) {for (let I = 0; i < children.length; i++ ) { let child = children[i]; // Recursively mount node patch(null, child, container); }}Copy the code
At this point, after a series of operations, we have successfully rendered our VNode to the page. Move a point, the function is also Ok ~
Component mount
Now that we’ve implemented simple label mounting, let’s look at how component mounting is implemented.
As mentioned earlier, the tag of a component is an object. Let’s first build a custom component.
// My component const MyComponent = {setup () {return () => {// render function return {tag: 'div', props: {style: {color: 'blue'}}, children: [{tag: 'h3', props: null, children: 'I am a custom component'}]}}}}Copy the code
Note that the setup method in Vue3 returns a function, a render function, to declare the component, as documented.
Pass: If we do not return a render function, vue internally compiles the template into a render function and mounts the result to the return value of setup.
We’ll put MyComponent in our vNode with the following structure:
{tag: MyComponent, props: null, // Children: null // socket}Copy the code
Pass: We don’t consider props and children here for the time being, that is, the props property of the component and the slot slot of the component.
mountComponent
methods
The process of component mounting is roughly as follows: first build a component instance, serve as the context of the component, call the setup method of the component to return the render function, call render to get the virtual node of the component, and finally render to the page through the patch method.
Function mountComponent (vnode, container) {const instance = {vnode: vnode, Subtree: null, // render returns the result} // declare Component const Component = vnode.tag; // Call setup to render instance.render = component. setup(vnode.props, instance); // Call render to return subtree instance.subtree = instance.render && instance.render(); // Render component patch(null, instance.subtree, container)}Copy the code
Add the mountComponent method to the vnode.tag === “object” branch.
Data response
Above we have implemented the normal tag and component rendering operations, and the event is also a simple alert, which we then need to associate with data.
Let’s start by declaring a data source for the page,
const data = {
count: 0
}
Copy the code
Make a simple change to the children section of vNode:
{tag: 'h1', props: {style: {color: 'red'}}, children: 'count ', {tag: 'button', props: { onClick: () => data.count++ }, children: 'Increment! ' }, { tag: 'button', props: { onClick: () => data.count-- }, children: 'Minus! '}Copy the code
The rendering result is shown below:
Now we need to implement a simple requirement that when we click the increment and minus buttons, the current count value will be either incremented or subtracted by 1.
However, when we click, the page doesn’t change at all. In fact, the value of count is updated, so you can hit a breakpoint to see.
The reason for this is that we haven’t tied the view to our data yet, missing a bridge, like Watcher in VUE2.
Reactive and Effect methods in VUE3 are used to collect data dependencies and implement side effects. For details, please talk about reactive principles in VUE3.
First, through reactive method, data is proxy:
const data = reactive({
count: 0
})
Copy the code
Then they are connected using the effect method:
effect(() => { const vnode = { tag: 'div', props: { style: { textAlign: 'center' } }, children: [ { tag: 'h1', props: {style: {color: 'red'}}, children: 'count ', {tag: 'button', props: {onClick: () => data.count++ }, children: 'Increment!' }, { tag: 'button', props: { onClick: () => data.count-- }, children: }, {tag: MyComponent, props: null, // children: null // slot}]} render(vnode, app)})Copy the code
With Effect wrapped, reactive does the dependency collection to connect the data to the view.
Let’s click on it and see the result below:
The count result is correct, but we find that whenever we click increment or minus, a new VNode will be created and inserted into the page again. This is because we haven’t done dom-diff for the moment, which we will solve later.
Local updates to components
Let’s look ata problem in the component. Let’s add a num property to data and make the following changes to our custom component:
{ tag: 'div', props: { style: { color: 'blue' } }, children: [ { tag: 'h3', props: null, children: Num: '+ data.num}, {tag: 'button', props: {onClick: () => {data.num++;}}, children:' update num'}]}Copy the code
Click on the “Update num” button and see something similar to the “update count” button.
This is where we update the component’s num, which has nothing to do with count. Instead of updating the DOM associated with count, we should update the component itself.
Therefore, we need to do a dependency collection internally for each component to achieve a partial refresh of the component.
We just need to add effCT when we make patch:
Effect (()=>{// call setup to render instance.render = component.setup (vnode.props, instance); // Call render to return subtree instance.subtree = instance.render && instance.render(); Render component patch(null, instance.subtree, container)})Copy the code
This enables a partial update of the component.
DOM-DIFF
The reason why the above data updates and the page keeps appending is that there is no DOM-diff. Let’s talk about it and make a simple DOM-diff.
Pass: The author has limited ability and has not considered component diff for the time being;
Let’s start with an example from Li:
const oldVNode = {
tag: 'ul',
props: null,
children: [
{
tag: 'li',
props: { style: { color: 'red' }, key: 'A' },
children: 'A'
},
{
tag: 'li',
props: { style: { color: 'orange' }, key: 'B' },
children: 'B'
},
]
}
render(oldVNode, app)
setTimeout(() => {
const newVNode = {
tag: 'ul',
props: null,
children: [
{
tag: 'li',
props: { style: { color: 'red' }, key: 'A' },
children: 'A'
},
{
tag: 'li',
props: { style: { color: 'orange' }, key: 'B' },
children: 'B'
},
{
tag: 'li',
props: { style: { color: 'blue' }, key: 'C' },
children: 'C'
},
{
tag: 'li',
props: { style: { color: 'green' }, key: 'D' },
children: 'D'
}
]
}
render(newVNode, app)
}, 1500)
Copy the code
The above vnode means that oldVNode is rendered first to obtain three different Li A, B and E. After 1.5s, the color attribute of B is modified first, then E is deleted, and finally two new Li, C and D are added.
It involves dom reuse (A), attribute modification (B), deletion (E) and addition (C,D);
patchProps
methods
Let’s look at the simplest comparison operation for the props property. The idea is to add the new properties, replace the old properties with the new values, and delete the properties that the old properties have and the new properties don’t.
function patchProps (el, oldProps, newProps) { if ( oldProps ! Const prev = oldProps[key]; // for (let key in newProps) {// const prev = oldProps[key]; // New attribute value const next = newProps[key]; if ( prev ! Hostops. hostPatchProps(el, key, prev, next)}} /* 2. */ for (let key in oldProps) {if (! HasOwnProperty (key)) {// Empty the new property nodeops. hostPatchProps(EL, key, oldProps[key], null)}}}}Copy the code
This completes the comparison of attributes, followed by the comparison of child elements.
patchChildren
methods
Comparison of child elements can be classified as follows:
- The child element of the new node is a simple string, which is simply a string substitution to set the new text to the corresponding node
- Otherwise, the new node is an array, or there are two cases. First, the old node is a simple string, then delete the old node and mount the new node. Second, the old node is also an array. In the most complicated case, the new node needs to be compared with the old node one by one.
Function patchChildren (n1, n2, container) {const c1 = n1. Children; const c2 = n2.children; If (typeof c2 == 'string') {// new child element is string, text replaces if (c1! == c2 ) { nodeOps.hostSetElementText(container, c2); If (typeof c1 == "string") {if (typeof c1 == "string"); New content is then inserted nodeOps. HostSetElementText (container, "); // Hang in new children mountChildren(c2, container); } else {// New and old children are arrays}}}Copy the code
The above method can complete simple text replacement and new node mount. In the case that children of both new and old elements are arrays, the patchKeyChildren method is needed to achieve this.
patchKeyChildren
Not to be considered for the time beingkey
()
This method first generates an index mapping table according to the key of the new node, then goes to the old node to find whether there are corresponding elements, if there are, it needs to reuse, then deletes the redundant part of the old node, adds the new part of the new node, and finally determines whether to move by determining the key and attribute value.
The longest increasing subsequence LIS algorithm is used in the official source code to determine the element index that does not move and improve performance.
function patchKeyChildren (c1, c2, container) { // 1. Let e1 = c1. Length-1; // select * from t1 where e2 = c2.length - 1; // new last index // const keyToNewIndexMap = new Map(); for ( let i = 0; i <= e2; i++ ) { const currentEle = c2[i]; Keytonewindexmap. set(currentele.props. Key, I)} // 2. Const newIndexToOldIndexMap = new Array(e2 + 1); For (let I = 0; i <= e2; i++ ) newIndexToOldIndexMap[i] = -1; for ( let i = 0; i <= e1; i++ ) { const oldVNode = c1[i]; // newIndex let newIndex = keytonewindexmap.get (oldvnode.props. Key); If (newIndex === undefined) {// old, Remove (oldvNode.el) // delete old node} else {// reuse // compare attributes newIndexToOldIndexMap[newIndex] = I + 1; patch(oldVNode, c2[newIndex], container); } } let sequence = getSequence(newIndexToOldIndexMap); Let j = sequence. length-1; // Insert for (let I = e2; i >= 0; i-- ) { let currentEle = c2[i]; const anchor = (i + 1 <= e2) ? c2[i + 1].el : null; If (newIndexToOldIndexMap[I] === -1) {// The new element needs to be inserted into the list patch(null, currentEle, container, anchor); If (I === sequence[j]) {j--; if (I === sequence[j]) {j--; } else {// Insert element nodeops. insert(currentele. el, container, anchor); }}}}Copy the code
GetSequence algorithm source code please stamp.
Add the key element (props) to the children element of vNode
conclusion
So far, we have implemented a very rudimentary version of Vue3, with basic VNode rendering and simple DOM-diff operations, giving us a glimpse into the internal implementation of VUe3.
Vue3 real internal implementation, far more complex than this, there are a lot of code implementation ideas and methods of personal understanding is more difficult, it is worth learning from us. This article is also my knowledge accumulation and personal record in the process of learning VUE3, hoping to play a role of throwing a brick to attract jade to everyone, everyone come on ~
Finally, the github address is attached, looking forward to your criticism and correction.