Vue.js 源码分析(三十) 高级应用 函数式组件 详解

函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的<router-view>组件就是一个函数式组件。

因为函数式组件只是函数,所以渲染开销也低很多,当需要做这些时,函数式组件非常有用:

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

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

函数式组件的定义和普通组件类似,也是一个对象,不过而且为了区分普通的组件,定义函数式组件需要指定一个属性,名为functional,值为true,另外需要自定义一个render函数,该render函数可以带两个参数,分别如下:

      createElement                  等于全局的createElement函数,用于创建VNode

      context                             一个对象,组件需要的一切都是通过context参数传递

context对象可以包含如下属性:

        parent        ;父组件的引用
        props        ;提供所有prop的对象,经过验证了
        children    ;VNode 子节点的数组
        slots        ;一个函数,返回了包含所有插槽的对象
        scopedSlots    ;个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
        data        ;传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
        listeners    ;组件的自定义事件
        injections     ;如果使用了 inject 选项,则该对象包含了应当被注入的属性。

例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <smart-list :items=items></smart-list>
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        Vue.component('smart-list', {
            functional: true,                       //指定这是一个函数式组件
            render: function (createElement, context) {
                function appropriateListComponent (){
                    if (context.props.items.length==0){             //当父组件传来的items元素为空时渲染这个
                        return {template:"<div>Enpty item</div>"}
                    }
                    return 'ul'
                }
                return createElement(appropriateListComponent(),Array.apply(null,{length:context.props.items.length}).map(function(val,index){
                    return createElement('li',context.props.items[index].name)
                }))
            },
            props: {
                items: {type: Array,required: true},
                isOrdered: Boolean
            }
        });
        var app  = new Vue({
            el: '#app',
            data:{
                items:[{name:'a',id:0},{name:'b',id:1},{name:'c',id:2}]
            }
        })
    </script>    
</body>
</html>

输出如下:

对应的DOM树如下:

如果items.item为空数组,则会渲染成:

这是在因为我们再render内做了判断,返回了该值

源码分析


组件在Vue实例化时会先执行createComponent()函数,在该函数内执行extractPropsFromVNodeData(data, Ctor, tag)从组件的基础构造器上获取到props信息后就会判断options.functional是否为true,如果为true则执行createFunctionalComponent函数,如下:

  function createComponent (  //第4181行 创建组件节点
  Ctor, 
  data,
  context,
  children,
  tag
) {
  /**/
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);                 //对props做处理
 
  // functional component
  if (isTrue(Ctor.options.functional)) {                                      //如果options.functional为true,即这是对函数组件
    return createFunctionalComponent(Ctor, propsData, data, context, children)  //则调用createFunctionalComponent()创建函数式组件
  }
  /**/

例子执行到这里对应的propsData如下:

也就是获取到了组件上传入的props,然后执行createFunctionalComponent函数,并将结果返回,该函数如下:

function createFunctionalComponent (      //第4026行  函数式组件的实现
  Ctor,                                       //Ctro:组件的构造对象(Vue.extend()里的那个Sub函数)
  propsData,                                  //propsData:父组件传递过来的数据(还未验证)
  data,                                       //data:组件的数据
  contextVm,                                  //contextVm:Vue实例 
  children                                    //children:引用该组件时定义的子节点
) {
  var options = Ctor.options;
  var props = {};
  var propOptions = options.props;
  if (isDef(propOptions)) {                   //如果propOptions非空(父组件向当前组件传入了信息)
    for (var key in propOptions) {              //遍历propOptions
      props[key] = validateProp(key, propOptions, propsData || emptyObject);    //调用validateProp()依次进行检验
    }
  } else {
    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    if (isDef(data.props)) { mergeProps(props, data.props); }
  }

  var renderContext = new FunctionalRenderContext(      //创建一个函数的上下文
    data,
    props,
    children,
    contextVm,
    Ctor
  );

  var vnode = options.render.call(null, renderContext._c, renderContext);     //执行render函数,参数1为createElement,参数2为renderContext,也就是我们在组件内定义的render函数

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options)
  } else if (Array.isArray(vnode)) {
    var vnodes = normalizeChildren(vnode) || [];
    var res = new Array(vnodes.length);
    for (var i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options);
    }
    return res
  }
}

 FunctionalRenderContext就是一个函数对应,new的时候会给当前对象设置一些data、props之类的属性,如下:

function FunctionalRenderContext (      //第3976行 创建rendrer函数的上下文 parent:调用当前组件的父组件实例
  data,
  props,
  children,
  parent,
  Ctor
) {
  var options = Ctor.options;
  // ensure the createElement function in functional components
  // gets a unique context - this is necessary for correct named slot check
  var contextVm;
  if (hasOwn(parent, '_uid')) {                 //如果父Vue含有_uid属性(是个Vue实例)
    contextVm = Object.create(parent);            //以parent为原型,创建一个实例,保存到contextVm里面
    // $flow-disable-line
    contextVm._original = parent;
  } else {
    // the context vm passed in is a functional context as well.
    // in this case we want to make sure we are able to get a hold to the
    // real context instance.
    contextVm = parent;
    // $flow-disable-line
    parent = parent._original;
  }
  var isCompiled = isTrue(options._compiled);
  var needNormalization = !isCompiled;

  this.data = data;                                                       //data
  this.props = props;                                                     //props
  this.children = children;                                               //children
  this.parent = parent;                                                   //parent,也就是引用当前函数组件的Vue实例
  this.listeners = data.on || emptyObject;                                //自定义事件
  this.injections = resolveInject(options.inject, parent);
  this.slots = function () { return resolveSlots(children, parent); };

  // support for compiled functional template
  if (isCompiled) {
    // exposing $options for renderStatic()
    this.$options = options;
    // pre-resolve slots for renderSlot()
    this.$slots = this.slots();
    this.$scopedSlots = data.scopedSlots || emptyObject;
  }

  if (options._scopeId) {
    this._c = function (a, b, c, d) {
      var vnode = createElement(contextVm, a, b, c, d, needNormalization);
      if (vnode && !Array.isArray(vnode)) {
        vnode.fnScopeId = options._scopeId;
        vnode.fnContext = parent;
      }
      return vnode
    };
  } else {
    this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };    //初始化一个_c函数,等于全局的createElement函数
  }
}

对于例子来说执行到这里FunctionalRenderContext返回的对象如下:

回到createFunctionalComponent最后会执行我们的render函数,也就是例子里我们自定义的smart-list组件的render函数,如下:

render: function (createElement, context) {
    function appropriateListComponent (){
        if (context.props.items.length==0){             //当父组件传来的items元素为空时渲染这个
            return {template:"<div>Enpty item</div>"}
        }
        return 'ul'
    }
    return createElement(appropriateListComponent(),Array.apply(null,{length:context.props.items.length}).map(function(val,index){  //调用createElement也就是Vue全局的createElement函数
        return createElement('li',context.props.items[index].name)
    }))
},

在我们自定义的render函数内,会先执行appropriateListComponent()函数,该函数会判断当前组件是否有传入items特性,如果有则返回ul,这样createElement的参数1就是ul了,也就是穿件一个tag为ul的虚拟VNode,如果没有传入items则返回一个内容为Emptry item的div

createElement的参数2是一个数组,每个元素又是一个createElement的返回值,Array.apply(null,{length:context.props.items.length})可以根据一个数组的个数再创建一个数组,新数组每个元素的值为undefined

猜你喜欢

转载自www.cnblogs.com/greatdesert/p/11277686.html