欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-21 21:35:08
函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的组件就是一个函数式组件。 因为函数式组件只是函数,所以渲染开销也低很多,当需要做这些时,函数式组件非常有用: 程序化地在多个组 ......

函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如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/vue@2.5.16/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>

输出如下:

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

对应的dom树如下:

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

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

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

这是在因为我们再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如下:

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

也就是获取到了组件上传入的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返回的对象如下:

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

回到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