vue2源码学习 (12) 虚拟DOM-2.VNode

36 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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实例 的类型

  1. 注释节点
  2. 文本节点
  3. 克隆节点
  4. 元素节点
  5. 组件节点
  6. 函数节点

由前面的知识可以知道,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. 函数组件

关于 函数式组件 官网的解释

  1. 简单来说:vue2 中的函数式组件,是无状态 (没有 data) 和无实例 (没有 this 上下文)的一个组件。

  2. 使用场景:性能优化,函数式组件初始化速度远远快于状态式组件(stateful components)

  3. 所以没有使用到 状态和实例 的组件,可以优化成函数式组件。

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, 独有 componentInstancecomponentOptions属性

end

  • 本文主要阅读了 VNode 相关源码。
  • 简单理解 vnode 就是一个对象,通过属性的不同,来表示不同的 DOM 元素。
  • 虚拟 DOM 其实就是多个 vnode 形成的树结构。

h 函数render 函数,后续查看编译相关的逻辑再仔细研究。