vue2 中如何使用函数式组件
- 用 render 定义函数式组件如何处理 props
- 如何在函数式组件中触发自定义事件?
- injection
- 如何使用 computed 和 methods
- 定义一个函数式组件的 MyButton
- 函数式组件有何优势
- 哪种场景适合使用函数式组件
- 函数式组件的问题
- 参考
函数式组件(functional component)是一个不持有状态 data
、实例 this
和生命周期的组件。
函数式组件没有 data、生命周期和
this
,函数式组件又叫无状态组件(stateless component)。
模板定义:template functional
<template functional><div><h1>{{ props.title }}</h1></div>
</template><script>export default {name: 'FunOne',props: {title: [String]}}
</script><style></style>
render 函数定义 – functional: true
export default {name: 'FunTwo',functional: true,props: {title: [String]},render(h, {props}) {return h('div', {}, [h('h1', {}, props.title)])}
}
不能这样定义:
<template><div><h1>{{ title }}</h1></div>
</template><script>export default {name: 'FunOne',// NOTE 不能这样定义functional: true,props: {title: [String]}}
</script><style></style>
定义一个函数式组件 MyInput,看看如何使用 props、data 以及事件
MyInput.jsx
export default {name: 'MyInput',// 声明是一个函数式组件functional: true,props: {value: {type: [String, Number],default: ''}},// NOTE 函数式组件没有 thisrender(h, context) {const {props,listeners,data} = contextreturn h('input', {// DOM 属性domProps: {value: props.value},on: {input: ({target}) => {data.on['my-change'](Math.random().toString(36))// listeners 是 data.on 的别名listeners['my-input'](target.value)listeners.input(target.value)}}})}
}
在 render 函数中使用 MyInput
import MyInput from './MyInput'
export default {name: 'MyInputExample',data() {return {value: ''}},render(h) {// MyInput 是一个组件对象选项return h(MyInput, {model: {value: this.value,callback: value => {console.log('model', value)this.value = value}},on: {// NOTE 在父组件的 MyInputExample 上监听 event-name 事件,// 在函数式组件的 listeners 对象// 上就会有一个 event-name 方法// 用于发送数据到外部'my-input': value => {console.log('my-input', value)},'my-change': value => {console.log('my-change', value)}}})}
}
注意
在父组件的 MyInputExample 上监听 event-name 事件,在函数式组件的 listeners 对象上才会有
一个 event-name 方法。
用 render 定义函数式组件如何处理 props
vue 在 render 函数的第二个参数中提供了 context
,用于访问 props
、 slots
等属性:
props: 组件 props 对象。
data: 组件的数据对象,即 h 的第二个参数。
listeners: 组件上监听的事件对象,在组件上监听 `event-name`,listeners 对象就有 `event-name` 属性,值为函数,数据可通过该函数的参数抛到父组件。listeners 是 `data.on` 的别名。
slots: 函数,返回了包含所有插槽的对象。
scopedSlots: 对象,每个属性为返回插槽的 VNode 的函数,可传递参数。
children:子节点数组,可直接传入 `h` 函数的第三个参数。
parent: 父组件,可通过它修改父组件的 data 或者调用父组件的方法。
injection:注入对象。
props
和普通组件的 props 一样,不要求强制,但是声明后可对其类型进行约束,组件接口也更加清晰。
注意
slots() 和 children 的区别?
slots()
返回所有插槽的 对象
,children 是一个 VNode 数组
,不包含 template 上的 v-slot
slots() 返回对象
<FunTwo><p slot="left">left</p><span style="color:red;">按钮</span><template v-slot:right><div>right</div></template><template slot="middle"><span>left</span></template>
</FunTwo>
children 中包含 p
、 span
、 span
,不包含 div
。
children 是一个数组
slots 和 children 同时提供,由你决定渲染谁。
slots 和 scopedSlots 的区别?
slots
是函数,返回包含所有插槽的 VNode 的对象,属性为插槽名字,不能传参数。
scopedSlots
是对象,属性为插槽名,是一个函数,该函数返回对插槽的 VNode,可传参。
scopedSlots
更加强大一些,可传递参数。
注意
children、slots、scopedSlots 使用谁?
组件外部使用 v-slot
指定插槽,优先使用 scopedSlots
,因为它可以传参。
data 对象
<FunTwo :class="'fun-com'" class="class2" :style="{ 'background-color': '#ccc', color, padding: '20px' }" style="font-size:20px;" :title="title" dataKey="title" @click="onClick" />
包含下列属性:
on
属性是一个对象,key 是组件上监听的事件, on.click(params)
可把 params 发送到父组件,即和 this.$emit('click',params)
效果一样。
attrs
非 props 属性。
样式处理:包含动态的 class 、静态 staticClass 静态 staticStyle、动态 style,vue 会把 style 归一化。
如何在函数式组件中触发自定义事件?
data.on['event-name'](params)
event-name 是组件上监听的事件名称,
listeners['event-name'](params)
.
event-name 是组件上监听的事件名称,params 是事件的实参。
注意:父组件监听不监听该事件,就没有该属性。
injection
父组件提供实例:
provide() {return {parent: this}},
在子组件中引入:
inject: ['parent'],
injection
就是一个包含 parent
属性的对象了。
不能用
injection.parent.$emit()
触发自定义事件。$emit 会在内部绑定 this,而 函数式组件 没有实例。
如何使用 computed 和 methods
函数式组件没有像普通组件那样使用计算属性和方法。
模板定义的函数式组件有一个办法 — 直接定义函数,然后在模板中调用 ,比如下面的fullName
<template functional><div><h1>{{ props.title }} {{ $options.fullName(props) }}</h1></div>
</template><script>export default {name: 'FunOne',props: {title: [String]},fullName(props) {return props.title + 'jack' + 'chou'}}
</script>
render 定义的函数式组件,不能把函数声明在 props 同级的地方,可在 render 内部声明。
export default {name: 'FunButton',functional: true,props: {title: [String]},render(h, {data,props,children,slots,scopedSlots,injections,parent}) {const fullName = props => {return props.title + 'jack' + 'chou'}return h('div', data, [h('button', {}, fullName(props)), props.title])}
}
定义一个函数式组件的 MyButton
MyButton.jsx
export default {name: 'MyButton',functional: true,props: {person: {type: Object,default: () => ({name: 'jack',age: 23})}},render(h, {props,scopedSlots,listeners}) {// NOTE default 是关键字,需要重命名const {left,right,default: _defaultSlot} = scopedSlotsconst defaultSlot = (_defaultSlot && _defaultSlot({person: props.person})) || < span > 按钮 < /span>const leftSlot = (left && left()) || ''const rightSlot = right && right(props.person)const button = h('button', {on: {click: () => {listeners.click && listeners.click(props.person)}}},[defaultSlot])return ( <div > {leftSlot} {button} {rightSlot} </div>)}
}
模板定义方式
<template functional><div><slot name="left"></slot><button @click="listeners['click'] && listeners['click'](props.person)"><slot :person="props.person"><span>按钮</span></slot></button><slot name="right" :age="props.age"></slot></div>
</template><script>export default {name: 'MyButton',props: {person: {type: Object,default: () => ({name: '函数式组件',age: 24})}}}
</script>
函数式组件有何优势
函数式组件可读性差,为何还有呢?
快速,即性能好。
函数式组件没有状态,也就不需要针对 Vue 反应式系统等额外的初始化了
。
会对新传入的 props 等做出反应,但对于组件自身,并不知晓其数据何时改变,因为其并不维护自己的状态,即没有 data。
哪种场景适合使用函数式组件
- 纯展示的组件,这类组件往往逻辑简单。
- v-for 循环很多的情况,把这部分代码提取成函数式组件。
- 动态得选择多个组件中一个来渲染。
- 在将 children、props、data slots 传递给子组件之前操作它们 ----
相当于使用函数式组件作为其父组件,对其二次封装
。 - 高阶组件(HOC)— 通过 props 接收一个返回 VNode 的函数,
函数的第一个参数为h
,在一个函数式组件
中执行该函数,此函数式组件就是高阶组件。
高阶组件的例子:
export default {name: 'Container',functional: true,render(h, {props}) {return props.renderContainer(h, props.data)}
}
在模板中使用高阶组件:
<template><div class="zm-form-table"><ul><li v-for="(item, index) in titleList" :key="index"><Container v-if="typeof item.prop === 'function'" :renderContainer="item.prop" :data="data" /><span v-else>{{![null, void 0, ''].includes(data[item.prop] &&data[item.prop] ||''}}</span></div></li></ul></div>
</template><script>import Container from './container.js'export default {name: 'FormTable',components: {Container,},props: {titleList: {type: Array,default: () => {return []},},data: {type: Object,default: () => {return {}},},},}
</script>
这样传递 props:
<template><FormTable :titleList="titleList" :data="detail" />
</template>
<script>export default {name: 'BaseInfo',data() {return {detail: {},titleList: [{title: '家长身份',// NOTE data 是从 Container 的 render 函数里传入的,prop 返回 VNode 能是组件扩展性更好prop: (h, data) => {const options = [{label: '爸爸',value: 'father'},{label: '妈妈',value: 'mother'},{label: '爷爷',value: 'grandpa'},{label: '奶奶',value: 'grandma'},{label: '外公',value: 'grandfather'},{label: '外婆',value: 'grandmother'},{label: '其他',value: 'other'}]const identity = options.find(item => data[key] === item.value)// 返回 jsx 实现自定义展示内容return <span > {identity && identity.label} < /span>}},{title: '家长微信',prop: 'weiXin'}]}},created() {setTimeout(() => {// 模拟接口返回this.detail = {identity: 'father',weiXin: 'jack8848'}}, 1000)}}
</script>
说明:
把 render 函数通过 props 传入组件,这种模式极为有用,它能组件的使用真的自定义渲染内容。
react 中高阶组件:
组件作为入参,新组件作为返回值的函数,在返回之前,可以做一些其他处理,做其他处理才是高阶组件的目的。vue 也能实现这样的组件,但是有点麻烦了。
可参考这里: 探索 Vue 高阶组件
函数式组件的问题
- 样式的 scoped 在函数式组件和状态组件表现不同。
scoped 样式的函数式组件把 scoped 样式的函数式组件作为子组件,css 选择器相同,父组件的样式生效,即子组件的 scoped 没有生效。
参考
Scoped styles inconsistent between functional and stateful components
Renderless Components in Vue.js
[译]: Vue.js 函数式组件:what, why & when?