深度剖析render函数、函数式组件与JSX之间的爱恨情仇

2,571 阅读12分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

  往期精彩回顾(以下三篇都是自己精心总结的文章,有兴趣的朋友可以传送过去看看):

  无论如何,你都必须得掌握的JS知识

  传送门——金石计划一期之面试不面试,你都必须得掌握的vue知识

  传送门——金石计划一期之我的css世界

前言

  大家好,我是前端贰货道士。最近在强烈的求知欲掘金金石计划的'双重激励'下(手动滑稽.gif),我对vue2render函数做了一个小总结,诞生了这篇文章。本文由vue2的render函数、JSX函数式组件三部分构成。函数式组件那章关于children属性和slots属性的差别,是我在敲代码的过程中无意发现的。因为是自己的理解,所以可能会存在问题。如果大家对那块有自己独到的见解,也欢迎在评论区中留言。最近项目没那么忙,但好像也一直抽不出时间...所整理的笔记有很大一部分都是我下班之后抽空整理的。如果本文对您有帮助,烦请大家一键三连哦, 蟹蟹大家~

render函数

1. render函数template模板之间不得不说的故事:

  render函数template模板虽然都能创建html模板, 但我们平时在vue文件中写的html模板,其实会在beforeMount生命周期的钩子函数中,经过编译生成render函数,因此两者主要区别如下:

  • template模板理解起来相对容易,但自定义render函数灵活性高,但是写起来会更为麻烦;
  • 由于render函数省去了编译步骤,因此性能更高,优先级也会更高;

特别注意:

  • template模板不可与render函数一起使用。两者同时使用时,会以template模板为准; (注:我不清楚为什么网上都说render函数具有更高的优先级,可能都是复制来复制去的。如果有大佬知道,欢迎在评论区指出)
  • render函数也可以用在函数式组件中,详见下文函数式组件,此时render函数会多出一个上下文参数的形参。切记:如果未在js中(非render函数内部)添加functional: true, render函数只会有一个参数;反之如果有添加,就会变形成函数式组件,此时render函数会有两个参数
  • 一个vue文件至少需要保留一个template模板或者render函数,或为文件自带,或为混入、继承等方式。如果这两者都没有,则vue文件会报错。

image.png

2. render函数createElement参数:

return createElement(, {}, [])
  • 第一个参数(必填):主要是用于提供dom中的html内容,类型可以是字符串、对象或函数
  • 第二个参数(可选):用于设置这个dom中的一些样式、属性、传的组件的参数、绑定事件之类
  • 第三个参数(可选,类型是数组/字符串。可以使用字符串类型生成虚拟文本节点,也可以使用数组类型递归创建虚拟dom节点): 代表子节点VNode
`传送门(参考官网):https://v2.cn.vuejs.org/v2/guide/render-function.html`

`第二个参数:`
{  
    // 与 `v-bind:class` 的 API 相同,  
    // 接受一个字符串、对象或字符串和对象组成的数组
    class: {  
        foo: true,  
        bar: false  
    },  
    
    // 与 `v-bind:style` 的 API 相同,  
    // 接受一个字符串、对象,或对象组成的数组 
    // 注意style中的样式需要用驼峰命名
    style: {  
        color: 'red',  
        fontSize: '14px'  
    },  
    
    // 普通的 HTML attribute  
    attrs: {  
        id: 'foo'  
    },  
    
    // 组件 prop 
    // (特别注意) 区别于vue文件中的props,此处的props是向定义的render组件绑定对应的props,是写死的
    // 比如可以使用这种方式使用el-button组件的某些props,比如size和type等之类
    // 而在vue文件中定义的props,是可以动态调用的
    props: {  
        myProp: 'bar'  
    },  
    
    // DOM property
    `innerText不识别html标签,非标准`
    `innerHTML识别html标签,W3C标准`
    
    domProps: {
        innerText: 'baz',
        innerHTML: 'baz'
    },  
    
    // 事件监听器在 `on` 内,  
    // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。  
    // 需要在处理函数中手动检查 keyCode。  
    on: {  
        click: this.clickHandler  
    },  
    
    // 仅用于组件,用于监听原生事件,而不是组件内部使用  
    // `vm.$emit` 触发的事件。  
    // 相当于监听自定义组件的某些原生事件
    nativeOn: {  
        click: this.nativeClickHandler  
    },  
    
    // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`  
    // 赋值,因为 Vue 已经自动为你进行了同步。  
     directives: [  
        {  
            name: 'my-custom-directive',  
            value: '2',  
            expression: '1 + 1',  
            arg: 'foo',  
            modifiers: {  
                bar: true  
            }  
        }  
    ],  
    
    // 作用域插槽的格式为  
    // { name: props => VNode | Array<VNode> }  
    scopedSlots: {  
        default: props => createElement('span', props.text)  
    },  
    
    // 如果组件是其它组件的子组件,需为插槽指定名称  
    slot: 'name-of-slot', 
    
    // 其它特殊顶层 property  
    key: 'myKey',  
    
    ref: 'myRef',  
    
    // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,  
    // 那么 `$refs.myRef` 会变成一个数组。  
    refInFor: true  
}

3. vue2常用语法与render函数之间的转换:

(1) v-ifv-for

// vue文件
// render函数实现方式
// v-if借助原生js的if语句来判断
// v-for借助原生js的map方法来实现
 
<script>
export default {
  data() {
    return {
      list: [{ name: 'fl' }, { name: 'yyl' }, { name: 'lgy' }]
    }
  },
  
  render(h) {
    // 对于render函数中的createElement参数中的第二个参数的class,其实也可以换成staticClass,效果一样
    // 即 this.list.map((item) => h('div', { staticClass: 'mt10', key: item.name }, item.name))
    
    if (this.list.length)
      return h(
        'div',
        this.list.map((item) => h('div', { class: 'mt10', key: item.name }, item.name))
      )
    return h('div', 'No items found')
  }
}
</script>


<style lang="scss" scoped>
.mt10 {
  margin-top: 10px;
}
</style>
// 上述的render函数写法等价于下面的html模板

<template>
  <div v-if="list.length">
    <div class="mt10" v-for="{ name } in list" :key="name">
      {{ name }}
    </div>
  </div>
  <div v-else> No items found </div>
</template>

(2) v-model双向绑定

a. el-input的双向绑定

// vue文件
// el-input的双向绑定
// render函数实现方式

<script>
export default {
  data() {
    return {
      value: ''
    }
  },
  
  render(h) {
    return h('el-input', {
      props: {
        size: 'small',
        // 之所以把value放在props中,是因为el-input的v-model的绑定值是value,也就是默认为:value
        value: this.value
      },
      
      style: {
        width: '240px'
      },
      
      on: {
        input: (event) => {
          this.value = event
        }
      }
      
    })
  }
}
</script>
// vue文件的模板语法,等价于上方的render函数
<el-input v-model="value" size="small" style="width: 240px" />

b. 原生input的双向绑定

// vue文件
// 原生input的双向绑定
// render函数实现方式

<script>
export default {
  data() {
    return {
      value: ''
    }
  },

  render(h) {
    return h('input', {
    
      // value是dom上的属性,需要挂载在domProps上,注意与el-input的区别
      domProps: {
        value: this.value
      },

      style: {
        width: '240px'
      },

      on: {
        input: (event) => {
          // 原生js事件的event,注意与el-input的区别
          this.value = event.target.value
        }
      }
    })
  }
}
</script>
// vue文件的模板语法,等价于上方的render函数

<template>
  <input :value="value" @input="inputHandler" />
</template>

methods: {
    inputHandler(event) {
      this.value = event.target.value
    }
}

(3) 事件 && 按键修饰符

 这一小节主要参考:vue2 render函数官网

image.png

(4) 插槽

1. 默认插槽 (使用`this.$slots.default`)

<div>
  <slot></slot>
</div>

render(h) { return h('div', this.$slots.default) }
 
2. 向父组件提供作用域插槽 (使用`this.$scopedSlots`)

<div>
  <slot :text="message"></slot>
</div>

使用[],代表在父级div标签下创造插槽节点
render(h) {
  return h('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

3. 向子组件传递作用域插槽(非常重要)

a. 父组件:
<div>
  <child v-slot="props">
    <span>{{ props.hobby }}</span>
  </child>
</div>

b. 子组件:
<template>
  <h1>
    我是{{ personObj.name }}, 我的爱好是
    <slot v-bind="personObj"></slot>
  </h1>
</template>

<script>
export default {
  data() {
    return {
      personObj: {
        name: 'cxk',
        age: 18,
        hobby: 'sing, dance and rap'
      }
    }
  }
}
</script>

<style lang="scss" scoped></style>

// 父组件的html模板可以转换为下面的render函数
// 在数据对象中传递`scopedSlots`
// 格式为 { name: 子组件传递过来的数据props => VNode | Array<VNode> }

<script>
import children from './children'

export default {
  components: { children },
  render(h) {
    return h('div', [
      h('children', {
        scopedSlots: {
          
          第一种写法:
          default: (props) => h('span', props.hobby)
          
          第二种写法(其实使用这种写法,render函数会把它转换为字符串,生成文本节点):
          default: (props) => props.hobby
          
          扩展:如何在render函数中返回子组件传递过来的整个props对象?
          
          错误写法:`我是cxk, 我的爱好是 undefined`
          default: (props) => props   
          
          正确写法:既然将整个对象转换为字符串会出错,那我们就手动将其转换为JSON格式的数据
          返回格式为一串带有JSON格式的文字,这点和在html模板中渲染对象得到的结果一样
          返回结果为`我是cxk, 我的爱好是 { "name": "cxk", "age": 18, "hobby": "sing, dance and rap"}`
          default: (props) => JSON.stringify(props)
        }
      })
    ])
  }
}
</script>

效果如下:

    image.png

4. 约束条件:

  之所以把这一小点单独拿出来作为一小节,是因为它比较重要

<script>
export default {
  render(h) {
    const helloNode = h('p', 'hello')
    // 在v2.5.18之前,使用两个相同的VNode是可以成功渲染的,但是后续页面更新只会更新最后一个节点
    // 因为vue当前实现会将vnode会与其渲染出来的dom元素进行一对一关联。当同一个vnode渲染两次后, 
    // vnode最终只会与最后一个渲染出来的dom元素相关联,所以在patch阶段只有最后一个dom可以被更新
    // 从v2.5.18开始,vue已经开始支持vnode复用了,可以成功渲染并更新
    // 特别注意:使用数组包裹子节点时,不能使用()包裹,[(helloNode, helloNode)]这种写法就是错误的
    // 因为使用括号包裹起来后,js会将两个helloNode当作一个整体,从而执行逗号运算符,永远取最后一个的值
    return h('div', [helloNode, helloNode])
  }
}
</script>

  对应地,在vue2.5.18之前,如果需要多次渲染相同dom节点,可以先创建数组,然后在数组里面循环创建虚拟节点:

<script>
export default {
  data() {
    return {
      text: 123
    }
  },
  
  render(h) {
    const helloNode = h('p', this.text)
    return h(
      'div',
      {
        on: {
          click: () => {
            this.text = 456
          }
        }
      },
      Array.apply(null, { length: 2 }).map(() => helloNode)
    )
  }
}
</script>

5.render函数的实际应用(render函数template模板更加灵活的实例):

  • 案例1: 使用Vnode优雅地为element UI的$message添加自定义按钮

  假设我们有这样一种需求,需要在消息提示右方自定义添加按钮。通过查阅element UI的$message官方文档,发现message属性其实是支持VNode形式的,那我们就可以以VNode的形式表示message

successCallBack() {
  // 使用vue的api方法this.$createElement创建虚拟节点
  const h = this.$createElement
  
  const textVNode = h(
    'span',
    { style: { color: '#13CE66', fontSize: '14px' } },
    '组合产品已创建完成,下一步去创建自定义底板吧!'
  )
  
  const btnVNode = h(
    'el-button',
    {
      props: { type: 'primary', size: 'mini' },
      on: { click: () => this.$router.push('/customFloorDesign/editCustomFloorDesign') }
    },
    '去设置'
  )
  
  this.$message({
    type: 'success',
    message: h(
      'div',
      // 使用数组包裹div父级的两个子节点textVNode和btnVNode,因此可以一直递归嵌套写html
      // 之所以需要使用一个textVNode,是为了显示文字,如果在div父级上添加domProps, 并绑定innerText
      // 最终的结果会以父级的innerText为准,子集的内容会被覆盖掉
      [textVNode, btnVNode]
    )
  })
}

效果如下:

        image.png

  • 案例2(动态调用h1 ~ h6标签展示标题文字)

  假设有这样一个业务场景:我们需要动态调用h1 ~ h6标签展示标题文字。如果用vue组件化的思想,我们最先想到的会是,封装这么一个组件:因为除去vue框架is属性template模板是无法支持html动态标签的。所以,只能通过传入不同的props,分别处理h1 ~ h6标题对应的组件逻辑。当需要处理的类似逻辑较多时,这样处理的坏处就是代码过于冗长,而且可读性较低

<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</template>

<script>
export default {
  props: {
    level: Number
  }
}
</script>

  但是如果我们使用render函数进行封装,会更加间接明了,而且可读性高。

在父组件中:

<template>
  <div class="app-container">
    <title :level="1">我是h1标题</home>
    <title :level="2">我是h2标题</home>
  </div>
</template>

<script>
import title from './title'

export default {
  components: {
    title
  }
}
</script>

在js子组件中:

export default {
  props: {
    level: Number
  },
  
  render(h) {
    `1. render函数的写法:`
    return h(`h${this.level}`, this.$slots.default)
    
    `2. JSX的写法:`
    const tag = `h${this.level}`
    return (
      <tag>{this.$slots.default}</tag>
     )
     
    `3. 函数式组件的写法`
    const { props, children} = context
    const tag = `h${props.level}`
    return <tag>{ children }</tag>
  }
}

效果如下:

                  image.png

  • 案例3: 展示不同样式的按钮

  假设有这样一个业务场景:我们需要展示不同样式的按钮。

// customButton.vue

<script>
export default {
  props: {
    type: String
  },
  
  render(h) {
    return h(
      'span',
      {
        `class可以是具有判断条件的对象类型,也可以是数组类型`
        class: {
          btn: true,
          'btn-success': this.type === 'success',
          'btn-danger': this.type === 'danger',
          'btn-warning': this.type === 'warning',
          'btn-primary': this.type === 'primary'
        },
        on: {
          click: this.clickHandler
        },
        ------分割线(注释)
        // 此处可以使用innerText来代替this.$slots.default,但是要多引入一个名为text的prop
        // 因此会比较麻烦,所以不推荐
        domProps: {
           innerText: this.text
        }
        ------
      },
      this.$slots.default
    )
  },
  
  methods: {
    clickHandler() {
      this.$emit('clickHandler')
    }
  }
  
}
</script>

<style lang="scss" scoped>
.btn {
  display: inline-block;
  font-size: 14px;
  color: #495060;
  padding: 8px;
  margin: 0 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  cursor: pointer;
}

.btn-primary,
.btn-warning,
.btn-success,
.btn-danger {
  color: white;
}

.btn-primary {
  background: #384edb;
}

.btn-warning {
  background: #e6a23c;
}

.btn-success {
  background: #67c23a;
}

.btn-danger {
  background: #f56c6c;
}
</style>
// 父vue组件

<template>
  <div class="app-container">
    <customButton type="default" @clickHandler="clickHandler('default')">默认按钮</customButton>
    <customButton type="primary" @clickHandler="clickHandler('primary')">主按钮</customButton>
    <customButton type="warning" @clickHandler="clickHandler('warning')">警告按钮</customButton>
    <customButton type="success" @clickHandler="clickHandler('success')">成功按钮</customButton>
    <customButton type="danger" @clickHandler="clickHandler('danger')">危险按钮</customButton>
  </div>
</template>

<script>
import customButton from './customButton'

export default {
  components: {
    customButton
  },
  
  methods: {
    clickHandler(type) {
      switch (type) {
        case 'default':
          // 对默认按钮做点击事件处理,因为是伪代码,就不在方面下定义了
          // 在此仅仅是为了展示点击事件的处理,所以使用了五个不同类型的按钮进行展示
          // 其实对相同类型的按钮,也可以使用具有不同type的字符串进行区分,处理不同的逻辑
          this.doSomethingForDefaultButton()
          break
        case 'primary':
          this.doSomethingForPrimaryButton()
          break
        case 'warning':
          this.doSomethingForWarningButton()
          break
        case 'success':
          this.doSomethingForSuccessButton()
          break
        case 'danger':
          this.doSomethingForDangerButton()
          break
      }
    }
  }
  
}
</script>

效果如下:

        image.png

6. 利用vue调试工具可视化render函数

  • 在谷歌浏览器上安装vue-devtools调试工具
  • 选中需要解析的vue组件
  • 点击转换为render函数
image.png
  • vue-devtools调试工具就会自动帮你转换为render函数
image.png

JSX

1. JSX出现的意义以及如何让vue识别JSX语言:

  当html页面嵌套较少,且结构较为简单时,使用createElement方法创建组件还是挺简单的。但随着html页面嵌套的逐步加深,你会发现需要嵌套多层createElement方法,代码写起来比较繁琐,可读性较差,而且维护成本还高。为解决这一痛点JSX横空出世。

  JSX语法虽然很好用,但是vue无法识别出这种语法结构。如果希望在vue框架中成功使用JSX语法,需要在项目中安装插件:

npm i @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D

2. JSX在vue中的语法规范(具体细节详见第3小点):

a. 插值表达式需使用{}。区别于模板语法,如果未在render函数中提前解构变量,插值表达式必须使用this才能访问到定义在data中的变量和methods中的方法;

b. 点击事件需要使用onClick

c. props前不需要使用

d. 父组件接收子组件的自定义事件,需要使用on + 自定义事件的名称

e. render函数returnJSX代码。包含return那一行,如果有多行,需要使用()包裹。如果只有一行,可以省略()

f. 如果要设置dom中元素的值, 需要使用domProps + dom属性的形式挂载dom属性

g. render函数中支持直接定义变量

3. JSX与vue2常用语法之间的转换

  • a. 自定义组件:

// 子组件customButton.vue

<script>
export default {
  props: {
    count: Number
  },

  methods: {
    clickHandler() {
      this.$emit('clickHandler', this.count)
    }
  },

  render() {
    return (
      <div>
        <el-button type="primary" onclick={this.clickHandler}>
        点我数量加1
        </el-button>
        <p style="color: red; margin-top: 10px">当前数量为:{this.count}</p>
      </div>
    )
  }
}
</script>
// 父组件: index.vue

<script>
import customButton from './customButton'

export default {
  components: { customButton },

  data() {
    return {
      count: 0
    }
  },

  methods: {
    clickHandler() {
      this.count = this.count + 1
    }
  },

  render() {
    return <customButton count={this.count} onclickHandler={this.clickHandler}></customButton>
  }
}
</script>

效果浏览:

image.png

  • b. class与style的碰撞:

`class与动态class的应用:`

<script>
export default {
  data() {
    return {
      isRed: true,
      isBold: true
    }
  },
  
  render() {
    return <p class="is-red is-bold">我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
    
    // 1. 如果要使用插值表达式, 可以用[]包裹需要添加的类。
    // 切记不能使用()包裹或者不使用任何符号包裹, 这样都会直接触发逗号运算符, 从而取到最后一个类
    return <p class={['is-red','is-bold']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
    
    // 2. 动态class的使用:
    return <p class={[this.isRed && 'is-red', this.isBold && 'is-bold']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
    return <p class={[this.isBold ? 'is-bold' : '', this.isRed ? 'is-red' : '']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
  }
}
</script>

<style lang="scss" scoped>
.is-red {
  color: red;
}

.is-bold {
  font-weight: bold;
}
</style>

效果浏览:

image.png

`style与动态style的应用:`
 
 <script>
export default {
  data() {
    return {
      isRed: true,
      isBold: true
    }
  },
  
  render() {
    // 1. style的常规写法:
    return <p style="color: red; font-weight: bold">我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
    // 2. 动态style的写法:
    return <p style={ this.isRed && 'color: red' }>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
  }
}
</script>

效果浏览:

image.png

  • c. v-html的写法:

`v-html的实现: 通过对传入的props指定domPropsInnerHTML属性即可达到v-html的作用`

`使用domProps + dom属性的形式挂载dom属性:`
`a. domPropsId: 指定id`
`b. domPropsInnerHTML: 指定innerHTML`
`c. domPropsClass: 指定class`
`d. domPropsInnerText: 指定InnerText。当div标签有文字时,以传入的msg,即domPropsInnerText为准。`

`1. 子组件`
<script>
export default {
  props: {
    msg: String
  },

  render() {
    return <div domPropsInnerHTML={ this.msg }></div>
  }
}
</script>

`2. 父组件`

<template>
  <customDom msg=
    "<div style='color: red;font-weight: bold'>
      我是cxk, 我今年18啦,我的爱好是sing, dance and rap
    </div>" 
  />
</template>

<script>
import customDom from './customDom'

export default {
  components: { customDom }
}
</script>

效果浏览:

image.png

  • d. v-for 的写法:

`v-for的使用:借助map来实现`

<script>
export default {
  data() {
    return {
      hobby: ['增', '删', '改', '查']
    }
  },

  render() {
    return (
      <div>
        {this.hobby.map((item) => (
          <el-button>{item}</el-button>
        ))}
      </div>
    )
  }
}
</script>

效果浏览:

image.png

  • e. v-if 的写法:

`v-if的使用:借助条件判断语句实现`

<script>
export default {
  data() {
    return {
      show: true
    }
  },

  render() {
    return (
      <div>
        爱我还是他:{this.show ? '我' : '他'}
      </div>
    )
  }
}
</script>
  • f. v-model的写法:

`v-model双向绑定的使用:
方法一: 使用v-model={ 需要绑定的data变量 }
方法二: 使用监听时间来实现v-model
`

`1. 使用v-model直接绑定:`

<script>
export default {
  data() {
    return {
      name: 'cxk'
    }
  },
  watch: {
    name(val) {
      console.log('我变化啦:', val)
    }
  },

  render() {
    return (
      <div>
        <el-input style="width: 240px" v-model={this.name} />
      </div>
    )
  }
}
</script>

`2. 使用监听事件来实现v-model:`

`方式一:在点击事件中调用定义好的方法`
<script>
export default {
  data() {
    return {
      name: 'cxk'
    }
  },
  methods: {
    inputHandler(e) {
      this.name = e
    }
  },

  render() {
    return (
      <div>
        <el-input style="width: 240px" type="text" value={this.name} onInput={this.inputHandler} />
      </div>
    )
  }
}
</script>

`方式二:直接在点击事件中使用箭头函数`

<script>
export default {
  data() {
    return {
      name: 'cxk'
    }
  },

  render() {
    return (
      <div>
        <el-input style="width: 240px" type="text" value={this.name} onInput={(e) => this.name = e} />
      </div>
    )
  }
}
</script>

效果浏览:

image.png
  • g. 事件监听的写法:

`事件监听:`

`事件名称首字母需大写,使用驼峰式命名`
`1. 单个事件的监听:
方法一:使用on事件名称={this.方法} 
方法二:使用{...{on: {input: this.handleInput}}}
`
`2. 多个事件的监听: 
方法一:使用on事件名称1={this.方法1} on事件名称2={this.方法2}
方法二:使用{...{on: {事件名称1: this.方法1, 事件名称2: this.方法2 }}}`
<input type="text" value={this.value} {...{ on: { input: this.handleInput, focus: this.handleFocus } }} />

`3. 监听自定义组件的事件:
方法一:使用nativeOn事件名称={this.方法}
方法二:使用 {...{nativeOn:{事件名称: this.方法}}}
`

`4.重点:如果监听事件的方法中有传参,如果直接调用,会在render的时候自动执行一次事件方法`
<script>
import { Button } from 'element-ui'
export default {
  methods: {
    tell(data) {
      console.log("我被调用了")
      return `我是${data}`
    }
  },

  render() {
    `不能直接调用事件方法,这样会在render的时候就自动执行一次事件方法,并在后续点击时报错`
    return <el-button type="primary" onClick={this.tell('cxk')}>自我介绍</el-button>
    
    `解决方法一: 使用bind方法`
     return (
      <el-button type="primary" onClick={this.tell.bind(null, 'cxk')}>
        自我介绍
      </el-button>
    )
    
    `解决方法二:定义箭头函数传递参数`
    return (
      <el-button type="primary" onClick={() => this.tell('cxk')}>
        自我介绍
      </el-button>
    )
  }
}
</script>
  • h. 事件修饰符的写法:

`事件修饰符:`

`1. .stop: 阻止事件冒泡,在JSX的函数中使用event.stopPropagation()来代替`
`2. .prevent: 阻止默认行为,在JSX的函数中使用event.preventDefault()来代替`
`3. .self: 使用if(event.target !== event.currentTarget) return, 写后续逻辑`
`4. .enter与keyCode的结合:if(event.keyCode === 13) { ...dosomething }`

<script>
export default {
  methods: {
    handleClick(e) {
      console.log('click事件:' + e.target)
    },
    handleInput(e) {
      console.log('input事件:' + e.target)
    },
    handleMouseDown(e) {
      console.log('mousedown事件:' + e.target)
    },
    handleMouseUp(e) {
      console.log('mouseup事件' + e.target)
    }
  },
  
  render() {
    return (
      <div
        {...{
          on: {
            // 相当于 :click.capture
            '!click': this.handleClick,
            // 相当于 :input.once
            '~input': this.handleInput,
            // 相当于 :mousedown.passive
            '&mousedown': this.handleMouseDown,
            // 相当于 :mouseup.capture.once
            '~!mouseup': this.handleMouseUp
          }
        }}
      >
        点击模块
      </div>
    )
  }
}
</script>
  • i. JSX与el组件的写法:

`JSX与el组件的花火:`

<script>
export default {
  data() {
    return {
      visible: false
    }
  },

  methods: {
    renderFooter() {
      return (
        <div>
          <el-button type="primary" onClick={this.closeDialog}>
            保存
          </el-button>
          <el-button onClick={this.closeDialog}>取消</el-button>
        </div>
      )
    },

    showDialog() {
      this.visible = true
    },

    closeDialog() {
      this.visible = false
    }
  },

  render() {
    const footerHTML = this.renderFooter()
    return (
      <div>
        <el-button type="primary" onClick={this.showDialog}>
          显示弹框
        </el-button>
        <el-dialog visible={this.visible} title="自我介绍" width="380px" before-close={this.closeDialog}>
          <div>我是cxk, 我今年18啦,我的爱好是sing, dance and rap</div>
          <template slot="footer">{footerHTML}</template>
        </el-dialog>
      </div>
    )
  }
}
</script>

效果浏览:

image.png
  • j. 默认插槽与具名插槽的写法:

`默认插槽与具名插槽:
    子组件使用this.$slots.插槽名称创建插槽
    父组件使用slot="插槽名称",接收子组件定义的默认插槽和具名插槽
`

`1. 子组件:`

<script>
export default {
  render() {
    return (
      <div>
        <p>{this.$slots.default}</p>
        <p>{this.$slots.footer}</p>
      </div>
    )
  }
}
</script>

`2. 父组件:`

`a. 父组件render函数的写法:`

<script>
import testSlot from './testSlot'

export default {
  components: { testSlot },
  render() {
    return (
      <testSlot>
        <template slot="default">我是cxk</template>
        <template slot="footer">我今年18啦</template>
      </testSlot>
    )
  }
}
</script>

`b. 父组件template模板的写法:`

<template>
  <testSlot>
    <template #default>我是cxk</template>
    <template #footer>我今年18啦</template>
  </testSlot>
</template>

<script>
import testSlot from './testSlot'

export default {
  components: { testSlot }
}
</script>

效果浏览:

image.png

  • k. 作用域插槽的写法:

`作用域插槽:
   子组件使用this.$scopedSlots.插槽名称(需要传给父组件的对象数据),创建作用域插槽
   父组件使用scopedSlots={{ 插槽名称: 定义箭头函数,返回需要渲染的html }}接收子组件创建的作用域插槽
`

`1. 子组件:`

<script>
export default {
  render() {
    return (
      <div>
        // this.$scopedSlots.插槽名称(需要传给父组件的对象数据),创建作用域插槽
        <p>{this.$scopedSlots.name({ name: 'cxk' })}</p>
        <p>{this.$scopedSlots.age({ age: 18 })}</p>
        <p>{this.$scopedSlots.hobby({ hobby: 'sing, dance and rap' })}</p>
      </div>
    )
  }
}
</script>

`2. 父组件:`

<script>
import testSlot from './testSlot'

export default {
  components: { testSlot },
  render() {
    return (
      <testSlot
        scopedSlots={{
          // 插槽名称定义箭头函数返回需要渲染的html
          name: ({ name }) => <p>我是{name}</p>,
          age: ({ age }) => <p>我今年{age}啦</p>,
          hobby: ({ hobby }) => <p>我的爱好是{hobby}</p>
        }}
      >
      </testSlot>
    )
  }
}
</script>

效果浏览:

image.png

函数式组件

1. 函数式组件的特点和优点:

  • a. 组件自身没有实例,即没有this,参数全凭render函数中的context参数来传递
  • b. 没有生命周期方法
  • c. 只接收一些prop函数
  • d. 渲染开销低,因为它只是函数
  • e. 渲染速度快,因为组件内部没有状态,不需要经过额外的响应式初始化
  • f. 适用于页面逻辑比较简单、较多静态文本展示的情形,比如详情说明页面

2. 函数式组件的写法(template模板 + render函数)

a. vue2官网的context参数介绍:

函数式组件.png

b. 函数式组件template模板写法总结: 需要在最外层template标签上添加functional字段

`1. 基础写法:`
<template functional>
   ...
</template>

`2. 实现v-bind="$attrs"和v-on="$listeners": `

`a. 子组件:`

<template functional>
  <el-button v-bind="data.attrs" v-on="data.on">
    <slot></slot>
  </el-button>
</template>

`b. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  methods: {
    clickHandler() {
      console.log("我被调用了")
    }
  },

  render() {
    return <test type="primary" onClick={this.clickHandler}>自我介绍</test>
  }
}
</script>

`3. class与style的动态绑定: 用于向父组件抛出自定义class, 修改样式`

`
格式为:

a. 在jsx中的显示:

 <div class={ [data.class, data.staticClass] } style={data.staticStyle} >
  <h1>{{ props.title }}</h1>
 </div>

b. 在template中的显示:

<template functional>
 <div :class="[data.class, data.staticClass]" :style="data.staticStyle">
  <h1>{{ props.title }}</h1>
 </div>
</template>
`

具体应用:

`子组件:`

<template functional>
  <div>
    <div :class="[data.class, data.staticClass]">自我介绍</div>
    <slot></slot>
  </div>
</template>

`父组件:`

<script>
import test from './test'
export default {
  components: { test },

  render() {
    return <test class="red" staticClass="bold" onClick={this.clickHandler}>我是cxk, 我今年18啦, 我的爱好是sing, dance and rap</test>
  }
}
</script>

<style lang="scss" scoped>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>

`4. 巧用$options做过滤器:`

`1. 子组件:`

<template functional>
  <div>{{ $options.updateName(props.name) }}</div>
</template>

<script>
export default {
  props: {
    name: String
  },
  
  updateName(name) {
    return `我是${name}`
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  render() {
    return <test name="cxk"></test>
  }
}
</script>

`5. (重点)在模板的插值表达式中,可以直接使用parent拿到父组件的数据`

动态class效果浏览:

image.png

c. 函数式组件render函数写法总结:

  • js模块中必须添加functional: true
  • 由于函数式组件中没有this。因此,如果需要展示props的值,需要使用render函数中的context形参来解构获取props的值
  • 同理,对于默认插槽和具名插槽,需要使用context形参解构出slots,利用slots().插槽名称来创建具名插槽
  • 对于作用域插槽,需要使用context形参解构出scopedSlots,利用scopedSlots.插槽名称(需要传递的对象数据)来创建作用域插槽
  • 函数式组件的事件传递,需要使用context形参解构出的listeners,利用render函数中的{on: {click: listeners.click}}向父组件传递事件,父组件利用@click进行接收
  • 函数式组件同样支持JSX语法
  • 特别重要:切记在函数式组件中,如果有使用到默认插槽和context形参的children属性,一定不要乱用template标签,这点会在下文第e和f点中详细指明
  • context形参解构出的children参数,用来接收引用的函数式组件除具名插槽以外的所有信息
  • context形参解构出的data参数, 作用在createElement函数上,相当于为创建的组件绑定v-bind="data.attrs"v-on="listeners"。因此,又常有
<script>
export default {
  functional: true,
  render (h, {data, children}) {
    `1. data是对象`
    `2. children为VNode子节点`
    `以上两点完美符合render函数createElement参数的用法`
    return h('el-button', data, children)
  }
}
</script>

d. props的用法:

特别注意:在vue2.3.0之前,如果函数式组件想要使用prop, 必须在组件中提供props。在vue2.3.0及之后版本,在组件中可以省略props,组件上的某些attribute(会排除class之类的属性)会自动隐式解析为prop。建议最好写上,因为这样结构会比较清晰。

`在vue2.3.0及之后版本,会自动将组件上的attribute转换为prop: `
`未定义props的情况:`

`1. 子组件: `
<script>
export default {
  functional: true,
  render(h, ctx) {
    console.log('ctx', ctx)
  }
}
</script>

`2. 父组件:`
<script>
import test from './test'
export default {
  components: { test },

  render() {
    return <test name="cxk" age={18} hobby="sing, dance and rap" />
  }
}
</script>

效果浏览:

image.png

`定义props的情况:`

`1. 子组件:`

<script>
export default {
  functional: true,
  props: {
    obj: Object
  },

  render(h, { props: { obj } }) {
    return <h1>{JSON.stringify(obj)}</h1>
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  data() {
    return {
      obj: {
        name: 'cxk',
        age: 18,
        hobby: 'sing, dance and rap'
      }
    }
  },

  render() {
    return <test obj={this.obj} />
  }
}
</script>

效果浏览:

image.png
`子组件:`

<script>
export default {
  functional: true,

  render(h, { props: { customClass, showLabel }, scopedSlots }) {
    const show = showLabel || showLabel == undefined
    return (
      <div class={[show && 'title-bar', customClass]}>
        <p class="text">{scopedSlots.default()}</p>
      </div>
    )
  }
}
</script>

<style scoped lang="scss">
.title-bar {
  height: 20px;
  .text {
    font-size: $text-medium;
    color: $color-content;
    border-left: 3px solid $--color-primary;
    padding-left: 6px;
    line-height: 20px;
  }
}
</style>
`父组件:`

<titleBar>开关</titleBar>

e. children的用法:children记录引用的函数式组件的所有子节点信息,包含各类插槽信息

`使用children属性,在遇到template标签时,哪怕加上插槽,也不会渲染template标签里面的内容
因为在经过vue框架的内部处理后,text属性被置为undefined`

`1. 子组件:`
<script>
export default {
  functional: true,
  render(h, { children }) {
    console.log('children', children)
    return <div>{children}</div>
  }
}
</script>

`2. 父组件:`
<script>
import test from './test'
export default {
  components: { test },

  render() {
    return (
      <test>
        <h1>自我介绍</h1>
        <template>template——准备好表演了吗?</template>
        <p slot="default">带默认插槽的p标签——准备好表演了吗?</p>
        <template slot="default">带默认插槽的template标签——准备好表演了吗?</template>
        <template slot="name">我是cxk</template>
        <template slot="age">我今年18啦</template>
        <template slot="hobby">我的爱好是sing, dance and rap</template>
      </test>
    )
  }
}
</script>

效果浏览:

image.png

打印结果展示:

image.png

将父组件中JSX中的template标签全部替换为块级元素,比如div标签,就会全部展示,效果如下:

image.png

f. slots的用法:和children类似,但两者在遇到template标签时,结果会有细微差别

`使用slots属性,在遇到template标签时,如果有加上插槽并且有调用,则会渲染标签里面的内容,否则不会渲染里面的内容。
   a. 对于带有插槽的template标签,vue框架会在内部将其转换为text文本
   b. 对于未佩戴插槽的template标签,vue框架会在内部将template标签的text内容置为undefined
`

`1. 子组件:`

<script>
export default {
  functional: true,
  render(h, { slots }) {
    console.log('slots()', slots())
    return (
      <div>
        {slots().default}
        <div>{slots().name}</div>
        <div>{slots().age}</div>
        <div>{slots().hobby}</div>
      </div>
    )
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  render() {
    return (
      <test class="red">
        <h1>自我介绍</h1>
        <template>template——准备好表演了吗?</template>
        <p slot="default">带默认插槽的p标签——准备好表演了吗?</p>
        <template slot="default">带默认插槽的template标签——准备好表演了吗?</template>
        <template slot="name">我是cxk</template>
        <template slot="age">我今年18啦</template>
        <template slot="hobby">我的爱好是sing, dance and rap</template>
      </test>
    )
  }
}
</script>

效果浏览:

image.png

打印截图: image.png

将父组件中JSX中的template标签全部替换为块级元素,比如div标签,就会全部展示,效果如下:

image.png

g. scopedSlots的用法:和JSX使用作用域插槽的方式极度相似

`1. 子组件:`

<script>
export default {
  functional: true,

  render(h, { scopedSlots }) {
    return (
      <div>
        {scopedSlots.name({ name: 'cxk' })}
        {scopedSlots.age({ age: 18 })}
        {scopedSlots.hobby({ hobby: 'sing, dance and rap' })}
      </div>
    )
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  render() {
    return (
      <test
        scopedSlots={{
          // 插槽名称定义箭头函数返回需要渲染的html
          name: ({ name }) => <p>我是{name}</p>,
          age: ({ age }) => <p>我今年{age}啦</p>,
          hobby: ({ hobby }) => <p>我的爱好是{hobby}</p>
        }}
      >
      </test>
    )
  }
}
</script>

效果浏览:

image.png

特别注意: 与上面相同地,如果将父组件中调用插槽的标签改为template,同样不会展现标签里面的内容

h. data的用法:

  • 用于在组件上绑定v-bind="$attrs"v-on="$listeners"(譬如el-button),函数式组件事件传递的另一种方法
`1. 子组件:直接把data属性绑定在组件上`

<script>
export default {
  functional: true,
  render(h, { data, children }) {
    return <el-button {...data}>{children}</el-button>
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  methods: {
    clickHandler() {
      console.log("我被点击啦")
    }
  },

  render() {
    return <test type="primary" onClick={this.clickHandler}>自我介绍</test>
  }
}
</script>

效果浏览:

image.png

  • 在父组件上绑定需要传入的样式名称,用于样式传递(个人感觉意义不大,因为直接在需要的地方定义样式,同样也会生效)
`1. 子组件:`

<script>
export default {
  functional: true,
  render(h, { slots, data }) {
    return <div class={data.class}>{slots().name}</div>
  }
}
</script>

<style lang="scss" scoped>
.red {
  color: red;
}
</style>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  render() {
    return (
      <test class="red">
        <h1>自我介绍</h1>
        <div slot="name">我是cxk</div>
      </test>
    )
  }
}
</script>

效果浏览:

image.png

i. parent的用法:相当于父组件的实例

  利用好这个属性,我们可以在函数式组件中进行路由跳转

`子组件:`

<script>
`一个路由下可能存在多级子路由,此处最好用路由名称判断,因为是例子,所以就随意了。`
`注意: currentIndex需要作为公共变量放在外面,因为render里有路由跳转方法。路由跳转后,又会重新走render。`
`所以,如果将这个变量放render里面,则每次render渲染都永远为0,侧边栏菜单点击后不会有背景的变化。`
`但是如果放在外面,这个变量就成为了公共变量,因此会有背景变化。`
let currentIndex = 0

export default {
  functional: true,

  render(h, ctx) {
 
    const { title, routerList, customClass } = ctx.props
    const routerItems = routerList.map(({ name, path }, index) => {
      return h(
        'div',
        {
          key: path,
          class: [index === currentIndex && 'active', 'name'],
          on: {
            click: () => {
              currentIndex = index
              ctx.parent.$router.push({ name: path })
            }
          }
        },
        name
      )
    })

    return h('div', { class: ['personal-wrapper', customClass] }, [
      h('div', { class: 'title' }, title),
      h('div', { class: 'router-warpper mt10' }, routerItems)
    ])
  }
}
</script>

<style lang="scss" scoped>
.personal-warpper {
  font-size: $text-small;
}

.title {
  color: $color-title;
}

.router-warpper {
  width: 200px;
  color: $color-content;
  border: 1px solid $color-background--extensive;
}

.name {
  padding-left: 48px;
  height: 40px;
  line-height: 40px;
  cursor: pointer;
  &:hover {
    color: $--color-primary;
  }
}

.active {
  background: $color-background--extensive;
}
</style>
`父组件:`

<personalLayout customClass="mt40" title="账户管理" :routerList="routerList" />

routerList: [
    { name: '个人信息', path: '/personalCenter' },
    { name: '收货地址', path: '/address' },
    { name: '我的收藏', path: '/collect' },
    { name: '操作日志', path: '/record' }
]

image.png

j. listeners的用法:用来接收父组件传递过来的事件,详见第l点

k. injections的用法:用于祖先组件向后代组件传值

`注意: 必须得先在祖先组件中提供需要传给后代组件的值,然后在子组件中使用inject接收,不然无法获取到injections。
 祖先组件传入的值如果是引用数据类型,则会多引入其它两个属性,具体见打印截图
 祖先组件传入的值如果是基本数据类型,则会原封不动地展示对应值
`

`传入的值如果是引用数据类型:`

`1. 子组件:`

<script>
export default {
  functional: true,
  inject: ['obj'],
  
  render(h, { injections: { obj } }) {
    console.log('obj', obj)
    return (
      <div>
        我是{obj.name}, 我今年{obj.age}啦, 我的爱好是{obj.hobby}
      </div>
    )
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  provide() {
    return {
      obj: {
        name: 'cxk',
        age: 18,
        hobby: 'sing, dance and rap'
      }
    }
  },

  render() {
    return <test />
  }
}
</script>

`传入的值如果是基本数据类型:`

`a. 子组件:`

<script>
export default {
  functional: true,
  inject: ['description'],
  
  render(h, { injections: { description } }) {
    console.log('description', description)
    return <div>{description}</div>
  }
}
</script>

`b. 父组件:`

<script>
import test from './test'
export default {
  components: { test },

  provide() {
    return {
      description: '我是cxk, 我今年18啦, 我的爱好是sing, dance and rap'
    }
  },

  render() {
    return <test />
  }
}
</script>

效果浏览:

image.png

引用数据类型打印截图:

image.png

基本数据类型打印截图:

image.png

l. 函数式组件事件传递:

`1. 子组件:`

<script>
export default {
  functional: true,
  
  `在子组件中使用listeners.click,接收父组件的点击事件`
  render(h, { listeners: { click }, children }) {
    return (
      <el-button type="primary" onClick={click}>
        {children}
      </el-button>
    )
  }
}
</script>

`2. 父组件:`

<script>
import test from './test'

export default {
  components: { test },

  methods: {
    clickHandler() {
      console.log('我是cxk, 我今年18啦,我的爱好是sing, dance and rap')
    }
  },

  render() {
    return <test onClick={this.clickHandler}>自我介绍</test>
  }
}
</script>

效果浏览: image.png

m. 函数式组件实战:

`子组件:`

<script>
export default {
  functional: true,

  render(h, { props: { title, pie, data } }) {
  
    // 函数式组件没有this, 直接将需要定义的data放到render函数中
    const piesClass = {
      3: 'w33',
      4: 'w25',
      5: 'w20'
    }

    return (
      <div class="detail-warpper">
        {title ? <h4>{title}</h4> : ''}
        <div class="flex-wrap">
          {data.map(({ description, prop }) => (
            <div key={prop} class={['mb20', piesClass[pie]]}>
              {description} : { data[prop] || '暂无' }
            </div>
          ))}
        </div>
      </div>
    )
  }
}
</script>
`父组件:`

<flexDetail title="任务详情" :data="list" :pie="5" />

list: [
    {
      description: '任务编号',
      prop: 'taskNo'
    },
    
    {
      description: '报文类型',
      prop: 'type'
    },
    
    {
      description: '电商企业',
      prop: 'businessEnterprise'
    },
    
    {
      description: '物流企业',
      prop: 'freightEnterprise'
    },
    
    {
      description: '监管场所',
      prop: 'inspectPlace'
    },
    
    {
      description: '订单总数',
      prop: 'totalAmount'
    },
    
    {
      description: '状态',
      prop: 'status'
    },
    
    {
      description: '创建时间',
      prop: 'createTime'
    }
]

效果截图:

image.png

参考文献

vue官网

深入剖析函数式组件

vue的函数式组件

结语

  本文只是简单介绍了render函数函数式组件JSX的基础用法, 帮助大家构建气海山田。熟能生巧, 更高阶的用法和写法,还需要大家不断去敲代码尝试和摸索。同时也欢迎大家在评论区提出你的高见,大概就这样吧~