通过上节课的学习,我们了解到 createElement 就是用来创建一个 vnode
1. 关于vNode
既然 createElement 是返回一个 vnode,那就有必要了解一下 vnode 的一些概念。
- 关于
vnode构造函数可以看这里 vnode就是js对象。避免频繁操作 DOM 以提高性能。- 组件的
vnode有两种:- 占位符
vnode:vm.$vnode只有组件实例才有 - 渲染
vnode:vm._vnode可以通过这个VNode直接映射成真实DOM - 它们是父子关系:
vm._vnode.parent = vm.$vnode - 这里先了解一下概念,在组件patch章节还会提及
- 占位符
2. createElement
createElement 返回一个 vnode
// src/core/vdom/create-element.js
// 常量定义
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 参数重载,意思是data参数实际上是可选的
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 的一个封装,做了两件事:
- 如果第三个参数是一个 数组 或者 原始类型(不包括null和undefined),那么就参数重载。
- 判断
alwaysNormalize是否为true然后将normalizationType = ALWAYS_NORMALIZE
// 通过模板编译生成的 render 函数调用的内部函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 提供给用户编写的render函数所使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
注意:
- 用户手动编写的
render函数时,normalizationType一定是常量ALWAYS_NORMALIZE - 通过编译生成的
render函数时,normalizationType的值根据调用vm._c时传入的具体值而定
normalizationType 的作用主要是为了区分以何种方式规范 children,这个我们下面再说
接下来就是执行真正的处理函数 _createElement
3. _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> {
// 一些边缘情况,暂时不需要关注:
// 1. 传入的 data 参数不能是被观察的 data
// 2. 动态组件处理
// 3. key值如果不是原始类型则抛出警告
// 4. support single function children as default scoped slot
// 核心逻辑1:规范化chidlren
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 核心逻辑2:创建vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 是否HTML原生保留标签
if (config.isReservedTag(tag)) {
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
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))) {
vnode = createComponent(Ctor, data, context, children, tag)
// 未知或未列出的命名空间元素
// 等在运行时检查,因为在其父级标准化子级时可能会为其分配一个名称空间
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
// 返回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()
}
}
3.1 规范化children
- 如果
normalizationType是常量ALWAYS_NORMALIZE,也就是用户编写的render函数的情况,那么就用normalizeChildren来规范化子节点 - 如果
normalizationType是常量SIMPLE_NORMALIZE,那么就用simpleNormalizeChildren
为什么需要规范化 children 呢?
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. There are two cases where extra normalization is needed:
翻译:
模板编译器尝试通过在编译时静态分析模板来避免 normalization 。
对于纯HTML标记的字符串模板,可以完全跳过 normalization,因为可以保证所生成的 render 函数返回 Array<VNode>。
但是有两种情况需要额外的规范化:
3.1.1 simpleNormalizeChildren
// src/core/vdom/helpers/normalize-children.js
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
}
children中可能包含了函数式组件。函数式组件返回一个数组而不是一个根节点。如果children中有数组,我们需要去做扁平化处理,即将children转换为一维数组。- 使用
Array.prototype.concat将其faltten。由于函数式组件已经对其自己的子级进行了规范化,因此保证深度仅为1级。
3.1.2 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
}
以下情形需要调用 normalizeChildren 来规范化
- 手写
render函数或者JSX时,children允许写成基础类型用来创建单个简单的文本节点,这种情况会调用createTextVNode创建一个文本节点的VNode - 当编译
<template>、slot、v-for的时候会产生嵌套数组,这会调用normalizeArrayChildren方法
下面看看 normalizeArrayChildren 的真面目吧:
// src/core/vdom/helpers/normalize-children.js
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]
// 如果是嵌套数组
if (Array.isArray(c)) {
if (c.length > 0) {
// 递归处理
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// 如果存在两个连续的 text 节点,会把它们合并成一个 text 节点
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)) {
// 如果存在两个连续的 text 节点,会把它们合并成一个 text 节点
// 这对于 SSR hydration 来说是必须的,因为文本节点渲染成 HTML 时实质上已经合并了
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
// 已经是vnode类型
} else {
if (isTextNode(c) && isTextNode(last)) {
// 如果存在两个连续的 text 节点,会把它们合并成一个 text 节点
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// 如果 children 是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key (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 接收 2 个参数:
children表示要规范的子节点nestedIndex表示嵌套的索引,因为单个child可能是一个数组类型
normalizeArrayChildren 主要的逻辑就是遍历 children,获得单个节点 c,然后对 c 的类型判断
- 数组:递归调用
normalizeArrayChildren - 基础类型:通过
createTextVNode方法转换成VNode类型 - vnode类型:如果
children是一个v-for列表,则根据nestedIndex去更新它的key。- 在编译生成的
render函数中通过vm._l(...)会调用renderList函数,这个函数会挂载一个_isVList变量用来表示这是v-for列表 - 当
v-for一个普通HTML标签才会自动处理key,v-for一个组件时,组件的key如果没传就是undefined
- 在编译生成的
// src/core/instance/render-helpers/render-list.js
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
// ...
(ret: any)._isVList = true
return ret
}
在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text 节点,会把它们合并成一个 text 节点。
经过对 children 的规范化,children 变成了一个类型为 VNode 的 Array
3.2 创建vnode
当 tag 是一个字符串时:
- 调用
config.isReservedTag函数判断如果tag是内置标签则直接创建一个对应的VNode对象。 - 如果
tag如果是已注册的组件名,则调用createComponent函数。 tag是一个未知的标签名,这里会直接按标签名创建vnode,然后等运行时再来检查,因为它的父级规范化子级时可能会为其分配命名空间。
当 tag 不是字符串时:
- 通过
createComponent创建组件类型的VNode的过程我们以后再介绍
总结
那么至此,我们大致了解了 createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
我们已经知道 vm._render 是如何创建了一个 VNode,接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的,下一章一起来看下。