# render函数

# 什么是render函数

关于渲染函数render的基础及概念,可参考官方文档 (opens new window)。简单来说,render的核心,就是通过createElement (opens new window)(通常我们简写为h函数)函数来创建vnode,而不是模板的方式。

通常情况下,我们在编写vue组件时,都是基于模板的单文件组件的方式,这种方式结构清晰,简洁易懂,即下面这种:

//demo.vue
<template>
    <div class="my-modal">
        <header class="header">header</header>
        <main class="main">main</main>
        <footer class="footer">footer</footer>
    </div>
</template>
1
2
3
4
5
6
7
8

但有的时候,使用模板文件并不是很方便,可以参考官方的案例 (opens new window)。对比来看,这种情况下,使用render函数来编写要更简洁一些。

# 使用render函数来开发组件

什么时候需要使用render函数呢?

简单来说:当你发现使用.vue模板文件来开发一个组件特别麻烦,或者代码太过于冗余,这个时候就可以考虑使用render函数。

我们先从一个简单的案例出发:
如何将下面的一个**.vue**文件改造为一个用render函数开发的组件。在改造前,建议先看下render函数的基本用法 (opens new window),以及深入数据对象 (opens new window)这2个章节。

<template>
    <div class="my-modal">
        <header class="header">header</header>
        <main class="main">content</main>
        <footer class="footer">footer</footer>
    </div>
</template>
1
2
3
4
5
6
7

使用render函数进行改造(基于h函数):

export default {
	props: {},
  render(h) {
  	let vnode = h('div',{ class: 'my-modal' },[
    	h('header',{ class: 'header' }, ['header']),
      h('main',{ class: 'main' }, ['content']),
      h('footer',{ class: 'footer' }, ['footer']),
    ]);
    return vnode;
  }
}
1
2
3
4
5
6
7
8
9
10
11

我们发现,每一个html标签,我们都需要使用h函数来创建对应的vnode。同时,vnode的children,也需要使用h函数来创建。当层级不多时,这种写法还勉强能接受。
如果层级多,结构复杂,这种时候,这种手写的方式简直无法忍受。为了解决这个问题,Vue官方推出了一个jsx插件 (opens new window)来简化我们频繁使用h函数的方式。官方也给出了对应的demo,参考vue-jsx的使用 (opens new window)。

根据文档,我们将上面的代码进行改造(基于jsx):

export default {
	props: {},
  render(h) {
  	let vnode = (
    	<div class='my-modal'>
      	<div class="header">header</div>
        <div class="main">content</div>
        <div class="footer">footer</div>
      </div>
    );
    return vnode;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这时我们发现,使用jsx来编写,既能保证js的灵活,又保证了同vue模板文件一样清晰的结构。

# 使用jsx开发组件

在我们日常的业务开发中,开发自定义组件时,为了保证组件的灵活性,通常我们会使用slot插槽。接下来,我们将通过一个稍微复杂的案例,来展示如何使用jsx (opens new window)来编写组件,同时学习如何在jsx中使用插槽 (opens new window)。

假设我们需要实现一个Modal组件,包括header、content、footer三个部分。每个部分,用户都可以自定义。使用.vue,组件实现如下:

//modal.vue
<template>
    <div class="my-modal">
        <p>{{ title }} </p>
				<!-- 具名插槽 -->
        <header class="header">
            <slot name="header">
                <p>header</p>
            </slot>
        </header>
        <main class="main">
          	<!-- 默认插槽 -->
            <slot>
                <div class="wrapper">content </div>
            </slot>
        </main>
        <footer class="footer">
          	<!-- 作用域插槽 -->
            <slot name="footer" v-bind:user="infos">
               <p @click="handler">footer:{{ infos }}</p>
            </slot>
        </footer>
    </div>
</template>
<script>
export default {
    name: 'modal',
    inheritAttrs: false,
    props: {
        infos: {
            type: Object,
            required: true
        },
        title: {
            type: String
        }
    },
    methods: {
        handler(){
            this.$emit('footer-click',this.infos)
        }
    }
}
</script>
<style>
.my-modal {
    border: 1px solid  #c9c8c8;
    padding: 5px;
    margin: 2px 0;
}
</style>
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

我们使用jsx来改造:

在使用jsx之前,你需要了解$slot (opens new window)与$scopedSlots (opens new window)的区别。官方文档中已注明,所有的$slots现在都会作为函数暴露在$scopedSlots中,因此为了兼容Vue3的代码,推荐使用$scopedSlots来访问插槽内容。

// modal.js
export default {
    name: 'modal',
    inheritAttrs: false,
    props: {
        infos: {
            type: Object,
            required: true,
        },
        title: {
            type: String
        }
    },
    methods: {
        handler() {
            this.$emit('footer-click', this.infos)
        }
    },
    render(h) {
        let { infos, title, $scopedSlots } = this;
        // header slot,处理当slot为header的插槽不存在时,使用默认的vnode
        let headerVnode = $scopedSlots.header ?
            $scopedSlots.header() : (<p>header</p>);
        // default slot,,处理当默认插槽不存在时,使用默认的vnode
        let mainVnode = $scopedSlots.default ?
            $scopedSlots.default() : (<div class="wrapper"> content </div>);
        let _attrs = {
            on: {
                click: this.handler
            }
        }
		// scopedSlots and params,处理作用域插槽并传递参数
        let footerVnode = $scopedSlots.footer ?
            $scopedSlots.footer({
                user: infos
            }) : (<p {..._attrs }>footer:{ JSON.stringify(infos) }</p>);
        let vnode = (
            <div class="my-modal">
                <p>{ title } </p>
                <header class='header'> { headerVnode } </header>
                <main class='main'>{ mainVnode }</main>
                <footer class='footer'>{footerVnode}</footer>
            </div>
        );
        return vnode;
    },
};
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

最终我们在App.vue中使用这个改造后的modal.jsx组件,并对比modal.vue组件:

// App.vue
<template>
    <div id="app">
        <modal-vue :infos="infos"  title="vue组件" @footer-click="clickHandle">
            <h4 slot="header">header</h4>
            <p slot="default">自定义默认插槽.</p>

            <template v-slot:footer="slotProps">
                <p>自定义作用域插槽:</p>
                <span>name: {{ slotProps.user.name }}</span>
                <span>age: {{ slotProps.user.age }}</span>
            </template>
        </modal-vue>

        <modal-jsx :infos="infos" title="jsx组件" @footer-click="clickHandle">
            <template v-slot:header>
                <h4>header</h4>
            </template>

            <template v-slot:default>
                <p>自定义默认插槽.</p>
            </template>

            <template v-slot:footer="slotProps">
                <p>自定义作用域插槽:</p>
                <span>name: {{ slotProps.user.name }}</span>
                <span>age: {{ slotProps.user.age }}</span>
            </template>
        </modal-jsx>
    </div>
</template>

<script>
import ModalVue from "./components/modal.vue";
import ModalJsx from './components/modal-jsx.js';
export default {
    name: "App",
    data() {
        return {
            infos: {
                name: "zhangsan",
                age: 100,
            },
        };
    },
    components: {
        ModalVue,
        ModalJsx
    },
    methods: {
        clickHandle(val) {
            console.log("foot-click", val);
        },
    }
};
</script>
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

image.png
两种方式创建的modal组件