大白话Vue2.x-render函数

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

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

Vue官方文档栗子

  • 使用 template 创建HTML
<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>
  • 使用 render 函数创建HTML
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

  • Props:
    • name - string, 用于命名插槽。
  • Usage:
    • <slot> 元素作为组件模板之中的内容分发插槽。 <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个部分

  • 第一个参数是HTML 标签字符串(必需的参数)
  • 第二个参数是包含模板相关属性的数据对象(可为空object)
  • 第三个参数HTML的子节点(可为空。是一个createElement的数组或者是简单的使用字符串来生成“文本结点”)
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 函数的双向绑定的方法貌似有两个(可能有更多)

  • 直接借助$parent的方法操作父元素的数据或方法(不赞同使用)
  • 借助父子组件中的$emit来实现, 在render 函数中监听数据的变化并通过$emit向上传递事件

下面是通过$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 中的节点。

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

  • 通过$slots访问(官方栗子)
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函数还是蛮折腾人的,一不留神就报错了(当然也是因为我的技术渣渣啊 (╥╯^╰╥) )

参考

原文链接-疯子的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容