Also read other articles speak vue diff process, but it feels just talked about some of them (compare mode), not on the part of the details of the detailed explanations, such as
- If a match is made
patchVnode
is doing what? Why is there to be followed dom operation, some do not? - During the diff, the pointer of how specific mobile? And which parts have changed?
insertedVnodeQueue
What is the use? Why been with?- Then also puzzled for a long time, a lot of articles in this part of the direct oldChildren mobile operations, but oldChildren move happen? So in the end who happened to move it?
Here it does not directly began to speak diff, in order so that we can know the detailed process of diff, before beginning the core of where some simple concepts and processes need to explain in advance what course is best for the hope that you have this patch vue Source part of some understanding.
Several concepts
Since the core is a process diff, it will first diff related to the core concepts briefly explain, if these still have questions can leave a message in the comments section:
1. vnode
Simply means that real dom description of the object, which is also one of the characteristics of vue - virtual dom. Since dom structure of native too complicated, when it is desired to obtain and understand the node information of the time, does not require complex operations dom, corresponding vue is first analyzed (diff Comparative i.e. contrast the vnode) with which describes the object, and then the reaction to the real dom.
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
functionalContext: Component | void; // real context vm for functional nodes
functionalOptions: ?ComponentOptions; // for SSR caching
functionalScopeId: ?string; // functioanl scope id support
constructor () {
...
}
}
复制代码
Note that later will be related to several properties:
children
Andparent
establish hierarchical relationships between its vnode through this, which is the corresponding hierarchy of real domtext
If there is value to prove that the corresponding vnode is a document node with children is a mutually exclusive relationship, you can not simultaneously have valuetag
Indicates that the current vnode, corresponding to the real dom tag name, such as 'div', 'p'elm
Is the current real dom vnode corresponding
2. patch
Reading the source code complex functions of tips: see 'a' 'one'. 'Head' refers to the reference, be able to read and extract the parameters can understand ( oldVnode
, vnode
, parentElm
), 'tail' refers to a result of the processing function, the return elm
. Therefore, according to the 'head to tail' summary, patch
after completion of the new vnode
on will generate a corresponding elm
, dom is true, and that has been mounted to the parentElm
dom at. Simply put, as vue instance initialization, data change caused the page updates, etc., we have to go through patch
method to generate elm.
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// ...
const insertedVnodeQueue = []
// ...
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
}
// ...
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}
// ...
return vnode.elm
}
复制代码
patch process (removal of boundary conditions) have mainly three kinds of case:
-
There is no oldVnode, is carried out
createElm
-
There is oldVnode and vnode, but
sameVnode
returns false, carried outcreateElm
-
There is oldVnode and vnode, it
sameVnode
returns true, the conductpatchVnode
3. sameVnode
I mentioned above sameVnode
, as follows:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
复制代码
For the simple case, for example, before a <div>
label, due to changes in logic, into a <p>
label, and then sameVnode
returns false
( a.tag === b.tag
returns false). So sameVnode
show that the above conditions is the same element, it can be conducted patchVnode
. Conversely understanding is that as long as any of the above changes, you do not need be pathchVnode
, directly from the vnode
conduct createElm
can be.
Note that sameVnode
returns true, can not explain is that the same as a vnode here means more consistent with the current indicators, their children may have changed, still need to patchVnode
be updated.
patchVnode
By the patch
way, we know the patchVnode
method and the createElm
final result of processing methods, as is to generate or update the current vnode corresponding dom.
After the above analysis, the conclusion is that when the need to generate dom, and performed before and after the vnode sameVnode
is true
the case, then performed patchVnode
.
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// ...
const elm = vnode.elm = oldVnode.elm
// ...
const oldCh = oldVnode.children
const ch = vnode.children
// ...
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 具体是何种情况下会走到这个逻辑???
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// ...
}
复制代码
These are patchVnode
part of the code, it shows this part of the logic, but also patchVnode
the core processing logic.
The above code, full of a lot of if
else
, you can ponder a few questions?
- Based on the above analysis of the code, for a vnode, it can be divided into three vnode: vnode text, the vnode chilren presence, absence Children's vnode. For oldVnode vnode and cross combination, it should have nine kinds of case, then there are all of the above code to cover all case it?
- That, for example, exactly which
case
will enter intoremoveVnodes
the logic?
Actually, this is the problem I thought when reading the end, I used the following way (against the code to draw a table) to solve this complex if
else
interpretation of logic:
oldVnode.text | oldCh | !oldCh | |
---|---|---|---|
vnode.text | setTextContent | setTextContent | setTextContent |
ch | addVnodes | updateChildren | addVnodes |
!ch | setTextContent | removeVnodes | setTextContent |
It corresponds to the form, and corresponds to the code, I believe you can find the answer.
updateChildren
After the above analysis, only oldCh
and ch
will be implemented in the case are present updateChildren
, then the parameters are oldCh
and ch
, so you can know is that updateChildren
carried out at the same level is children
updated comparison, that is 'legendary' diff the .
Before starting analysis, the can consider: If now js to operate a native dom the <ul>
list, of course, this list is by native js implemented, now if one of the data sequence is changed, the first to be discharged to the end or the specific a location, or have new data, deleting data, how to operate.
let listData = [
'测试数据1',
'测试数据2',
'测试数据3',
'测试数据4',
'测试数据5',
]
let ulElm = document.createElement('ul');
let liStr = '';
for(let i = 0; i < listData.length; i++){
liStr += `<li>${listData[i]}</li>`
}
ulElm.append(liStr)
document.body.innerHTML = ''
document.body.append(ulElm)
复制代码
This time due to changes in uncertainty, does not want to maintain the business logic in the code tedious insertBefore
, , appendChild
, removeChild
, replaceChild
can think immediately brutal solution is, we get the latest listData
, the top surface of the flow created go again.
However vue adopted a diff algorithm, simply means that:
- Or the same as above, is still the first to get the latest
listData
- Then new data for
_render
the operation to obtain new vnode - Before and after comparison vnode, which is patch process
- For nodes at the same level, we will conduct
updateChildren
operations (diff), minimal changes
diff
updateChildren
code show as below:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
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, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
复制代码
Prior to the analysis, oldCh
and ch
represents the same level of vnode list, that is, two arrays
Before you begin defines a set of variables are as follows:
oldStartIdx
Start pointer oldCh the head portion to be treated, i.e. the corresponding vnodeoldStartVnode
oldEndIdx
End pointer oldCh the tail portion to be treated, i.e. the corresponding vnodeoldEndVnode
newStartIdx
Start pointer ch the head portion to be treated, i.e. the corresponding vnodenewStartVnode
newEndIdx
End pointer ch in the tail portion to be treated, i.e. the corresponding vnodenewEndVnode
oldKeyToIdx
Is a map, where the key is often written in the for loopv-bind:key
value, value that corresponds to the current vnode, that is, you can find the corresponding vnode in the map by a unique key
updateChildren
Using a while loop to update dom, where the exit condition is !(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
, in other kinds of ways to understand: oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
what does it mean, as long as there is a happening 'cross' (The following example will cross) to exit the loop.
For chestnuts
OldCh original sequence is A, B, C, D, E, F, G, after the order is updated to ch F, D, A, H, E, C, B, G.
Illustrate
For a better understanding of the subsequent round, look under the tag before the start of the relevant accord
diff process
Round1: Comparison sequence: AF -> GG, the matching is successful, then:
- To G in
patchVnode
the operation, updateoldEndVnode
G andnewEndVnode
ELM of G - Pointer movement, two tail pointer moves to the left, i.e.,
oldEndIdx--
newEndIdx--
round2: Comparison sequence: AF -> FB -> AB -> FF, the matching is successful, then:
- F is for
patchVnode
operation, updatingoldEndVnode
F andnewEndVnode
elm F's - Pointer movement, cursor movement, i.e.,
oldEndIdx--
newStartIdx++
- Found
oldStartVnode
elm dom in position A is located, and then inserted at the front of the updated F
Round3: Comparison sequence: AD -> EB -> AB -> ED, unsuccessful, taking the D key, the oldKeyToIdx
lookup, to find the corresponding D, the search is successful, then:
- D assigned to the extracted
vnodeToMove
- For the D
patchVnode
operation, updatevnodeToMove
D andnewStartVnode
ELM of D - Pointer movement, cursor movement, i.e.,
newStartIdx++
- The oldCh corresponding vnode opposed to D
undefined
- Found in the dom
oldStartVnode
elm elm corresponding to the node A, and then inserted at the front of the updated D
round4: comparative sequence: AA, comparison is successful, then:
- A carry out of
patchVnode
operation, updatesoldStartVnode
A andnewStartVnode
ELM A of - Pointer movement, two tail pointer moves to the left, i.e.,
oldStartIdx++
newStartIdx++
round5: Comparison sequence: BH -> EB -> BB , comparison is successful, then:
- B is on the
patchVnode
operation, updateoldStartVnode
B andnewStartVnode
ELM B of - Pointer movement, i.e.,
oldStartIdx++
newEndIdx--
- Dom found in
oldEndVnode
E elm of thenextSibling
node (i.e., the G elm), and then inserted at the front of the updated B elm
round6: Comparison sequence: CH -> EC -> CC , a successful comparison, then (with round5):
- C to carry out
patchVnode
the operation, updateoldStartVnode
C andnewStartVnode
ELM C of - Pointer movement, i.e.,
oldStartIdx++
newEndIdx--
- Dom found in
oldEndVnode
E elm of thenextSibling
node (i.e., the just inserted elm B), and then inserted at the front of the updated C elm
round7: Get oldStartVnode fails (because round3 step 4), then:
- Pointer movement, i.e.,
oldStartIdx++
round8: Comparison sequence: EH, EE, the matching is successful, then (with round1):
- E conducted to
patchVnode
operate, updateoldEndVnode
E andnewEndVnode
E of elm - Pointer movement, two tail pointer moves to the left, i.e.,
oldEndIdx--
newEndIdx--
last after round8 oldCh occurred early 'crossover' to exit the loop.
last:- Found
newEndIdx+1
corresponding to element A - Portion to be processed (i.e.
newStartIdx
-newEndIdx
the vnode in) was added portion without patch, directcreateElm
- All these parts to be treated, are located behind the inserted position elm Step A in dom 1
Need some attention:
- oldCh ch and their position does not change in the process
- Into the real operation is
updateChildren
passedparentElm
, father of elm vnode - while each of the loop, and I call back, which is round
- Mentioned several times
patchVnode
, look forwardpatchVnode
portion, which is the result of the processing has been updated oldVnode.elm and vnode.elm - There are many times the native operating dom, the
insertBefore
focus is to first find a place to insert
to sum up
Each round (related to the above example) do as follows (top to bottom priority):
- No
oldStartVnode
mobile (see round6) - Comparative head, and is updated successfully moved (refer to round4)
- Comparative tail, and the mobile is updated successfully (see Round1)
- Comparative head and tail, and the mobile is updated successfully (see round5)
- Comparative tails, and the mobile is updated successfully (see round2)
- In
oldKeyToIdx
according tonewStartVnode
the lookup can be performed, and the mobile is updated successfully (see Round3) (updated and movement: patchVnode updates the corresponding vnode the ELM, and move the pointer)
Insert questions about why some dom operation immediately carried out, some do not? When in oldStartVnode
the elm forward runs, when at oldEndVnode
the elm of nextSibling
forward runs?
Just remember here, oldCh
and ch
are reference, which ch
is our goal sequence, and oldCh
we used to understand the current reference dom order, which is mentioned at the beginning of the introduction of vnode. Diff so the whole process, is the comparison oldCh
and ch
, to confirm the current round, oldCh
how to move closer ch
, since the oldCh
part to be treated is still in the dom, it can oldCh
in oldStartVnode
the elm, and oldEndVnode
position elm to determine how successful matching elements insert.
- 'Head' match on success proves the current
oldStartVnode
position is the current position without moving, bepatchVnode
updated to - 'Tail tail' matches with the 'head' matches, and without moving
- If 'tails successfully matched', i.e.,
oldEndVnode
thenewSatrtVnode
matching is successful, success is noted herenewSatrtVnode
, it is to be treated dom head forward runs. As part round2, currently pending, which isoldCh
part of the black blocks, i.e. the headoldStartVnode
. That is, inoldStartVnode
front of the elm insertnewSatrtVnode
the elm. - Similarly, if the 'head to tail successful match', i.e.,
oldStartVnode
thenewEndVnode
matching is successful, success is noted here thatnewEndVnode
the tail, it is to be treated dom insertion (that is, the next element of the forward runs trailing elements). As part round5, currently pending, which isoldCh
part of the black blocks, i.e. the tailoldEndVnode
. That is, first findoldEndVnode
the elm innextSibling
front of the insertionnewEndVnode
of the elm.
(Here referred to 'block to be processed', you can see a schematic diagram specifically, attention oldCh
to be processed in the block portion and dom portion to be processed)
Above already contains updateChildren
most of the content, of course, there are some that are not covered not setting them, specific we can look at the source code, to find examples of the whole process can go.
Finally, there is a problem did not answer, insertedVnodeQueue
what is the use? Why been with?
This section refers to the process of patch components, where you can simply under: assembly $mount
after immediately after the function does not trigger component instance mounted
hook, but the current instance push
to insertedVnodeQueue
, and then the second to last line in the patch, it will perform invokeInsertHook
, which is the trigger for all component instances of insert
hooks and assembly insert
hook function will trigger component instance mounted
hook. For example, in the course of the patch, patch multiple components vnode, they have carried out $mount
which generates dom, but did not trigger immediately $mounted
, but so the whole patch
complete, one by one trigger.
Reproduced in: https: //juejin.im/post/5cfbee14e51d45776147610d