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.