Virtual DOM
The concept of Virtual DOM is more or less familiar to anyone who knows or has used Vue, React and other MVVM frameworks. DOM in browsers is “expensive”, and there are certain performance problems when we frequently update DOM:
let div = document.createElement('div');
let str = ""
for(let key in div) {
str+=key+""
}
console.log(str)
Copy the code
The code above gives you a look at the design inside the DOM. If you are interested in running it, you will find that DOM design is very complex, which is why Virtual DOM comes into being.
In essence, Virtual DOM can be regarded as a mapping of DOM nodes on the browser in JavaScript. A DOM node is described by native JS objects. In Vue, the Virtual DOM is described using the vnode Class, which is defined in the SRC /core/vdom/vnode.js file:
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;
fnContext: Component | void; // real context vm for functional nodesfnOptions: ? ComponentOptions;// for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtoolsfnScopeId: ? string;// functional scope id support
constructor (tag? : string, data? : VNodeData, children? :?Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
Copy the code
Although Vue’s definition of Virtual DOM is somewhat complex, because it contains many features of Vue, its core is nothing more than a few key attributes, tag names, data, child nodes, key values, etc., other attributes are used to extend the flexibility of VNode. Since VNode is just a rendering that maps to the real DOM, it doesn’t need to include methods to manipulate the DOM, so it’s very lightweight and simple.
In addition to the definition of its data structure, the Virtual DOM needs to go through the process of VNode create, diff, patch and so on when mapping to the real DOM.
In Vue, the Virtual DOM is created using the previously enhanced createElement method, which is defined in the SRC /core/vdom/create-element.js file:
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
The createElement method is essentially a wrapper around the _createElement method, allowing for more flexibility in the parameters passed in. After processing these parameters, call _createElement, the function that actually creates VNode:
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
`Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render! ',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if(! tag) {// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
Copy the code
The _createElement method takes five arguments. Context represents the VNode context, which is of type Component. Tag represents a tag, which can be either a string or a Component. Data represents VNode data. It is a VNodeData type and can be found in flow/vnode.js. Children represents the children of the current VNode, which can be of any type and then needs to be normalized as a standard VNode array; NormalizationType represents the type of child node specification, and the method varies depending on the type specification, depending on whether the Render function is compiled or handwritten.
We will focus on two key processes: —————— children processing and VNode creation.
children
The processing of
The Virtual DOM is actually a tree structure, and each VNode may have several child nodes, which should also be the type of the VNode. The fourth argument received by _createElement is children of any type, so we need to treat them as vNodes. The normalizeChildren(children) and simpleNormalizeChildren(children) methods are called, depending on the normalizationType. Their definitions are in SRC/core/vdom/helpers/normalzie – children. In js:
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
Copy the code
The simpleNormalizeChildren method call scenario is that the Render function is compiled. The functional Component returns an array instead of a root node. So the array.prototype. concat method is used to flatten the entire children Array so that it is only one layer deep. The normalizeChildren method has two cases. The first case is that the render function is written by the user. When children have only one node, Vue. In this case createTextVNode is called to create a VNode of a text node; The normalizeArrayChildren method is called when slot, V-for is compiled to generate a nested array:
function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if(c ! = =' ') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__ `
}
res.push(c)
}
}
}
return res
}
Copy the code
NormalizeArrayChildren takes two arguments, children for the child node to process and nestedIndex for the nestedIndex, since a single child can be an array type. So when we get a single node C, we do a type check, and if it’s an array, we recursively call normalizeArrayChildren; If it is a base type, convert createTextVNode to vNode directly, and update its key according to nestedIndex if multiple lists are nested. And if there are consecutive text nodes during the traversal, they are merged into a single node. Finally, children becomes an Array of type VNode.
The creation of a VNode
The createElement function, after processing children, goes back and creates a VNode instance:
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
Copy the code
CreateComponent Creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. Otherwise, create a VNode with an unknown label. Create a Component VNode if the tag is not string or Component.
conclusion
So here we have the general ideacreateElement
The process of creating a VNode is required for each VNodechildren
.childern
The elements of the DOM Tree are vNodes, and then form a VNode Tree corresponding to the DOM Tree.
Vue2 source code analysis (four) vue2 source code analysis (three) VUE2 source code analysis (two) VUe2 source code analysis (a)