对Vue functional组件的尝试与小结
1/12/2021, 12:52:22 PM 1/12/2021, 1:08:57 PM

Vue组件由于具有自己的状态,计算开销相对较大。对于简单而大量重复的的组件,这会带来较大的性能开销。 而Vue函数式组件的出现,则让构建轻量级组件成为了可能。下面将粗略介绍Vue functional component。注意:这里并没有使用SFC。

示例

下面的代码片段摘自这个网站的部分源码,作用是将一个文字块渲染为简单的<p>节点。

// https://github.com/zzs-web/website/blob/master/components/bml/Text.ts
import Vue from 'vue'
import md from '~/utils/markdown'

export default Vue.extend({
  name: 'BmlText',
  functional: true,
  props: {
    value: {
      type: String,
      required: true
    }
  },
  render(h, ctx) {
    return h(
      'div',
      Object.assign({}, ctx.data, {
        class: [ctx.data.class],
        domProps: {
          innerHTML: md.utils
            .escapeHtml(ctx.props.value)
            .replace(/\n/g, '<br/>')
        }
      })
    )
  }
})

可以看到,唯一的魔术就在于这行functional: true,其将这个组件显式声明为函数式组件。 从下面的定义文件(摘自Vue options.d.ts)可见它和普通的组件区别主要是无状态:不能在其中访问this、不能定义watchercomputeddatamethods

export interface FunctionalComponentOptions<Props = DefaultProps, PropDefs = PropsDefinition<Props>> {
  name?: string;
  props?: PropDefs;
  model?: {
    prop?: string;
    event?: string;
  };
  inject?: InjectOptions;
  functional: boolean;
  render?(this: undefined, createElement: CreateElement, context: RenderContext<Props>): VNode | VNode[];
}

那么,如何访问props和其他的属性呢?继续分析代码:

export interface RenderContext<Props=DefaultProps> {
  props: Props;
  children: VNode[];
  slots(): any;
  data: VNodeData;
  parent: Vue;
  listeners: { [key: string]: Function | Function[] };
  scopedSlots: { [key: string]: NormalizedScopedSlot };
  injections: any
}

可以看到,通过render的第二个参数context,我们可以获取到props等相关的属性。这些属性都是响应式的。

函数式组件的v-model

我们可以看到,函数式组件也支持定义model。但是既然无法访问this,我们也无法用this.$emit更新数据。让我们看另一端代码:

// https://github.com/zzs-web/website/blob/master/components/editor/BasicEditor.ts
import Vue from 'vue'

export default Vue.extend({
  name: 'SimpleEditor',
  functional: true,
  model: {
    prop: 'value',
    event: 'input'
  },
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  render(h, ctx) {
    const input = (e: any) => {
      const handler = ctx.listeners.input as Function
      handler(e.target.value)
    }
    return h(
      'div',
      {
        class: 'px-2 fill-width'
      },
      [
        h('textarea', {
          domProps: { value: ctx.props.value },
          on: { input },
          class: 'z-editor-basic fill-width px-3'
        })
      ]
    )
  }
})

我们可以直接使用ctx.listeners.EVENT_NAME来访问父组件的事件处理函数,并直接将事件参数喂给它。同样,v-bind.sync也可也这么实现。