• 6.2 函数式组件
    • 6.2.1 使用场景
    • 6.2.2 源码分析

    6.2 函数式组件

    Vue提供了一种可以让组件变为无状态、无实例的函数化组件。从原理上说,一般子组件都会经过实例化的过程,而单纯的函数组件并没有这个过程,它可以简单理解为一个中间层,只处理数据,不创建实例,也是由于这个行为,它的渲染开销会低很多。实际的应用场景是,当我们需要在多个组件中选择一个来代为渲染,或者在将children,props,data等数据传递给子组件前进行数据处理时,我们都可以用函数式组件来完成,它本质上也是对组件的一个外部包装。

    6.2.1 使用场景

    • 定义两个组件对象,test1,test2
      1. var test1 = {
      2. props: ['msg'],
      3. render: function (createElement, context) {
      4. return createElement('h1', this.msg)
      5. }
      6. }
      7. var test2 = {
      8. props: ['msg'],
      9. render: function (createElement, context) {
      10. return createElement('h2', this.msg)
      11. }
      12. }
    • 定义一个函数式组件,它会根据计算结果选择其中一个组件进行选项
      1. Vue.component('test3', {
      2. // 函数式组件的标志 functional设置为true
      3. functional: true,
      4. props: ['msg'],
      5. render: function (createElement, context) {
      6. var get = function() {
      7. return test1
      8. }
      9. return createElement(get(), context)
      10. }
      11. })
    • 函数式组件的使用
      1. <test3 :msg="msg" id="test">
      2. </test3>
      3. new Vue({
      4. el: '#app',
      5. data: {
      6. msg: 'test'
      7. }
      8. })
    • 最终渲染的结果为:
      1. <h2>test</h2>

    6.2.2 源码分析

    函数式组件会在组件的对象定义中,将functional属性设置为true,这个属性是区别普通组件和函数式组件的关键。同样的在遇到子组件占位符时,会进入createComponent进行子组件Vnode的创建。由于functional属性的存在,代码会进入函数式组件的分支中,并返回createFunctionalComponent调用的结果。注意,执行完createFunctionalComponent后,后续创建子Vnode的逻辑不会执行,这也是之后在创建真实节点过程中不会有子Vnode去实例化子组件的原因。(无实例)

    1. function createComponent(){
    2. ···
    3. if (isTrue(Ctor.options.functional)) {
    4. return createFunctionalComponent(Ctor, propsData, data, context, children)
    5. }
    6. }

    createFunctionalComponent方法会对传入的数据进行检测和合并,实例化FunctionalRenderContext,最终调用函数式组件自定义的render方法执行渲染过程。

    1. function createFunctionalComponent(
    2. Ctor, // 函数式组件构造器
    3. propsData, // 传入组件的props
    4. data, // 占位符组件传入的attr属性
    5. context, // vue实例
    6. children// 子节点
    7. ){
    8. // 数据检测合并
    9. var options = Ctor.options;
    10. var props = {};
    11. var propOptions = options.props;
    12. if (isDef(propOptions)) {
    13. for (var key in propOptions) {
    14. props[key] = validateProp(key, propOptions, propsData || emptyObject);
    15. }
    16. } else {
    17. // 合并attrs
    18. if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    19. // 合并props
    20. if (isDef(data.props)) { mergeProps(props, data.props); }
    21. }
    22. var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor);
    23. // 调用函数式组件中自定的render函数
    24. var vnode = options.render.call(null, renderContext._c, renderContext)
    25. }

    FunctionalRenderContext这个类最终的目的是定义一个和真实组件渲染不同的render方法。

    1. function FunctionalRenderContext() {
    2. // 省略其他逻辑
    3. this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };
    4. }

    执行render函数的过程,又会递归调用createElement的方法,这时的组件已经是真实的组件,开始执行正常的组件挂载流程。

    问题:为什么函数式组件需要定义一个不同的createElement方法?- 函数式组件createElement和以往唯一的不同是,最后一个参数的不同,之前章节有说到,createElement会根据最后一个参数决定是否对子Vnode进行拍平,一般情况下,children编译生成结果都是Vnode类型,只有函数式组件比较特殊,它可以返回一个数组,这时候拍平就是有必要的。我们看下面的例子:

    1. Vue.component('test', {
    2. functional: true,
    3. render: function (createElement, context) {
    4. return context.slots().default
    5. }
    6. })
    7. <test>
    8. <p>slot1</p>
    9. <p>slot</p>
    10. </test>

    此时函数式组件testrender函数返回的是两个slotVnode,它是以数组的形式存在的,这就是需要拍平的场景。

    简单总结一下函数式组件,从源码中可以看出,函数式组件并不会像普通组件那样有实例化组件的过程,因此包括组件的生命周期,组件的数据管理这些过程都没有,它只会原封不动的接收传递给组件的数据做处理,并渲染需要的内容。因此作为纯粹的函数可以也大大降低渲染的开销。