函數式組件比較特殊,也非常的靈活,它可以根據傳入該組件的內容動態的渲染成任意想要的節點,在一些比較復雜的高級組件里用到,比如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) })) },
在我們自定義的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
