什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
一、什么是虚拟DOM
1、虚拟 DOM (Virtual DOM ) 这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-Native 和 Weex )
2、实际上它只是一层对真实DOM 的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
3、在Javascript 对象中,虚拟DOM 表现为一个 Object 对象。并且最少包含标签名 (tag )、属性 (attrs ) 和子元素对象 (children ) 三个属性,不同框架对这三个属性的名命可能会有差别
4、创建虚拟DOM 就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM 对象的节点与真实DOM 的属性一一照应
5、在vue 中同样使用到了虚拟DOM 技术
6、定义真实DOM
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>
7、实例化vue
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})
8、观察render 的render ,我们能得到虚拟DOM
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},
[_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})
9、通过VNode ,vue 可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff 算法得出一些需要修改的最小单位,再更新视图,减少了dom 操作,提高了性能
二、为什么需要虚拟DOM
1、DOM 是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM 操作引起的
2、真实的DOM 节点,哪怕一个最简单的div 也包含着很多属性,可以打印出来直观感受一下: 3、由此可见,操作DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验
举个例子
1、你用传统的原生api 或jQuery 去操作DOM 时,浏览器会从构建DOM 树开始从头到尾执行一遍流程
2、当你在一次操作时,需要更新10个DOM 节点,浏览器没这么智能,收到第一个更新DOM 请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程
3、而通过VNode ,同样更新10个DOM 节点,虚拟DOM 不会立即操作DOM ,而是将这10次更新的diff 内容保存到本地的一个js 对象中,最终将这个js 对象一次性attach 到DOM 树上,避免大量的无谓计算
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI
三、如何实现虚拟DOM
1、首先可以看看vue 中VNode 的结构
2、源码位置:src/core/vdom/vnode.js
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;
functionalContext: Component | void;
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void;
parent: VNode | void;
raw: boolean;
isStatic: boolean;
isRootInsert: boolean;
isComment: boolean;
isCloned: boolean;
isOnce: boolean;
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = 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
}
get child (): Component | void {
return this.componentInstance
}
}
3、这里对VNode 进行稍微的说明:
- (1)所有对象的
context 选项都指向了 Vue 实例 - (2)
elm 属性则指向了其相对应的真实 DOM 节点
4、vue 是通过createElement 生成VNode
5、源码位置:src/core/vdom/create-element.js
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)
}
6、上面可以看到createElement 方法实际上是对 _createElement 方法的封装,对参数的传入进行了判断
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: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context`
)
return createEmptyVNode()
}
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
return createEmptyVNode()
}
...
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 ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
...
}
7、可以看到_createElement 接收5个参数:
- (1)
context 表示 VNode 的上下文环境,是 Component 类型 - (2)
tag 表示标签,它可以是一个字符串,也可以是一个 Component - (3)
data 表示 VNode 的数据,它是一个 VNodeData 类型 - (4)
children 表示当前 VNode 的子节点,它是任意类型的 - (5)
normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的
8、根据normalizationType 的类型,children 会有不同的定义
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
9、simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的
10、normalizeChildren 方法调用场景分为下面两种:>
- (1)
render 函数是用户手写的 -
- (2)编译
slot 、v-for 的时候会产生嵌套数组
11、无论是simpleNormalizeChildren 还是normalizeChildren 都是对children 进行规范(使children 变成了一个类型为 VNode 的 Array ),这里就不展开说了
12、规范化children 的源码位置在:src/core/vdom/helpers/normalzie-children.js
13、在规范化children 后,就去创建VNode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
14、createComponent 同样是创建VNode
15、源码位置:src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
resolveConstructorOptions(Ctor)
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
installComponentHooks(data)
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
16、稍微提下createComponent 生成VNode 的三个关键流程:
- (1)构造子类构造函数
Ctor - (2)
installComponentHooks 安装组件钩子函数 - (3)实例化
vnode
小结
1、createElement 创建 VNode 的过程,每个 VNode 有 children ,children 每个元素也是一个VNode ,这样就形成了一个虚拟树结构,用于描述真实的DOM 树结构
参考文献
- (1)https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/create-element.html#children-%E7%9A%84%E8%A7%84%E8%8C%83%E5%8C%96
- (2)https://juejin.cn/post/6876711874050818061
|