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

輸出如下:

對應的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)
    }))
},

writer by:大沙漠 QQ:22969969

在我們自定義的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM