Skip to content

Latest commit

 

History

History
418 lines (339 loc) · 14.8 KB

vdom——VNode.md

File metadata and controls

418 lines (339 loc) · 14.8 KB

本篇文章,我们讲的是VNode对象的一个基本组成,以及与创建VNode相关的一些函数。

经过compile编译模板字符串变成了render函数,在src/core/instance/render.js中,我们通过vnode = render.call(vm._renderProxy, vm.$createElement)调用了render方法并最终返回了一个VNode对象实例。VNode其实就是我们所说的虚拟dom,接下来我们一步步来揭开它的神秘面纱。

VNode的基本构造

VNode的构造函数是在src/core/vdom/vnode.js中,该文件主要定义了VNode对象包含的基本数据都有哪些。同时还定义了几个比较简单的创建特殊VNode对象的方法。

我们先来看看它的基本组成:

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.functionalContext = 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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

构造函数可以接收的参数最多有七个,分别是tag标签名、data结点相关数据、children子结点对象数组、text文本内容、elm原生结点元素、context指当前元素所在的Vue实例、componentOptions保存自定义组件上部分组件属性。

我们看到它内部还有许许多多的属性,这些值我会在VNode中,说明每个的含义。

createElement

在我们的src/core/instance/render.js文件中,有两个函数内部调用的都是createElement方法。

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

vm._c是我们编译模板生成的render函数执行时调用的,而vm.$createElement是我们自己编写render函数时,作为参数传递给render函数,见如下代码:

 vnode = render.call(vm._renderProxy, vm.$createElement)

我们就来看看createElement做了什么事儿

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
  return _createElement(context, tag, data, children, normalizationType)
}

createElement接收六个参数,第一个是当前的vm对象,第二个是标签名,第三个是结点相关的属性,第四个是子元素,第五个是子元素归一化的处理的级别,最后一个表示总是归一化处理。我们注意到内部调用的vm._c最后一个参数传入的是false,而vm.$createElement传入的是true,说明自定义的render函数总是对子元素进行归一化处理。

Array.isArray(data) || isPrimitive(data)如果返回true,说明该元素没有相关的属性,此时第三个参数实际上是children的值,所以后面的值依次向前移动。

if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE说明vm.$createElement会对子元素进行最高级的归一化处理。

最后调用了内部的_createElement方法,参数一眼明了。

_createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {
  ...
  if (!tag) {
    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
  if (typeof tag === 'string') {
    let Ctor
    ns = config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 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 = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (vnode) {
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

首先判断tag是不是为空,如果为空则直接返回一个空的VNode

接着如果子元素只有一个函数,则作为默认的slot,由于slot涉及到了从模板解析到渲染页面的整个过程,内容比较多,之后我会单独写一篇文章讲解相关内容。

之后就是对子元素进行归一化,在children的归一化处理中我们已经讲解了它的处理逻辑。

后面就是创建VNode对象的主要内容了:

1、如果tag是字符串,且是平台保留标签名。则直接创建VNode对象。

2、否则如果tag是字符串,则执行resolveAsset(context.$options, 'components', tag)

看一眼resolveAsset的实现:

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  ...
  const assets = options[type]
  
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  ...
  return res
}

其实这里处理的是我们自定义的组件,例如:

<div id="app">
  <my-component></my-component>
</div>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    components: {
      'my-component': {
        render: function(h){
          return h('div', "test");
        }
      }
    }
  });
</script>

当前解析的正是我们自定义的my-componentresolveAsset方法其实就是获取context.$options.componentsmy-component所对应的值,从上面的代码我们也可以看出,这里的'my-component'可以是myComponent,也可以是MyComponent,我们的Vue都可以正常解析。

如果返回的resCtor不为空,则执行vnode = createComponent(Ctor, data, context, children, tag)

createComponent我们后续讲解。

3、如果tag是字符串,但既不是平台保留标签名,也不是components中的自定义标签,则执行vnode = new VNode(tag, data, children, undefined, undefined, context)创建VNode对象。

4、如果tag不是字符串,则执行vnode = createComponent(tag, data, context, children)创建对象。

之后有对命名空间的一些处理,比较简单,大家自己看一眼就好,接下来我们就说一说这个createComponent

createComponent

从函数名我们就知道,该方法是创建一个组件,也就是我们自己定义的组件。

代码如下:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data?: VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  // Ctor为空表示从context的components属性上没找到tag对应的属性
  if (!Ctor) {
    return
  }

  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  ...
  resolveConstructorOptions(Ctor)

  data = data || {}

  // transform component v-model data into props & events
  if (data.model) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractProps(data, Ctor, tag)

  // 函数化组件 https://cn.vuejs.org/v2/guide/render-function.html#函数化组件
  // functional component
  if (Ctor.options.functional) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  data.on = data.nativeOn

  if (Ctor.options.abstract) {
    // abstract components do not keep anything
    // other than props & listeners
    data = {}
  }

  // merge component management hooks onto the placeholder node
  mergeHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }
  )
  return vnode
}

上面的baseCtor其实就是我们的Vue对象。

如果Ctor是对象,则执行baseCtor.extend(Ctor),这里对应的是我们上面提到的第2种情况,这也就是我为什么之前先讲了Vue.extend的实现。

如果Ctor不是对象,则跳过这一步骤。因为我们createElement方法第一个参数可能是一个Vue的子对象,此时tag就不是字符串,对应上面提到的第4中情况,例子如下:

<div id="app">
</div>
<script type="text/javascript">
  new Vue({
    render: function(h){
      return h(Vue.extend({
        template: '<div>test</div>'
      }))
    }
  }).$mount('#app');
</script>

经过这一步的处理,第2、4种情况就统一了。

接着是异步组件相关内容,后续单独讲解。

resolveConstructorOptions方法是递归合并父对象上的options属性,具体见Vue.extend

data.model是对元素上v-model指令的处理,后续单独讲解各个指令。

extractProps从函数名称上,我们也可以知道它是抽取props的数据。

function extractProps (data: VNodeData, Ctor: Class<Component>, tag?: string): ?Object {
  const propOptions = Ctor.options.props
  if (!propOptions) {
    return
  }
  const res = {}
  const { attrs, props, domProps } = data
  if (attrs || props || domProps) {
    for (const key in propOptions) {
      const altKey = hyphenate(key)
      // 提示dom中的属性应该用kebab-case格式的值
      ...

      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey) ||
      checkProp(res, domProps, key, altKey)
    }
  }
  return res
}

function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve?: boolean
): boolean {
  if (hash) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}

我们在子组件中获取父组件的方法和数据时,是通过props来传递的。使用的时候,我们需要在子组件中定义props属性,来指定使用父组件传递的哪些数据,以及每个属性的类型是什么。上面代码中Ctor.options.props就是在子组件中指定的props,如果没有指定,则直接返回。

data中的attrs是绑定在子元素上的属性值,因为父级组件传递数据到子组件是通过把数据绑定在子元素的属性上,所以我们传递的数据在attrs中;props暂时没有找到添加的地方;domProps是必须通过props来绑定的属性,比如inputvalueoptionselected属性等。

遍历propsOptions中的属性,所以props中没有指定的属性,即使在父组件中绑定了,子组件也找不到。altKey是驼峰命名属性的中划线连接式,例如myName转换为my-name

checkProp方法是从hash中找keyaltKey属性,如果有就返回true,没找到则返回false。没有传递preserve参数,则表示找到该key的值时删除hash上对应的属性。extractProps获取值的优先级从高到低分别是propsattrsdomProps,从之前parse方法中我们知道,模板解析的结果中domPropsattrs是不会重复的。

函数化组件之后单独讲。

data.on上保存的是我们绑定在元素上的事件,且该事件没有加native修饰符。data.nativeOn保存的是添加了native修饰符的事件。关于事件的细节也比较多,我们之后再细说。这里我们把data.on赋值给listenersdata.on保存的是data.nativeOn的值。

Ctor.options.abstractKeepLive等抽象组件,data上只能包含props & listeners

mergeHooks是合并data对象上的一些钩子函数。

const hooksToMerge = Object.keys(componentVNodeHooks)
function mergeHooks (data: VNodeData) {
  if (!data.hook) {
    data.hook = {}
  }
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const fromParent = data.hook[key]
    const ours = componentVNodeHooks[key]
    data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours
  }
}

function mergeHook (one: Function, two: Function): Function {
  return function (a, b, c, d) {
    one(a, b, c, d)
    two(a, b, c, d)
  }
}

hooksToMerge共有四个值initprepatchinsertdestroy。具体实现后面讲解,从函数名上我们也可以猜到,它们分别是正在VNode对象初始化、patch之前、插入到dom中、VNode对象销毁时调用。

mergeHooks合并钩子函数的流程很简单,如果data.hook上已经有了同名的钩子函数,则创建一个新的函数,其内部分别调用这两个同名函数,否则直接添加到data.hook对象上。

最后会创建一个vnode对象,并返回。所以,上面提到的第2种和第4种情况,最终会返回一个标签名为vue-component-cid-name格式的VNode对象。