Renderer.ts: fue-next /packages/ Runtime-core/SRC /renderer.ts
At the time of writing (2021-10-30 22:44), vUE (V3.2.1) is still the fastest running JS Web frameworks benchmark among the three major frameworks (Angular, React, vUE). Personally think vue3 diFF algorithm is very interesting, and the implementation of this paper is a broad DIFF algorithm – patch, and due to the pursuit of simplification, part of the implementation of this paper and the source code has a certain difference, but the basic principle is consistent, you can rest assured reference
To introduce
In a project, updating page elements is unavoidable. If you simply empty the page elements every time and re-render them all, no one can accept such performance consumption. In the above simple implementation of a Render module without patch, However, if a render without patch is used to render the following nodes
render(
h('ul'.null, [h('li'.null.'first'), h('li'.null.'second'), h('li'.null.'third')),document.body
);
render(
h('ul'.null, [h('li'.null.'first'), h('li'.null.'I'm not second'), h('li'.null.'third')),document.body
);
Copy the code
As you can see, only one text has been changed between rendering and rendering, but you still need to clear the entire VNode tree and create a new one, which is really stupid. Patch is used to compare the changes of elements in the specified container before and after rendering to minimize the impact of the update. In other words, patch can update DOM nodes in the smallest unit, corresponding to the above example, only the text of the second Li in the original page will be updated, and the performance improvement is beyond doubt
Look at the
The old VNode tree is n1, and the new VNode tree is N2
In fact, it’s easy to think of the following points
- We need to compare n1 and n2, so we have to pass in N2, and we have to get n1
- You can’t just add and subtract, so you need the associated unmount operation to unload unwanted nodes
- Different node types need different operation processes for comparison. For example, text nodes only need to compare text content, while element nodes need to compare tag types, node attributes and child nodes. Therefore, different functions are needed to compare different nodes separately
Draw a diagram
Note that the operations of different types of nodes in the figure are simply summarized as Patchxxx, processxxx, etc. The specific implementation is not so simple, and the general patch process can be understood through this figure
The following is a detailed explanation
- in
render
, determine whether N2 exists. If n2 exists, it means that patch needs to be performed; if n2 does not exist, it means that the original N1 needs to be unloaded - in
patch
In, n1 and N2 are of the same type and can be directly processed. If they are different, it means that the whole node changes, and N1 needs to be unmounted first and then N2 needs to be mounted - in
processxxx
If n1 exists, you can reuse the node of N1 and update the content instead of creating a new node. If n1 does not exist, you need to remount it
Write about the
After the above analysis, we can try to rewrite the render part of the original (above)
unmount
The implementation of unmount is actually much, much simpler because there are no other types to consider
const unmount = vnode= > {
// The previous el is useful
const { el } = vnode;
el.parentNode.removeChild(el);
}
Copy the code
unmountChildren
UnmountChildren implementation and mountChildren can not be said exactly the same, can only say very similar, later to use, incidentally write together
const unmountChildren = children= > {
children.forEach(child= > {
unmount(child);
});
};
Copy the code
render
To get n1 in render, you can store the entire N1 in a container as a _vnode attribute, as follows
const render = (n2, container) = > {
/ / for n1
const n1 = container._vnode;
if(! n2) { n1 && unmount(n1); }else {
patch(n1, n2, container);
}
// Save n2 as n1 of the next patch
container._vnode = n2;
}
Copy the code
patch
Patch needs to determine whether the element node type is the same or not, and call different processXXX based on the different node type to further process
const patch = (n1, n2, container) = > {
// If n1 exists and n1 n2 nodes are of different types
if(n1 && n1.type ! == n2.type) {// Uninstall and empty n1
unmount(n1);
n1 = null;
}
const { shapeFlag } = n2;
Call different processxxx methods depending on the n2 node type
if(shapeFlag & SHAPEFLAGS.TEXT) {
processText(n1, n2, container);
} else if (shapeFlag & SHAPEFLAGS.ELEMENT) {
processElement(n1, n2, container);
}
// Leave a hole in the classic, later implementation
else if(shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container); }}Copy the code
processText
In fact, processxxx method is quite simple, is a routine, according to the above flow chart to write, in addition, here can separate a patchTextNode but there is no need to write directly
const processText = (n1, n2, container) = > {
// the presence of n1 means that n1 nodes can be reused directly
// Just update the node content
if (n1) {
n2.el = n1.el;
n1.el.textContent = n2.children;
}
// if n1 does not exist, it needs to be remounted
else{ mountTextNode(n2, container); }}Copy the code
processElement
And processText similarly
const processElement = (n1, n2, container) = > {
if (n1) {
patchElement(n1, n2);
} else{ mountElement(n2, container); }}Copy the code
patchElement
What patchElement has to do looks simple, but actually requires patch properties and child nodes, which is very troublesome
const patchElement = (n1, n2) = > {
// N1 must exist, just take it
n2.el = n1.el
patchProps(n1.props, n2.props);
patchChildren(n1, n2, n2.el);
}
Copy the code
patchProps
In the whole patch process, the most troublesome part is actually the patchProps and patchChildren, but the overall idea is the same. It can only be changed when changes are made. In addition, patchProps isolated a method, patchDomProp, to filter out properties that do not need comparison and can be handled directly
const patchProps = (oldProps, newProps, el) = > {
if (oldProps === newProps) {
return;
}
oldProps = oldProps || {};
newProps = newProps || {};
// Remove old attributes, new attributes do not have
for (const key in oldProps) {
// If the current attribute is 'key', skip
if (key === 'key') {
continue;
}
if (newProps[key] == null) {
patchDomProp(oldProps[key], null, key, el); }}// Add new attributes that the old attributes do not have
for (const key in newProps) {
if(key === 'key') {
continue;
}
if(oldProps[key] ! == newProps[key]) { patchDomProp(oldProps[key], newProps[key], key, el) } } }Copy the code
patchDomProp
The overall implementation of patchDomProp is very similar to the previous mountProps. It looks very messy and complicated. In fact, it simply removes the unnecessary attributes and adds the required attributes
const eventReg = /^on[A-Z]/;
const patchDomProps = (prev, next, key, el) = > {
switch (key) {
case 'class':
el.className = next || ' ';
break;
case 'style':
if (next == null) {
el.removeAttribute('style');
} else {
// get rid of what you don't need
if (prev) {
for (const styleName in prev) {
if (next[styleName] == null) {
el.style[styleName] = ' '; }}}// Add what is needed
for (const styleName innext) { el.style[styleName] = next[styleName]; }}break;
default:
if (eventReg.test(key)) {
// 'onClick' -> 'click'
const eventName = key.slice(2).toLowCase();
// get rid of what you don't need
if (prev) {
el.removeEventListener(eventName, prev);
}
// Add what is needed
if(next) { el.addEventListener(eventName, next); }}else {
// get rid of what you don't need
if (next == null || next === false) {
el.removeAttribute(key);
}
// Add what is needed
else{ el.setAttribute(key, next); }}break; }}Copy the code
patchChildren
The diff algorithm that we will talk about later is also from here, called patchKeyedChildren. This article will not introduce the diff algorithm, but leave a pit for now
PatchChilren needs to consider the comparison of different child node types, which can be said to be very troublesome. There are a lot of merged in the source code, but in fact, there are almost the same. For the sake of intuitive appearance, the following implementation does not merge for a little analysis, the child node has the following three types:
- TEXT_CHILDREN: text child, set to
a
- ARRAY_CHILDREN: array children, set to
b
- Null: no child. Set this parameter to
c
Therefore, there are 9 (3 x 3) scenarios for comparing the children of the two nodes:
- (a a): Both are text child nodes, just change the content
- (a b): The original text, now changes the element, the original content needs to be cleared, and then re-mount the element child node
- (a c): The original text, now missing, directly empty the original content
- (b a): Original element, now text, unload the original element child node, and then change the content
- (b b): Both are element children, patchArrayChildren, more on that later
- (b c): The original element, now missing, uninstall the original element child node
- (c a): Not before, now there is a text, mount
- (c b): Not before, now there are some elements, mount
- (c c): No one has it
const patchChildren = (n1, n2, container) = > {
const { shapeFlag: prevShapeFlag, children: prevChildren } = n1;
const { shapeFlag: nextShapeFlag, children: nextChildren } = n2;
if (prevShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
/ / 1
container.textContent = nextChildren;
} else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
/ / 2
container.textContent = ' ';
mountChildren(nextChildren, container);
} else {
/ / 3
container.textContent = ' '; }}else if (prevShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
/ / 4
unmountChildren(prevChildren);
container.textContent = nextChildren;
} else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
/ / 5
// This is where the diff algorithm starts
patchArrayChilren(prevChildren, nextChildren, container);
} else {
/ / 6unmountChildren(prevChildren); }}else {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
/ / case 7
container.textContent = nextChildren;
} else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
/ / is 8
mountChildren(nextChildren, container);
}
// Case 9}}Copy the code
patchArrayChildren
With the diff algorithm, this method is called patchUnkeyedChildren
PatchArrayChildren is used to patch an array of two element child nodes. The element child nodes are essentially two arrays, but what needs to be compared is not the value, but the nodes in the array. The implementation idea is as follows
- Extract the common length part for patch
- Determine whether to mount or unmount the remaining parts based on the ownership of the remaining parts
const patchArrayChildren = (prev, next, container) = > {
const oldLength = prev.length;
const newLength = next.length;
const commonLength = Math.min(oldLength, newLength);
// Patch the public part
for (let i = 0; i < commomLength; i++) {
patch(prev[i], next[i], container);
}
// If the original length is larger than the current length
// Unmount those outside the public part
/ / such as
// Original: a b C
// Now: a b
if (oldLength > newLength) {
unmountChildren(prev.slice(commomLength));
}
// If the length is now greater than the original length
// The public part should be mounted
/ / such as
// Original: a b
// Now: a b C
else if(oldLength < newLength) { mountChildren(next.slice(commomLength), container); }}Copy the code
Run the
The test code is as follows
render(
h('ul'.null, [
h('li'.null.'I am the first'),
h('li'.null, [
h('li'.null.'I'm the first of second'),
h('li'.null.'I'm the second of second'),
h('li'.null.'I'm the third of second'),
]),
h('li'.null.'third'),]),document.body
);
setTimeout(() = > {
render(
h('ul'.null, [
h('li'.null.'first'),
h('li'.null, [
h('li'.null.'I am the first of second'),
h('li'.null.'I am second of second'),
h('li'.null.'I'm the third of the second.'),
]),
h('li'.null.'third'),]),document.body
);
}, 2000);
Copy the code
The page looks like this
Two seconds later
To summarize
In fact, still similar to the previous render, most of them are some process operations, the most troublesome is the need to handle according to different situations, and the logic of thinking to be clear, so draw a picture will be very helpful to understand. The key of the whole patch is to accurately determine the minimum update unit
Q&A
Q: Are you lazy? Why does it look so easy? A: There are many types in the source code, and there is also a very important fragment type. When this fragment type is used for patch, a new attribute anchor, namely anchor point, is needed to control the insertion position. Core and the diff algorithm is also using this stuff, but this article only text node and element nodes, it is not necessary to use, and if it is wished to understand the process of patch, or a look from the beginning of a simple is better, the core principle of general process in the above, understand the element node and the operation of the text node, The rest are pretty much the same (not so much the same)
Q: How can you write so complicated, who can understand it, is your code too bad? A: In fact, as said above, has stolen a lot of lazy beggar version, so the content is much, much, much, much, much, much less, and in order to look more intuitive and easier to understand, without any coding skills, the source used a lot, a lot, a lot, a lot of chain ternary expressions of what, But personally, I think it’s the simplest if-else, which looks intuitive, so I’ll do whatever I want, just to understand the principle and the process. There are a lot of situations to consider, so most of them are flow control, and once you grasp the key principles, you should have no problem understanding them. Finally, or really too much I write write smelly can go to see the source code or others mini- Vue implementation
Q: The former core principle, the latter core principle, too stream of consciousness, what is it? A: To be honest, I really don’t find any subtle principles of this patch that are worth talking about in detail, so the key is an operation process and how two VNode trees are compared, which is quite key. So in other words, if you understand my picture above, you will understand most of this patch
The last
Put the complete code in this section for easy reference
// vnode
const SHAPEFLAGS = {
ELEMENT: 1.TEXT: 1 << 1.TEXT_CHILDREN: 1 << 4.ARRAY_CHILDREN: 1 << 5};const Text = Symbol('Text');
// h
const h = (type, props, children) = > {
let shapeFlag = 0;
if (typeof type === 'string') {
shapeFlag |= SHAPEFLAGS.ELEMENT;
} else if (type === Text) {
shapeFlag |= SHAPEFLAGS.TEXT;
}
if (typeof children === 'string') {
shapeFlag |= SHAPEFLAGS.TEXT_CHILDREN;
} else if (typeof children === 'number') {
shapeFlag |= SHAPEFLAGS.TEXT_CHILDREN;
children = children.toString();
} else if (Array.isArray(children)) {
shapeFlag |= SHAPEFLAGS.ARRAY_CHILDREN;
}
return {
type,
props,
children,
shapeFlag,
el: null.key: props && props.key,
};
};
// render
const render = (vnode, container) = > {
const prevVNode = container._vnode;
if (vnode) {
patch(prevVNode, vnode, container);
} else {
prevVNode && unmount(prevVNode);
}
container._vnode = vnode;
};
// process
const processTextNode = (n1, n2, container) = > {
if (n1) {
patchTextNode(n1, n2);
} else{ mountTextNode(n2, container); }};const processElement = (n1, n2, container) = > {
if (n1) {
patchElement(n1, n2);
} else{ mountElement(n2, container); }};// patch
const patch = (n1, n2, container) = > {
if(n1 && n1.type ! == n2.type) { unmount(n1); n1 =null;
}
const { shapeFlag } = n2;
if (shapeFlag & SHAPEFLAGS.TEXT) {
processTextNode(n1, n2, container);
} else if (shapeFlag & SHAPEFLAGS.ELEMENT) {
processElement(n1, n2, container);
}
// Leave a hole in the classic, later implementation
else if(shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container); }};const patchTextNode = (n1, n2) = > {
n2.el = n1.el;
n1.el.textContent = n2.children;
};
const patchElement = (n1, n2) = > {
n2.el = n1.el;
patchProps(n1.props, n2.props, n2.el);
patchChildren(n1, n2, n2.el);
};
const patchChildren = (n1, n2, container) = > {
const { shapeFlag: prevShapeFlag, children: prevChildren } = n1;
const { shapeFlag: nextShapeFlag, children: nextChildren } = n2;
if (prevShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
container.textContent = nextChildren;
} else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
container.textContent = ' ';
mountChildren(nextChildren, container);
} else {
container.textContent = ' '; }}else if (prevShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
unmountChildren(prevChildren);
container.textContent = nextChildren;
} else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
patchArrayChilren(prevChildren, nextChildren, container);
} else{ unmountChildren(prevChildren); }}else {
if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
container.textContent = nextChildren;
} else if(nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) { mountChildren(nextChildren, container); }}};const patchArrayChilren = (prev, next, container) = > {
const oldLength = prev.length;
const newLength = next.length;
const commomLength = Math.min(oldLength, newLength);
for (let i = 0; i < commomLength; i++) {
patch(prev[i], next[i], container);
}
if (oldLength > newLength) {
unmountChildren(prev.slice(commomLength));
} else if(oldLength < newLength) { mountChildren(next.slice(commomLength), container); }};const patchProps = (oldProps, newProps, el) = > {
if (oldProps === newProps) {
return;
}
for (const key in oldProps) {
if (newProps[key] == null) {
patchDomProps(oldProps[key], null, key, el); }}for (const key in newProps) {
const prev = oldProps[key];
const next = newProps[key];
if(prev ! == next) { patchDomProps(prev, next, key, el); }}};const eventReg = /^on[A-Z]/;
const patchDomProps = (prev, next, key, el) = > {
switch (key) {
case 'class':
el.className = next || ' ';
break;
case 'style':
if (next == null) {
el.removeAttribute('style');
} else {
if (prev) {
for (const styleName in prev) {
if (next[styleName] == null) {
el.style[styleName] = ' '; }}}for (const styleName innext) { el.style[styleName] = next[styleName]; }}break;
default:
if (eventReg.test(key)) {
const eventName = key.slice(2).toLowerCase();
if (prev) {
el.removeEventListener(eventName, prev);
}
if(next) { el.addEventListener(eventName, next); }}else {
if (next == null || next === false) {
el.removeAttribute(key);
} else{ el.setAttribute(key, next); }}break; }};// mount
const mountTextNode = (vnode, container) = > {
const textNode = document.createTextNode(vnode.children);
container.appendChild(textNode);
vnode.el = textNode;
};
const mountElement = (vnode, container) = > {
const { type, props, children, shapeFlag } = vnode;
const el = document.createElement(type);
patchProps(null, props, el);
if (shapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
mountTextNode(vnode, el);
} else if (shapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
mountChildren(children, el);
}
container.appendChild(el);
vnode.el = el;
};
const mountChildren = (children, container) = > {
children.forEach(child= > {
patch(null, child, container);
});
};
// unmount
const unmount = vnode= > {
const { el } = vnode;
el.parentNode.removeChild(el);
};
const unmountChildren = children= > {
children.forEach(child= > {
unmount(child);
});
};
// test
render(
h('ul'.null, [
h('li'.null.'I am the first'),
h('li'.null, [
h('li'.null.'I'm the first of second'),
h('li'.null.'I'm the second of second'),
h('li'.null.'I'm the third of second'),
]),
h('li'.null.'third'),]),document.body
);
setTimeout(() = > {
render(
h('ul'.null, [
h('li'.null.'first'),
h('li'.null, [
h('li'.null.'I am the first of second'),
h('li'.null.'I am second of second'),
h('li'.null.'I'm the third of the second.'),
]),
h('li'.null.'third'),]),document.body
);
}, 2000);
Copy the code