美文网首页
Vue之render函数

Vue之render函数

作者: wenmingxing | 来源:发表于2020-02-09 22:55 被阅读0次

    在很多文章类型的网站中,都区分一级标题、二级标题、三级标题等,为方便分享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 实战》

    相关文章

      网友评论

          本文标题:Vue之render函数

          本文链接:https://www.haomeiwen.com/subject/cqomxhtx.html