Vue之render函数

在很多文章类型的网站中,都区分一级标题、二级标题、三级标题等,为方便分享url,它们都被做成了锚点,点击一下,会将内容加载网址后面,以#分割。

将其封装为组件,一般写法如下:

<!-- 锚点一般写法 -->
<body>
    <div id="app">
        <anchor :level="2" title="特性">特性</anchor>
        <script type="text/x-template" id="anchor">
            <div>
                <h1 v-if="level===1">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h1>
                <h2 v-if="level===2">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h2>
                <h3 v-if="level===3">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h3>
                <h4 v-if="level===4">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h4>
            </div>
        </script>
    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        Vue.component('anchor', {
            template: '#anchor',   //将id赋给template
            props: {
                level: {
                    type: Number,
                    required: true,
                },
                title: {
                    type: String,
                    default: ''
                }
            }
        });

        var app = new Vue({
            el: '#app',
        })
    </script>
</body>  

以上写法没有任何错误,只是缺点明显,代码冗长,组件的template大部分代码都是重复的,只是heading元素的级别不同,在这必须插入一个根元素<div>,这是组件的要求。

事实上,prop: level已经具备了heading级别的含义,所以希望能像拼接字符串的形式构造heading元素,比如"h"+this.level。在render函数中可以这样完成:

<!-- render函数完成锚点 -->
<body>
    <div id="app">
        <anchor :level="2" title="特性">特性</anchor>
    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        Vue.component('anchor', {
            template: '#anchor',   //将id赋给template
            props: {
                level: {
                    type: Number,
                    required: true,
                },
                title: {
                    type: String,
                    default: ''
                }
            },
            render: function(createElement) {
                return createElement(
                    'h' + this.level,
                    [
                        createElement(
                            'a',
                            {
                                domProps: {
                                    href: '#'+this.title
                                }
                            },
                            this.$slots.default
                        )
                    ]
                )
            }
        });

        var app = new Vue({
            el: '#app',
        })
    </script>
</body>

render函数通过createElement参数创建虚拟DOM

1. createElement

createElement函数的参数组成如下:

createElement(
    // {String | Object | Function}
    //一个HTML标签,组件选项,或一个函数
    //必须return上述其中一个
    'div',

    //{Object}
    //一个对应属性的数据对象,可选
    //可以在template中使用
    {
        //后面详细介绍
    },

    //子节点VNodes,可选
    [
        createElement('h1', 'hello world'),
        createElement(MyComponent, {
            props: {
                someProps: 'foo'
            }
        }),
        'bar'
    ]
)  

第一个参数必选,可以是一个HTML标签,也可以是一个组件或函数;第二个参数是可选参数,数据对象,在template中使用。第三个是子节点,也是可选参数。

详细说明第二个参数“数据对象”,其具体选项如下:

{
    //和v-bind:class一样的API  
    'class': {
        foo: true,
        bar: false
    },

    //和v-bind:style一样的API
    style: {
        color: 'red',
        fontSize: '14px'
    },

    //正常的HTML特性
    attrs: {
        id: 'foo'
    },

    //组件props
    props: {
        myProp: 'bar'
    },

    //DOM属性  
    domProps: {
        innerHTML: 'baz'
    },

    //自定义事件监听器"on"
    //不需要如v-on:keyup.enter的修饰器
    //需要手动匹配keyCode
    on: {
        click: this.clickHandler
    },

    //仅对于组件,用于监听原生事件
    //而不是组件使用vm.$emit触发的自定义事件
    nativeOn: {
        click: this.nativeClickHandler 
    },

    //自定义指令
    directives: [
        {
            name: 'my-custom-directive',
            value: '2',
            expression: '1+1',
            arg: 'foo',
            modifiers: {
                bar: true
            }
        }
    ],

    //作用域slot
    //{name: props=>VNode | Array<VNode>}
    scopedSlots: {
        default: props=>h('span', props.text)
    },

    //如果子组件有定义slot的名称
    slot: 'name-of-slot'

    //其他特殊顶层属性
    key: 'myKey',
    ref: 'myRef'
}  

之前在template中,我们都是在组件的标签上使用形容v-bind:classv-bind:stylev-on:click这样的指令,在render函数中都将其卸载数据对象中。

如下面使用传统template写法的组件例子:

<body>
    <div id="app">
        <ele></ele>
    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        Vue.component('ele', {
            template: '\
                <div id="element"\
                :class="{show:show}"\
                @click="handleClick">文本内容</div>',
            
            data: function() {
                return {
                    show: true
                }
            },

            methods: {
                handleClick: function() {
                    console.log('clicked');
                }
            }
        });

        var app = new Vue({
            el: '#app',
        })
    </script>
</body>  

使用render函数的写法如下:

<!-- 使用render函数 -->
<body>
    <div id="app">
        <ele></ele>
    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        Vue.component('ele', {
            render: function(createElement) {
                return createElement(
                    'div',

                    {
                        class: {
                            'show': this.show
                        },

                        attrs: {
                            id: 'element'
                        },

                        on: {
                            click: this.handleClick
                        }
                    },

                    '文本内容'
                )
            },

            data: function() {
                return {
                    show: true
                }
            },

            methods: {
                handleClick: function() {
                    console.log('clicked');
                }
            }
        });

        var app = new Vue({
            el: '#app',
        })
    </script>
</body>  

由上述代码可见,template的写法明显比render写法可读性更强而且简介,所以要在合适的场景下使用render函数,否则会增加负担。

在render函数中,无法使用Vue内置指令,如v-ifv-for等,这些需要用原生JavaScript完成。

2. 函数化组件

Vue提供了一个functional的布尔值选项,设置为true可以使组件无状态和无实例,也就是没有data和this上下文。这样用render函数返回虚拟节点可以更容易渲染,因此函数化组件只是一个函数,渲染开销会很小。

使用函数化组件时,render函数提供了第二个参数context来提供临时上下文。组件需要的data, props, slots, children, parent都时通过上下文来传递的,如this.level需要改写问context.props.levelthis.$slot.default该谢文context.children

例如,下面代码示例用函数化组件展示了一个根据数据智能选择不同组件的场景:

<!-- 函数化组件 -->
<body>
    <div id="app">
        <smart-item :data="data"></smart-item>
        <button @click="change('img')">goto img</button>
        <button @click="change('video')">goto video</button>
        <button @click="change('text')">goto text</button>
    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        //图片组件选项
        var ImgItem = {
            props: ['data'],
            render: function (createElement) {
                return createElement('div', [
                    createElement('p', '图片组件'),
                    createElement('img', {
                        attrs: {
                            arc: this.data.url
                        }
                    })
                ]);
            }
        };

        //视频组件选项
        var VideoItem = {
            props: ['data'],
            render: function(createElement) {
                return createElement('div', [
                    createElement('p', '视频组件'),
                    createElement('vidio', {
                        attrs: {
                            src: this.data.url,
                            controls: 'controls',
                            autoplay: 'autoplay'
                        }
                    })
                ]);
            }
        };

        //文本组件选项
        var TextItem = {
            props: ['data'],
            render: function(createElement) {
                return createElement('div', [
                    createElement('p', '纯文本组件'),
                    createElement('p', this.data.text)
                ]);
            }
        };

        Vue.component('smart-item', {
            //函数化组件
            functional: true,
            render: function (createElement, context) {
                //根据传入的数据,智能判断显示哪种组件
                function getComponent() {
                    var data = context.props.data;
                    //判断prop:data的type字段是属于那种类型的组件
                    if (data.type === 'img') return ImgItem;
                    if (data.type === 'video') return VideoItem;
                    return TextItem;
                }
                return createElement(
                    getComponent(),
                    {
                        props: {
                            //把smart-item的prop:data传给上面智能选择的组件
                            data: context.props.data
                        }
                    },
                    context.children
                )
            },

            props: {
                data: {
                    type: Object,
                    required: true
                }
            }
        });

        var app = new Vue({
            el: '#app',

            data: {
                data: {}
            },

            methods: {
                //切换不同类型组件的主句
                change: function(type) {
                    if (type === 'img') {
                        this.data = {
                            type: 'img',
                            url: 'https://raw.githubusercontent.com/iview/iview/master/assets/logo.png'
                        }
                    } else if (type === 'video') {
                        this.data = {
                            type: 'video',
                            url: 'http://vjs.zencdn.net/v/oceans.mp4'
                        }
                    } else if (type === 'text') {
                        this.data = {
                            type: 'text',
                            content: '这是一段纯文本'
                        }
                    }
                }
            },

            created: function() {
                //初始化时,默认设置图片组件的主句
                this.change('img');
            }
        })
    </script>
</body>  

函数化组件在业务中并不是很常见,而且也有其他类似的方法来实现,比如上例可以用组件的is特性来动态挂载。总结起来,函数化组件主要适用于以下两个场景:

  • 程序化的在多个组件中选择一个;
  • 在将children,props,data传递给子组件之前操作它们。

3. JSX

使用render函数不友好的地方在于模板比较简单时,写起来很复杂,难以阅读出DOM结构,尤其是在子节点嵌套较多时,例如用template书写的模板为:

<Anchor :level="1">
    <span>一级</span>标题
</Anchor>

使用createElement改写后为:

return createElement('Anchor', {
    props: {
        level: 1
    }
}, [
    createElement('span', '一级'),
    '标题'
]);

为了让render函数更好的书写和阅读,Vue提供了插件babel-plugin-transform-vue-jsx来支持JSX语法。

JSX是一种看起来像HTML,但实际上是JavaScript的语法扩展,它更接近DOM结构的形式来描述一个组件的UI和状态信息。

上述代码用JSX改写后为:

new Vue({
    el: '#app',
    render(h) {
        return (
            <Anchor level={1}>
                <span>一级</span>标题
            </Anchor>
        )
    }
})  

上面代码无法直接运行,需要在webpack中配置插件编译后才可以。

参考

  1. 《Vue.js 实战》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值