Vue2源码学习笔记 - 19.渲染与编译—createElement 函数

87 阅读4分钟

我正在参与掘金创作者训练营第5期,点击了解活动详情

虽然我们多数时候只写组件的模板HTML字符串,但是有时候我们也需要手写 render 函数来渲染页面,这比模板更接近编译器。初始化时它省去了编译操作,直接进入 mount 环节。在渲染时调用 render 函数获得其 vnode 以便渲染页面。

createElement 使用方法

通常我们编写如下这类 render 函数,其中传入了 createElement,这个是创建 vnode 的关键函数,它在 render 函数被调用时传入。执行后它会返回节点的 vnode,它不是一个实际的 DOM 元素。createElement 更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

我们来看看 createElement 的参数:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // 接受一个字符串、对象或字符串和对象组成的数组
    'class': {
      foo: true,
      bar: false
    },
    // 与 `v-bind:style` 的 API 相同,
    // 接受一个字符串、对象,或对象组成的数组
    style: {
      color: 'red',
      fontSize: '14px'
    },
    // 普通的 HTML attribute
    attrs: {
      id: 'foo'
    },
    // 组件 prop
    props: {
      myProp: 'bar'
    },
    // DOM property
    domProps: {
      innerHTML: 'baz'
    },
    // 事件监听器在 `on` 内
    on: {
      click: this.clickHandler
    },
    ...
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

参数有点多,不必一下全部熟悉,可在用到时再查阅。多数时候我们只用编译模板生成的 render 函数,我们简单编译一个模板看看它的内容:

var html = `<div id="cpmt">
  <p v-if="show">hello {{name}}</p>
  <p v-else>hi: <span>{{firstname}}</span><span>{{lastname}}</span></p>
  </div>`
var r = Vue.compile(html)

// 输出如下 >>>>>
// $options.render == r.render
// r.render 为一个匿名函数:
anonymous() {
  with(this){
    return _c(
    'div',
    {attrs:{"id":"cpmt"}},
    [
      (show) ? _c('p',[_v("hello "+_s(name))]) : _c('p',[_v("hi: "),_c('span',[_v(_s(firstname))]),_c('span',[_v(_s(lastname))])])
    ])
  }
}

编译后生成的 render 函数使用了 with 语法,省去了其中代码的 this。这其中的 _c$createElement 都是基于 createElement 函数的包装,前者用于编译后产生的 render 函数后者用于用户手写的 render 函数。它们在该文件的 initRender 函数中有定义:

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)

createElement 前置流程

前面我们学习了 renderWatcher 会在每次收到更新通知时去执行 updateComponent 函数,在这里面做更新渲染页面的工作。

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

我们看 _render 的代码:

file: /src/core/instance/render.js

Vue.prototype._render = function (): VNode {
  ...
  const { render, _parentVnode } = vm.$options
  ...
  // render self
  let vnode
  try {
    currentRenderingInstance = vm
    // 调用执行 vm.$options.render
   // vm._renderProxy 可简单理解为与 vm 等价
   vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    ...
  } finally {...}
  ...
  return vnode
}

在这个方法中,主要是调用了 vm.options.render方法,它就是渲染函数render,可能是我们手写的,也可能是在入口的options.render 方法,它就是渲染函数 render,可能是我们手写的,也可能是在入口的 mount 方法中由 compileToFunctions(与 Vue.compile同)函数编译模板后得到。

createElement

再来看 createElement 这个函数,它在的功能模块单独在一个文件中:

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

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
// 对 _createElement 的包装
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
  }
  // 布尔类型转具体的整型 normalizationType
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 响应式的 data 直接返回空注释的 vnode
  if (isDef(data) && isDef((data: any).__ob__)) {
    ...
    return createEmptyVNode()
  }
  // 如有 is 属性则把其值当组件名赋给 tag
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // 无tag 则创建空注释 vnode
    return createEmptyVNode()
  }
  ...
  // 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
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
      // 简单扁平化 children
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // tag 为字符串
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      ...
      // 创建 HTML\svg 标签的 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // tag 为组件,调用 createComponent创建 vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 除了上面两种情况的 vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // tag 为 组件选项 或 组件构造函数,调用 createComponent创建 vnode
    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()
  }
}

如上代码,createElement 在简单处理之后实际调用的是函数 _createElement。在 _createElement 在处理过程中先规范化 children 节点,然后在根据 tag 不同做不同处理,最后返回 VNode 对象或者 VNode 数组

对于纯 HTML 标记,可以完全跳过规范化,因为生成的渲染函数保证返回 Array。 有两种情况需要额外的规范化。一个是编译生成的 render 函数多采用 simpleNormalizeChildren 去扁平化,因为其中可能有函数式组件,它可能返回 VNode 数组而不是单个 VNode 对象。这种情况就是简单的扁平为深度一层的数组。

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

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

vue VNode 扁平化

第二个情况是用户手写的 render 函数,因为其中会有 template、slot、v-for 等产生嵌套的数组节点,这会调用 normalizeChildren 深度规范化。它根据情况做不同处理,对于数组会调用 normalizeArrayChildren 处理,它会遍历数组元素然后创建对应的 vnode。

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 !== '') {
        // 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
}

规范化 children,看上去挺复杂,但是它的总体思想就是对于同一深度层次的元素,不管是单个元素还是该元素在数组中,只要是属于同一深度层次,都扁平化存于一个一维的,深度相同的数组中。

在规范化 children 之后,_createElement 接着根据 tag 不同做不同处理,对于 HTML\SVG 等标签直接创建其 vnode,对于组件标签,调用 createComponent 创建其 vnode。createComponent 函数我们后面会详细研究学习,这里不做过多介绍。

vue createElement 编译

总结:

createElement 函数主要是在调用 render 函数时创建元素的 vnode。对于各种类型的子节点在调用 normalizeChildren\simpleNormalizeChildren 规范化扁平化之后存于 children 属性中。对于HTML\SVG 类标签直接 new VNode 创建,组件则调用 createComponent 创建。最后返回 Vnode 对象或者数组,以供后续渲染操作。