概览

名词解释

组件占位 VNode 与 组件渲染 VNode

<section class="nav-ctn">
  <AppNav></AppNav>
</section>
1
2
3

AppNav组件的定义:

<template>
  <div class="app-nav">
    <!-- ... -->
  </div>
</tempalte>
<script>
  export default {
    name: 'AppNav',
    // ...
  }
</script>
1
2
3
4
5
6
7
8
9
10
11

组件占位 VNode

为组件标签创建的 VNode 节点,如上的组件标签为AppNav的组件,为该组件创建的组件占位 VNode 的vnode.tagvue-compoment-${递增的 cid}-AppNav,该 VNode 在最终创建的 DOM Tree 并不会存在一个 DOM 节点与之一一对应,即它只出现在 VNode Tree 里,但不出现在 DOM Tree 里。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    // vnode.componentOptions
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

组件渲染 VNode

“组件渲染 VNode”是我为该系列的源码分析文章创建的用词,因为在 Vue 官方文档里找不到对应的用词。

针对用户手动编写的render函数来说,组件渲染 VNode 就是render函数返回的 VNode。

针对于有模板且还未编译的组件来说,组件渲染 VNode 指的是为模板的根节点创建的 VNode,在app-nav组件里,组件的渲染 VNode 就是为模板的根节点.app-nav节点创建的 VNode,其vnode.tagdiv。而事实上,组件的模板最终将编译为render函数,render函数返回的就是为模板根节点创建的 VNode。

连续嵌套组件

连续嵌套组件,指的是父组件的模板的根节点是子组件,下面以连续的两个嵌套组件来说明。(子组件的模板的根节点还可以是孙组件,这样就是连续三个嵌套组件,以此类推)

父组件的定义:

<template>
  <Child></Child>
</tamplate>
<script>
  import Child from './Child.vue'
  export default {
    name: 'Parent',
    components: {
      Child
    }
    // ...
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

子组件的定义:

<template>
  <div class="child-root">
    <!-- 其他子节点 -->
  </div>
</tamplate>
<script>
  export default {
    name: 'Child',
    // ...
  }
</script>
1
2
3
4
5
6
7
8
9
10
11

针对这种情况,最终生成的 VNode Tree 大概是这样的(以 Vnode 的tag来表示 Vnode 节点):

- vue-component-${cid}-Parent
    - vue-component-${cid}-Child
        - div.child-root
1
2
3

假设这样调用Parent组件:

<div class="ctn">
  <Parent></Parent>
</div>
1
2
3

最终Parent插入到文档之后:

<div class="ctn">
  <div class="child-root">
    <!-- 其他子节点 -->
  </div>
</div>
1
2
3
4
5

数据来源及关系梳理

vm.$options.parent、vm.$parent、vnode.parent

vm.$options.parent

vm.$options.parent是子组件渲染时执行vm.__patch__时当前活跃的组件实例,也就是子组件实例的父组件实例

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  if (!prevVnode) {
    // 首次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 数据更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { activeInstance } from '../instance/lifecycle'
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      // ...
    ) {
      // ...
    } else {
      // 创建子组件
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // ...
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
5
6
7
8
9
10
11
12
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // ...
  vm._isVue = true
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  }
  // ...
}
1
2
3
4
5
6
7
8
9
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  // ...
}
1
2
3
4
5
6

vm.$parent

vm.$parent是组件实例的第一个非抽象的父组件实例

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // 注意:keep-alive 组件和 transition 组件是 abstract 的
  // 初始化组件的 $options 时,vm.$options.parent 已经指向父组件
  // 此处将组件加入到第一个非抽象父组件的 $children 里
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  // 第一个非抽象父组件
  vm.$parent = parent
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

vnode.parent

其中vnode是组件实例通过_render生成的渲染 VNode,而vnode.parent是指组件占位 VNode

export function renderMixin (Vue: Class<Component>) {
  // ...
  /**
   * 调用 vm.$options.render() 生成 VNode 节点
   */
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // 若是组件实例,则会存在 _parentVnode
    const { render, _parentVnode } = vm.$options
    // ...
    // 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 {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // ...
    }
    // ...
    // set parent
    // _parentVnode 是组件实例的组件占位 VNode
    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
23
24
25
26
27

提示

只有组件的渲染 VNode 才有vnode.parent属性哦!

vm._vnode、vm.$vnode

vm._vnode.parent === vm.$vnode

vm._vnode

vm._vnode是组件实例经过vm._render()创建的渲染 Vnode。

export function mountComponent (
  // ...
) {
  if (...) {
    // ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // ...
}
1
2
3
4
5
6
7
8
9

vm.$vnode

vm.$vnode是子组件的属性,指向子组件的占位 Vnode;而根组件的vm.$vnode一直为undefined

  • 子组件在调用_init初始化时,会调用initRender,此时会首次添加vm.$vnode,指向子组件的组件占位 VNode。
export function initRender (vm: Component) {
  // ...
  const options = vm.$options
  // options._parentVnode 是子组件的组件占位 VNode,因此根组件不存在
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  // ...
}
1
2
3
4
5
6
7
  • 子组件在调用_render时,也会更新vm.$vnode,指向子组件的组件占位 VNode。
export function renderMixin (Vue: Class<Component>) {
  // ...
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // 若是组件实例,则会存在 _parentVnode
    const { render, _parentVnode } = vm.$options
    // ...

    // 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 {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    }
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 子组件在组件更新时,也会再次更新vm.$vnode
const componentVNodeHooks = {
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // ...
  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

释疑

模板里使用了 vm 上不存在的方法或属性时的报错,是如何实现的?

非开发环境下,执行render函数时第一个参数传入的是vm._renderProxy而不是vmvm._renderProxy是对vm的代理,其内实现了报错逻辑。

TODO: 待完成一篇详细的分析文章