前言

一般vue组件都是用<template>写html, 但实际还可以在js代码中通过render函数生成dom. 最主要常见组件库也需要配合h函数使用。

render函数

render是组件的一个选项, 他的返回值会被作为组件的DOM结构.示例:

1
2
3
4
5
6
7
8
import { defineComponent} from "vue";
const App = defineComponent({
render(){
return '123456789'
}
});

createApp(App).mount("#app");
image-20230422225101064

同样可以插入dom元素:

1
2
3
4
5
6
7
8
import { defineComponent } from "vue";
const App = defineComponent({
render(){
return '<h2>123456789</h2>'
}
});

createApp(App).mount("#app");
image-20230422225319980

直接使用字符串写入会发现vue并没有解析dom,而是将内容当做字符串直接写入到元素表中去了。那么这里需要怎么插入HTML元素呢?这就需要用到vue里面的一个 VNode的东西。

VNode是vue对于DOM节点的一个描述,TS中是一个类型对象。

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
export declare interface VNode<HostNode = RendererNode, HostElement = RendererElement, ExtraProps = {
[key: string]: any;
}> {
/* Excluded from this release type: __v_isVNode */
/* Excluded from this release type: __v_skip */
type: VNodeTypes;
props: (VNodeProps & ExtraProps) | null;
key: string | number | symbol | null;
ref: VNodeNormalizedRef | null;
/**
* SFC only. This is assigned on vnode creation using currentScopeId
* which is set alongside currentRenderingInstance.
*/
scopeId: string | null;
/* Excluded from this release type: slotScopeIds */
children: VNodeNormalizedChildren;
component: ComponentInternalInstance | null;
dirs: DirectiveBinding[] | null;
transition: TransitionHooks<HostElement> | null;
el: HostNode | null;
anchor: HostNode | null;
target: HostElement | null;
targetAnchor: HostNode | null;
/* Excluded from this release type: staticCount */
suspense: SuspenseBoundary | null;
/* Excluded from this release type: ssContent */
/* Excluded from this release type: ssFallback */
shapeFlag: number;
patchFlag: number;
/* Excluded from this release type: dynamicProps */
/* Excluded from this release type: dynamicChildren */
appContext: AppContext | null;
/* Excluded from this release type: ctx */
/* Excluded from this release type: memo */
/* Excluded from this release type: isCompatRoot */
/* Excluded from this release type: ce */
}

VNode先不用研究,这里只用知道使用到了这个就行。

接下来要实现渲染HTML元素,就需要一个名为h的函数来调用这个VNode类型来完成。

h函数

示例

1
2
3
4
5
6
7
8
import { defineComponent } from "vue";
const App = defineComponent({
render() {
return h('h2', { style: {color: 'red'} }, '123456');
}
});

createApp(App).mount("#app");
image-20230422230353279

这样一个带有字体为红色样式的h2标签就生成了。

h函数源码分析

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
// h函数导出主体
// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode { // 第二、三个参数可不传
const l = arguments.length
if (l === 2) { // 传入了第二个参数
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // 如果propsOrChildren参数是一个对象而不是一个数组
// single vnode without props:没有props[参数]的单个vnode
if (isVNode(propsOrChildren)) { // 判断是否为VNode,也就是vue中的虚拟DOM
return createVNode(type, null, [propsOrChildren]) // 在第二个参数为对象时,这个第二个参数还是创建VNode的children参数
}
// props without children
return createVNode(type, propsOrChildren) // 如果propsOrChildren不是VNode,则说明这个参数是props,则直接创建
} else { // propsOrChildren是一个数组
// omit props
return createVNode(type, null, propsOrChildren) // propsOrChildren 认定为 children 参数
}
} else {
if (l > 3) { // 传入三个以上的参数
children = Array.prototype.slice.call(arguments, 2) // 将从第三个参数开始后的所有参数转为一个数组赋值给children
} else if (l === 3 && isVNode(children)) { // 正好三个参数,直接放在数组里
children = [children]
}
return createVNode(type, propsOrChildren, children) // 创建VNode
}
}

所以可以明白:vue中的template其实并不是在写HTML,知识在写h函数来创建HTML。

实现创建vue项目的初始效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp, defineComponent, h } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const img =require('./assets/logo.png'); // eslint-disable-line

const App = defineComponent({
render() {
return h('div', { id: 'app' }, [
h('img', {
alt: 'vue logo',
src: img
}),
h(HelloWorld, {
msg: 'Welcome to Your Vue.js + TypeScript App',
age: 12
})
]);
}
});

createApp(App).mount("#app");

上面的代码相当于下面的vue代码:

1
2
3
4
5
6
<template>
<div id="app">
<img src="./assets/logo.png" alt="vue logo">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" :age="12" />
</div>
</template>

最终效果图

image-20230423235956423

小记

由以上已经知道其实h函数在最后执行的还是createVNode函数,所以我们可以直接调用该函数来实现,不过需要注意的是这个函数的第三个参数必须是数组即可。

以上的代码就可以领写为下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp, defineComponent, h, createVNode } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const img =require('./assets/logo.png'); // eslint-disable-line

const App = defineComponent({
render() {
return createVNode('div', { id: 'app' }, [
createVNode('img', {
alt: 'vue logo',
src: img
}),
createVNode(HelloWorld, {
msg: 'Welcome to Your Vue.js + TypeScript App',
age: 12
})
]);
}
});

createApp(App).mount("#app");

:使用JSX编译的代码里面其实是看不到h函数的,只会存在createVNode函数。

因为createVNode函数有一些帮助优化渲染组件的参数。

1
2
3
export declare const createVNode: typeof _createVNode;

declare function _createVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props?: (Data & VNodeProps) | null, children?: unknown, patchFlag?: number, dynamicProps?: string[] | null, isBlockNode?: boolean): VNode;

比如上面的后几位参数:patchFlagdynamicPropsisBlockNode

__END__