Vue 组件 render

9/17/2021 Vue


# VNode

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 // 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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom (opens new window) 的实现。

Vnode 定义的属性差不多有 20 几个,显然用 Vnode 对象要比真实 DOM 对象描述的内容要简单得多,它只用来单纯描述节点的关键属性,例如标签名,数据,子节点等。并没有保留跟浏览器相关的 DOM 方法。除此之外,Vnode 也会有其他的属性用来扩展 Vue 的灵活性。

# 为什么使用 Virtual DOM?

性能方面考量

  • 创建真实 DOM 的代价高:真实的 DOM 节点 node 实现的属性很多,而 vnode 仅仅实现一些必要的属性,相比起来,创建一个 vnode 的成本比较低
  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升,引入虚拟 DOM 可以简化 DOM 的复杂操作。其实为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题
  • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM,Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
  • 通过比较前后两次状态的差异实现真实 DOM 的最小化更新,这样可以减少浏览器的重绘及回流:使用 vnode 相当于加了一个缓冲,让一次数据变动所带来的所有 node 变化,先在 vnode 中进行修改,然后 diff 之后对所有产生差异的节点集中一次对 DOM tree 进行修改,以减少浏览器的重绘及回流。

性能方面的考量仅仅是一方面,性能受场景的影响是非常大的,不同的场景可能造成不同实现方案之间成倍的性能差距,所以依赖细粒度绑定及 Virtual DOM 哪个的性能更好还真不是一个容易下定论的问题

  • Vue 之所以引入了 Virtual DOM,更重要的原因是为了解耦 HTML 依赖,这带来两个非常重要的好处是:
    • 不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率:通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升;
    • 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex 等框架应用的就是这一特性。

# 生成 VNode

# Vue.prototype._render

去掉部分代码,只保留关键逻辑。










 













Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  vm.$vnode = _parentVnode

  let vnode
  try {
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    vnode = vm._vnode
  } finally {
    currentRenderingInstance = null
  }
  // ...

  // 设置 parent
  vnode.parent = _parentVnode
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这样看下来,_render 方法的内部实现就很简单,主要就是通过调用 vm.$options.render 生成这个组件的 VNode 并返回。vm.$options.render方法可以是用户手写的 render 也可以是 模板通过编译生成的 render。下面就通过一个例子来看下这两种情况下的 render 函数

# 一个例子

这里我们采用 compiler 版本的 Vue 来构建项目。

  • App.vue
<template>
  <div class="content-wrapper">{{ name }}</div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      name: 'App',
    }
  },
  created() {
    console.log('this is App.vue')
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • main.js
import App from './App.vue'

new Vue({
  components: { App }
  data: {
    name: 'main',
  },
  created() {
    console.log('this is main.js')
  },
  template: '<div id="app"><App /></div>',
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12

# template 运行时编译

由于提供的是 template,所以 vue 实例在挂载过程(运行时)中,会将 template 编译成如下形式的 render 函数,

function anonymous() {
  with (this) {
    return _c('div', { attrs: { id: 'app' } }, [_c('App')], 1)
  }
}
1
2
3
4
5

# App 组件构建时编译

组件 App.vue 则会在项目构建时被 vue-loader 处理成如下形式的一个组件对象,

{
  name: "App",
  data: function data() {
    return {
      name: 'App'
    };
  },
  created: function created() {
    console.log('this is App.vue');
  },
  render: function() {
    var _vm = this
    var _h = _vm.$createElement
    var _c = _vm._self._c || _h
    return _c("div", { staticClass: "content-wrapper" }, [
      _vm._v(_vm._s(_vm.name))
    ])
  },
  _compiled: true,
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. App 组件的模板部分也被编译成了 render 函数,这个是通过 vue-template-compiler 处理后生成的。
  2. 而且通过 vue-template-compiler 编译生成的 render 函数的内部调用的都是 _c 函数,而不是用户手写 render 时传入的 h 函数

# h_c

无论是 h 函数还是 _c 函数,它们的内部都调用了 createElement 方法,二者的差别,仅在调用 createElement 时最后一个参数alwaysNormalize不同。

这个参数的含义是:是否要对传入的组件对象进行规范化。用户手写 render 的情况,是需要进行规范化的

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

vm.$createElement = function (a, b, c, d) {
  return createElement(vm, a, b, c, d, true)
}
1
2
3
4
5

回到 render 函数的调用逻辑上来,vue 实例的 render 会首先被调用,

function anonymous() {
  with (this) {
    return _c('div', { attrs: { id: 'app' } }, [_c('App')], 1)
  }
}
1
2
3
4
5

外层的 _c 方法执行之前,会先执行内部的 _c('App'),所以子组件 App 的 VNode 会先被创建

# createElement

function createElement(
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

该方法内部对参数做了一些处理之后,最终又调用了_createElement

# _createElement

该方法就是真正生成 VNode 的方法。有关子组件的规范化逻辑,文章后面会提到,_c('App') 的逻辑中尚不涉及,所以先跳过

// prettier-ignore
function _createElement(context, tag, data, children, normalizationType) {
  // ========= 数据对象不能是响应式数据 =========== //
  if (isDef(data) && isDef(data.__ob__)) {
    // 返回注释节点
    return createEmptyVNode()
  }
  // =========== 处理动态组件 =========== //
  if (isDef(data) && isDef(data.is)) {
    // 如果是动态组件就会读取组件的 is 属性,作为当前的 tag
    tag = data.is
  }
  if (!tag) {
    // 防止动态组件 :is 属性设置为false
    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 (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }


  // ============== 不同类型标签的处理 =============== //
  let vnode, ns
  // 一. 标签为字符串类型,比如 div、App 这种组件占位符
  if (typeof tag === 'string') {
    let Ctor
    // 1. 如果是平台内置的节点类型,如 div
    if (config.isReservedTag(tag)) { 
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      )
    } else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
      // 2. 如果是已注册的 组件 类型
      vnode = createComponent(Ctor, data, context, children, tag)
    } else { 
      // 3.其他情况
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // 二. 组件对象的场景,比如:
    /**
     * import App from './App'
     * 
     * // ...
     * 
     * render(h) { 
     *  h(App)
     * }
     * */ 
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    // ...省略
    if (isDef(data)) {
      registerDeepBindings(data)
    }
    return vnode
  } else {
    return createEmptyVNode()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

上面说到 _c('App') 会先被执行,此时传递给_createElement的参数如下:

  • context:vue 实例
  • tag:'App'
  • data:undefined
  • children:undefined
  • normalizationType:undefined

此时,'App' 是一个 string 类型的组件占位符 tag,而且它不是平台内置的,并且它已经在 vue 实例的$options.components中注册, 所以 vnode 的创建会走到 2. 如果是已注册的 组件 类型那里

vnode = createComponent(Ctor, data, context, children, tag)
1

传给createComponent的参数如下:

  • Ctor

即上面的 App 组件对象

{
  name: "App",
  data: function data() {
    return {
      name: 'App'
    };
  },
  created: function created() {
    console.log('this is App.vue');
  },
  render: function() {
    var _vm = this
    var _h = _vm.$createElement
    var _c = _vm._self._c || _h
    return _c("div", { staticClass: "content-wrapper" }, [
      _vm._v(_vm._s(_vm.name))
    ])
  },
  _compiled: true,
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • context:vue 实例

  • tag:'App'

  • data & children:undefined

接下来就看下 createComponent方法的实现,

# createComponent

function createComponent(Ctor, data, context, children, tag) {
  if (isUndef(Ctor)) {
    return
  }

  // ======== 1. 基于 Vue 构造器创建一个子类构造器 ========= //
  var baseCtor = context.$options._base

  if (isObject(Ctor)) {
    // 这里就相当于 Ctor = Vue.extend(App)
    Ctor = baseCtor.extend(Ctor)
  }

  // ============== 异步组件相关 =============== //
  var asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
  }

  // 组件数据
  data = data || {}

  // ============= 2. 获取构造器上的配置对象,并完成配置合并操作 ========== //
  resolveConstructorOptions(Ctor)

  // 处理 v-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 处理 props
  var propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // ============== 函数式组件相关 =============== //
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // 处理 event
  var listeners = data.on
  data.on = data.nativeOn

  // =============== 抽象组件相关 =============== //
  if (isTrue(Ctor.options.abstract)) {
    var slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // ===============  3. 安装组件钩子函数 =============== //
  installComponentHooks(data)

  // 如果组件对象存在 name 属性,则组件名称以 name 为准
  var name = Ctor.options.name || tag

  // ===============  4. 创建组件 VNode =============== //
  var vnode = new VNode(
    'vue-component-' + Ctor.cid + (name ? '-' + name : ''),
    data,
    undefined,
    undefined,
    undefined,
    context,
    {
      Ctor: Ctor,
      propsData: propsData,
      listeners: listeners,
      tag: tag,
      children: children,
    },
    asyncFactory
  )
  // 返回组件 VNode 实例
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

整个流程比较复杂,分支逻辑比较多,这里只说一下涉及到的处理逻辑:

  1. 创建子组件构造器

通过 Ctor = baseCtor.extend(Ctor),让子组件继承 Vue,并返回一个子组件的构造器

  1. 完成子组件的配置合并

通过 resolveConstructorOptions(Ctor);

  1. 为 App 这个组件对象安装一些钩子函数,这些钩子都是在后续的 patch 过程中会被调用

最后,看下 App 组件最终生成的 VNode 究竟长什么样子:

{
  tag: "vue-component-1-App",
  data: {
    hook: {
      init: function init (vnode, hydrating) {
        // ...
      },
      prepatch: function prepatch (oldVnode, vnode) {
        // ...
      },
      insert: function insert (vnode) {
        // ...
      },
      destroy: function destroy (vnode) {
        // ...
      },
    },
  },
  // ...
  componentOptions: {
    Ctor: function VueComponent (options) {
      this._init(options);
    },
    propsData: undefined,
    listeners: undefined,
    tag: "App",
    children: undefined,
  },
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这里可以重点关注一下 data.hook 以及 componentOptions 两个属性,因为后面的 patch 过程中会涉及到

# 子组件的规范化

说完了 App 组件的 VNode 创建过程,现在再次回到 vue 实例的 render 方法中来,此时的 _c('App') 已经完成构建,返回了上面的 VNode 对象,这里我们以 VApp 来代表

function anonymous() {
  with (this) {
    return _c('div', { attrs: { id: 'app' } }, [VApp], 1)
  }
}
1
2
3
4
5

此时传递给 _createElement方法的参数就成了:

  • context:vue 实例
  • tag:'div'
  • data:{ attrs: { id: 'app' } }
  • children:[VApp]
  • normalizationType:1

因为此时传递的 children 属性不为空,所以就会涉及到 children 的规范化操作,规范化的目的就是要将 children 里的所有元素都标准化为 VNode 类型

if (normalizationType === 2) {
  children = normalizeChildren(children)
} else if (normalizationType === 1) {
  children = simpleNormalizeChildren(children)
}
1
2
3
4
5

# normalizeChildren

// prettier-ignore
function normalizeChildren(children) {
  // 递归调用,直到子节点是基础类型,就创建文本节点Vnode
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

// 判断是否基础类型
function isPrimitive(value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个方法的调用场景分为两种:

# 第一种

是用于处理模板中包含 templateslotv-for 的情况,这种情况下会产生嵌套数组,此时会调用 normalizeArrayChildren 方法递归处理 children

举个例子

new Vue({
  el: '#app',
  components: { App },
  template: `<div id="app"><template v-for="i in 2"><App/></template></div>`,
  data: {
    name: 'main.js',
  },
  created() {
    console.log('this is main.js')
  },
})

// ======= template 编译后生成的 render 函数 ======== //
function anonymous() {
  with (this) {
    return _c(
      'div',
      { attrs: { id: 'app' } },
      [
        _l(2, function (i) {
          return [_c('App')]
        }),
      ],
      2
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这种情况下,_createElement 方法的 children 参数就会是如下的形式:

;[
  [
    [
      {
        tag: 'vue-component-1-App',
        // ...
      },
    ],
    [
      {
        tag: 'vue-component-1-App',
        // ...
      },
    ],
  ],
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

可以看到这是一个嵌套了三层的数组,并且在第二层数组对象上还会有一个 _isVList: true 的属性,这个属性是与数组的 length 属性平级的。

这种情况下,经过规范化处理之后,会变成如下的形式,被拍成了只有一层的数组

[
  {
    tag: 'vue-component-1-App',
    // ...
  },
  {
    tag: 'vue-component-1-App',
    // ...
  }
],
1
2
3
4
5
6
7
8
9
10

# 第二种

是用于处理 用户手写 render 函数 的情况,就不举例子了。

# simpleNormalizeChildren

处理编译生成的 render 函数。理论上 template 模板通过编译生成的 render 函数都是 Vnode 类型,但是有一个例外,函数式组件返回的是一个数组,这个时候处理结果依然是将整个 children 拍平成一维数组

function simpleNormalizeChildren(children) {
  for (var i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
1
2
3
4
5
6
7
8

不再举例。

# 总结

先用一个例子来总结一下:

  • HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      msg: 'HelloWorld',
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • App.vue
<template>
  <div class="content-wrapper">
    <HelloWorld />
    {{ name }}
  </div>
</template>

<script>
import HelloWorld from './HelloWorld.vue'
export default {
  name: 'App',
  components: { HelloWorld },
  data() {
    return {
      name: 'App',
    }
  },
  created() {
    console.log('this is App.vue')
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • main.js
import App from './HelloWorld.vue'
new Vue({
  el: '#app',
  components: { App },
  template: `<div id="app"><App/></div>`,
  data: {
    name: 'main.js',
  },
  created() {
    console.log('this is main.js')
  },
})
1
2
3
4
5
6
7
8
9
10
11
12

上面这个例子最终构建出来的 VNode 如下:

{
  tag: "div",
  data: {
    attrs: {
      id: "app",
    },
  },
  children: [
    {
      tag: "vue-component-1-App",
      data: {
        on: undefined,
        hook: {
          init: function init (vnode, hydrating) {
            // ...
          },
          prepatch: function prepatch (oldVnode, vnode) {
            // ...
          },
          insert: function insert (vnode) {
            // ...
          },
          destroy: function destroy (vnode) {
            // ...
          },
        },
      },
      componentOptions: {
        Ctor: function VueComponent (options) {
          this._init(options);
        },
        tag: "App",
      },
      componentInstance: undefined,
    },
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

可以看到,生成的 VNode 里并没有 HelloWorld 组件相关的内容,至于为什么,在下一篇 patch 过程的时候再做说明。

最后,用一张图来总结下 组件 VNode 的创建过程:

图片来自 (opens new window)

另外,记住一点,如果某个组件内部引入了其他组件的话,那子组件 VNode 的创建是优先于父组件的

Last Updated: 10/21/2024, 4:15:17 PM