Vue 虚拟node

virtual-node

Posted on 2018-06-12

Vue生命周期源码解析 中,我们介绍了Vue的挂载过程,其中在 src/core/instance/lifecycle.js 文件下的mountComponent 中调用了 vm._render() ,该方法即是用来生成虚拟DOM,在生成虚拟DOM之前,先来看下VNode的定义

// src/core/vdom/vnode.js

export default class VNode {
  // 节点的标签名,如div,section等
  tag: string | void;
	// 节点的数据信息, 为VNodeData类型
  data: VNodeData | void;
	// 节点的子节点
  children: ?Array<VNode>;
  // 节点的文本
  text: string | void;
	// 节点对应的真实dom节点
  elm: Node | void;
  // 节点的命名空间namespace
	ns: string | void;
	// 编译作用域
  context: Component | void; // rendered in this component's scope
	// 节点的key
  key: string | number | void;
	// 组件的 options 属性
  componentOptions: VNodeComponentOptions | void;
	// 节点对应的组件的实例
  componentInstance: Component | void;
	// 父节点
  parent: VNode | void;
	// 是否是原始HTML
  raw: boolean; // contains raw HTML? (server only)
	// 是否是静态节点
  isStatic: boolean;
	// 是否作为根节点插入
  isRootInsert: boolean;
	// 是否是注释节点
  isComment: boolean;
	// 是否是克隆节点
  isCloned: boolean;
	// 是否有 v-once 指令
  isOnce: boolean;
	// 异步组件的工厂函数
  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
  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

// src/core/instance/lifecycle.js

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options
  ...
  let vnode
  try {
    // 调用render函数
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    ...
  }
  ...
  return vnode
}

上述代码的核心在于调用了 render 函数, 通过该函数去生成的VNode,并传入 vm.$createElement 作为参数

render: function (createElement) {
  return createElement(...)
}

可见 createElement 就是实例绑定的 $createElement 属性

const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)

那么什么时候给实例添加的$createElement属性呢?$createElement 值又是什么呢?这一切可以在 initRender 函数中找到答案

// src/core/instance/render.js

import { createElement } from '../vdom/create-element'
export function initRender (vm: Component) {
  
  ...
  
  // bind the createElement fn to this i8679ou
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  // 模板编译成render函数时调用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always app+lied for the public version, used in
  // user-written render functions.
  // 手写render函数时调用
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  
  ...

}

综上所述,我们可以知道,调用了vm._render() 会 通过调用render 函数,接着调用 createElement 去生成vnode

// src/core/vdom/create-element

export function createElement (
  context: Component, // 实际上为vue实例
  tag: any, // 标签
  data: any, // 标签上的数据
  children: any, // 子标签(子节点)
  normalizationType: any, // 子节点规范的类型
  alwaysNormalize: boolean // 是否始终规范化子节点
): VNode | Array<VNode> {
  // data 为数组且是原始数据类型
  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)
}

createElement会对参数处理后接着调用_createElement方法,该方法会真正的创建VNode

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()
  // }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  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
  
  // tag是string类型
  if (typeof tag === 'string') {
    let Ctor
    // 获取namespace
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    
    // 是否是内置节点,如果是则直接创建VNode
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 已注册的组件名,则通过createComponent创建VNode
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 直接创建VNode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

创建VNode的过程可以总结如下:

  1. 判断tag类型是否是string,如果是则执行步骤2,否则直接步骤3
  2. 如果是内置节点或者已注册的组件,则创建VNode, 否则直接根据传入的参数创建VNode
  3. createComponent 去创建VNode