Vue源码探秘(七)(createElement)

2020/3/30 11:02:22

本文主要是介绍Vue源码探秘(七)(createElement),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引言

回顾Vue 源码探秘(五)(_render 函数的实现)vm._cvm.$createElement最终都调用了createElement函数来实现。

并且我们知道:vm._c是内部函数,它是被模板编译成的 render 函数使用;而 vm.$createElement是提供给用户编写的 render 函数使用的。

这一节,我带大家一起来看下createElement函数的内部实现。

createElement

createElement函数定义在src/core/vdom/create-element.js文件中:

// src/core/vdom/create-element.js

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  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 方法的封装。对传入 _createElement 函数的参数进行了处理。

这里的第一个if语句判断data如果是数组或者是原始类型(不包括nullundefined),就调整参数位置,即后面的参数都往前移一位,children参数替代原有的data

这里意思就是可以不传data参数。有点函数重载的意思在里面。

第二个if语句判断传入createElement的最后一个参数alwaysNormalize的值,如果为true,就赋给normalizationType一个常量值ALWAYS_NORMALIZE,然后再将normalizationType传给_createElement。关于常量的定义在这里:

// src/core/vdom/create-element.js

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
复制代码

最后返回调用_createElement的返回值。下面我们来重点分析下_createElement

// src/core/vdom/create-element.js

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()
  }

  // ...
}
复制代码

先来看第一段,对data参数进行校验。这里判断data是否含有__ob__属性。

Vue 中被观察的 data 都会添加上 __ob__ ,这一块我们会在响应式原理模块具体介绍。

_createElement 函数要求传入的 data 参数不能是被观察的 data,如果是会抛出警告并返回一个利用createEmptyVNode方法创建的空的 VNode

继续往下看:

// src/core/vdom/create-element.js

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  //...

  // 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
  }

  // ...
}
复制代码

先判断data有没有is属性(也就是判断是否是动态组件)。

关于动态组件不是太清楚的话,可直接去官网查询。

如果有的话,将data.is赋值给tag。接着判断如果tag不存在(也就是判断data.isfalse),返回一个空的VNode

接下来检查data.key,如果不是原始类型则抛出警告。

最后的if语句涉及到插槽(slot)的内容,这部分我会在后面介绍插槽部分的源码时具体分析。我们接着往下看:

// src/core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...

  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

  // ...
}
复制代码

这一段是对参数children的处理,将其规范化。

回顾Vue 源码探秘(五)(_render 函数的实现),我们知道vm._cvm.$createElement函数在调用createElement函数时最后一个参数normalizationType分别是falsetrue。对应_createElementnormalizationType分别是SIMPLE_NORMALIZEALWAYS_NORMALIZE。我们先来看下simpleNormalizeChildren函数:

// src/core/vdom/helpers/normalize-children.js

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
复制代码

看下函数前面的两段注释:

  • 第一段的意思是如果用户使用的是纯HTML标记的字符串模板,就可以跳过处理。因为render函数是由内部编译出来的,可以保证render函数会返回Array<VNode>。但是有两种特殊情况需要处理,来规范化children参数。
  • 第二段的注释说明了第一种特殊情况,就是children中包含了函数式组件。函数式组件可能返回一个数组也可能只返回一个根节点。如果返回的是数组,我们需要去做扁平化处理,即将children转换为一维数组。

看完注释,我们再来看simpleNormalizeChildren函数就很清晰了。就是简单的把二维数组拍平成一维数组。接着看下normalizeChildren函数:

// src/core/vdom/helpers/normalize-children.js

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
复制代码

这里就是上面提到的第二种情况。依旧我们先来看下注释,注释提到了两种适用的情况:

  • 一是编译<template><slot>v-for的时候会产生嵌套数组
  • 二是用户手动编写了render函数JSX

先判断children是不是原始类型,是的话返回一个数组,数组项是createTextVNode(children)的返回值。来看下createTextVNode函数:

// src/core/vdom/vnode.js

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}
复制代码

函数比较简单,就是返回一个文本节点的VNode

回到normalizeChildren函数,如果不是原始类型,再判断children是否是数组。如果是数组的话返回normalizeArrayChildren(children)的返回值,否则返回undefined

我们回到 _createElement 函数,继续看下一段代码:

// src/core/vdom/create-element.js

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...

  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.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 ((!data || !data.pre) && isDef(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 语句判断 tag如果是字符串的情况,这里调用的 config.isReservedTag 函数是判断如果 tag 是内置标签则直接创建一个对应的 VNode 对象。

然后判断 tag 如果是已注册的组件名,则调用 createComponent 函数。

最后一种情况是tag是一个未知的标签名,这里会直接按标签名创建 VNode,然后等运行时再来检查,因为它的父级规范化子级时可能会为其分配命名空间。

else里面的逻辑涉及到组件化和createComponent函数,这块我会放在后面的组件化源码解读部分详细说明。

接着看下_createElement 函数的最后一段代码:

// src/core/vdom/create-element.js

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...

  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又做了一次校验,最后返回vnode

总结

到这里,我们就把通过createELement 函数创建一个 VNode 的过程分析清楚了。回顾Vue 源码探秘(四)(实例挂载$mount),我们在分析 $mount 函数时了解到,创建 Watcher 对象后会执行 updateComponent 函数:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

现在我们已经将 _render() 函数包括涉及到的createElement 函数分析完了,下一节我们就来一起看下_update 函数。



这篇关于Vue源码探秘(七)(createElement)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程