虚拟 DOM 的创建
什么是虚拟 DOM
Virtual DOM即虚拟 DOM 节点。是通过JS对象来模拟DOM中节点,然后通过特定的render方法将虚拟DOM渲染成真实的DOM节点
为什么使用虚拟 DOM
虚拟 DOM 是为了解决频繁的操作 DOM 元素引发性能问题的产物。当使用 JS 脚本操作 DOM 元素时,会引发浏览器的回流或者重绘。我们来聊几一下回流和重绘的概念:
-
回流: 当我们对
DOM元素的修改引发元素尺寸的变化时,浏览器需要重新计算元素的大小和位置,最后将重新计算的结果绘制到屏幕上,这个过程被称为回流 -
重绘: 当我们对
DOM元素的修改值只改变元素的颜色时,浏览器此时并不需要重新计算元素的大小和位置,而只要重新绘制新样式。这个过程被称为重绘
很显然,回流会比重绘更加耗费性能。在使用虚拟 DOM 时,我们对 DOM 的操作,首先会操作在虚拟 DOM 上,虚拟 DOM 会将多个改动合并成一个批量的操作,从而减少 DOM 重排的次数,进而缩短生成渲染树和绘制的所花的时间。
Vue 中的虚拟 DOM
在 Vue 中,使用了 VNode 这样一个构造函数描述一个 DOM 节点。
VNode 构造函数
/**
* Vnode 构造函数
* @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;
};
Vue 通过 VNode 这个构造函数来描述 DOM 节点,下面简单介绍一下注释节点和文本节点的创建方法
创建注释节点
/**
* 创建注释节点(空节点)
* @param {*} text
* @returns
*/
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
创建文本节点
/**
* 创建文本节点
* @param {*} val
* @returns
*/
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
虚拟 DOM 的创建
在 Vue 的挂载流程中,在获取到 render 函数之后,调用 vm._render 方法,将 render 函数转换成虚拟 DOM ,来看一下 vm._render 方法的实现
// /core/instance/render.js
// 将 Vue 实例转换成虚拟 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
// 调用 render 函数创建生成虚拟 DOM,将 $createElement 方法作为 render 函数的第一个参数,这与手写 render 函数相同
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 (!(vnode instanceof 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
}
可以看到,在 _render 方法中,最核心就是 vnode = render.call(vm._renderProxy, vm.$createElement),这时候,会将 render 函数转换成虚拟 DOM 。我们会想一下,在我们手写 render 函数时的过程,来看下面这个例子
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
在手写 render 函数时, render 函数接受一个函数作为参数,这个参数其实就是 vm.$createElement , $createElement 其实是对 createElement 方法的封装,
而 createElement 又是对 _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> {
/**
* 判断是否传递了 data 参数
* 这里判断是否传递了 data 参数的标准是第三个参数是一个 object 类型(data 选项一般为 对象)
*/
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 区分手写的 render 方法和内部 template 编译得到的 template 方法
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> {
// data 选项中的属性不能使用响应式对象
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${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
// 防止动态组件中 :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) {
// 手写 render
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// template 编译得到的 render 函数
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断是否为内置标签,例如 浏览器中的 html 标签
if (config.isReservedTag(tag)) {
// platform built-in elements
// 内置标签,直接创建虚拟 DOM
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
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()
}
}
可以看到 createElement 是对 _createElement 的封装,我们主要分析 _createElement 的实现,在 _createElement 方法中,首先是对数据的规范校验
数据规范校验
-
data中不能使用响应式对象作为属性
// data 选项中的属性不能使用响应式对象
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
-
- 当特殊属性
key的值为非字符串、非数字类型等非原始数据类型时
- 当特殊属性
// 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
)
}
}
子节点规范化
接下来是对子节点规范化,虚拟 DOM 是由每个 VNode 以树状形式拼成的虚拟 DOM 树,因此我们需要保证每一个字节点都是 VNode 类型,这里需要对 _render 函数的两种来源分别进行分析
- 用户定义的
render函数。 在对用户定义的render函数进行规范化时,如果childrenNode为数组(例如children中有v-for),则需要进行遍历;如果依旧存在数组,则进行递归
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
}
- 由模版编译得到的
render函数 由模板编译得到的render函数都是VNode类型(函数式组件得到的是一个数组,这个后面再分析),这是我们只需要将整个children转换成一维数组
/**
* 将数组扁平化,转换成一维数组
* @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
}