【Vue源码】06. createElement

515 阅读2分钟

createElement

04._render 中我们知道,vm._render 最终是执行 createElement 方法并返回 VNode,下面我们一起了解 createElement 的细节。

createElement 方法定义在 src/core/vdom/create-element.js 中。

// 其实是对 _createElement的一层封装,可以更加灵活的传入参数

// 正常传参(有data参数)
// render(createElement) {
//   return createElement('h1', { class: 'title'}, ['内容1', '内容2'])
// }
export function createElement(
  context: Component, // vm 实例
  tag: any, // 标签
  data: any, // 数据对象
  children: any, // 子级虚拟节点 (VNodes)
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 对传参进行处理,如果data不是对象,那么把参数后移一位
  if (Array.isArray(data) || isPrimitive(data)) {
    // data参数不传
    // render(createElement) {
    //   return createElement('h1', ['内容1', '内容2'])
    // }
    normalizationType = children;
    children = data;
    data = undefined;
  }
  // 用户手写render时
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  return _createElement(context, tag, data, children, normalizationType);
}

createElement 方法其实是对 _createElement 方法进行了一层封装,可以兼容不同的入参格式。 官方的示例有两种

一:

render(createElement) {
  return createElement('h1', { class: 'title'}, ['内容1', '内容2'])
}

二:

render(createElement) {
  return createElement('h1', ['内容1', '内容2'])
}

下面这块代码就是对传参不同做降级处理(如果第二个参数 data 不是对象的话,参数整体后移,data 设置为 undefined)

if (Array.isArray(data) || isPrimitive(data)) {
  normalizationType = children;
  children = data;
  data = undefined;
}

createElement 方法最后返回 _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();
  }
  // object syntax in v-bind
  // <component :is="name"></component> 组件会含有 .is
  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;
  }
  // 把children转换为VNode格式的一维数组
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 调用场景:
    // 用户手写的 render
    // 当编译 slot、v-for 的时候会产生嵌套数组
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // render 函数是编译生成
    children = simpleNormalizeChildren(children);
  }
  //...
}

_createElement 方法中,依次对 datatag、插槽做了处理,然后再处理最重要的 children

因为我们只需要看用户手写 render 才会走的流程,所以着重看 normalizeChildren 方法。

normalizeChildren

定义在src/core/vdom/helpers/normalize-children.js

export function normalizeChildren(children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
    ? normalizeArrayChildren(children)
    : undefined;
}

这里进行了一些判断:

  1. isPrimitive 检查是否为原始类型,是的话创建 TextVNode 节点
  2. 判断是否为数组,是的话使用 normalizeArrayChildren 方法标准化 children
  3. 都不是的话返回 undefined

normalizeArrayChildren

// children 表示要规范的子节点
function normalizeArrayChildren(
  children: any,
  nestedIndex?: string
): Array<VNode> {
  const res = [];
  let i, c, lastIndex, last;
  for (i = 0; i < children.length; i++) {
    c = children[i];
    if (isUndef(c) || typeof c === "boolean") continue;
    lastIndex = res.length - 1;
    last = res[lastIndex];
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ""}_${i}`);
        // merge adjacent text nodes
        // 合并文本节点
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
          c.shift();
        }
        res.push.apply(res, c);
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        // 合并文本节点
        res[lastIndex] = createTextVNode(last.text + c);
      } else if (c !== "") {
        // 基础类型,则通过 createTextVNode 方法转换成 VNode 类型
        // convert primitive to vnode
        res.push(createTextVNode(c));
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text);
      } else {
        // default key for nested array children (likely generated by v-for)
        if (
          isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)
        ) {
          c.key = `__vlist${nestedIndex}_${i}__`;
        }
        res.push(c);
      }
    }
  }
  return res;
}

normalizeArrayChildren 方法主要做了:

  1. 循环入参 children
  2. 当前项为数组时,递归处理 children
  3. 当前项为普通数据类型时,传入当前项生成 TextVNode 标签

在处理的同时,还针对文本做了合并处理。最后返回一个 VNode 类型的数组。

VNode 的处理

回到 createElement 方法,在处理 children 后,执行以下代码创建一个 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
    // 创建HTML内置类型的VNode节点
    vnode = new VNode(
      config.parsePlatformTagName(tag),
      data,
      children,
      undefined,
      undefined,
      context
    );
  } else if (
    isDef((Ctor = resolveAsset(context.$options, "components", tag)))
  ) {
    // component
    // 创建组件类型的VNode节点
    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 (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();
}

这里先对 tag 做判断,如果是 string 类型,则再判断如果是为 HTML 内置标签类型,就直接创建一个普通 VNode 节点,如果是已注册的组件名,则通过 createComponent 创建一个组件 VNode,否则创建一个未知标签的 VNodecreateComponent 方法本质是返回一个 VNode,暂且不表。

总结

看完本文,就大概了解了 createElement 创建 VNode 的过程。每个 VNode 都有 childrenchildren 的每一个元素也都是一个 VNode,这样就组合成了 VNode Tree。 回到 mountComponent,我们已经知道 vm._render 的大致流程,那么在最终渲染成一个真实 DOM 的过程中还有 vm._update 没有了解,接下来分析。

源码分析 GitHub 地址