回顾Vue 源码探秘(五)(_render 函数的实现),vm._c
和vm.$createElement
最终都调用了createElement
函数来实现。
并且我们知道:vm._c
是内部函数,它是被模板编译成的 render
函数使用;而 vm.$createElement
是提供给用户编写的 render
函数使用的。
这一节,我带大家一起来看下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
如果是数组或者是原始类型(不包括null
、undefined
),就调整参数位置,即后面的参数都往前移一位,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.is
为false
),返回一个空的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._c
和vm.$createElement
函数在调用createElement
函数时最后一个参数normalizationType
分别是false
和true
。对应_createElement
的normalizationType
分别是SIMPLE_NORMALIZE
和ALWAYS_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
函数。