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