美文网首页
9.Render函数

9.Render函数

作者: NoNothing | 来源:发表于2020-05-31 22:21 被阅读0次

    1.场景:在很多文章类型的网站中(比如文档、博客)都有区分一级标题、二级标题、三级标题……为方便分享url,它们都做成了锚点,点击一下,会将内容加在网址后面,以“#”分割。比如“特性” 是一个<h2>标签,内容含有一个<a href="#特性">#</a>的链接,点击后,url就带有了锚点信息,别人打开时,会直接聚焦到“特性”所在的位置。
    如果把它封装为一个组件,一般写法可能是:

    <div id="app">
        <anchor :level="3" 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>
                <h5 v-if="level===5">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h5>
                <h6 v-if="level===6">
                    <a :href="'#'+title">
                        <slot></slot>
                    </a>
                </h6>
            </div>
        </script>
    </div>
    <script>    
        Vue.component('anchor',{
            template:'#anchor',
            props:{
                level:{
                    type:Number,
                    required:true
                },
                title:{
                    type:String,
                    default:''
                }
            }
        });
    
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    这样写没有任何错误,只是缺点明显,代码冗长,组件的template大部分代码是重复的,只是heading元素的级别不同,再者必须插入一个根元素<div>,这是组件的要求。
    template写法在大多数时候是很好用的,但到了这里使用起来就很别扭。事实上,prop:level 已经具备了heading级别的含义,我们更希望能像拼接字符串的形式来构造heading元素,比如“h”+this.level。在Render函数改写后的代码:

    <div id="app">
        <anchor :level="3" title="特性">特性</anchor>
    </div>
    
    
    <script>    
        Vue.component('anchor',{
            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>
    

    Render 函数通过 createElement 参数来创建Virtual Dom,结构精简了很多。在第7章组件中介绍slot时,有提到过访问slot的用法,使用场景就是在Render函数。Render函数所有神奇的地方都在这个createElement里。
    2.createElement基本参数:

    createElement(
            //{String | Object | Function}
            // 一个HTML标签,组件选项,或一个函数
            // 必须Return 上述其中一个
            'div',
            // {Object}
            // 一个对应属性的数据对象,可选
            // 您可以在template中使用
            {
                //稍后介绍
            },
            //{String | Array}
            // 子节点(VNode),可选
            [
                createElement('h1','hello world'),
                createElement(MyComponent,{
                    props:{
                        someProp:'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>}
                scopeSlots:{
                    default:props => h('span',props.text)
                },
                //如果子组件有定义slot的名称
                slot:'name-of-slot',
                //其他特殊顶层属性
                key:'myKey',
                ref:'myRef'
            }
    

    以往在template里,我们都是在组件的标签上使用形如 v-bind:class、v-bind:style、v-on:click这样的指令,在Render函数都将其写在了数据对下里,比如下面的组件,使用传统的template写法是:

    <div id="app">
        <ele></ele>
    </div>
    
    
    <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>
    

    使用Render改写后的代码如下:

        Vue.component('ele',{
            render:function(createElement){
                return createElement(
                    'div',
                    {
                        //动态绑定class,同:class
                        class:{
                            'show':this.show
                        },
                        //普通html特性
                        attrs:{
                            id:'element'
                        },
                        //给div绑定click事件
                        on:{
                            click:this.handleClick
                        }
                    },
                    '文本内容'
                )
            },
            data:function(){
                return {
                    show:true
                }
            },
            methods:{
                handleClick:function(){
                    console.log('clicked')
                }
            }
        });
    
        var app = new Vue({
            el:'#app'
        })
    

    就此例而言,template的写法明显比Render写法要可读而且简洁,所以要在合适的场景使用Render函数,否则只会增加负担。
    3.约束。所有的组件树中,如果VNode是组件或含有组件的slot,那么VNode必须唯一。以下错误示例:

    // 示例一:重复使用组件
    <div id="app">
        <ele></ele>
    </div>
    <script>    
        //局部声明组件
        var Child = {
            render:function(createElement){
                return createElement('p','text')
            }
        };
        Vue.component('ele',{
            render :function(createElement){
                //创建一个子节点,使用组件child
                var ChildNode = createElement(Child);
                return createElement('div',[
                    ChildNode,ChildNode
                ]);
            }
        });
        var app = new Vue({
            el:'#app'
        })
    </script>
    
    //示例二:重复使用含有组件的slot
    <div id="app">
        <ele>
            <div>
                <Child></Child>
            </div>
        </ele>
    </div>
    
    <script>    
        //全局注册组件
        Vue.component('Child',{
            render :function(createElement){
                return createElement('p','text')
            }
        });
        Vue.component('ele',{
            redner:function(createElement){
                return createElement(
                    'div',
                    [
                        this.$slots.default,
                        this.$slots.default
                    ]
                )
            }
        })
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    这两个示例都期望在子节点内渲染出两个Child组件,也就是两个<p>text</p> 节点,实际预览时只渲染出了一个,因为在这种情况下,VNode受到了约束。
    对于重复渲染多个组件(或元素)的方法有很多,示例:

    <div id="app">
        <ele>
        </ele>
    </div>
    
    <script>    
        //局部声明组件
        var Child = {
            render:function(createElement){
                return createElement('p','text')
            }
        };
        Vue.component('ele',{
            render :function(createElement){
                return createElement(
                        'div',
                        Array.apply(null,{length:5}).map(function(){
                            return createElement(Child)
                        })
    
                    )
            }
        });
    
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    上例通过一个循环和工厂函数就可以渲染5个重复的子组件Child。对于含有组件的slot,复用就要稍微复杂一点了,须要将slot的每个子节点都克隆一份。示例:

    <div id="app">
        <ele>
            <div>
                <Child></Child>
            </div>
        </ele>
    </div>
    
    <script>    
        //全局注册组件
        Vue.component('Child',{
            render:function(createElement){
                return createElement('p','text')
            }
        });
        Vue.component('ele',{
            render :function(createElement){
                //克隆slot节点的方法
                function cloneVNode(vnode){
                    //递归遍历所有子节点,并克隆
                    const cloneChildren = vnode.children &&
                    vnode.children.map(function(vnode){
                        return cloneVNode(vnode);
                    });
                    const cloned = createElement(
                        vnode.tag,
                        vnode.data,
                        cloneChildren
                    );
                    cloned.text = vnode.text;
                    cloned.isComment = vnode.isComment ;
                    cloned.componentOptions = vnode.componentOptions;
                    cloned.elm = vnode.elm;
                    cloned.context = vnode.context;
                    cloned.ns = vnode.ns;
                    cloned.isStatic = vnode.isStatic;
                    cloned.key = vnode.key;
    
                    return cloned;
                }
                const vNodes = this.$slots.default;
                const clonedVNodes = vNodes.map(function(vnode){
                    return cloneVNode(vnode)
                });
                return createElement('div',[
                    vNodes,
                    clonedVNodes
                ])
            }
        });
    
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    在Render函数里创建了一个cloneVNode的工厂函数,通过递归将slot所有子节点都克隆了一份,并对VNode的关键属性也进行复制。深度克隆slot的做法有点偏黑科技,不过在一般业务中几乎不会遇到这样的需求,主要还是运用在独立组件中。
    4.在Render函数中,不再需要Vue内置的指令,比如v-if、v-for,当然,也没办法使用它们。无论要实现什么功能,都可以用原生JavaScript。比如v-if、v-else可以这样写:

    <div id="app">
        <ele :show="show"></ele>
        <button @click="show= !show">切换 show</button>
    </div>
    
    <script>    
        Vue.component('ele',{
            render:function(createElement){
                if(this.show){
                    return createElement('p','show的值为true')
                }else{
                    return createElement('p','show的值为false')
                }
            },
            props:{
                show:{
                    type:Boolean,
                    default:false
                }
            }
        })
    
        var app = new Vue({
            el:'#app',
            data:{
                show:false
            }
        })
    </script>
    

    上例直接使用了JavaScript的if和else语句来完成逻辑判断。对于v-for,可以用一个简单的for循环来实现:

    <div id="app">
        <ele :list="list"></ele>
    </div>
    
    <script>    
        Vue.component('ele',{
            render:function(createElement){
                var nodes = [];
                for(var i=0;i<this.list.length;i++){
                    nodes.push(createElement('p',this.list[i]))
                }
                return createElement('div',nodes)
            },
            props:{
                list:{
                    type:Array
                }
            }
        })
    
        var app = new Vue({
            el:'#app',
            data:{
                list:[
                    '《Vue.js实战》',
                    '《JavaScript 高级程序设计》',
                    '《JavaScript 语言精粹》'
                ]
            }
        })
    </script>
    

    在一开始接触Render写法时,可能会有点不适应,毕竟这种用createElement 创建DOM节点的方法不够直观和可读,而且受Vue内置指令的影响,有时会绕不过弯。不过只要把它当作JavaScript一个普通的函数来使用,写习惯后就没有那么难理解了,说到底,它只是JavaScript而已。下例展示了JavaScript的if、else语句和数组map方法充分配合使用来渲染一个列表:

    <div id="app">
        <ele :list="list"></ele>
        <button @click="handleClick">显示列表</button>
    </div>
    
    <script>    
        Vue.component('ele',{
            render:function(createElement){
                if(this.list.length){
                    return createElement('ul',this.list.map(function(item){
                        return createElement('li',item)
                    }));
                }else{
                    return createElement('p','列表为空')
                }
            },
            props:{
                list:{
                    type:Array,
                    default:function(){
                        return []
                    }
                }
            }
        })
    
        var app = new Vue({
            el:'#app',
            data:{
                list:[]
            },
            methods:{
                handleClick:function(){
                    this.list = [
                        '《Vue.js实战》',
                        '《JavaScript 高级程序设计》',
                        '《JavaScript 语言精粹》'
                    ]
                } 
            }
        })
    </script>
    

    首先是判断prop:list是否为空,如果是空,就渲染一个“列表为空”的<p>元素;如果不为空数组,那就把每一项作为<li>渲染,放在<ul>下。
    5.map()方法是快速改变数组结构,返回了一个新数组。如果你不熟悉数组的这种链式操作(map常和filter,sort等方法一起使用,因为它们返回的都是新数组),可以使用简单的for循环,这样更容易理解。
    上例的Render函数对应的template写法如下:

    <ul v-if="list.length">
        <li v-for="item in list"> {{ item }} </li>
    </ul>
    <p v-else>列表为空</p>
    

    6.Render函数里也没有与v-model对应的API,需要自己来实现逻辑。示例:

    <div id="app">
        <ele></ele>
    </div>
    <script>    
        Vue.component('ele',{
            render:function(createElement){
                var _this = this;
                return createElement('div',[
                    createElement('input',{
                        domProps:{
                            value:this.value
                        },
                        on:{
                            input:function(event){
                                _this.value = event.target.value;
                            }
                        }
                    }),
                    createElement('p','value:'+this.value)
                ])
            },
            data:function(){
                return {
                    value:''
                }
            }
        })
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    事实上,v-model就是prop:value 和 event:input 组合使用的一个语法糖,虽然在Render里写起来比较复杂,但是可以自由控制,深入到更底层。上例的Render函数对应的template写法:

    <div>
        <input v-model="value">
        <p>value: {{ value }} </p>
    </div>
    

    对于事件修饰符和按键修饰符,基本也需要自己实现:

    修饰符 对应的句柄
    .stop event.stopPropagation()
    .prevent event.preventDefault()
    .self if(event.target !== event.currentTarget) return
    .enter、.13 if(event.keyCode !== 13) return 替换13位需要的 keyCode
    .ctrl、.alt、.shift、.meta if(!event.ctrlKey) return 根据需要替换 ctrlKey 为 altKey、shiftKey 或 metaKey

    对于事件修饰符.capture 和 .once ,Vue提供了特殊的前缀,可以直接写在on的配置里,如下表:

    修饰符 前缀
    .capture !
    .once ~
    .capture.once 或 .once.capture ~!

    写法如下:

    on:{
        '!click':this.doThisInCapturingMode,
        '~keyup':this.doThisOnce,
        '~!mouseover':this.doThisOnceInCapturingMode
    }
    

    例如,下面的示例简单模拟了聊天发送内容的场景。代码:

    <script>    
        Vue.component('ele',{
            render:function(createElement){
                var _this = this;
                //渲染聊天内容列表
                if(this.list.length){
                    var listNode = createElement('url',this.list.map(function(item){
                        return createElement('li',item);
                    }));
                }else{
                    var listNode = createElement('p','暂未聊天内容');
                }
                return createElement('div',[
                    listNode,
                    createElement('input',{
                        attrs:{
                            placeholder:'输入内容,按回车键发送'
                        },
                        style:{
                            width:'200px'
                        },
                        on:{
                            keyup:function(event){
                                //如果不是回车键,不发送数据
                                if(event.keyCode !== 13){
                                    return ;
                                }
                                //添加输入的内容到聊天列表
                                _this.list.push(event.target.value);
                                // 发送后,清空输入框
                                event.target.value = '';
                            }
                        }
                    })
                ])
            },
            data:function(){
                return {
                    value:'',
                    list:[]
                }
            }        
        });
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    7.对于slot,我们已经介绍过可以用this.$slots来访问,在Render函数中会大量使用,不过没有使用slot时,会显示一个默认的内容,这部分逻辑要我们自己实现。示例代码:

    <div id="app">
        <ele></ele>
        <ele>
            <p>slot 的内容</p>
        </ele>
    </div>
    
    <script>    
        Vue.component('ele',{
            render:function(createElement){
                if(this.$slots.default === undefined){
                    return createElement ('div','没有使用slot时显示的文本')
                } else{
                    return createElement('div',this.$slots.default)
                }
            }
        });
    
        var app = new Vue({
            el:'#app'
        })
    </script>
    

    this.$slots.default 等于 undefined,就说明父组件中没有定义slot,这时可以自定义显示的内容。
    8.函数化组件。Vue.js提供了一个functional的布尔值选项,设置为true可以使组件无状态和无实例,也就是没有data 和 this上下文。这样用render函数返回虚拟节点可以更容易渲染,因为函数化组件只是一个函数,渲染开销要小很多。
    使用函数化组件时,Render函数提供了第二个参数context来提供临时上下文。组件需要的data、props、slots、children、parent都是通过这个上下文来传递的,比如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children。
    例如,下面的示例用函数化组件展示了一个根据数据智能选择不同组件的场景:

    <div id="app">
        <smart-item :data="data"></smart-item>
        <button @click="change('img')">切换为图片组件</button>
        <button @click="change('video')">切换为视频组件</button>
        <button @click="change('text')">切换为文本组件</button>
    </div>
    
    <script>    
        //图片组件选项
        var ImgItem = {
            props:['data'],
            render:function (createElement){
                return createElement('div',[
                    createElement('p','图片组件'),
                    createElement('img',{
                        attrs:{
                            src:this.data.url
                        }
                    })
                ]);
            }
        };
        
        //视频组件选项
        var VideoItem = {
            props:['data'],
            render: function(createElement){
                return createElement('div',[
                    createElement('p','视频组件'),
                    createElement('video',{
                        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',
                            context:'这是一段纯文本'
                        }
                    }
                }
            },            
            created:function(){
                //初始化时,默认设置图片组件的数据
                this.change('img');
            }
        })
    </script>
    

    代码片段比较长,逐步分析一下实现的内容。ImgItem、VideoItem、TextItem这3个对象分别是图片组件、视频组件和纯文本组件的选项,它们都接收一个prop:data。在函数化组件smart-item里,也有props:data,通过getComponent函数来判断其字段type的值,选择这条数据适合渲染的组件。通过createElement把getComponent() 返回的对象设置为第一个参数,然后通过第二个参数把smart-item的data传递到选择的组件里的prop:data,组件渲染出不同的内容。
    根实例app中的方法change用来生成不同的数据,通过3个button来切换。
    该示例难理解的地方在于smart-item和3个功能组件都有prop:data,它们的传递顺序和原理看起来比较含糊。
    函数化组件在业务中并不是很常用,而且也有其他类似的方法来实现,比如上例也可以用组件的is特性来动态挂载。总结起来,函数化组件主要适用于以下两个场景:

    • 程序化地在多个组件中选择一个。
    • 在将children,props,data传递给子组件之前操作它们。
      9.JSX。使用Render函数最不友好的地方就是在模版比较简单时,写起来也很复杂,而且难以阅读出DOM结构,尤其当子节点嵌套比较多时,嵌套的createElement就像盖楼一样一层层延伸下去。举例:
    //使用template书写的模板是:
    <Anchor :level="1">
        <span>一级</span>标题
    </Anchor>
    //使用createElement改写后应该是:
        return createElement('Anchor',{
            props:{
                level:1
            }
        },[
            createElement('span','一级'),'标题'
        ]);
    

    为了让Render函数更好地书写和阅读,Vue.js提供了插件 babel-plugin-transform-vue-jsx来支持JSX语法。
    JSX是一种看起来像HTML,但实际是JavaScript的语法扩展,它用更接近DOM结构的形式来描述一个组件的UI和状态信息,最早在React.js里大量应用。
    比如上面的Render用JSX改写后的代码是:

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

    上面的代码无法直接运行,需要在webpack里配置插件 babel-plugin-transform-vue-jsx 编译后才可以,后面章节会介绍到webpack的用法。
    这里的render 使用了ES2015的语法缩写了函数,也会在后面的章节提到。参数h不能省略,否则使用时会触发错误。
    使用createElement时,常用的配置示例如下:

    render(createElement){
                return createElement('',{
                    props:{
                        text:'some text'
                    },
                    attrs:{
                        id:'myDiv'
                    },
                    domProps:{
                        innerHTML:'content'
                    },
                    on:{
                        change:this.changeHandler
                    },
                    nativeOn:{
                        click:this.clickHandler
                    },
                    class:{
                        show:true,
                        on:false
                    },
                    style:{
                        color:'#fff',
                        background:'#f50'
                    },
                    key:'key',
                    ref:'element',
                    refInFor:ture,
                    slot:'slot'
                })
            }
    

    上面的示例使用JSX后等同于下面的代码:

    render(h){
            return (
                <div id="myDiv"
                    comPropsInnerHTML="content"
                    onChange={this.changeHandler}
                    nativeOnClick={this.clickHandler}
                    class={{show:true,on:false}}
                    style={{color:'#fff',background:'#f50'}}
                    key="key"
                    ref="element"
                    refInFor
                    slot="slot"></div>
            )
        }
    

    JSX仍然是JavaScript而不是DOM,如果你的团队不是JSX强驱动的,建议还是以模板template的方式为主,特殊场景(比如锚点标题)使用Render的createElement辅助完成。

    相关文章

      网友评论

          本文标题:9.Render函数

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