Vue3 source code parsing – bidirectional binding
In Vue, bidirectional binding mainly refers to the change of corresponding DOM after the change of responsive data, and the way that DOM change affects responsive data with also belongs to bidirectional binding. Its essence is a series of changes caused by the change of responsive data, including the trigger of responsive methods, the generation of new VNodes, The diff process of the old and new VNodes corresponds to the generation and rendering of DOM nodes that need to be changed. The overall process is shown in the figure:
Let’s modify the demo code from the previous chapter to trigger a reactive data change, as shown below:
<div id="app">
<div>
{{name}}
</div>
<p>123</p>
</div>
const app = Vue.createApp({
data(){
return {
attr : 'attr'.name : 'abc'}},mounted(){
setTimeout(() = >{
// Change the responsive data
this.name = 'efg'
},1000*5)
}
}).mount("#app")
Copy the code
When this. Name is modified, the corresponding name value on the page will change accordingly. The whole process to the final DOM change is executed at the source level as shown below (from bottom to top).
The above process includes reactive method triggering, generation of new VNodes, diff process of comparison between old and new VNodes, generation and rendering of DOM nodes that need to be changed. When the setElementText method is executed, the DOM of the page is modified as follows:
setElementText: (el, text) = > {
el.textContent = text// Change to efg
}
Copy the code
Below, explain these processes one by one.
Responsive triggering
In the previous reactive principle, listening will be collected when creating reactive data. In the source code reactivity/ SRC /effect.ts, track method, its core code is as follows:
export function track(target: object, type: TrackOpTypes, key: unknown) {...// Get the depsMap of the current target object
let depsMap = targetMap.get(target)
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the deP dependency for the current key
let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) {// Collect current effects as dependencies
dep.add(activeEffect)
// The current effect collects the DEP collection as a dependency
activeEffect.deps.push(dep)
}
}
Copy the code
After collecting the listener, you get the targetMap, and when the listener trigger is triggered, you get the current target from the targetMap.
Name is a responsive data, so the trigger name value changes, will enter the corresponding Proxy set method in the handler for this object, in source reactivity/SRC/baseHandlers ts, its core code as shown below:
function createSetter() {...// Trigger the listener
trigger(target, TriggerOpTypes.SET, key//name, value//efg, oldValue//abc). }Copy the code
In the source code reactivity/ SRC /effect.ts, trigger method, its core code is as follows:
export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {...// Get the dependency mapping table for the current target
const depsMap = targetMap.get(target)
if(! depsMap) {// never been tracked
return
}
// Declare a collection and method to add the dependency collection corresponding to the current key
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > effects.add(effect))
}
}
// Declare a scheduling method
const run = (effect: ReactiveEffect) = > {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// Add the current key dependency to effects in different ways depending on the type.// Loop over runs the dependency in a certain scheduling manner
effects.forEach(run)
}
Copy the code
The trigger method, summed up, does the following:
- First, obtain the dependency mapping table corresponding to the current target. If no, the target has no dependency. Otherwise, go to the next step.
- You then declare a collection of ReactiveEffects and a method to add elements to the collection.
- Add a dependency for the current key to a ReactiveEffect in different ways depending on the type.
- Declare a scheduling method, using different scheduling run methods based on the parameters we passed into the ReactiveEffect function, and loop through the execution.
The main purpose of the trigger method is to schedule the call of the method, that is, to run a ReactiveEffect object bound to the run method. How to bind the corresponding run method? Let’s look at the implementation of ReactiveEffect, in the source code reactivity/ SRC /effect.ts, its core code is as follows:
export class ReactiveEffect<T = any> {
...
constructor(
public fn: () => T, // Pass in the callback method
public scheduler: EffectScheduler | null = null.// Scheduling functionscope? : EffectScope |null
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if(! effectStack.includes(this)) {
try{...// Perform the binding method
return this.fn()
} finally{... }}}}Copy the code
Renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts ();
/ / setupRenderEffect () method.const effect = new ReactiveEffect(
componentUpdateFn,// Bind to the run method, which includes VNode generation logic
() = > queueJob(instance.update),
instance.scope // track it in component's effect scope
)
Copy the code
Through ReactiveEffect, reactive logic and VNode logic are linked, which itself is an event object based on publish/subscribe mode. Track is responsible for subscription, i.e. collecting listener; trigger is responsible for publishing, i.e. triggering listener; Effect is a bridge, storing event data.
ReactiveEffect also exposes the effect method of the Composition API, which can be customized to add listener collection. In the source code of reactivity/ SRC /effect.ts, the core code is as follows:
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// Create a ReactiveEffect object
const effect = createReactiveEffect(fn, options)
if(! options.lazy) { effect() }return effect
}
Copy the code
When used, the following code looks like:
// this. Name changes when triggered here
Vue.effect(() = >{
console.log(this.name)
})
Copy the code
Based on the explanation of the principle of responsiveness in the previous chapter, we summarize the process of the triggering of responsiveness into a flow chart for readers to understand, as shown in the figure.
When the reactive triggering is complete, the VNode generation process is initiated.
Generate new VNode
Renderer.ts () {componentUpdateFn () {renderer.ts () {renderer.ts () {renderer.ts () {renderer.ts () {componentUpdateFn ();}
const componentUpdateFn = () = > {
// Find the corresponding DOM mount for the first rendering, no need to compare old and new vNodes
if(! instance.isMounted) { ... instance.isMounted =true. }else {
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// Determine if the update was brought by the parent
if (next) {
next.el = vnode.el
// Child component updates
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
...
// Get a new VNode (based on the new responsive data, execute render method to get the VNode)
const nextTree = renderComponentRoot(instance)
// Get the old VNode from the subTree field
const prevTree = instance.subTree
// Assign the new value to the subTree field
instance.subTree = nextTree
// Compare the old and new vNodes
patch(
prevTree,
nextTree,
/ / teleport judgmenthostParentNode(prevTree.el!) ! ./ / fragments
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
}
}
Copy the code
RenderComponentRoot is also used in the previous chapter of the virtual DOM. It will execute the render method of the component internally. The render method can be used to obtain the new VNode. Assign the new VNode to the subTree field for next comparison.
Then the patch method will be entered to conduct the comparison diff of virtual DOM.
Virtual DOM diff
The core of the diff process of the virtual DOM is the patch method, which mainly uses patchFlag (or type) in the compile stage to deal with updates under different circumstances. This can also be understood as a divide-and-conquer strategy. In this method, DOM nodes are not updated by violence directly through the current VNode. Instead, patchflags of the old and new vNodes are compared according to the situation, and then the different attributes or nodes are found out through the comparison results, so as to reduce unnecessary overhead and improve performance.
In the process of patch, the following things are mainly accomplished:
- Create a node to be added.
- Remove an obsolete node.
- Move or modify nodes that need to be updated.
In the whole process, patchFlag will be used for judgment. In the process from AST tree to render and then to VNode generation, corresponding patchFlag will be added according to the node type. It is not enough to have patchFlag alone, but also depends on the setting of shapeFlag. The corresponding createVNode method in the source code is as follows:
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
) :VNode {
const shapeFlag = isString(type)
...
const vnode = {
__v_isVNode: true["__v_skip" /* SKIP */] :true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null.component: null.suspense: null.ssContent: null.ssFallback: null.dirs: null.transition: null.el: null.anchor: null.target: null.targetAnchor: null.staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null.appContext: null}; .return vnode
}
Copy the code
The _createVNode method is used to standardize vNodes and add corresponding shapeFlag and patchFlag. The shapeFlag value is a number, and each shapeFlag represents a different VNode type. The shapeFlag value is based on the NodeType of the AST tree, so the shapeFlag value is similar to the NodeType, as shown below:
export const enum ShapeFlags {
ELEMENT = 1./ / element string
FUNCTIONAL_COMPONENT = 1 << 1.// 2 function
STATEFUL_COMPONENT = 1 << 2.// 4 object
TEXT_CHILDREN = 1 << 3./ / 8 text
ARRAY_CHILDREN = 1 << 4./ / 16 array
SLOTS_CHILDREN = 1 << 5./ / 32 slot
TELEPORT = 1 << 6.// 64 teleport
SUSPENSE = 1 << 7.// 128 suspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8.// 256 Keep alive component
COMPONENT_KEPT_ALIVE = 1 << 9.// 512 Keep alive component
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT / / component
}
Copy the code
PatchFlag means that different policies are adopted during update, and each meaning is as follows:
export const enum PatchFlags {
// Dynamic text content
TEXT = 1./ / dynamic class
CLASS = 1 << 1.// Dynamic style
STYLE = 1 << 2./ / dynamic props
PROPS = 1 << 3.// There is a dynamic key, that is, the key of the props object is not certain
FULL_PROPS = 1 << 4.// Merge events
HYDRATE_EVENTS = 1 << 5.// children fragment in order
STABLE_FRAGMENT = 1 << 6.// children fragment with key node
KEYED_FRAGMENT = 1 << 7.// Fragment of children without key
UNKEYED_FRAGMENT = 1 << 8.// Only non-props need patch, such as' ref '
NEED_PATCH = 1 << 9.// Dynamic slots
DYNAMIC_SLOTS = 1 << 10.// Special flags, which are not used in optimization, are built-in special flags. SPECIAL FLAGS// Indicates that it is a static node, its content never changes, and there is no need to diff its children during hydrate
HOISTED = -1.// The diff used to indicate a node should end
BAIL = -2,}Copy the code
Including shapeFlag and patchFlag, which have the same meaning as its name, are actually a series of flags to identify how a node should be updated. CLASS = 1 << 1 is used to represent the bit operation, that is, each patchFlag is represented by a certain digit in binary. So that we can more convenient extension, for example, the TEXT | CLASS can get 0000000011, this value can be said that he has the characteristics of the TEXT, namely also has the characteristics of the CLASS, if need to add a new flag, num is left with a new number one can directly: 1 < < num.
ShapeFlag can be understood as the VNode type, while patchFlag is more like the VNode change type.
For example, in the demo code, we bind the responsive variable attr to props as follows:
. <div :data-a="attr"></div>
...
Copy the code
The patchFlag obtained is 8 (1<<3). In the source code compiler – core/SRC/transforms/transformElement ts can see the corresponding Settings in logic, the core code is as follows:
.// Multiple values can be set by bit each time
if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS
} else {
if(hasClassBinding && ! isComponent) { patchFlag |= PatchFlags.CLASS }if(hasStyleBinding && ! isComponent) { patchFlag |= PatchFlags.STYLE }if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
}
Copy the code
Renderer. ts/run-time core/ SRC /renderer.ts /renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) = > {
// The old and new vNodes are the same object, so no comparison is made
if (n1 === n2) {
return
}
// Patching & vNodes that are not of the same type are uninstalled from the node tree
if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
n1 = null
}
// PatchFlag is the BAIL type
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) { // Determine according to the Vnode type
case Text: // Text type
processText(n1, n2, container, anchor)
break
case Comment: // Comment type
processCommentNode(n1, n2, container, anchor)
break
case Static: // Static node type
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
}
break
case Fragment: / / type of fragments
processFragment(/* Ignore the argument */)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) { // Element type
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // Component type. }else if (shapeFlag & ShapeFlags.TELEPORT) { / / TELEPORT type. }else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { / / type of SUSPENSE. }}}Copy the code
N1 is the old VNode and n2 is the new VNode. If the old and new vnodes are the same object, no comparison will be made. If the old node exists and the new and old nodes are not of the same type, the old node will be uninstalled from the node tree. In this case, ShapeFlag is used. For common HTML ELEMENT types, the default branch is entered. Let’s take ELEMENT as an example and enter the processElement method. Renderer. Ts/run-time core/ SRC /renderer.ts
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) = > {
// If the old node does not exist, render directly
if (n1 == null) {
mountElement(
n2,
container,
anchor
...
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
Copy the code
The logic of the processElement method is relatively simple, but adds another layer of judgment. When there are no old nodes, the render process goes directly, which is what happens when the root instance is called to initialize createApp. Renderer.ts/runtime-core/ SRC /renderer.ts /renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) = > {
let { patchFlag, dynamicChildren, dirs } = n2
...
// Trigger some hooks
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')}...// When a new VNode has a dynamic node, update the dynamic node first (efficiency improvement)
if(dynamicChildren) { patchBlockChildren( n1.dynamicChildren! , dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds )if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if(! optimized) {/ / the diff
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false)}// Use different update logic according to different patchFlag
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// If the element props contains dynamic keys, a full comparison is required
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
/ / dynamic class
if (patchFlag & PatchFlags.CLASS) {
if(oldProps.class ! == newProps.class) { hostPatchProp(el,'class'.null, newProps.class, isSVG)
}
}
/ / dynamic style
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
/ / dynamic props
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if(next ! == prev || key ==='value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// Interpolate text
if (patchFlag & PatchFlags.TEXT) {
if(n1.children ! == n2.children) { hostSetElementText(el, n2.childrenas string)
}
}
} else if(! optimized && dynamicChildren ==null) {
/ / the diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
...
}
Copy the code
At the beginning of the processElement method, some hook functions are executed, and the new node is checked to see if it has identified dynamic nodes (i.e. the static promotion part of the optimization, separating the dynamic nodes from the static nodes). If so, the new node is updated first (no comparison is required, so it is faster). Then update the props, style, and class of the node using the patchProps method. The main logic is as follows:
- If patchFlag is FULL_PROPS, it indicates that the element may contain dynamic keys and needs to perform full props diff.
- When patchFlag is CLASS, when the classes of the old and new nodes are inconsistent, CLASS atch will be performed. When the CLASS attributes of the new and old nodes are completely consistent, no operation is required. This Flag is added when an element has a dynamic class binding.
- When patchFlag is a STYLE, the STYLE will be updated, which will be done every patch. This Flag will be added when there is a dynamic STYLE binding.
- When patchFlag is PROPS, note that this Flag is added when the element has dynamic properties or attrs bindings. Unlike class and style, these dynamic prop or attrs keys are saved for faster iteration.
- When patchFlag is TEXT, hostSetElementText will be called for updating if the TEXT of the child nodes in the old and new nodes changes. This Flag is added when the child nodes of the element contain only dynamic text.
The corresponding methods of each patchFlag will eventually enter the logic of DOM operation. For example, for STYLE update, the setStyle method will be entered. In the source runtime-dom/ SRC /modules/style.ts, the core code is as follows:
function setStyle(style: CSSStyleDeclaration, name: string, val: string | string[]) {
if (isArray(val)) { // Multiple styles can be set simultaneously
val.forEach(v= > setStyle(style, name, v))
} else {
if (name.startsWith(The '-')) {
// custom property definition
style.setProperty(name, val)/ / dom operation
} else {
const prefixed = autoPrefix(style, name)
if (importantRE.test(val)) {
/ /! important
style.setProperty(
hyphenate(prefixed),
val.replace(importantRE, ' '),
'important')}else {
style[prefixed as any] = val
}
}
}
}
Copy the code
text hi is also a child node. For the child node, the patchChildren method is used. Renderer. Ts/run-time core/ SRC /renderer.ts
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) = > {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
if (patchFlag > 0) {
// The key value is Fragment: KEYED_FRAGMENT
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
...
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// The key value is UNKEYED_FRAGMENT
patchUnkeyedChildren(
...
)
return}}// The new node is a text type child node (single child node)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// The old node is an array type, so the new node is overwritten
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
// Set a new node
if(c2 ! == c1) { hostSetElementText(container, c2as string)
}
} else {
// The new node is an array type child node.
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// New and old metropolitan array types, then full diff
patchKeyedChildren(
...
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)}}else {
// Set an empty string
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, ' ')}// mount new if array
if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( ... ) }}}}Copy the code
In the above code, the patchFlag is first judged:
- If patchFlag is a Fragment: KEYED_FRAGMENT with a key value, patchKeyedChildren is called to continue processing child nodes.
- When patchFlag is UNKEYED_FRAGMENT without key value, patchUnkeyedChildren is called to process the child node without key value.
Then judge according to shapeFlag:
- If the new child node is a text type and the old child node is an array type (containing multiple children), the child node of the old node is simply unloaded and replaced with the new node.
- If the old child node type is array type, and if the new child node is also array type, then patchKeyedChildren is called for full diff. If the new child node is not array type, it indicates that there is no new child node, and the old node can be directly uninstalled from the tree.
- If the old child node is a text type, you can be sure that the new child node is not a text type because you have already determined whether the new child node is a text type in the first place, and you can simply set the element’s text to an empty string.
- If the type of the new child node is array but that of the old child node is not array, mount the new child node to the tree.
No matter how complicated the nesting of node arrays is, in the end, it all comes down to basic DOM operations, including creating nodes or deleting nodes, modifying node properties, etc., but the core is how to find the nodes that need to be changed between the old and new trees, and that’s the heart of diff. The real Diff needs to get inside patchUnkeyedChildren and patchKeyedChildren to find out. Renderer. Ts/SRC/patchUnkeyedChildren /renderer.ts
const patchUnkeyedChildren = () = >{... c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARRconst oldLength = c1.length
const newLength = c2.length
// Get the minimum length of the old and new nodes
const commonLength = Math.min(oldLength, newLength)
let i
// Patch through the old and new nodes
for (i = 0; i < commonLength; i++) {
Clone a new VNode if it has already been mounted. Otherwise, create a new VNode
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch()
}
// If the number of old nodes is greater than the number of new nodes
if (oldLength > newLength) {
// Uninstall the redundant nodes directly
unmountChildren( )
} else {
// old length < new length => Create new length
mountChildren()
}
}
Copy the code
The main logic is to first get the shortest public length of the old and new nodes, then traverse the public part, recursively perform patch method for the public part again, if the number of old nodes is greater than the number of new nodes, directly uninstall the redundant nodes, or new nodes.
It can be seen that in the case of no key, DIff is relatively simple, but the performance is relatively low. DOM reuse is rarely realized, and more nodes are created and deleted. This is why Vue recommends adding unique key values to array nodes.
Below is patchKeyedChildren method in source runtime – core/SRC/renderer. Ts, its core code as shown below:
const patchKeyedChildren = () = > {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. Iterate through the head, continue with the same node, and break out of the loop with different nodes
while(i <= e1 && i <= e2) {... }// 2. Perform tail traversal, continue when the same node is encountered, and jump out of loop when different nodes are encountered
while(i <= e1 && i <= e2) {... }// 3. If the old node has been traversed and the new node is still available, the new node is traversed and added
if (i > e1) {
if (i <= e2) {...}
}
// 4. If the new node has been traversed and the old node is still available, unmount the new node
else if (i > e2) {
while (i <= e1) {...}
}
// 5. The old and new nodes are not traversed completely
else {
// 5.1 Create a map to store key-value pairs for the remaining new nodes. The mapping is key => index
// 5.2 Traverse the remaining old nodes, compare the old and new data, and remove the unused old nodes
// 5.3 Get the longest increment subsequence to move or mount}}Copy the code
The patchKeyedChildren method is the core of the whole DIFF, and it contains specific algorithms and logic, which is complicated to explain in code. Here, a simple example is used to illustrate what this method does. There are two arrays as shown in the following code:
/ / the old array [" a ", "b", "c", "d", "e", "f", "g", "h"] / / new array (" a ", "b", "d", "f", "c", "e", "x", "y", "g", "h"]Copy the code
Each element in the array above represents a key. Perform the following steps:
- 1. Compare from beginning to end, [a, B] is sameVnode, enter patch, stop at [C].
- 2. Compare from end to end, [h,g] is sameVnode, enter patch, and stop at [f].
- 3. Check whether the old data has been compared. The unnecessary information is added and needs to be mounted.
- 4. Check whether the new data has been compared. Unnecessary information is deleted and unmount is required.
- To enter here, the order is out of order, to enter 5:
- 5.1 to create a new data haven’t compare the index Map: [{d: 2}, {f: 3}, {c: 4}, {e: 5}, {x: 6}, {y: 7}].
- 5.2 According to the length of uncompared data, build an array [0,0,0,0,0] filled with 0, and then loop through the old remaining data to find the index ARr of the uncompared data :[4(d),6(f),3(c),5(e),0,0]. If it is not found in the new remaining data, it indicates that it is deleted and unmounted. If you find it, you can match it with the previous patch.
- Index arr = 0; index arR = 0; index arR = 0; index arR = 0;
- Let’s move f before C.
- Let’s move d in front of f.
- After moving, c will naturally move to the front of e, which can be found by the previous arr index by the longest increasing subsequence, so that the corresponding c and e of [3,5] need not move.
So far, this is the core content and principle of diff in the whole patchKeyedChildren method. Of course, there are many code details. Readers who are interested can read the complete source code of patchKeyedChildren.
The actual DOM modification is complete
No matter how complex the nested node array is, it will eventually fall to the basic DOM operations, including creating or deleting nodes, modifying node properties, etc. When the result is obtained, the corresponding DOM operation method will be called. This part of the logic in the source code runtime-dom\ SRC \ nodeops.ts. Store are some tools and methods, the core code is as follows:
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// Insert elements
insert: (child, parent, anchor) = > {
parent.insertBefore(child, anchor || null)},// Delete elements
remove: child= > {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// Create the element
createElement: (tag, isSVG, is, props): Element= >{... },// Create text
createText: text= > doc.createTextNode(text),
// Create a comment
createComment: text= > doc.createComment(text),
// Set the text
setText: (node, text) = > {
node.nodeValue = text
},
// Set the text
setElementText: (el, text) = > {
el.textContent = text
},
parentNode: node= > node.parentNode as Element | null.nextSibling: node= > node.nextSibling,
querySelector: selector= > doc.querySelector(selector),
// Set element attributes
setScopeId(el, id) {
el.setAttribute(id, ' ')},/ / clone DOM
cloneNode(el){... },// Insert static content, including processing SVG elements
insertStaticContent(content, parent, anchor, isSVG){... }}Copy the code
This part of the logic is normal DOM operations, relatively simple, you can directly read the source code. This completes the bidirectional binding process.