Preface
vue-next has made a series of optimizations for virtual dom patch updates, adding blocks from compile time to reduce the number of comparisons between vdoms, and hoisted operations reduce memory overhead. I will write this article for myself and make a record of knowledge points. If there are any mistakes, please give me some advice.
VDOM
Simply put, the concept of VDOM is to use js objects to simulate real DOM trees. Due to the architecture of MV**, the real DOM tree should change with the change of data (data in it). These changes may be the following aspects:
- v-if
- v-for
- Dynamic props (such as: class, @click)
- Changes to children
- etc.
What the Vue framework needs to do is actually very single: when the user changes data, correctly updates the DOM tree, which is its core VDOM patch and diff algorithm.
How to do it
In this case, when the data is changed, all nodes need to be patched and diffed. Such as the following DOM structure:
<div> <span class="header">I'm header</span> <ul> <li>The first staticli</li> <li v-for="item in mutableItems" :key=""> {{ }}</li> </ul> </div>
The real DOM will be generated when the first mount node is mounted, and then if
({ key: 'asdf', desc: 'a new li item' })
The expected result is that a new li element appears on the page, and the content is a new li item. In this, the diff operation is performed on the children of the vnode corresponding to the ul element during patch. The specific operation is not intensively investigated here, but this operation requires comparing all vnodes corresponding to li.
insufficient
It is precisely because the diff operation in the version requires traversing all elements, this example includes span and the first li element, but these two elements are static and do not need to be compared. No matter how the data changes, the static elements will not be changed again. vue-next optimizes this operation at compile time, i.e. Block.
Block
Enter the above template, the rendering function generated in vue-next is:
const _Vue = Vue const { createVNode: _createVNode } = _Vue const _hoisted_1 = _createVNode("span", { class: "header" }, "I'm header", -1 /* HOISTED */) const _hoisted_2 = _createVNode("li", null, "First static li", -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode: _createVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString } = _Vue return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _createVNode("ul", null, [ _hoisted_2, (_openBlock(true), _createBlock(_Fragment, null, _renderList(, (item) => { return (_openBlock(), _createBlock("li", { key: }, _toDisplayString(), 1 /* TEXT */)) }), 128 /* KEYED_FRAGMENT */)) ]) ], 64 /* STABLE_FRAGMENT */)) } }
We can see that the openBlock and createBlock methods are called, and the code implementation of these two methods is also very simple:
const blockStack: (VNode[] | null)[] = [] let currentBlock: VNode[] | null = null let shouldTrack = 1 // openBlock export function openBlock(disableTracking = false) { ((currentBlock = disableTracking ? null : [])) } export function createBlock( type: VNodeTypes | ClassComponent, props?: { [key: string]: any } | null, children?: any, patchFlag?: number, dynamicProps?: string[] ): VNode { // avoid a block with patchFlag tracking itself shouldTrack-- const vnode = createVNode(type, props, children, patchFlag, dynamicProps) shouldTrack++ // save current block children on the block vnode = currentBlock || EMPTY_ARR // close block () currentBlock = blockStack[ - 1] || null // a block is always going to be patched, so track it as a child of its // parent block if (currentBlock) { (vnode) } return vnode }
For more detailed comments, please refer to the comments in the source code. They are written in detail and are easy to understand. Here openBlock is to initialize a block, createBlock is to generate a block for the currently compiled content. The line of code here: = currentBlock || EMPTY_ARR is to collect dynamic child nodes. We can look at the functions running at compile time:
// createVNode function _createVNode( type: VNodeTypes | ClassComponent, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null ) { /** * A series of codes **/ // presence of a patch flag indicates this node needs patching on updates. // component nodes also should always be patched, because even if the // component doesn't need to update, it needs to persist the instance on to // the next vnode so that it can be properly unmounted later. if ( shouldTrack > 0 && currentBlock && // the EVENTS flag is only for hydration and if it is the only flag, the // vnode should not be considered dynamic due to handler caching. patchFlag !== PatchFlags.HYDRATE_EVENTS && (patchFlag > 0 || shapeFlag & || shapeFlag & ShapeFlags.STATEFUL_COMPONENT || shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) ) { (vnode) } }
The above function is a function called to generate VNode after the template is compiled into ast, so there is the patchFlag flag. If it is a dynamic node and the Block is turned on at this time, the node will be stuffed into the Block, so dynamicChildren will be included in the VNode returned by the createBlock.
Up to this point, the following structure vnode was generated through the template compilation and render function running through the cases in this article:
const result = { type: Symbol(Fragment), patchFlag: 64, children: [ { type: 'span', patchFlag: -1, ...}, { type: 'ul', patchFlag: 0, children: [ { type: 'li', patchFlag: -1, ...}, { type: Symbol(Fragment), children: [ { type: 'li', patchFlag: 1 ...}, { type: 'li', patchFlag: 1 ...} ] } ] } ], dynamicChildren: [ { type: Symbol(Fragment), patchFlag: 128, children: [ { type: 'li', patchFlag: 1 ...}, { type: 'li', patchFlag: 1 ...} ] } ] }
The above result is incomplete, but we only care about these properties for the time being. The first element that can be seen is span, patchFlag=-1, and result has a dynamicChildren array, which only contains two dynamic lis. If the data is changed in the future, the new one will have a third li element.
patch
The patch part is actually not much different, just perform different patch operations according to the type of vnode:
function patchElement(n1, n2) { let { dynamicChildren } = n2 // A series of operations if (dynamicChildren) { patchBlockChildren ( !, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) } else if (!optimized) { // full diff patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG ) } }
As you can see, if dynamicChildren is available, then the diff operation in the version will be replaced with patchBlockChildren() and the parameter is only dynamicChildren, which means that the diff operation is not performed statically. If there is no dynamicChildren in the patch of vue-next, the complete diff operation will be performed and the subsequent code of full diff written in the comment is included.
Ending
This article does not explain in-depth the implementation level of the code. First, because I am not strong enough, I am still reading the source code. Second, I personally think that reading the source code should not be stubborn. I should take a look at the overall situation and then slowly draw it. First understand the role of each part and then think about it and you can gain more.
This is the end of this article about the detailed explanation of VDOM improvements in Vue3. For more related Vue3 VDOM content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!