Series Article Directory
[VUE] — Principle of watch listener
Article directory
1. Introduction
In vue
will maintain DOM
an vnode
object corresponding to the node.
vnode
The object of the corresponding child node in the children
array vnode
, so in vue
the map by vnode
and the real DOM
tree , we also call it a virtual tree.
It is with the virtual tree, when the data is updated. We can compare the differences between the new data and vnode
the old data to achieve accurate updates.oldVnode
And our algorithm to compare the difference is used diff
. By diff
comparing the difference of the virtual tree, the difference patch
is updated to the corresponding real DOM
node .
2. Source code analysis
2.1 patch function
patch
The function is the entry function of the diff
process . First of all, we need to know that it patch
will be loaded once when the page is rendered , and then vnode
called when it changes.
// 首次渲染是DOM元素,后面是vnode
function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// 不是一个vnode就创建一个空的vnode并关联在DOM元素上
if (!isVnode(oldVnode)) {
// 创建一个空的vnode,并关联DOM元素
oldVnode = emptyNodeAt(oldVnode)
}
if (sameVnode(oldVnode, vnode)) {
// key、tag相同,说明是同一个vnode
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// key、tag不相同,说明是不同的vnode
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
// 创建新的DOM元素
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 插入新的DOM元素
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 移除老的DOM元素
removeVnodes(parent, [oldVnode], 0, 0)
}
}
return vnode
}
The whole logic is:
if both nodes are the same, then check their children in depth. If the two nodes are not the same, it means that vnode
they have been completely changed and can be replaced directly oldVnode
.
2.2 sameVnode
// 判断新旧节点是否一样
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
2.3 patchVnode
If the two nodes are the same, it will enter patchVnode
the method to judge its child nodes and text nodes. This is to patch reusable nodes, that is, to distribute updates.
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
...
// vonde为新的vnode oldvnnode为老的vnode
// 设置vnode关联的DOM元素
const elm = vnode.elm = oldVnode.elm!
// 老children
const oldCh = oldVnode.children as VNode[]
// 新children
const ch = vnode.children as VNode[]
if (oldVnode === vnode) return
...
// 新vnode 无text 有children
if (isUndef(vnode.text)) {
// 新vnode 有children 老vnode 有chidren
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
// 新vnode 有children 旧vnode 无children 有text
} else if (isDef(ch)) {
// 清空text
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 添加children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 新vnode 无children 旧vnode 有children
} else if (isDef(oldCh)) {
// 移除children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 老vnode 有text
} else if (isDef(oldVnode.text)) {
// 清空text
api.setTextContent(elm, '')
}
// 新vnode 有text 无children
// 老vnode text 不等于 新vnode text
} else if (oldVnode.text !== vnode.text) {
// 老vnode 有children
if (isDef(oldCh)) {
// 移除老vnode children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 设置新vnode text
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
patchVnode
The logic of the function:
- Find the corresponding
DOM
nodeelm
and assign it to the new onevnode.elm
- Determine the new node type
(vnode.text)
, if it is a text node , just update theelm
text - Under non-text nodes , determine the child nodes of the old and new nodes
- If the old and new nodes have child nodes , go through the same layer comparison process of the child nodes
updateChildren
- If only the new node has children , use directly
addVnodes
toelm
add child nodes (delete the text first) - If only the old node has child nodes , use to
removeVnodes
remove - If there is no child node , judge whether the old data has a text node, and clear it.
2.4 updateChildren
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// 1.老开始和新开始对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 2.老结束和新结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 3.老开始和新结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 4.老结束和新开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 以上四种情况都没有命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 拿到新开始的key,在老children里去找有没有某个节点有对应这个key
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 没有在老children里找到对应的
if (isUndef(idxInOld)) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
// 在老children里找到了对应的
} else {
// 找到了对应key的元素(key相等)
elmToMove = oldCh[idxInOld]
// key相等 判断tag是否相等
if (elmToMove.sel !== newStartVnode.sel) {
// key相等 tag不相等
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// key相等 tag相等
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
//当节点遍历完之后,会存在两种情况,“新数组已经遍历完,但旧数组没有遍历完成” 和 “旧数组遍历完成,但新数组没有遍历完成”。故源代码的判断如下:
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
analyze:
- Step 1: Determine whether
oldStartVnode, newStartVnode
the two pointersvnode
are matched. If they match, the pointeroldStartVnode,newStartVnode
moves to the right, and the position of the real DOM remains unchanged. - Step 2:
oldEndVnode, newEndVnode
Whether they match, if they match, the pointeroldEndVnode, newEndVnode
moves to the left, and the position of the real DOM remains unchanged. - Step 3:
oldStartVnode, newEndVnode
Determine whether it matches, if it matches, the first node in the real DOM will be moved to the end - Step 4:
oldEndVnode, newStartVnode
Judging that the match is matched. If it matches, the last node in the real DOM will move to the front, and the two pointers on the match will move to the middle. - Step 5: If none of the four matches is successful, there are two cases
- If both old and new child nodes exist
key
, a table will be generated according to the corresponding table, and it will be used tooldChild
match the table . If the match is successful, it will be judged whether the matching node is or not . If it is, the successful node will be moved to the front in the real DOM. Otherwise, the generated corresponding node will be inserted into the corresponding position in , the pointer will move to the middle, and the matching node will be set to .key
hash
newStartVnode
key
hash
newStartVnode
sameNode
newStartVnode
DOM
newStartVnode
newStartVnode
old
null
- If not
key
, directly insert thenewStartVnode
new node into the real oneDOM
(ps: this can explain whyv-for
it needs to be setkey
, if not,key
then only four kinds of matching will be done, even if there are reusable nodes in the middle of the pointer, they cannot be duplicated. used)
- If both old and new child nodes exist
3. Optimization of diff algorithm in vue3
Vue3.0 is filtered for "brainless" patchVnode
- static type Vnode
:
old version of the source code:
Here, we will repeat the comparison and update logic of the vue2.x series:
the new version has been vue3
added 静态类型Vnode
, if 静态类型的vnode
so, skip the update directly and modify the new one Node reference is enough:
Remarks: comment
The type is currently turned to its source code and only changes the reference, and the source code author adds a line of comment.
Here is another sentence, the flagment fragment type is the newly added vnode type, namely: