前言
vue 源码系列的分享我会尽可能的表述清楚一些,简单一些。
createElement
上一节中我们知道 vue 生成虚拟 DOM 时候,使用的是 createElement 方法。下面我们就看下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
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
所以实际上,createElement方法是对_createElement方法的封装。真正创建虚拟 DOM 的是 _createElement 函数。继续看下_createElement方法。
_createElement
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
// 第一部分
if (normalizationType === ALWAYS_NORMALIZE) {
// render 函数是用户手写的时候调用,作用是将数组打平为一维数组
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 当 render 是编译生成的时候调用,作用是将数组打平一层
children = simpleNormalizeChildren(children)
}
...
}
它先对 children 做个处理,是将 children 数组打平一个层级。
-
children 的处理
先看下简单点的 simpleNormalizeChildren 方法。
- simpleNormalizeChildren
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
}
实际上是使用 Array.prototype.concat.apply([], children) 将 children 数组直接打平一层。尝试手动调用下 _c 方法。
const h = vm._c;
console.log(h('div', null, ['test1', [h('p'), ['test2']]], 1));
结果如下,可以看到,只打平了一层数组。
- normalizeChildren
而 normalizeChildren 方法则是,将children数组打平为一个一维数组。我们来看下它是如何实现的。
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
// 如果是原始类型(string、number、symbol、boolean),直接创建文本节点
? [createTextVNode(children)]
: Array.isArray(children)
// 如果是数组,调用 normalizeArrayChildren 方法
? normalizeArrayChildren(children)
: undefined
}
接下来,我们看下 normalizeArrayChildren 方法做了什么操作。
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 ,将数组内的元素都转为文本节点
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
if (isTextNode(c[0]) && isTextNode(last)) {
// 合并文本节点,是将 res 的最后一个节点和 c 的第一个节点的文本内容进行合并
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
// 利用 apply 将数组 c 的元素逐一 push 到 res 中,从而将多维数组转为一维数组
// 相当于 res.push(...c)
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// 如果 res 的最后一个元素是文本节点,将其文本与 c 进行合并
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// 如果 c 是文本节点,将 c 的文本与 res 的最后一个元素的文本进行合并
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}__`
}
// 否则 c 已经是 VNode 类型了
res.push(c)
}
}
}
return res
}
可以看到,normalizeArrayChildren的主要的作用是将 children 数组打平为一个一维数组,并且将 children 内的元素全部转为虚拟节点 VNode,res 最终返回的结果是 [VNode, VNode, ...]。
我们尝试调用下$createElement方法。
const h = app.$createElement;
console.log(h('div', null, ['test1', [h('p'), ['test2']]], 1));
结果如下图,可以看到,children 数组被打平为一个一维数组了,并且children 数组内的元素全部被转为虚拟DOM了。
-
生成虚拟 DOM
继续看_createElement第二部分代码。这部分比较简单,代码如下:
// 第二部分
if (typeof tag === 'string') {
if (config.isReservedTag(tag)) {
// tag 是内置的标签,如 div,创建普通元素 vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果是已注册的组件名,创建一个组件类型的 vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 创建一个未知标签的 vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 如果 tag 不是字符串,则创建组件类型的 vnode
vnode = createComponent(tag, data, context, children)
}
- 元素节点:直接使用
new VNode直接生成虚拟DOM。 - 组件节点:使用
createComponent生成虚拟DOM。
然鹅createComponent又是什么呢。
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// context.$options._base 对应的是 Vue 本身
const baseCtor = context.$options._base
if (isObject(Ctor)) {
// 将组件对象 Ctor 转为 Vue 的子类,使其拥有 Vue 的完整的功能
Ctor = baseCtor.extend(Ctor)
}
...
// 安装钩子函数,包含 init、prepatch、insert、destory,在将 vnode 转为 真实DOM时会用到
installComponentHooks(data)
const name = Ctor.options.name || tag // 用于拼接组件的tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // 对应tag
data, // 父组件自定义事件和patch时用到的方法
undefined, // children
undefined, // text
undefined, // 节点
context, // 当前实例
{ Ctor, propsData, listeners, tag, children }, // 对应componentOptions属性
asyncFactory
)
return vnode
}
这个有点绕,我们一点一点看吧。
首先,context.$options._base 是什么呢。这个是在 Vue 初始化时候,调用了 initGlobalAPI 函数。其中有两行代码是:
export function initGlobalAPI (Vue: GlobalAPI) {
...
// 经过 _init() 中的 mergeOptions 后,vm.$options 中的 _base 也就会等于 Vue
Vue.options._base = Vue
initExtend(Vue)
}
所以,baseCtor 就是 Vue 本身。而 initExtend 是什么呢。
export function initExtend(Vue: GlobalAPI) {
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// vue 基类构造函数
const Super = this
...
// 定义一个VueComponent构造函数
const Sub = function VueComponent(options) {
this._init(options) // 继承 Vue 的 _init 方法
}
// 继承基类 Vue 的原型方法
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 合并options
Sub.options = mergeOptions(
Super.options, // Vue 自身的options
extendOptions // 用户手写的options
)
// 将基类Super的静态方法赋值给子类Sub
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
...
return Sub
}
}
所以,vue.extend() 会返回一个 VueComponent 函数,它拥有 vue 的完整功能。
installComponentHooks(data)
installComponentHooks安装一些钩子,如 init、prepatch、insert、destory,在将 vnode 转为 真实 DOM 时会用到。
createComponent最后一部分,就是创建组件类型的虚拟 DOM。
总结
createElement对_createElement做了个封装。_createElement包含了两部分,一是children的处理,一是创建虚拟DOMchildren的处理,根据render的不同,处理方式也有所区别。- 当
render是由template编译产生时,使用的是simpleNormalizeChildren将children数组降低一层。 - 当
render是用户手写传入时,使用的是normalizeChildren将children数组转为一个一维数组,其内部的元素全部由vnode组成。
- 当
- 创建虚拟
DOM时。- 若是元素节点,直接使用
new Vnode()创建虚拟DOM。 - 若是创建组件节点,则使用
createComponent,它的作用,一是将组件对象转为Vue的子类,使其具有Vue的完整功能。二是初始化一些将虚拟DOM转为真实DOM的钩子。三是使用new Vnode()创建组件类型的虚拟DOM,并将组件相关的信息保存在componentOptions中。
- 若是元素节点,直接使用