持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 16 天,点击查看活动详情
12. VNode
start
- 前一节了解了虚拟 DOM 是什么。
从本质上来说,Virtual DOM 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。
- 虚拟 DOM 中,首当其冲的
VNode
类。
VNode 源码
\src\core\vDOM\vnode.js
// 1. 在Vue中存在一个 VNode类,使用它可以实例化不同类型的 vnode实例。 不同的实例,各自表示不同类型的 DOM元素; (节点描述对象)
export default class VNode {
// 2. 这里再复习一下, class的基础知识,class顶部定义的属性是实例对象自身的属性。
// 所以 new 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 nodes
fnOptions: ?ComponentOptions // for SSR caching
devtoolsMeta: ?Object // used to store functional render context for devtools
fnScopeId: ?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
}
}
vnode 是什么?
简单来说,VNode
是一个类,使用它可以实例化不同类型的 VNode
实例。 不同的实例,各自表示不同类型的 DOM 元素; (节点描述对象)
虚拟 DOM 可以理解为是对应整个真实 DOM 的树状映射。
VNode
实例 可以理解是对应真实 DOM 中一个元素的映射。
VNode
实例 的类型
- 注释节点
- 文本节点
- 克隆节点
- 元素节点
- 组件节点
- 函数节点
由前面的知识可以知道,VNode实例
本身是一个对象。为了对应不同种类的元素,VNode实例
有很多类型。
不同类型的
VNode
实例,实际就是自身的属性不同。 下面看看源码,VNode
实例 不同的类型,到底有哪些区别。
1. 注释节点
// `\src\core\vDOM\vnode.js`
// 1. 创建一个注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
// 注释中的内容
node.text = text
// vnode中的 isComment(是注释) 就是注释节点
node.isComment = true
return node
}
/*
例如 真实的注释是:
<!-- 番茄 -->
这里创建的 vnode就是
{
text:"番茄",
isComment:true
}
*/
注释节点,表示文档中的注释的内容。最大的特点:isComment=true
。
2. 文本节点
// `\src\core\vDOM\vnode.js`
// 2. 创建一个文本节点
export function createTextVNode(val: string | number) {
// 对标上述的 VNode的constructor,第四个参数就是 text
return new VNode(undefined, undefined, undefined, String(val))
// 所以输出的其实就是 { text:"番茄" }
}
文本节点和注释节点很相似,但是没有 isComment=true
。
3. 克隆节点
// `\src\core\vDOM\vnode.js`
// 3. 克隆节点 (优化静态节点和插槽节点)
/*
以静态节点为例:组件的的某一个状态发生变化,静态节点因为他的内容不会改变,所以除了第一次执行渲染函数,后续不需要通过渲染函数来生产 vnode,直接拷贝一份vnode即可,提升性能。
*我个人理解:渲染函数生成 vnode ,有些静态节点不用执行渲染函数的一些逻辑,直接拷贝之前的静态节点即可。*
*/
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// 拷贝子元素数组, 防止对原本子元素数组的修改。 (当然这里是 slice 浅拷贝)
// a child.
// (Vue中很多这中 && 连接的表达式)
// 例如: `var a = b && b.slice;` 可以理解为:b 不存在直接返回 b,b 存在就返回 b.slice
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
// 可以看到上述代码,克隆节点,只需要把所有的属性全部赋值到新节点中,即可、
// 当然两者也有差异, ①存储的堆地址是不一样的(两者完全是不一样的对象); ②克隆节点的 isCloned (是否克隆) 属性为 true
cloned.isCloned = true
return cloned
}
我个人理解:渲染函数生成 vnode ,有些静态节点不用执行渲染函数的一些逻辑,直接拷贝之前的静态节点即可。
4. 元素节点
元素节点,其实就是用 js 对象来表达我们 DOM 节点的元素。
简化相关逻辑,可以看做下面的代码。
// `\src\core\vDOM\create-element.js`
vnode = new VNode(tag, data, children, undefined, undefined, context)
实际的 元素节点的 vnode 示例
/* 原本的元素 */
// <div id='app'>
// ...
// </div>
/* 对应的 vnode */
{
"children": (5) [VNode, VNode, VNode, VNode, VNode]
"context": Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
"data": {attrs: {…}}
"elm": div#app,
"tag": 'div'
}
/* 属性解释 */
// {
// "children": 子元素
// "context": 当前组件的 Vue.js实例
// "data": 属性
// "elm": 对应的真实DOM
// "tag": 元素标签
// }
5. 组件节点
// `\src\core\vDOM\create-component.js`
// 核心方法 createComponent
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
实际的组件节点
/* 原本的元素 */
// <child></child>
/* 对应的 vnode */
{
"componentInstance": {…}
"componentOptions": {…}
"context": {…}
"data": {attrs: {…}}
"tag": "vue-component-1-child"
}
/* 属性解释 */
// {
// "componentOptions": 组件节点的选项,包括 propsData tag children
// "componentInstance": 组件的 Vue.js实例, (每一个组件,都是一个Vue.js实例)
// }
这里可以看到,这里的 vnode 类型,主要是表达组件类型。其中有三个属性比较特殊,
- tag:加入了组件的 id 来确保唯一性。
- componentOptions:组件节点的选项
- componentInstance:组件的 Vue.js 实例
6. 函数组件
关于 函数式组件 官网的解释
简单来说:vue2 中的函数式组件,是无状态 (没有 data) 和无实例 (没有 this 上下文)的一个组件。
使用场景:性能优化,函数式组件初始化速度远远快于状态式组件(stateful components)
所以没有使用到 状态和实例 的组件,可以优化成函数式组件。
Vue 2 的函数式组件写法
export default {
functional: true, // 标识是函数式组件
props: ['level'], // 接收的传参
render(h, { props, data, children }) {
// h函数 暂时可以理解为:
// createElement("div",{id:"app",[this.name,createElement("p",this.age)]})
// 所以第一个传参为标签名,第二个传参为标签的属性对应的数据,第三个传参为子节点
return h(`h${props.level}`, data, children)
},
}
Vue2 中 SFC 的写法
<template functional>
<component :is="`h${props.level}`" v-bind="attrs" v-on="listeners" />
</template>
<script>
export default {
props: ['level'],
}
</script>
使用示例
<template>
<div id="app">
<demo :level="level"> 你好呀</demo>
</div>
</template>
<script>
export default {
components: {
demo,
},
data() {
return {
level: '2', // 这里传入的值标识渲染 几级h标签
}
},
}
</script>
对应的函数组件的源码 \src\core\vDOM\create-functional-component.js
实际的函数节点
{
"componentInstance": {…},
"componentOptions": {…},
"context": {…},
"data": {…},
"tag": 'h2',
}
函数式组件的 vnode
, 独有 componentInstance
和 componentOptions
属性
end
- 本文主要阅读了 VNode 相关源码。
- 简单理解 vnode 就是一个对象,通过属性的不同,来表示不同的 DOM 元素。
- 虚拟 DOM 其实就是多个 vnode 形成的树结构。
h 函数
,render 函数
,后续查看编译相关的逻辑再仔细研究。