本文章主要分为两部分,第一部分是转载自别人和官网的对于渲染函数/函数式组件的介绍,第二部分是本人原创的公司项目中用到了函数式组件的场景
1. 函数式组件的介绍
以下1.1和1.2的内容转载自:
作者:deniro
原文地址:https://juejin.im/post/5c5bd129f265da2db0736691
来源:掘金
有时候,我们不需要复杂的组件,甚至在一些场景下,我们不需要那些组件保持自己的状态。比如说构建那些内部不需要太多逻辑的 UI 组件。
像这样的情况,函数式组件实在是再合适不过了。Vue.js 组件提供了一个 functional 开关,设置为 true 后,就可以让组件变为无状态、无实例的函数化组件。因为只是函数,所以渲染的开销相对来说,较小。
函数化的组件中的 Render 函数,提供了第二个参数 context 作为上下文,data、props、slots、children 以及 parent 都可以通过 context 来访问。
1.1 示例
这里,我们用 functional 函数化组件来实现一个智能组件。
html:
<div id="app">
<smart-component :data="data"></smart-component>
<button @click="change('img')">图片组件</button>
<button @click="change('video')">视频组件</button>
<button @click="change('text')">文本组件</button>
</div>
js:
//图片组件设置
var imgOptions = {
props: ['data'],
render: function (createElement) {
return createElement('div', [
createElement('p', '图片组件'),
createElement('img', {
attrs: {
src: this.data.url,
height: 300,
weight: 400
}
})
]);
}
};
//视频组件设置
var videoOptions = {
props: ['data'],
render: function (createElement) {
return createElement('div', [
createElement('p', '视频组件'),
createElement('video', {
attrs: {
src: this.data.url,
controls: 'controls',
autoplay: 'autoplay'
}
})
]);
}
};
//文本组件设置
var textOptions = {
props: ['data'],
render: function (createElement) {
return createElement('div', [
createElement('p', '文本组件'),
createElement('p', this.data.content)
]);
}
};
Vue.component('smart-component', {
//设置为函数化组件
functional: true,
render: function (createElement, context) {
function get() {
var data = context.props.data;
console.log("smart-component/type:" + data.type);
//判断是哪一种类型的组件
switch (data.type) {
case 'img':
return imgOptions;
case 'video':
return videoOptions;
case 'text':
return textOptions;
default:
console.log("data 类型不合法:" + data.type);
}
}
return createElement(
get(),
{
props: {
data: context.props.data
}
},
context.children
)
}
})
var app = new Vue({
el: '#app',
data: {
data: {}
},
methods: {
change: function (type) {
console.log("输入类型:" + type);
switch (type) {
case 'img':
this.data = {
type: 'img',
url: 'http://pic-bucket.ws.126.net/photo/0001/2019-02-07/E7D8PON900AO0001NOS.jpg'
}
break;
case 'video':
this.data = {
type: 'video',
url: 'http://wxapp.cp31.ott.cibntv.net/6773887A7F94A71DF718E212C/03002002005B836E73A0C5708529E09C1952A1-1FCF-4289-875D-AEE23D77530D.mp4?ccode=0517&duration=393&expire=18000&psid=bbd36054f9dd2b21efc4121e320f05a0&ups_client_netip=65600b48&ups_ts=1549519607&ups_userid=21780671&utid=eWrCEmi2cFsCAWoLI41wnWhW&vid=XMzc5OTM0OTAyMA&vkey=A1b479ba34ca90bcc61e3d6c3b2da5a8e&iv=1&sp='
}
break;
case 'text':
this.data = {
type: 'text',
content: '《流浪地球》中的科学:太阳何时吞并地球?科学家已经给出时间表'
}
break;
default:
console.log("data 类型不合法:" + type);
}
}
},
created: function () {
//默认为图片组件
this.change('img');
}
});
效果:
- 首先定义了图片组件设置对象、视频组件设置对象以及文本组件设置对象,它们都以 data 作为入参。
- 函数化组件 smart-component,也以 data 作为入参。内部根据 get() 函数来判断需要渲染的组件类型。
- 函数化组件 smart-component 的 render 函数,以 get() 作为第一个参数;以 smart-component 所传入的 data,作为第二个参数:
return createElement(
get(),
{
props: {
data: context.props.data
}
},
context.children
)
- 根实例 app 的 change 方法,根据输入的类型,来切换不同的组件所需要的数据。
1.2 应用场景
函数化组件不常用,它应该应用于以下场景:
- 需要通过编程实现在多种组件中选择一种。
- children、props 或者 data 在传递给子组件之前,处理它们。
2. 实际应用场景
以下部分是本人原创内容
本人在公司中所参与的一个电商平台,本来只面向于国内用户,后面又除了一个国际版本,国内版本和国际版本一开始是两个不同的项目,后面为了方便维护(提高代码复用和减少维护人力),leader决定将两个版本的项目代码合并到一个项目中,然后发布时按版本单独发布,于是在项目中就会存在页面中的某个模块需要维护两个版本的情况,那么我们在项目中是这样处理的
这里game_card是一个通用的vue组件,里面包含了index.es6.js(主入口)、mainland.vue(国内版使用的组件)、mixin.es6.js(国内版国际版通用的mixin)、wegamex.vue(国际版使用的组件)。
在需要调用game_card组件的地方(父组件)像常规的使用组件的方式去引入该组件即可:
<li v-for="(game, idx) in gameList">
<gameCard
:item="game"
:index="gameIdx"
:funcName="'goToDetail'"
></gameCard>
</li>
然后我们看下game_card/index.es6.js里面的内容
import { env } from "xxx";
import WeGameX from "./wegamex.vue";
import MainLand from "./mainland.vue";
export default {
functional: true,
render: (createElement, option) => {
// 当前环境是否为国际版
let isOversea = env.isOversea();
option.props["isOversea"] = isOversea;
// 绑定点击监听,函数体中通过option.parent触发父组件的方法
option.on = {
clickCard: function(funcName, game, idx, blockIdx) {
option.parent[funcName](game, idx, blockIdx);
}
};
console.log("funcName: ", option.props.funcName); // goToDetail
// 根据当前是国内版还是国际版选择渲染的组件
let component = isOversea ? WeGameX : MainLand;
return createElement(component, option);
}
};
可以看到有几个比较重要的点:
- 1 在组件的开头就标明functional: true来将该组件标记为函数式组件
- 2 使用render函数来渲染该组件,且在render函数体中根据某些逻辑来动态决定渲染的组件(根据isOversea决定渲染maindland.vue还是oversea.vue)
- 3 在render函数体中,可以通过第二个参数option.props去访问从父组件传递过来的props的内容。
3. 函数式组件的相关知识
(1) 将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。因此,如果在第2部分的场景的render函数中,想要用this去访问vue实例的内容的话,浏览器会报错
(2) render函数为了弥补缺少的实例(this),提供了第二个参数作为上下文
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
render: function (createElement, context) {
// ...
}
})
在这里的render函数体中,我们可以通过第二个参数context (第2部分的场景中的option) 去访问组件需要的一切内容,context它包含了以下的内容:
- props: 提供所有 prop 的对象
- children: VNode 子节点的数组
- slots: 一个函数,返回了包含所有插槽的对象
- scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
- data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入子组件
- parent:对父组件的引用
- listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
- injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性
4. render函数的使用场景
4.1 动态标签
有时候我们可能会在一个组件中要根据不同的判断条件显示不同的标签,如下:
<template>
<div>
<div v-if="level === 1"> <slot></slot> </div>
<p v-else-if="level === 2"> <slot></slot> </p>
<h1 v-else-if="level === 3"> <slot></slot> </h1>
<h2 v-else-if="level === 4"> <slot></slot> </h2>
<strong v-else-if="level === 5"> <slot></slot> </stong>
<textarea v-else-if="level === 6"> <slot></slot> </textarea>
</div>
</template>
其中level是data中的变量,可以看到这里有大量重复代码,如果逻辑复杂点,加上一些绑定和判断就更复杂了,这里可以利用 render 函数来对要生成的标签加以判断。
使用render方法根据参数来生成对应标签可以避免上面的情况发生
<template>
<div>
<child :level="level">Hello world!</child>
</div>
</template>
<script type='text/javascript'>
import Vue from 'vue'
Vue.component('child', {
render(h) {
const tag = ['div', 'p', 'strong', 'h1', 'h2', 'textarea'][this.level]
return h(tag, this.$slots.default)
},
props: {
level: { type: Number, required: true }
}
})
export default {
name: 'hehe',
data() { return { level: 3 } }
}
</script>
4.2 动态组件
我们在业务中使用:is的场景,也可以使用 render函数代替
<template>
<div>
<button @click='level = 0'>嘻嘻</button>
<button @click='level = 1'>哈哈</button>
<hr>
<child :level="level"></child>
</div>
</template>
<script type='text/javascript'>
import Vue from 'vue'
import Xixi from './Xixi'
import Haha from './Haha'
Vue.component('child', {
render(h) {
const tag = ['xixi', 'haha'][this.level]
return h(tag, this.$slots.default)
},
props: { level: { type: Number, required: true } },
components: { Xixi, Haha }
})
export default {
name: 'hehe',
data() { return { level: 0 } }
}
</script>