createElement
逻辑:回到mountComponent函数的过程,至此已经知道vm._render是如何创建了一个VNode,接下来就是要把这个 VNode 渲染成一个真实的DOM并渲染出来,这个过程是通过vm._update完成的,接下来分析一下这个过程。
Vue.js 利用createElement方法创建VNode,它定义在src/core/vdom/create-element.js中:
src/core/vdom/create-element.js文件解析
createElement()
createElement的定义支持六个参数,第一个参数context是vm实例,第二个tag是VNode标签(tag: "div"),第三个data是跟VNode相关的数据,第四个children是VNode的子节点(children: [VNode]),有children才能构造成VNode tree,可以完美映射到DOM Tree。
进行参数重载,检测参数,是对参数个数不一致的处理。即没有data,传入的是children,就会把参数往后移动。
对alwaysNormalize进行判断,然后为normalizationType赋值常变量。
createElement方法实际上是对_createElement方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode的函数_createElement。
// src/core/vdom/create-element.js
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 = childrenchildren = datadata = undefined}if (isTrue(alwaysNormalize)) {normalizationType = ALWAYS_NORMALIZE}return _createElement(context, tag, data, children, normalizationType)
}
_createElement()
_createElement函数的流程略微有点多,我们接下来主要分析 2 个重点的流程 —— children的规范化以及VNode的创建。
_createElement方法有5个参数,context表示VNode的上下文环境,它是Component类型;tag表示标签,它可以是一个字符串,也可以是一个Component;data表示VNode的数据,它是一个VNodeData类型,可以在flow/vnode.js中找到它的定义,这里先不展开说;children表示当前VNode的子节点,它是任意类型的,它接下来需要被规范为标准的VNode数组;normalizationType表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考render函数是编译生成的还是用户手写的。
_createElement对data进行校验,data不能是响应式的(有__ob__属性代表是响应式的),否则报警告“ VNode data 不能是响应式的 ”。然后调用createEmptyVNode函数。
createEmptyVNode方法定义在src/core/vdom/vnode.js文件中,即简单创建VNode实例,什么参数都不传,可以理解为是一个注释节点。
// src/core/vdom/vnode.js
export const createEmptyVNode = (text: string = '') => {const node = new VNode()node.text = textnode.isComment = truereturn node
}
判断data和data.is,如果component :is不是一个真值,也是返回一个注释节点。
对data参数,例如key不是基础类型则报错。
children的规范化
对children做normalizeChildren。当手写render函数时,对第三个参数传了this.message,那是一个普通的值,但是实际上children应该是个数组,而且每个数组都是VNode。normalizeChildren和simpleNormalizeChildren函数来自src/core/vdom/helpers/normalize-children.js文件。
(1)simpleNormalizeChildren
simpleNormalizeChildren对children进行了一层遍历。children是个类数组,遍历发现如果有元素是数组,就调用Array.prototype.concat.apply()方法把children拍平(只拍一次),就是让嵌套数组成为一维数组(是因为存在functional component函数式组件返回的是一个数组而不是一个根节点)。最终的期望就是children是个一维数组,每个都是一个VNode。
-
Array.isArray():判断传递的值是否是一个 Array 。如果对象是 Array ,则返回 true ,否则为 false 。
-
数组降维
let children = [1, 2, [3, [4, 5, 6], 7], 8, [9, 10]]; function simpleNormalizeChildren(children) {return Array.prototype.concat.apply([], children); } console.log(simpleNormalizeChildren(children)) // [1, 2, 3, [4, 5, 6], 7, 8, 9, 10]
(2)normalizeChildren
normalizeChildren最终目标也是返回一个一维的数组,每个都是VNode。
首先判断是否是个基础类型,是的话直接返回一维数组,数组长度为1,createTextVNode实例化一个VNode,前三个参数是undefined,第四个参数是个string,是返回一个文本VNode;不是基础类型则判断是否是Array类型,是的话调用normalizeArrayChildren方法,否则返回undefined。
export function normalizeChildren (children: any): ?Array<VNode> {return isPrimitive(children)? [createTextVNode(children)]: Array.isArray(children)? normalizeArrayChildren(children): undefined
}
(3)normalizeArrayChildren
normalizeArrayChildren返回的是res数组。遍历children,如果children[i]是Array数组(可能多层嵌套,例如编译slot、v-for的时候会产生嵌套数组的情况),递归调用normalizeArrayChildren,做个优化(如果最后一个节点和下次第一个节点都是文本,则把这两个合并在一起),再做一层push;如果是基础类型,判断是否是文本节点,是的话则通过createTextVNode方法转换成VNode类型,不是的话直接push;如果是VNode,例如v-for,如果children是一个列表并且列表还存在嵌套的情况,则根据nestedIndex去更新它的key。最终返回res。
normalizeArrayChildren主要是递归和合并。经过对children的规范化,children变成了一个类型为VNode的Array。
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {const res = []let i, c, lastIndex, lastfor (i = 0; i < children.length; i++) {c = children[i]if (isUndef(c) || typeof c === 'boolean') continuelastIndex = res.length - 1last = res[lastIndex]// nestedif (Array.isArray(c)) {if (c.length > 0) {c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// merge adjacent text nodesif (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 stringsres[lastIndex] = createTextVNode(last.text + c)} else if (c !== '') {// convert primitive to vnoderes.push(createTextVNode(c))}} else {if (isTextNode(c) && isTextNode(last)) {// merge adjacent text nodesres[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
}
VNode 的创建
对tag进行判断,是个string还是组件。如果是string,判断是不是 HTML 原生保留标签。如果是则创建一个普通的保留标签,然后直接创建一个普通vnode。vnode = render.call(vm._renderProxy, vm.$createElement)函数返回的vnode是createElement(vm, a, b, c, d, true)的返回值。同时把vnode返回给Vue.prototype._render。
这里先对tag做判断,如果是string类型,则接着判断是不是 HTML 原生保留标签,则直接创建一个普通vnode,如果是为已注册的组件名,则通过createComponent创建一个组件类型的vnode,否则创建一个未知的标签的vnode。如果是tag一个Component类型,则直接调用createComponent创建一个组件类型的vnode节点。对于createComponent创建组件类型的vnode的过程,本质上它还是返回了一个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> {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-bindif (isDef(data) && isDef(data.is)) {tag = data.is}if (!tag) {// in case of component :is set to falsy valuereturn createEmptyVNode()}// warn against non-primitive keyif (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 slotif (Array.isArray(children) &&typeof children[0] === 'function') {data = data || {}data.scopedSlots = { default: children[0] }children.length = 0}if (normalizationType === ALWAYS_NORMALIZE) {children = normalizeChildren(children)} else if (normalizationType === SIMPLE_NORMALIZE) {children = simpleNormalizeChildren(children)}let vnode, nsif (typeof tag === 'string') {let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)if (config.isReservedTag(tag)) {// platform built-in elementsif (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {warn(`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,context)}vnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// componentvnode = 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 childrenvnode = new VNode(tag, data, children,undefined, undefined, context)}} else {// direct component options / constructorvnode = 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()}
}