Vue函数式组件

12,328 阅读5分钟

什么是函数式组件?函数式组件和普通组件有啥区别?函数式组件在React很流行,Vue里我们怎么使用呢?

一、什么是函数式组件?

我们可以把函数式组件想像成组件里的一个函数,入参是渲染上下文(render context),返回值是渲染好的HTML

对于函数式组件,可以这样定义:

  • Stateless(无状态):组件自身是没有状态的
  • Instanceless(无实例):组件自身没有实例,也就是没有this

由于函数式组件拥有的这两个特性,我们就可以把它用作高阶组件(High order components),所谓高阶,就是可以生成其它组件的组件。就像日本的高精度的母机。

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

或者单文件定义函数式组件(2.5版本后)

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>

二、函数式组件和普通组件有啥区别?

  1. 不维护响应数据

  2. 没有instance实例
    所以在组件内部没有办法像传统组件一样通过this来访问组件属性
    实现原理见下面代码中的中文注释

  3. 渲染快

  4. 没有实例,意味着没有(this)

  5. 没有生命周期(没有钩子函数,没有响应式数据)

    function createComponent ( Ctor, data, context, children, tag ) { if (isUndef(Ctor)) { return }

    var baseCtor = context.$options._base;
    
    // 省略N行
    // functional component
    if (isTrue(Ctor.options.functional)) { // 在此判断是否是函数式组件,如果是return 自定义render函数返回的Vnode,跳过底下初始化的流程
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    // 省略N行
    // install component management hooks onto the placeholder node
    installComponentHooks(data); // 正常的组件是在此进行初始化方法(包括响应数据和钩子函数的执行)
    
    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );
    
    return vnode
    

    }

正是因为函数式组件精简了很多例如响应式和钩子函数的处理,因此渲染性能会有一定的提高,所以如果你的业务组件是一个纯展示且不需要有响应式数据状态的处理的,那函数式组件会是一个非常好的选择。

三、函数式组件的使用

1.以局部组件为例,将组件标记为functional=ture;

因为函数式没有实例,因此组件需要的一切都是通过context参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象children: VNode 子节点的数组slots: 一个函数,返回了包含所有插槽的对象scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data:传递给组件的整个数据对象,作为createElement的第二个参数传入组件parent:对父组件的引用listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on的一个别名。injections: (2.3.0+) 如果使用了inject选项,则该对象包含了应当被注入的属性。

在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level。

因为函数式组件只是函数,所以渲染开销也低很多。

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

  • 程序化地在多个组件中选择一个来代为渲染;

  • 在将 children、props、data 传递给子组件之前操作它们。

    data() {
    return { changer:1 } }, components: {
    MyCmp:{
    functional:true, //必要的设置 render: function (createElement, context) {
    function getcomp(cmp){
    console.info(this); //输出为undefined,证明没有实例
    if(cmp==1){
    return comp1;
    }else{
    return comp2
    }
    }
    return createElement(getcomp(context.props.changer),{
    props:{
    cmpData:context.props.data //为子组件传递数据 }
    }
    );
    },

 2. 定义要渲染的组件

var comp1={ 
 props:['cmpData'],
 render:function(createElement,context){   
 return createElement('el-input',{      props:{        type:this.cmpData      }    });  },
  mounted() {   
     console.log(this) //这个组件为正常组件
  },}
  var comp2={  props:['cmpData'],
  render:function(createElement,context){ 
  return createElement('el-button',{      props:{        type:this.cmpData      }    });  },
  mounted() {    console.log(this) //正常组件  },}

3.使用函数式组件

<template> 
 <div>   
 <el-input v-model="changer" placeholder="子组件"></el-input>  
  <my-cmp :changer="changer"></my-cmp> 
 </div></template><script>
</template>

四、函数式组件的参数

  1. functional
    设置为true 即表示该组件为一个函数组件

  2. props(可选)
    传递值到组件内部,2.3.0版本后可以省略,框架会自动将组件上的特性解析为prop

  3. render函数
    提供渲染函数来返回一个vnode,有两个参数一个是createElement,一个是context
    createElement 是创建虚拟dom的函数
    context 是函数式组件的上下文

    props:提供所有 prop 的对象 children: VNode 子节点的数组 slots: 一个函数,返回了包含所有插槽的对象 scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。 data:传递给组件的整个数据对象,作为createElement 的第二个参数传入组件 parent:对父组件的引用 listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。 injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。

接下来说一下createElement 接受的参数:

第一个参数:可以是 {String | Object | Function}

不管是那种类型,最终返回到都是需要渲染的普通DOM标签,

第二个参数:是一个对象,这个参数是可选的,定义了需要渲染组件的参数,相对于普通HTML标签的属性是一样的。

还可以自定义指令的,Vue特有的东西,只是抽象一些,没有直接用Vue.directive()用起来直观。

第三个参数:子级虚拟节点,如果你这个节点只是单节点,没有嵌套节点,这个参数可以忽略。如果有的你就要使用一个数据数组的值位cerateElement()返回的虚拟节点。套路都是一样的。

// @returns {VNode}createElement(
 // {String | Object | Function} 
// 一个 HTML 标签名、组件选项对象,或者 // resolve 了上述任何一种的一个 async 函数。必填项。
 'div', // {Object} // 一个与模板中属性对应的数据对象。可选。
 {  // 与 `v-bind:class` 的 API 相同,  
// 接受一个字符串、对象或字符串和对象组成的数组 
 'class': {    foo: true,    bar: false  }, 
 // 与 `v-bind:style` 的 API 相同,  
// 接受一个字符串、对象,或对象组成的数组  
style: {    color: 'red',    fontSize: '14px'  },
  // 普通的 HTML 特性  attrs: {    id: 'foo'  },
  // 组件 prop  props: {    myProp: 'bar'  }, 
 // DOM 属性  domProps: {    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',
 // 其它特殊顶层属性 key: 'myKey', ref: 'myRef', 
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
 // 那么 `$refs.myRef` 会变成一个数组。 refInFor: true  },
 // {String | Array} // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成, 
// 也可以使用字符串来生成“文本虚拟节点”。可选。
 [  '先写一些文字',  createElement('h1', '一则头条'),  createElement(MyComponent, {   props: {    someProp: 'foobar'   }  }) ])

由于函数式组件没有创建组件实例,所有传统的通过this来调用的属性,在这里都需要通过context来调用。例如:

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    return createElement('button', context.data, context.children)
  }
})

五、关于事件定义

函数式组件没有实例,事件只能由父组件传递。下面我们在App.vue上定义一个最简单的click事件

<template>
  <FunctionalButton @click="log">
    Click me
  </FunctionalButton>
</template>

对应的FunctionalButton.js

export default {
  functional: true,
  render(createElement, { props, listeners, children }) {
    return createElement(
      'button',
      {
        attrs: props,
        on: {
          click: listeners.click
        }
      },
      children
    );
  }
};