vue-wangyawei让前端飞前端开发那些事

大白话Vue2.x-render函数

2017-07-07  本文已影响249人  锋子锅锅

Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。

这是引用Vue官方文档的一句话,确实在很多情况下我们都需要掌握 render 函数,它更加灵活多变,但也是很容易写错(经常报错的我 o(////▽////)q ),在这里记录下自己学习render的笔记。

Vue官方文档栗子

<script type="text/x-template" id="anchored-heading-template">
  <div>
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-if="level === 6">
      <slot></slot>
    </h6>
  </div>
</script>
Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name 标签名称
      this.$slots.default // 子组件中的阵列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

从官方的栗子上看,很明显的可以看出在某些情况下,render 函数比 template 有无法比拟的优越性。在一些结构复杂或者逻辑较多的html中,建议还是使用 render 函数来创建比较好。

必须了解的知识

slot

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草案,使用特殊的 <slot> 元素作为原始内容的插槽。

slot 是在编写组件的时候最经常使用的,它的作用是什么呢?浅显的说就是占位,或者官方文档所说的插槽。就好像一个电脑主板,有很多插槽,你可以选择使用或者不使用,你也可以决定自己使用什么型号和品牌,slot 就是起到这样一个插槽的作用。

官方栗子

<!--假定 <my-component> 组件有下面模板:-->
<div>
  <h2>我是子组件的标题</h2>
  <slot>
    只有在没有要分发的内容时才会显示。
  </slot>
</div>

<!--父组件模版:-->
<div>
  <h1>我是父组件的标题</h1>
  <my-component>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </my-component>
</div>

<!--渲染结果:-->
<div>
  <h1>我是父组件的标题</h1>
  <div>
    <h2>我是子组件的标题</h2>
    <p>这是一些初始内容</p>
    <p>这是更多的初始内容</p>
  </div>
</div>

slot元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。仍然可以有一个匿名 slot,它是默认 slot,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot,这些找不到匹配的内容片段将被抛弃。

打个比方,例如电脑主板上有一个内存条插槽,一个独立显卡插槽,你可以只使用安装一根内存条,而不使用显卡插槽,这是完全没有任何影响的。

<!--假定我们有一个 app-layout 组件,它的模板为:-->

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!--父组件模版:-->

<app-layout>
  <h1 slot="header">这里可能是一个页面标题</h1>
  <p>主要内容的一个段落。</p>
  <p>另一个主要段落。</p>
  <p slot="footer">这里有一些联系信息</p>
</app-layout>

<!--渲染结果为:-->

<div class="container">
  <header>
    <h1>这里可能是一个页面标题</h1>
  </header>
  <main>
    <p>主要内容的一个段落。</p>
    <p>另一个主要段落。</p>
  </main>
  <footer>
    <p>这里有一些联系信息</p>
  </footer>
</div>

createElement 参数

createElement的参数包括3个部分

createElement(
  // {String | Object | Function}
  // 一个 HTML 标签字符串,组件选项对象,或者一个返回值类型为String/Object的函数,必要参数
  'div',
  {
     // {Object}
    // 一个包含模板相关属性的数据对象
    // 这样,您可以在 template 中使用这些属性.可选参数.
  },
  [
    // {String | Array}
    // 子节点 (VNodes),由 `createElement()` 构建而成,
    // 或简单的使用字符串来生成“文本结点”。可选参数。
  ]
)

完整参数如下

createElement(
  // {String | Object | Function}
  // 一个 HTML 标签字符串 ,组件选项对象,或者一个返回值类型为String/Object的函数,必要参数
  'div',

  // {Object}
  // 一个包含模板相关属性的数据对象
  // 这样,您可以在 template 中使用这些属性.可选参数.
  //这些参数就好比你在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
    },
    // 自定义指令. 注意事项:不能对绑定的旧值设值
    // Vue 会为您持续追踪
    directives: [
        {
            name: 'my-custom-directive',
            value: '2',
            expression: '1 + 1',
            arg: 'foo',
            modifiers: {
                bar: true
            }
        }
    ],
    // Scoped slots in the form of
    // { name: props => VNode | Array<VNode> }
    scopedSlots: {
        default: props => createElement('span', props.text)
    },
    // 如果组件是其他组件的子组件,需为 slot 指定名称
    slot: 'name-of-slot',
    // 其他特殊顶层属性
    key: 'myKey',
    ref: 'myRef'
  },
  // {String | Array}
  // 子节点 (VNodes),由 `createElement()` 构建而成,
  // 或简单的使用字符串来生成“文本结点”。可选参数。
  [
    '先写一些文字', //文本节点
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

createElement中的事件监听(on)可以使用事件 & 按键修饰符

实际开发

在平时开发中,我们更常见的是render: h => h(App)这种形式。其实这只是将h作为createElement的别名是一个通用惯例

简单的栗子

常见的写法


Vue.component('mySelect', {
    render: (h) => {  
        // 父节点 select  
        return h('select',
            //select的相关属性的数据对象
            {
                on:{
                    'change':(val)=>{
                        
                    }
                }
            },
            //两个子节点option
            [
                h('option', 
                    //option的相关属性的数据对象
                    {
                        domProps: {
                            value: 0,                            
                            innerHTML: '男'
                        }
                    }
                    //没有子节点,所以第三个参数为空
                ),
                h('option', {
                    domProps: {
                        value: 1,                        
                        innerHTML: '女'
                    },
                })
            ]
        )
})  

//mySelect的渲染结果
<select>
  <option value ="0">男</option>
  <option value ="1">女</option>
</select>

v-for 循环

当然,作为组件肯定不会把内容都写死,需要根据参数来决定渲染结果,那么下来看一个相当于V-for的render。 render 函数循环创建需要借助map函数,在这里需要注意的是,只用map函数才能检测数据变化,其他方法不能根据数据的改变而改变

Vue.component('myNewSelect', {
    // props 
    props:{  
        items:{  
            type:Array,  
            default: []
        }
    },
    render: (h) => {
        // 父节点 select  
        return h('select',
            Array.apply(null, this.items.map(function(item){  
                return h('option',
                    {  
                        domProps: {
                            value: item.value,                            
                            innerHTML: item.name
                        },
                    })  
                })
            )
        )
    }
})

//假设
items = [
    {
        value: 0,
        name: '第一个'
    },
    {
        value: 1,
        name: '第二个'
    },
    {
        value: 2,
        name: '第三个'
    },
]

//<myNewSelect :items=items></myNewSelect>的渲染结果
<select>
  <option value ="0">第一个</option>
  <option value ="1">第二个</option>
  <option value ="3">第三个</option>
</select>

v-if 判断

下面是 v-if 的 render 函数,要实现v-if的效果其实也很简单,只需要在return前判断下。

Vue.component('myList', {
    props:{  
        items:{  
            type:Array,  
            default: []
        }
    },
    render: (h) => {
        if(items.length > 0){
            return h('ul',
                Array.apply(null, this.items.map(function(item){  
                    return h('li',
                        {  
                            domProps: {
                                innerHTML: item.name
                            },
                        })  
                    })
                )
            )
        }else{
            return h('p', '没有数据')
        }
    }
})

v-model 双向绑定

render 函数的双向绑定的方法貌似有两个(可能有更多)

下面是通过$emit实现双向绑定的栗子

Vue.component('myInput', {
    props:{  
        value:{  
            type: String,  
            default: ''
        }
    },
    render: (h) => {
        let _this = this;
        return h('input',{
            domProps:{  
                value:_this.value 
            },
            on: {
                input: function (event) {
                    _this.change(event.target.value);
                }
            }
        })
    },
    methods:{  
        change(value){  
            this.$emit('valueChange', value);
        }  
    }  
})

父组件

<myInput :value=value></myInput>

var app=new Vue({  
    el:'#app',  
    data:{  
        value: 'default'
    },  
    methods:{  
        valueChange(value){  
            this.value = value;
        }  
    }  
})  

slot 内容分发

render函数中使用slot其实跟template差不多,只需要在h()函数里为 slot 指定名称即可,更直接的直接将slot的内容拿来渲染。
在render函数中,可以通过访问$slots来获取所有的slot

vm.$slots 用来访问被 slot 分发的内容。每个具名 slot 有其相应的属性(例如:slot="foo" 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名 slot 中的节点。

Vue.component('mySlot', {
    render: (h) => {
        let _this = this;
        return h('div',{
             slot: 'name-of-slot'
        })
    }
})

Vue.component('blog-post', {
  render: function (createElement) {
    var header = this.$slots.header
    var body   = this.$slots.default
    var footer = this.$slots.footer
    return createElement('div', [
      createElement('header', header),
      createElement('main', body),
      createElement('footer', footer)
    ])
  }
})

最后

没有什么特别的需求的话,还是别折腾render函数吧,老老实实用template,render函数还是蛮折腾人的,一不留神就报错了(当然也是因为我的技术渣渣啊 (╥╯^╰╥) )

参考

原文链接-疯子的博客

上一篇下一篇

猜你喜欢

热点阅读