Writing in the front
This article is a review of a series of recent Vue. Js source code articles (github.com/answershuto…). Summary, in the process of reading source code also really benefit, I hope their output will also want to learn vue.js source partners help. This article has also been published in our technical blog before, welcome everyone to pay attention to our technical blog, give a portal blog.souche.com/.
Because of the vue. js is very interested in, and the usual work of the technology stack vue. js, these months spent some time to study the vue. js source code, and do a summary and output.
Original address of the article: github.com/answershuto… .
In the process of learning, I added Chinese annotations for Vue github.com/answershuto… , hope can be helpful to other want to learn Vue source partners.
There may be some misunderstanding, welcome to point out, learn together, make progress together.
Start with a new Vue object
let vm = new Vue({
el: '#app'./*some options*/
});Copy the code
Many students wonder what happens inside a Vue object when it is new.
How does vue.js render data from data into a real host environment?
How do you modify data in a “reactive” way?
How is the template compiled into usable HTML in the real world?
Is the Vue instruction executed again?
With these questions in mind, let’s start with Vue’s constructor classes.
Vue structure class
function Vue (options) {
if(process.env.NODE_ENV ! = ='production' &&
!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')}/* Initialize */
this._init(options)
}Copy the code
Vue’s constructor class does only one thing, which is to call the _init function
So let’s look at the code for init
Vue.prototype._init = function (options? : Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
startTag = `vue-perf-init:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
/* A flag bit that prevents the VM instance itself from being observed */
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
/* Initialize the life cycle */
initLifecycle(vm)
/* Initialize the event */
initEvents(vm)
/* Initialize render*/
initRender(vm)
/* Call the beforeCreate hook function and trigger the beforeCreate hook event
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/* Initialize props, methods, data, computed, and watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
/* Calls the created hook function and fires the Created hook event */
callHook(vm, 'created')
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
/* Format the component name */
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
/* Mount components */
vm.$mount(vm.$options.el)
}
}Copy the code
_init does two main things:
1. Initialization (life cycle, events, render function, state, etc.).
2. $mount components.
State is initialized between the life hooks beforeCreate and Created. During this process, props, Methods, data, computed, and Watch are initialized in sequence. This is how vue.js “responds” to the data in options. For vue.js responsive principle of students who do not understand the author can first look at another article “Vue.js responsive principle”.
/* Initialize props, methods, data, computed, and watch*/
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
/* Initialize props*/
if (opts.props) initProps(vm, opts.props)
/* Initialization method */
if (opts.methods) initMethods(vm, opts.methods)
/* Initializes data*/
if (opts.data) {
initData(vm)
} else {
/* This component has no data bound to an empty object */
observe(vm._data = {}, true /* asRootData */)}/* Initialize computed*/
if (opts.computed) initComputed(vm, opts.computed)
/* Initialize Watchers */
if (opts.watch) initWatch(vm, opts.watch)
}Copy the code
Two-way binding
Using initData as an example, Oberver is bidirectional bound to option’s data. The core principle of bidirectional binding for other option parameters is the same.
function initData (vm: Component) {
/* Get data */
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
/* Check if it is an object */
if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
/* Iterate over the data object */
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
// Iterate over the data in data
while (i--) {
/* Ensure that the key in data does not duplicate the key in props, props takes precedence. If any conflict occurs, warning*/ will be generated
if(props && hasOwn(props, keys[i])) { process.env.NODE_ENV ! = ='production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if(! isReserved(keys[i])) {/* Check whether the field is reserved */
/* Here is the proxy we talked about earlier, which proxies the attributes on data to the VM instance */
proxy(vm, `_data`, keys[i])
}
}
/*Github:https://github.com/answershuto*/
// observe data
/* Observe this step as the root data and recursively bind the underlying object. Observe this step as the root data. * /
observe(data, true /* asRootData */)}Copy the code
Observe binds the Object in data bidirectionally via defineReactive and sets setter and getter methods for the Object via Object.defineProperty. Getter methods are mainly used for dependency collection. For those who are not familiar with dependency collection, please refer to another article of the author called “dependency Collection”. Setter methods fire when the object is modified (Vue. Set is used to add properties when no property is added), and the setter notifies the Dep in the closure, which has Watcher observer objects subscribed to the object’s changes, and the Dep notifies the Watcher object to update the view.
If you are modifying a member of an array, which is an object, you simply recursively bind the members of the array in both directions. But there was a problem,? How do we listen for these changes in the array if we do pop, push, and so on, and the objects we push are never bidirectionally bound, let alone pop? Vue.js provides methods to override the seven array methods push, POP, Shift, unshift, splice, sort, reverse. The code to modify the array prototype method can be found in observer/array.js and observer/index.js.
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
/ /...
if (Array.isArray(value)) {
/* If it is an array, replace the original method in the prototype of the array with the modified array method that can intercept the response, so as to listen for the array data change response. Here, if the current browser supports the __proto__ attribute, the native array method on the current array object prototype is overridden directly, if not, the array object prototype is overridden directly. * /
const augment = hasProto
? protoAugment /* Override the prototype directly to modify the target object */
: copyAugment /* Defines (overrides) a method of the target object or array */
augment(value, arrayMethods, arrayKeys)
/* If this is an array, observe*/ from each member of the group
this.observeArray(value)
} else {
/* If it is an object, walk the binding */
this.walk(value)
}
}
}
/** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */
/* Overrides the prototype directly to modify the target object or array */
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/** * Augment an target Object or Array by defining * hidden properties. */
/* istanbul ignore next */
/* Defines (overrides) a method of the target object or array */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}Copy the code
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype * /
import { def } from '.. /util/index'
/* Get the prototype of the native array */
const arrayProto = Array.prototype
/* Create a new array object and modify the array's seven methods to prevent contamination of the native array methods */
export const arrayMethods = Object.create(arrayProto)
/** * Intercept mutating methods and emit events */
/* This overwrites the methods of the array without polluting the prototype of the native array, intercepts the changes in the members of the array, and notifies all associated observers to respond while performing the native array operation */; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
.forEach(function (method) {
// cache original method
/* Caches the array's native methods, followed by */
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
/* Call the native array method */
const result = original.apply(this, args)
/* The newly inserted element in the array needs to be observed again to respond */
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
/* DEP notifies all registered observers for responsive processing */
ob.dep.notify()
return result
})
})Copy the code
Create an Object.create(arrayProto) Object from the prototype of the array. You can modify the prototype to ensure that the native array methods are not corrupted. If the current browser supports the proto property you can override that property and give the array object an overridden array method. If you don’t have a browser for this property, you have to iterate through def all the array methods that need to be overridden, which is less efficient, so the first option is preferred.
The first is to notify all registered observers to respond. The second is to observe the new member if it is an operation to add a member.
Vue. Js provides $set() and $remove() methods.
For a more detailed explanation of bidirectional data binding and the implementation of Dep and Watcher, see my article “Data Binding from a Source Code Perspective”.
The template compilation
The $mount process compiles the template into the Render Function if it is a standalone build build. Of course, you can also use run-time builds. For details, see run-time – compilers -vs- for run-time only.
How is template compiled into the Render function?
function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
/* Parse to get the AST tree */
const ast = parse(template.trim(), options)
/* The objective of the optimization is to generate a template AST tree to detect static subtrees that do not require DOM changes. Once these static trees are detected, we can do the following: 1. Make them constant so that we no longer need to create new nodes every time we re-render. 2. Skip the patch process. * /
optimize(ast, options)
/* Generate the required code from the AST tree (internal includes render and staticRenderFns) */
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}Copy the code
BaseCompile first parses the template to get an AST syntax tree, optimizes it with Optimize, and generates render and staticRenderFns.
parse
The source code for Parse can be found at github.com/answershuto… .
Parse parses data such as instructions, classes, and styles in the Template template using regexes to form an AST syntax tree.
optimize
Optimize is the ability to mark static nodes, which is an optimization of the Vue compilation process. The DIff algorithm skips the patch process when update is performed, reducing the comparison process and optimizing the patch performance.
generate
Generate is the process of converting an AST syntax tree into a Render funtion string, resulting in a render string and a staticRenderFns string.
For details about template compilation, see “Talk about Template compilation for vue.js”.
Watcher to view
The Watcher object updates the view by calling the updateComponent method. By default, Vue. Js stores the Watcher object in a queue and updates the view asynchronously on the next tick, which optimizes performance. For those interested in nextTick, please refer to vue. js Asynchronous DOM Update Strategy and nextTick.
updateComponent = (a)= > {
vm._update(vm._render(), hydrating)
}Copy the code
The _render function will return a new Vnode, pass it in _update to compare it with the old Vnode object, get the difference between the two vNodes through a patch process, and finally render the difference to the real environment to form a view.
What is a VNode?
VNode
In the slash-and-burn era, you needed to manipulate the DOM directly in event methods to modify views. But when the application is large, it becomes difficult to maintain.
How about abstracting the real DOM tree into an abstract tree made of JavaScript objects, converting the abstract tree to the real DOM after modifying the abstract tree data and redrawing it on the page? The virtual DOM emerges, which is an abstraction of the real DOM and uses properties to describe various features of the real DOM. When it changes, it changes the view.
But this JavaScript manipulation of the DOM to redraw the entire view layer is very performance consuming, so can we just update it every time? Therefore, VUe. js abstracts DOM into a virtual DOM tree with JavaScript objects as nodes. VNode nodes are used to simulate the real DOM. Operations such as node creation, node deletion and node modification can be carried out on this abstract tree without the need to operate the real DOM. You only need to manipulate JavaScript objects, greatly improving performance. After modification, diff algorithm is used to obtain some minimum units to be modified, and then the view of these small units is updated. Doing so reduces the number of unwanted DOM operations and greatly improves performance.
Vue uses such an abstract node, VNode, which is a layer of abstraction of the real DOM, and does not depend on a platform. It can be a browser platform, or weeX, and even node platform can create and delete such operations on such an abstract DOM tree, which also provides the possibility of front and back end isomorphism.
First take a look at the Vue. Js source code for the VNode class definition.
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
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
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?
constructor( tag? : string, data? : VNodeData, children? :? Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions ) {/* Label name of the current node */
this.tag = tag
/* The object corresponding to the current node, which contains specific data information, is a VNodeData type, you can refer to the VNodeData type data information */
this.data = data
/* The child of the current node is an array */
this.children = children
/* The text of the current node */
this.text = text
/* The actual DOM node corresponding to the current virtual node */
this.elm = elm
/* Namespace of the current node */
this.ns = undefined
/* compile scope */
this.context = context
/* Functional component scope */
this.functionalContext = undefined
/* The key attribute of the node, which is used as the node's identifier, is used to optimize */
this.key = data && data.key
/* The component's option */
this.componentOptions = componentOptions
/* The instance of the component corresponding to the current node */
this.componentInstance = undefined
/* The parent of the current node */
this.parent = undefined
InnerHTML is true, and textContent is false*/
this.raw = false
/* Static node flag */
this.isStatic = false
/* Whether to insert as the heel node */
this.isRootInsert = true
/* Is a comment node */
this.isComment = false
/* Whether the node is a clone */
this.isCloned = false
/* Whether there is a v-once instruction */
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}Copy the code
This is a basic VNode node that serves as a base class for other derived VNode classes and defines the following data.
Tag: indicates the tag name of the current node
Data: indicates the object corresponding to the current node, which contains specific data information. It is of the VNodeData type. For details, see the VNodeData type
Children: An array of children of the current node
Text: indicates the text of the current node
Elm: real DOM node corresponding to the current virtual node
Ns: indicates the namespace of the current node
Context: compilation scope of the current node
FunctionalContext: Functional component scope
Key: The key attribute of a node, which is used as a node identifier for optimization
ComponentOptions: Indicates the option of a component
ComponentInstance: componentInstance corresponding to the current node
Parent: indicates the parent of the current node
Raw: Simply means whether it is native HTML or plain text. True for innerHTML and false for textContent
IsStatic: indicates whether the node isStatic
IsRootInsert: Indicates whether to be inserted as the heel node
IsComment: Indicates whether it is a comment node
Isempirical: Whether there is a clone node
IsOnce: indicates whether the V-once command exists
For example, let’s say I have a VNode tree
{
tag: 'div'
data: {
class: 'test'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,VNode'
}
]
}Copy the code
This is what the rendering would look like
<div class="test">
<span class="demo">hello,VNode</span>
</div>Copy the code
For details about how to operate a VNode, see VNode Node.
patch
Finally, _update will patch the old and new vNodes once to find the smallest differences between the two vNodes and render these differences to the view.
Firstly, the core diff algorithm of Patch is discussed. Diff algorithm compares tree nodes of the same layer instead of searching and traversing the tree layer by layer. Therefore, it is a fairly efficient algorithm with only O(n) time complexity.
These two diagrams represent the patch process between the old VNode and the new VNode. They only compare vnodes at the same level to get changes (the squares of the same color in the second diagram represent vNodes that are compared with each other), and then modify the changed view, so it is very efficient.
In the process of patch, if two vnodes are considered to be the sameVnode (sameVnode), depth comparison will be conducted to obtain the minimum difference; otherwise, the old DOM node will be directly deleted and a new DOM node will be created.
What is sameVnode?
Let’s look at the sameVnode implementation.
/* To check whether two VNodes are the same node, the following conditions must be met: Key Same tag (tag name of the current node) same isComment (comment node) Same Data (object corresponding to the current node, contains specific data information, is a VNodeData type, When the tag , the type must be the same */
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)
)
}
// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
Some browsers do not support dynamic modification of types, so they are treated as different types */
function sameInputType (a, b) {
if(a.tag ! = ='input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}Copy the code
If the tag, key, and isComment of two VNodes are the same, and data is defined or not, and if the label is input, the type must be the same. In this case, the two VNodes are considered as Samevnodes, and patchVnodes can be directly operated.
The rules of patchVnode are as follows:
1. If the old and new VNodes are static, have the same key (representing the same node), and the new VNode is clone or once (marked with the V-once attribute, rendering only once), then only elm and componentInstance need to be replaced.
2. If both the old and new nodes have children, diff is performed on the children and updateChildren is called, which is also the core of the diff.
3. If the old node has no children and the new node has children, clear the text content of the DOM of the old node and add children to the current DOM node.
4. When a new node has no children and an old node has children, remove all children of the DOM node.
5. When the old and new nodes have no children, it is just a text replacement.
updateChildren
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, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
constcanMove = ! removeOnlywhile (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)) {
/* The first four cases are considered to be the same VNode when the key is specified, so patchVnode can be directly used to compare the four cases */ in which two nodes of oldCh and newCh = 2*2
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 {
/* Create a hash table with a key corresponding to the old VNode key (this hash table is only generated when undefined for the first time, which can be used to check duplicate keys later). For example, childre looks like this [{xx: xx, key: 'key0'}, {xx: Xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 result {key0:0, key1:1, key2:2} */
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
/* If the new VNode newStartVnode has a key and the key can be found in oldVnode, return the idxInOld (subscript) */ for this node
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
/*newStartVnode has no key or the key is not found in the old node
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
/* Get the old node */ with the same key
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && !elmToMove) {
/* If elmToMove does not exist, a new node has been placed in the DOM of this key before, indicating that duplicate keys may exist. Ensure that v-for items have unique keys */
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(elmToMove, newStartVnode)) {
/*Github:https://github.com/answershuto*/
/* If the new VNode is the same VNode as the obtained node with the same key, patchVnode*/
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
/* Since patchVnode has been added, assign undefined to the old node. If there are new nodes with the same key as this node, it can be detected that there is a duplicate key*/
oldCh[idxInOld] = undefined
/* When there is an identifier, canMove can be inserted directly before the actual DOM node corresponding to oldStartVnode */
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
/* Create a new node */ when the new VNode is not sameVNode with the same key as the one found (for example, with a different tag or input tag of a different type)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
/* If oldStartIdx > oldEndIdx is used, there are more new nodes than old ones, so the new nodes need to be created one by one and added to the real DOM */
refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/* If newStartIdx > newEndIdx is found after all the comparisons are completed, the new nodes have been traversed, and the old nodes need to be removed from the real DOM */
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}Copy the code
Looking directly at the source may be difficult to understand the relationship, let’s look at the diagram.
First, there is a variable mark on both sides of the left and right sides of the old and new VNodes, which converge to the middle during traversal. The loop ends when oldStartIdx <= oldEndIdx or newStartIdx <= newEndIdx.
Mapping between indexes and VNodes: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode
In traversal, if a key is present and sameVnode is satisfied, the DOM node is reused, otherwise a new DOM node is created.
First, oldStartVnode, oldEndVnode, newStartVnode, newEndVnode pairwise comparison, there are 2*2=4 comparison methods.
When the start or end of the new and old vNodes meets sameVnode, that is, sameVnode(oldStartVnode, newStartVnode) or sameVnode(oldEndVnode, newEndVnode), Directly patchVnode the VNode.
If oldStartVnode and newEndVnode satisfy sameVnode, that is sameVnode(oldStartVnode, newEndVnode).
At this time, oldStartVnode has run after oldEndVnode, and the real DOM node needs to be moved to the back of oldEndVnode while patchVnode is being performed.
If oldEndVnode and newStartVnode satisfy sameVnode, that is sameVnode(oldEndVnode, newStartVnode).
This indicates that oldEndVnode runs in front of oldStartVnode, and the real DOM node moves in front of oldStartVnode when patchVnode is performed.
If none of the above is true, createKeyToOldIdx yields an oldKeyToIdx containing a hash table containing the old VNode key and the corresponding index sequence. This hash table can be used to find whether there are old vNodes with the same key as newStartVnode. If both sameVnode and newStartVnode satisfy, PatchVnode moves the real DOM (elmToMove) in front of the real DOM corresponding to oldStartVnode.
It is also possible that newStartVnode cannot find the same key in the old VNode, or that it is not sameVnode even though the key is the same, in which case createElm will be called to create a new DOM node.
At this point the loop is over, so we’re left with extra or insufficient real DOM nodes to deal with.
1. When oldStartIdx > oldEndIdx ends, the old VNode has been traversed, but the new node has not. Insert the remaining vNodes into the real DOM. Call addVnodes (createElm) to add these nodes to the real DOM.
2. Similarly, when newStartIdx > newEndIdx, the new vNodes have been traversed, but the old nodes are still available. This indicates that the real DOM nodes are redundant and need to be deleted from the document. In this case, call removeVnodes to remove these redundant real DOM nodes.
For a more detailed implementation of diff refer to the author’s article VirtualDOM with Diff (vue.js implementation).markdown).
Map to the real DOM
Since Vue uses the virtual DOM, the virtual DOM can be used on any platform that supports JavaScript, such as the current vUe-supported browser platforms or WEEX, and the implementation of the virtual DOM is consistent. Finally, how does the virtual DOM map to the real DOM node?
Vue do a layer of adaptation layer for platform, platform/browser platforms/web/runtime/node – ops. Js and weex platform/platforms/weex/runtime/node – ops. Js. Different platforms provide the same external interface through the adaptation layer. When the virtual DOM operates the real DOM node, it only needs to call the interface of these adaptation layers, while the internal implementation does not need to be concerned, because it will change according to the platform change.
Now we have another problem. We just map the virtual DOM to the real DOM. How do you add attr, class, style, and other DOM attributes to the DOM?
This depends on the life hooks of the virtual DOM. The virtual DOM provides the following hook functions, which are called at different times.
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
/ * build CBS callback function, the web platform/platforms/web/runtime modules * /
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}Copy the code
In the same way, there are different implementations for different platforms. Let’s take the Web platform as an example. Web platform of hook function/platforms/Web/runtime/modules. There are operations on the DOM properties attr, class, props, Events, style, and Transition.
In the case of attR, the code is simple.
/* @flow */
import { isIE9 } from 'core/util/env'
import {
extend,
isDef,
isUndef
} from 'shared/util'
import {
isXlink,
xlinkNS,
getXlinkProp,
isBooleanAttr,
isEnumeratedAttr,
isFalsyAttrValue
} from 'web/util/index'
Update attr * / / *
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
/* If both the old and new vNodes have no attr attribute, */ is returned
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return
}
let key, cur, old
/* Dom instance of VNode */
const elm = vnode.elm
/* Attr */ of the old VNode
const oldAttrs = oldVnode.data.attrs || {}
/* Attr */ of the new VNode
let attrs: any = vnode.data.attrs || {}
// clone observed objects, as the user probably wants to mutate it
/* If the attr of the new VNode already has __ob__ (which means that it has been observed. obob__), make a deep copy */
if (isDef(attrs.__ob__)) {
attrs = vnode.data.attrs = extend({}, attrs)
}
/* Iterate over attr, replace */ if inconsistent
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if(old ! == cur) { setAttr(elm, key, cur) } }// #4391: in IE9, setting type can reset value for input[type=radio]
/* istanbul ignore if */
if(isIE9 && attrs.value ! == oldAttrs.value) { setAttr(elm,'value', attrs.value)
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else if(! isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } }/ * attr * /
function setAttr (el: Element, key: string, value: any) {
if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, key)
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true')}else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, value)
}
}
}
export default {
create: updateAttrs,
update: updateAttrs
}Copy the code
The attr simply updates the ATTr property of the DOM when the CREATE and UPDATE hooks are called.
The last
At this point, we’ve combed through the entire process from Template to the real DOM. Now if YOU look at this picture, isn’t it clearer?
about
Author: Ran Mo
Email: [email protected] or [email protected]
Github: github.com/answershuto
Blog: answershuto. Making. IO /
Zhihu homepage: www.zhihu.com/people/cao-…
Zhihu column: zhuanlan.zhihu.com/ranmo
The Denver nuggets: juejin. Cn/user / 289926…
OsChina:my.oschina.net/u/3161824/b…
Please indicate the source of reprint, thank you.
Welcome to follow my public number