Vue源码解析9-children归一化

归一化操作其实就是将多维的数组,合并转换成一个一维的数组。在 Vue 中归一化分为三个级别,

  • 0:不需要进行归一化
  • 1:只需要简单的归一化处理,将数组打平一层
  • 2:完全归一化,将一个 N 层的 children 完全打平为一维数组

归一化的代码位于src/core/vdom/create-element.jscreateElement中,我们的vm._cvm.$createElement都会调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

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

export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...

处理children归一化
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}

...
}

vm._c传入的alwaysNormalizefalse,而vm.$createElement传入的alwaysNormalizetrue。所以最终前者会调用simpleNormalizeChildren来处理children,后者会使用normalizeChildren

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.

1. simpleNormalizeChildren

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.

1
2
3
4
5
6
7
8
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;
}

如果有任何一个 child 是数组,那么直接整个 children 打平一层,例如:

1
2
var arr = [1,2,3,4,[5,6,[7,8]]]
Array.prototype.concat.apply([],arr ) => [1, 2, 3, 4, 5, 6, [7,8]]

我们可以试着手动调用一下vm._c

1
2
var h = vm._c;
h('div', null, ['test', [h('p'), h('p'), ['inner']]], 1);

返回的 vnode 结果关键属性有:

1
2
3
4
5
6
7
8
9
{
tag: "div",
children:[
'test',
 {tag"p"}, // VNode
 {tag"p"}, // VNode
 ['inner']
]
}

可以看到只打平了一层。

2. normalizeChildren

When the children contains constructs that always generated nested Arrays,
e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
export function normalizeChildren(children: any): ?Array<VNode> {
return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : undefined;
}

function isTextNode(node): boolean {
return isDef(node) && isDef(node.text) && isFalse(node.isComment);
}

// 将整个children完全打平成一维数组,如[1,2,[3,4,[5,6]]] => [1,2,3,4,5,6]
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}`); // 打平后的一维数组c
// merge adjacent text nodes, 将res最后一个元素和c的第一个元素合并
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;
}

其实就是利用递归来处理的,同时处理了一些边界情况。同样手动调用下vm.$createElement来触发此逻辑:

1
2
var h = vm.$createElement;
h('div', ['test', [h('p'), h('p'), ['inner']], null, true]);

返回 vnode 结果的关键属性:

1
2
3
4
5
6
7
8
9
{
tag: "div",
children: [
{ text"test", tagundefined }, // VNode
{ tag"p" } , // VNode
{ tag"p" } , // VNode
{ text"inner", tagundefined }, // VNode
]
}

可以看到全部都打平了。