3

函数式组件

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

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

模板定义:

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

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

<style></style>

render 函数定义

export default {
  name: 'FunTwo',
  functional: true,
  props: {
    title: [String],
  },
  render(h, { props }) {
    return h('div', {}, [h('h1', {}, props.title)])
  },
}
不能这样定义:
<template>
  <div>
    <h1>{{ title }}</h1>
  </div>
</template>

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

<style></style>
使用 render 函数定义输入框

MyInput.jsx

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

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等属性:

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

 title=')

<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

 title=

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

slots 和 scopedSlots 的区别?

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

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

 title=

scopedSlots 更加强大。

children、slots、scopedSlots 使用谁?

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

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

包含下列属性:

 title=

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

父组件提供实例:

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

在子组件中引入:

inject: ['parent'],

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

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

如何使用 computed 和 methods

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

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

<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 内部声明。

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

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>
    )
  },
}

模板定义方式

<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,在一个函数式组件中执行该函数,此函数式组件就是高阶组件。

高阶组件的例子:

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

在模板中使用高阶组件:

<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:

<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


JackZhouMine
184 声望4 粉丝

前端工程师