【vue源码解析】createElement

[TOC]

vue render 函数 跟 template 一样都是创建 html 模板的。render 函数即渲染函数,它是个函数,它的参数也是个函数(即 createElement),下面我们重点来说 createElement 。

表层:createElement用法

1
2
3
4
5
6
render: function (createElement) { // 有时候createElement用h作为别名
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},

createElement参数

接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// @returns {VNode}
createElement(
/**
第一个参数:
类型:{String | Object | Function}
说明:【必填项】一个 HTML 标签名、组件选项对象,或者resolve 了上述任何一种的一个async函数。
**/
'div',
-------------------------------
/**
第二个参数:
类型:{Object}
说明:【可选】模板中的数据
**/
{
// 要创建标签的class,与 `v-bind:class` 的 API 相同,接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 标签的内联样式,与 `v-bind:style` 的 API 相同,接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// HTML的attrs
attrs: {
id: 'foo'
},
// 组件的prop
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 组件上的事件监听,不再支持如 `v-on:keyup.enter` 这样的修饰器,需要在处理函数中手动检查 keyCode
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用`vm.$emit` 触发的事件
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令,vue directives可以参考https://www.jianshu.com/p/6a4811eb0efe
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为 { name: props => VNode | Array<VNode> } 相当于组件上使用slot-scoped
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果创建的组件是个slot,需为插槽指定名称,相当于组件上 slot="name-of-slot"
slot: 'name-of-slot',
//
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,那么 `$refs.myRef` 会变成一个数组。
refInFor: true
},
-------------------------------
/**
第三个参数:
类型:{String | Array}
说明:【可选】子级虚拟节点 (VNodes),由 `createElement()` 构建而成,也可以使用字符串来生成“文本虚拟节点”。
**/
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)

  • 第一个参数:【必填】{String | Object | Function} 要渲染的标签名称
  • 第二个参数:【可选】{Object} 模板中的数据
  • 第三个参数:【可选】子级虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成“文本虚拟节点”

createElement源码解析

  • 首先对createElement函数对参数做一层处理,对参数个数不一致情况做处理,然后调用_createElement函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 返回vnode或者vnode组件的数组数据
    */
    export function createElement (
    context: Component, // 上下文this
    tag: any, // 标签
    data: any, // vnode数据
    children: any, // 子节点
    normalizationType: any,
    alwaysNormalize: boolean
    ): VNode | Array<VNode> {
    if (Array.isArray(data) || isPrimitive(data)) { // 当vnode数据是数组或者基本类型的时候,对参数个数不一致的处理
    normalizationType = children
    children = data
    data = undefined
    }
    if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
    }
    return _createElement(context, tag, data, children, normalizationType) // 真实调用
    }
  • _createElement函数:

    • 中先对节点data做判断,如果data是响应式的则报警告并生成一个vnode的注释节点
    • 对children做规范化处理:分children是个二维数组和children多层数组嵌套两种情况,将他们拍平成一个一维数组
    • 创建vnode节点:html标签、组件…

_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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object, // html标签
data?: VNodeData, // vnode数据
children?: any, // 子节点
normalizationType?: number // 子节点规范类型 1 2
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) { // 如果data是响应式的报警告
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-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is // component is
}
if (!tag) { // 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
}
// 对children做规范化
if (normalizationType === ALWAYS_NORMALIZE) { // children下有多层嵌套,循环调用拍平成一个一维数组
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) { // 如果children下只有一层数组,将二维数组拍平成一个一维数组
children = simpleNormalizeChildren(children)
}
// 创建vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) { // 是否是html原生的一些保留标签
// platform built-in elements
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))) {
// 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 (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()
}
}

children规范化:

  • simpleNormalizeChildren:场景是render函数是编译生成的,

    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
    }
  • normalizeChildren:场景是render 函数是用户手写的,当 children 只有一个节点的时候,Vue 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,这种情况下回调用 createTextVNode 创建一个文本节点的 VNode;另一个场景就是当编译 slot、 v-for 的时候回产生嵌套数组的情况,回调用 normalizeArrayChildren 方法。
    normalizeArrayChildren 接收 2 个参数,children 表示要规范的子节点,nestedIndex 表示嵌套的索引,因为单个 child 可能是一个数组类型。normalizeArrayCHildren 主要的逻辑就是遍历 children,获得单个节点 c,然后对 c 的类型判断:
    如果是一个数组类型,则递归调用 normalizeArrayChildren;
    如果是基础类型,则通过 createTextVNode 方法转换成 VNode 类型;否则就已经是 VNode 类型了;
    如果 children 是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key。

    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
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    /**
    * 场景是render 函数是用户手写的, 当 children 只有一个节点的时候, Vue 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,
    * 这种情况下回调用 createTextVNode 创建一个文本节点的 VNode; 另一个场景就是当编译 slot、 v-for 的时候回产生嵌套数组的情况,
    * 回调用 normalizeArrayChildren 方法。
    * @param {*} children 要规范的子节点
    * @param {string} [nestedIndex] 嵌套的索引
    * @returns {Array<VNode>} 返回拍平的一维数组
    */
    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] // c是单个节点
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    // nested
    if (Array.isArray(c)) { // 如果是一个数组类型,则递归调用 normalizeArrayChildren
    if (c.length > 0) {
    c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
    // merge adjacent text nodes
    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)) { // 如果是基础类型,则通过 createTextVNode 方法转换成 VNode 类型;否则就已经是 VNode 类型了
    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 {
    /** 当前节点c是一个嵌套数组(比如v-for产生的) 比如:
    render: function (createElement) {
    if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
    return createElement('li', item.name)
    }))
    } else {
    return createElement('p', 'No items found.')
    }
    }
    根据 nestedIndex 去更新它的 key
    */
    // 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
    }

上面也大致了解了 createElement 创建 VNode 的过程。每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了 DOM Tree。下一个步骤就是要把这个 VNode 渲染成一个真实的 DOM 并且渲染出来,这个过程是通过 vm._update 完成的。

参考:

https://blog.csdn.net/sansan_7957/article/details/83014838
https://cn.vuejs.org/v2/guide/render-function.html
https://www.jianshu.com/p/709fc34e27b9

热评文章