Skip to content
On this page

函数式组件

函数式组件(functional component)是一个不持有状态data、实例this和生命周期的组件。

函数式组件没有 data、生命周期和this,函数式组件又叫无状态组件(stateless component)。

模板定义:

html
<template functional>
  <div>
    <h1>{{ props.title }}</h1>
  </div>
</template>

<script>
  export default {
    name: 'FunOne',
    props: {
      title: [String]
    }
  }
</script>

<style></style>

render 函数定义

js
export default {
  name: 'FunTwo',
  functional: true,
  props: {
    title: [String]
  },
  render(h, { props }) {
    return h('div', {}, [h('h1', {}, props.title)])
  }
}

不能这样定义:

html
<template>
  <div>
    <h1>{{ title }}</h1>
  </div>
</template>

<script>
  export default {
    name: 'FunOne',
    functional: true,
    props: {
      title: [String]
    }
  }
</script>

<style></style>

使用 render 函数定义输入框

MyInput.jsx

js
export default {
  name: 'MyInput',
  functional: true,
  props: {
    value: {
      type: [String, Number],
      default: ''
    }
  },
  // NOTE 函数式组件没有 this
  render(h, context) {
    const { props, listeners, data } = context
    return h('input', {
      // DOM 属性
      domProps: {
        value: props.value
      },
      on: {
        input: ({ target }) => {
          data.on['my-change'](Math.random().toString(36))
          // listeners 是 data.on 的别名
          listeners['my-input'](target.value)
          listeners.input(target.value)
        }
      }
    })
  }
}

在 render 函数中使用 MyInput

js
import MyInput from './MyInput'
export default {
  name: 'MyInputExample',
  data() {
    return { value: '' }
  },
  render(h) {
    // MyInput 是一个组件对象选项
    return h(MyInput, {
      model: {
        value: this.value,
        callback: value => {
          console.log('model', value)
          this.value = value
        }
      },
      on: {
        // NOTE 在父组件的 MyInputExample 上监听 event-name 事件,
        // 在函数式组件的 listeners 对象
        // 上就会有一个 event-name 方法
        // 用于发送数据到外部
        'my-input': value => {
          console.log('my-input', value)
        },
        'my-change': value => {
          console.log('my-change', value)
        }
      }
    })
  }
}

注意

在父组件的 MyInputExample 上监听 event-name 事件,在函数式组件的 listeners 对象上才会有一个 event-name 方法。

用 render 定义函数式组件

vue 在 render 函数的第二个参数中提供了context,用于访问 propsslots等属性:

bash
props: 组件 props 对象。
data: 组件的数据对象,即 h 的第二个参数。
listeners: 组件上监听的事件对象,在组件上监听 `event-name`,listeners 对象就有 `event-name` 属性,值为函数,数据可通过该函数的参数抛到父组件。listeners  `data.on` 的别名。
slots: 函数,返回了包含所有插槽的对象。
scopedSlots: 对象,每个属性为返回插槽的 VNode 的函数,可传递参数。
children:子节点数组,可直接传入 `h` 函数的第三个参数。
parent: 父组件,可通过它修改父组件的 data 或者调用父组件的方法。
injection:注入对象。

props 和普通组件的 props 一样,不要求强制,但是声明后可对其类型进行约束,组件接口也更加清晰。

注意

slots() 和 children 的区别?

slots() 返回所有插槽的对象,children 是一个VNode 数组,不包含 template 上的 v-slot

html
<FunTwo>
  <p slot="left">left</p>
  <span style="color:red;">按钮</span>
  <template v-slot:right>
    <div>right</div>
  </template>
  <template slot="middle">
    <span>left</span>
  </template>
</FunTwo>

children 中包含pspanspan,不包含 div

同时提供,由你决定渲染谁。

注意

slots 和 scopedSlots 的区别?

slots 是函数,返回包含所有插槽的 VNode 的对象,属性为插槽名字,不能传参数。

scopedSlots 是对象,属性为插槽名,是一个函数,该函数返回对插槽的 VNode,可传参。

scopedSlots 更加强大。

注意

children、slots、scopedSlots 使用谁?

组件外部使用 v-slot 指定插槽,优先使用 scopedSlots,因为它可以传参。

data 对象

html
<FunTwo
  :class="'fun-com'"
  class="class2"
  :style="{ 'background-color': '#ccc', color, padding: '20px' }"
  style="font-size:20px;"
  :title="title"
  dataKey="title"
  @click="onClick"
/>

包含下列属性:

on 属性是一个对象,key 是组件上监听的事件,on.click(params) 可把 params 发生到父组件,即和this.$emit('click',params) 一样。

attrs 非 props 属性。

样式处理:包含动态的 class 、静态 staticClass 静态 staticStyle、动态 style,vue 会把 style 归一化。

如何在函数式组件中触发自定义事件?

data.on['event-name'](params) event-name 是组件上监听的事件名称,

listeners['event-name'](params).

event-name 是组件上监听的事件名称,params 是事件的实参。

注意

父组件监听不监听该事件,就没有该属性。

injection

父组件提供实例:

js
  provide() {
    return { parent: this }
  },

在子组件中引入:

js
inject: ['parent'],

injection 就是一个包含parent 属性的对象了。

不能用injection.parent.$emit() 触发自定义事件。$emit 会在内部绑定 this,而 函数式组件没有实例。

如何使用 computed 和 methods

函数式组件不是响应式的,不能像普通组件那样使用计算属性和方法。

模板定义的函数式组件有一个办法,直接定义函数,然后在模板中调用。

html
<template functional>
  <div>
    <h1>{{ props.title }} {{ $options.fullName(props) }}</h1>
  </div>
</template>

<script>
  export default {
    name: 'FunOne',
    props: {
      title: [String]
    },
    fullName(props) {
      return props.title + 'jack' + 'chou'
    }
  }
</script>

render 定义的函数式组件,不能把函数声明在 props 同级的地方,可在 render 内部声明。

js
export default {
  name: 'FunButton',
  functional: true,
  props: {
    title: [String]
  },
  render(h, { data, props, children, slots, scopedSlots, injections, parent }) {
    const fullName = props => {
      return props.title + 'jack' + 'chou'
    }
    return h('div', data, [h('button', {}, fullName(props)), props.title])
  }
}

定义一个函数式组件的 MyButton

MyButton.jsx

js
export default {
  name: 'MyButton',
  functional: true,
  props: {
    person: {
      type: Object,
      default: () => ({ name: 'jack', age: 23 })
    }
  },
  render(h, { props, scopedSlots, listeners }) {
    // NOTE default 是关键字,需要重命名
    const { left, right, default: _defaultSlot } = scopedSlots
    const defaultSlot = (_defaultSlot && _defaultSlot({ person: props.person })) || <span>按钮</span>
    const leftSlot = (left && left()) || ''
    const rightSlot = right && right(props.person)
    const button = h(
      'button',
      {
        on: {
          click: () => {
            listeners.click && listeners.click(props.person)
          }
        }
      },
      [defaultSlot]
    )
    return (
      <div>
        {leftSlot}
        {button}
        {rightSlot}
      </div>
    )
  }
}

模板定义方式

html
<template functional>
  <div>
    <slot name="left"></slot>
    <button @click="listeners['click'] && listeners['click'](props.person)">
      <slot :person="props.person">
        <span>按钮</span>
      </slot>
    </button>
    <slot name="right" :age="props.age"></slot>
  </div>
</template>

<script>
  export default {
    name: 'MyButton',
    props: {
      person: {
        type: Object,
        default: () => ({ name: '函数式组件', age: 24 })
      }
    }
  }
</script>

函数式组件有何优势

函数式组件可读性差,为何还有呢?

快速,即性能好。

函数式组件没有状态,也就不需要针对 Vue 反应式系统等额外的初始化了

会对新传入的 props 等做出反应,但对于组件自身,并不知晓其数据何时改变,因为其并不维护自己的状态,即没有 data。

哪种场景适合使用函数式组件

  1. 纯展示的组件,这类组件往往逻辑简单。
  2. v-for 循环很多的情况,把这部分代码提取成函数式组件。
  3. 动态得选择多个组件中一个来渲染。
  4. 在将 children、props、data 传递给子组件之前操作它们 ---- 相当于使用函数式组件作为其父组件,对其二次封装
  5. 高阶组件(HOC)--- 通过 props 接收一个返回 VNode 的函数,函数的第一个参数为h,在一个函数式组件中执行该函数,此函数式组件就是高阶组件。

高阶组件的例子:

js
export default {
  name: 'Container',
  functional: true,
  render(h, { props }) {
    return props.renderContainer(h, props.data)
  }
}

在模板中使用高阶组件:

html
<template>
  <div class="zm-form-table">
    <ul>
      <li
        v-for="(item, index) in titleList"
        :key="index"
      >
          <Container v-if="typeof item.prop === 'function'" :renderContainer="item.prop" :data="data" />
          <span v-else>
            {{
              ![null, void 0, ''].includes(data[item.prop] &&
                data[item.prop] ||''
            }}
          </span>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
import Container from './container.js'
export default {
  name: 'FormTable',
  components: {
    Container,
  },
  props: {
    titleList: {
      type: Array,
      default: () => {
        return [
          // NOTE 测试数据
          /*
					{ title: '标题3', prop: 'key3' },
					{
						title: '标题3',
            // prop 可以是字符串,也可以是函数
						prop: (h, data) => {
							return (
								<el-button size='mini' type='primary'>
									按钮
								</el-button>
							)
						},
					},
          */
        ]
      },
    },
    data: {
      type: Object,
      default: () => {
        return {
          // 测试数据
          /*
					key1: '测试1',
					key2: '测试2',
          key3: '测试3',
          */
        }
      },
    },
  },
}
</script>

这样传递 props:

html
<template>
  <FormTable :titleList="titleList" :data="detail" />
</template>
<script>
  export default {
    name: 'BaseInfo',
    data() {
      return {
        detail: {},
        titleList: [
          {
            title: '家长身份',
            // NOTE data 是从 Container 的 render 函数里传入的,prop 返回 VNode 能是组件扩展性更好
            prop: (h, data) => {
              const options = [
                { label: '爸爸', value: 'father' },
                { label: '妈妈', value: 'mother' },
                { label: '爷爷', value: 'grandpa' },
                { label: '奶奶', value: 'grandma' },
                { label: '外公', value: 'grandfather' },
                { label: '外婆', value: 'grandmother' },
                { label: '其他', value: 'other' }
              ]
              const identity = options.find(item => data[key] === item.value)
              return <span>{identity && identity.label}</span>
            }
          },
          {
            title: '家长微信',
            prop: 'weiXin'
          }
        ]
      }
    },
    created() {
      setTimeout(() => {
        // 接口返回
        this.detail = {
          identity: 'father',
          weiXin: 'jack8848'
        }
      }, 1000)
    }
  }
</script>

说明

把 render 函数通过 props 传入组件,这种模式极为有用,在稍后的组件封装中可看到它的好处。

react 中高阶组件:组件作为入参,新组件作为返回值的函数,在返回之前,可以做一些其他处理,做其他处理才是高阶组件的目的。vue 也能实现这样的组件,但是有点麻烦了。 可参考这里: 探索 Vue 高阶组件

函数式组件的问题

  1. 样式的 scoped 在函数式组件和状态组件表现不同。

scoped 样式的函数式组件把 scoped 样式的函数式组件作为子组件,css 选择器相同,父组件的样式生效,即子组件的 scoped 没有生效。

参考

Scoped styles inconsistent between functional and stateful components

Renderless Components in Vue.js

[译]: Vue.js 函数式组件:what, why & when?

Released under the MIT License.