The diff algorithm is used to calculate the changed parts in the Virtual DOM, and then perform DOM operations on those parts without having to re-render the entire page. Rendering the entire DOM structure is a costly process, as it requires the browser to repaint and reflow the DOM structure. The diff algorithm allows the operation process to update only the modified part of the DOM structure without updating the entire DOM. This minimizes the operations on the DOM structure and reduces the scale of browser repaint and reflow to the greatest extent.
The basis of the diff algorithm is the Virtual DOM, which is a tree based on JavaScript objects. Each node is called a VNode, described using object properties. In essence, it is an abstraction of the real DOM. Ultimately, this tree can be mapped to the real environment through rendering operations. Simply put, the Virtual DOM is a JavaScript object used to describe the entire document.
When constructing a page in the browser, DOM nodes are used to describe the entire document.
If we use JavaScript objects to describe the above nodes and document, it would be similar to the following structure. Of course, this is not the object used to describe nodes in Vue. In Vue, the object used to describe a node includes many properties such as tag, data, children, text, elm, ns, context, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, etc. For specific properties, you can refer to the Vue source code in /dev/src/core/vdom/vnode.js.
When using the diff algorithm for partial updates, it is necessary to compare the differences between the old DOM structure and the new DOM structure. At this point, VNode is used to describe the entire DOM structure. First, a Virtual DOM is generated based on the real DOM. When the data of a VNode changes, a new VNode is created. Then, a comparison is made between newVNode and oldVNode. Any differences found are then patched into the corresponding real DOM nodes through the elm property of VNode, after which the old Virtual DOM is replaced with the new Virtual DOM.
When data changes, the set method will call Dep.notify to notify all subscribers (Watcher) of the data update. Subscribers will then call patch to compare and render the corresponding parts to the real DOM structure.
A complete diff requires a time complexity of O(n^3). This is a minimum edit distance problem. When comparing the minimum edit distance of strings using a dynamic programming approach, the required time complexity is O(mn). However, for a DOM, which is a tree structure, the time complexity of the minimum edit distance problem has evolved from O(m^3n^3) to O(n^3) over more than 30 years of development. If interested, one can research the paper at https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf.
It is obviously inappropriate for the diff algorithm, which was originally introduced to improve efficiency, to have a time complexity of O(n^3. With 1,000 nodes, for example, it would require a billion comparisons, making it an expensive algorithm. Therefore, some compromises must be made to speed up the process. By simplifying the comparisons through some strategies, the time complexity is reduced to O(n). Although it is not the minimum edit distance, this compromise between edit distance and time performance is a good solution.
The O(n) time complexity mentioned above is achieved through a certain strategy. React mentions two assumptions, which also apply in Vue:
key property attached to the renderer, developers can indicate which child elements might be stable.In simpler terms:
key can uniquely determine whether it's a move, create, or delete operation.Several scenarios may arise after comparison, and corresponding operations can be carried out:
→ add or remove a new node.→ change old attributes to new attributes.→ change the old content to the new content.tag or key is changed → if changed, remove and create a new element.The diff algorithm implementation in the Vue source code is in the dev/src/core/vdom/patch.js file. However, the implementation in the Vue source code is quite complex. The article analyzes the core part of the code after being simplified, with the commit ID being 43b98fe.
When the patch method is called, it checks whether it's a VNode. The isRealElement actually judges whether it is a real DOM based on the existence of the nodeType. A VNode does not have this field. If it's not a real DOM element and the two nodes are the same, then it will enter the if block, call patchVnode to perform diff on the children to determine how to update.
Next, let's look at the sameVnode method to determine what counts as the same node.
The main criteria for judgment here are:
key must be the same, or both being undefined also counts as the same.DOM elements must be the same.If the above conditions are met, then it is deemed as the same VNode, and the patchVnode operation can proceed. Otherwise, it is considered as a completely new VNode, and the createElm will be executed after the above judgment.
In summary, after entering patch, there are two branches that can be taken. If it's the first time to patch, i.e., when the component is first mounted, or if it is found that the elements have different tags, then it is considered as different elements, and a new DOM element is directly created through createElm to replace the old one. Otherwise, for the existing DOM elements, update is performed through patchVnode to optimize performance by conditionally updating. This way, the first principle in the strategy is implemented, i.e., different types of elements will result in different trees. Once two different element types are found, the old one is deleted and a new one is created, rather than recursively comparing them.
After recognizing these as the same VNode, it is necessary to compare and update the differences of the current element, as well as recursively compare the children, which is implemented in the patchVnode method.
cbs.update is mainly used to update attributes. The cbs here actually comes from hooks. hooks is defined on line 33 as follows: const hooks = ['create', 'activate', 'update', 'remove', 'destroy']. It is used to perform corresponding operations at various stages of VNode update. cbs.update contains the following callbacks: updateAttrs, updateClass, updateDOMListeners, updateDOMProps, updateStyle, update, updateDirectives, which are mainly used to update some related attributes of the current node.
Then, the child nodes need to be updated, which is divided into two cases:
textNode, it needs to be processed in three different cases.textNode, updating the text directly is sufficient.Three cases for children as VNode:
updateChildren.When both old and new children exist, then updateChildren method is called. For each child node, we still have these possibilities:
updateChildren is the core algorithm of diff, and its source code implementation is as follows.
Its children two arrays new and old separately used a pointer at the beginning, four pointers in total. Because the pointer only traversed the array once, the time complexity is O(n). Let's illustrate the diff process with a simple example.
First, the pointers are compared with each other, four types of comparisons, which are oldStartIdx and newStartIdx, oldStartIdx and newEndIdx, oldEndIdx and newStartIdx, and oldEndIdx and newEndIdx. If there is no equal, the process continues. At this point, there are two possibilities, with and without a key. Without a key, a new DOM Node is directly created and inserted before a(oldStartIdx). Assuming that a key exists here, with the key, take the key value of newStartIdx and go to old VNode to find it, record the current oldKeyToIdx, then adjust the VNode, move b to before a, then find the node value corresponding to oldKeyToIdx in old VNode and set it as undefined, newStartIdx pointer moves towards the middle, i.e., ++newStartIdx.
The loop continues, at this time comparing oldStartIdx and newStartIdx, oldStartIdx and newEndIdx, oldEndIdx and newStartIdx, oldEndIdx and newEndIdx, it is found that newStartIdx is the same as oldEndIdx, so move f in the DOM Node to before a in the DOM Node, at this point, newStartIdx and oldEndIdx pointers move towards the middle, i.e., ++newStartIdx and --oldEndIdx.
The loop continues, at this time comparing oldStartIdx and newStartIdx, oldStartIdx and newEndIdx, oldEndIdx and newStartIdx, oldEndIdx and newEndIdx. There are no same circumstances found. Take the key value of newStartIdx, go to old VNode to find it, no same value is found, so create a node directly and insert it before a(oldStartIdx) in the DOM Node, newStartIdx pointer moves towards the middle, i.e., ++newStartIdx.
At this point, the loop ends, there are two choices:
oldStartldx > oldEndldx, it means the old nodes have been traversed, and there are more new nodes, so these extra new nodes need to be created and added to the real DOM Node.newStartldx >newEndldx, it means the new nodes have been traversed, and there are more old nodes, so these extra old nodes need to be removed from the real DOM Node.At this point, we meet Scenario Two, so it is necessary to remove the nodes in the range [oldStartldx, oldEndldx] from the real DOM Node. According to the aforementioned content, it is necessary to delete the four nodes a c d e, and thus the diff is completed.
After the diff is completed, the new VNode is used as the old VNode for the next diff. Additionally, about the component diff, the component-level diff algorithm is relatively simple. If the nodes are different, create and replace, if the nodes are the same, update their child nodes. Lastly, createElm is called to create real DOM elements based on VNode. If it is a component, createComponent will return true, so the subsequent operation will not be performed, and if it is not a component, the node creation work will be performed, and the child nodes will be recursively created.