Creation of the virtual DOM
What is the virtual DOM
A Virtual DOM is a Virtual DOM node. It simulates the DOM nodes through JS objects, and then renders the virtual DOM into real DOM nodes through specific render methods
Why use the virtual DOM
The virtual DOM is an effort to solve performance problems caused by frequent manipulation of DOM elements. Manipulating DOM elements with JS scripts causes backflow or redrawing of the browser. Let’s talk a little bit about the concept of backflow and redraw:
-
Backflow: When our changes to a DOM element cause the element’s size to change, the browser recalculates the size and position of the element and eventually draws the recalculated result onto the screen, a process called backflow
-
Redraw: When we change the value of a DOM element to only change the color of the element, the browser does not need to recalculate the size and position of the element, but simply redraw the new style. This process is called redrawing
Obviously, refluxing is more performance costly than painting. When using the virtual DOM, we will operate on the virtual DOM first. The virtual DOM will combine multiple changes into a batch operation, so as to reduce the number of DOM rearrangement and shorten the time spent in generating rendering trees and drawing.
Virtual DOM in Vue
In Vue, a constructor called VNode is used to describe a DOM node.
VNode constructor
/** * Vnode constructor *@param {*} tag
* @param {*} data
* @param {*} children
* @param {*} text
* @param {*} elm
* @param {*} context
* @param {*} componentOptions
* @param {*} asyncFactory
*/
var VNode = function VNode (tag, data, children, text, elm, context, componentOptions, asyncFactory) {
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;
};
Copy the code
Vue uses the VNode constructor to describe DOM nodes. Here’s how to create comment nodes and text nodes
Creating a comment node
/** * Create comment node (empty node) *@param {*} text
* @returns * /
export const createEmptyVNode = (text: string = ' ') = > {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
Copy the code
Creating a text node
/** * Create text node *@param {*} val
* @returns * /
export function createTextVNode (val: string | number) {
return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code
Creation of the virtual DOM
In the mount process of Vue, after obtaining the render function, call vm._render method, convert the render function into virtual DOM, see the implementation of vM. _render method
// /core/instance/render.js
// Convert the Vue instance into a virtual DOM
Vue.prototype._render = function () :VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack becaues all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// Call the render function to create and generate the virtual DOM, using the $createElement method as the first argument to the render function, the same as the handwritten render function
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]}// return empty vnode in case the render function errored out
if(! (vnodeinstanceof VNode)) {
if(process.env.NODE_ENV ! = ='production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
Copy the code
As you can see, the core of the _render method is vnode = render. Call (vm._renderProxy, vm.$createElement), which converts the render function into a virtual DOM. We’ll think about what happens when we write the render function by hand, so let’s look at this example
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // Label name
this.$slots.default // Array of child nodes)},props: {
level: {
type: Number.required: true}}})Copy the code
$createElement (vm.$createElement); $createElement (vm.$createElement); CreateElement encapsulates _createElement.
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
/** * The third argument is an object (the data option is usually an object) */
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// Distinguish between the handwritten render method and the internal template compiled template method
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
// Attributes in the data option cannot use reactive objects
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
// Prevent dynamic components from returning an empty node when the is property is false
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) {
/ / handwritten render
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// template compiles the render function
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// Check whether it is a built-in tag, such as an HTML tag in a browser
if (config.isReservedTag(tag)) {
// platform built-in elements
// Create a virtual DOM directly with built-in tags
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// component
// Create a VNode of component type for a registered custom 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 is used to implement the _createElement method, and the _createElement method is used to validate the data
Data specification check
-
data
Cannot use reactive objects as properties in
// Attributes in the data option cannot use reactive objects
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()
}
Copy the code
-
- When a particular property
key
Is a non-original data type, such as a non-string or a non-numeric type
- When a particular property
// 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
)
}
}
Copy the code
Child node normalization
The next step is to normalize the child nodes. The virtual DOM is a virtual DOM tree composed of each VNode in the form of a tree, so we need to ensure that every byte point is a VNode type. Here we need to analyze the two sources of the _render function separately
- user-defined
render
Function.
When normalizing the user-defined render function, if the childrenNode is an array (for example, children with V-for), it needs to be traversed; If the array still exists, recurse
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
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
- Compiled by the template
render
function
The render functions compiled from the template are all of type VNode (the functional component is an array, more on that later), so we only need to convert the entire children to a one-dimensional array
/** * flattens the array to a one-dimensional array *@param {*} children
* @returns * /
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
}
Copy the code