本文正在参加「金石计划 . 瓜分6万现金大奖」
往期精彩
回顾(以下三篇都是自己精心总结
的文章,有兴趣的朋友可以传送过去看看):
传送门——金石计划一期之面试不面试,你都必须得掌握的vue知识
前言
大家好,我是前端贰货道士。最近在强烈的求知欲
和掘金金石计划
的'双重激励'下(手动滑稽.gif)
,我对vue2
的render
函数做了一个小总结,诞生了这篇文章。本文由vue2的render
函数、JSX
及函数式组件
三部分构成。函数式组件那章关于children属性和slots属性的差别,是我在敲代码的过程中无意发现的。因为是自己的理解,所以可能会存在问题。如果大家对那块有自己独到的见解,也欢迎在评论区中留言。最近项目没那么忙,但好像也一直抽不出时间...所整理的笔记
有很大一部分都是我下班之后抽空整理的。如果本文对您有帮助,烦请大家一键三连哦, 蟹蟹大家~
render函数
1. render函数
与template模板
之间不得不说的故事:
render函数
与template模板
虽然都能创建html模板
, 但我们平时在vue
文件中写的html模板
,其实会在beforeMount
生命周期的钩子函数中,经过编译生成render函数
,因此两者主要区别
如下:
template模板
理解起来相对容易,但自定义render
函数灵活性高,但是写起来会更为麻烦;- 由于
render函数
省去了编译
步骤,因此性能更高,优先级也会更高;
特别注意:
template模板
不可与render函数
一起使用。两者同时使用时,会以template模板
为准; (注:我不清楚为什么网上都说render函数具有更高的优先级,可能都是复制来复制去的。如果有大佬知道,欢迎在评论区指出)render函数
也可以用在函数式组件
中,详见下文函数式组件,此时render函数
会多出一个上下文参数的形参。切记:如果未在js
中(非render
函数内部)添加functional: true
,render
函数只会有一个参数;反之如果有添加,就会变形成函数式组件,此时render
函数会有两个参数- 一个
vue
文件至少需要保留一个template模板
或者render函数
,或为文件自带,或为混入、继承
等方式。如果这两者都没有,则vue
文件会报错。
2. render函数
的createElement
参数:
return createElement(, {}, [])
- 第一个参数(
必填
):主要是用于提供dom
中的html
内容,类型可以是字符串、对象或函数
。 - 第二个参数(可选):用于设置这个
dom
中的一些样式、属性、传的组件的参数、绑定事件之类
。 - 第三个参数(可选,类型是
数组/字符串
。可以使用字符串
类型生成虚拟文本节点,也可以使用数组
类型递归创建虚拟dom节点
): 代表子节点VNode
。
`传送门(参考官网):https://v2.cn.vuejs.org/v2/guide/render-function.html`
`第二个参数:`
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
class: {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
// 注意style中的样式需要用驼峰命名
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
// (特别注意) 区别于vue文件中的props,此处的props是向定义的render组件绑定对应的props,是写死的
// 比如可以使用这种方式使用el-button组件的某些props,比如size和type等之类
// 而在vue文件中定义的props,是可以动态调用的
props: {
myProp: 'bar'
},
// DOM property
`innerText不识别html标签,非标准`
`innerHTML识别html标签,W3C标准`
domProps: {
innerText: 'baz',
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
// 相当于监听自定义组件的某些原生事件
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
3. vue2
常用语法与render函数
之间的转换:
(1) v-if
和v-for
// vue文件
// render函数实现方式
// v-if借助原生js的if语句来判断
// v-for借助原生js的map方法来实现
<script>
export default {
data() {
return {
list: [{ name: 'fl' }, { name: 'yyl' }, { name: 'lgy' }]
}
},
render(h) {
// 对于render函数中的createElement参数中的第二个参数的class,其实也可以换成staticClass,效果一样
// 即 this.list.map((item) => h('div', { staticClass: 'mt10', key: item.name }, item.name))
if (this.list.length)
return h(
'div',
this.list.map((item) => h('div', { class: 'mt10', key: item.name }, item.name))
)
return h('div', 'No items found')
}
}
</script>
<style lang="scss" scoped>
.mt10 {
margin-top: 10px;
}
</style>
// 上述的render函数写法等价于下面的html模板
<template>
<div v-if="list.length">
<div class="mt10" v-for="{ name } in list" :key="name">
{{ name }}
</div>
</div>
<div v-else> No items found </div>
</template>
(2) v-model双向绑定
a. el-input的双向绑定
// vue文件
// el-input的双向绑定
// render函数实现方式
<script>
export default {
data() {
return {
value: ''
}
},
render(h) {
return h('el-input', {
props: {
size: 'small',
// 之所以把value放在props中,是因为el-input的v-model的绑定值是value,也就是默认为:value
value: this.value
},
style: {
width: '240px'
},
on: {
input: (event) => {
this.value = event
}
}
})
}
}
</script>
// vue文件的模板语法,等价于上方的render函数
<el-input v-model="value" size="small" style="width: 240px" />
b. 原生input的双向绑定
// vue文件
// 原生input的双向绑定
// render函数实现方式
<script>
export default {
data() {
return {
value: ''
}
},
render(h) {
return h('input', {
// value是dom上的属性,需要挂载在domProps上,注意与el-input的区别
domProps: {
value: this.value
},
style: {
width: '240px'
},
on: {
input: (event) => {
// 原生js事件的event,注意与el-input的区别
this.value = event.target.value
}
}
})
}
}
</script>
// vue文件的模板语法,等价于上方的render函数
<template>
<input :value="value" @input="inputHandler" />
</template>
methods: {
inputHandler(event) {
this.value = event.target.value
}
}
(3) 事件 && 按键修饰符
这一小节主要参考:vue2 render函数官网
(4) 插槽
1. 默认插槽 (使用`this.$slots.default`)
<div>
<slot></slot>
</div>
render(h) { return h('div', this.$slots.default) }
2. 向父组件提供作用域插槽 (使用`this.$scopedSlots`)
<div>
<slot :text="message"></slot>
</div>
使用[],代表在父级div标签下创造插槽节点
render(h) {
return h('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
3. 向子组件传递作用域插槽(非常重要)
a. 父组件:
<div>
<child v-slot="props">
<span>{{ props.hobby }}</span>
</child>
</div>
b. 子组件:
<template>
<h1>
我是{{ personObj.name }}, 我的爱好是
<slot v-bind="personObj"></slot>
</h1>
</template>
<script>
export default {
data() {
return {
personObj: {
name: 'cxk',
age: 18,
hobby: 'sing, dance and rap'
}
}
}
}
</script>
<style lang="scss" scoped></style>
// 父组件的html模板可以转换为下面的render函数
// 在数据对象中传递`scopedSlots`
// 格式为 { name: 子组件传递过来的数据props => VNode | Array<VNode> }
<script>
import children from './children'
export default {
components: { children },
render(h) {
return h('div', [
h('children', {
scopedSlots: {
第一种写法:
default: (props) => h('span', props.hobby)
第二种写法(其实使用这种写法,render函数会把它转换为字符串,生成文本节点):
default: (props) => props.hobby
扩展:如何在render函数中返回子组件传递过来的整个props对象?
错误写法:`我是cxk, 我的爱好是 undefined`
default: (props) => props
正确写法:既然将整个对象转换为字符串会出错,那我们就手动将其转换为JSON格式的数据
返回格式为一串带有JSON格式的文字,这点和在html模板中渲染对象得到的结果一样
返回结果为`我是cxk, 我的爱好是 { "name": "cxk", "age": 18, "hobby": "sing, dance and rap"}`
default: (props) => JSON.stringify(props)
}
})
])
}
}
</script>
效果如下:
4. 约束条件:
之所以把这一小点单独拿出来作为一小节,是因为它比较重要。
<script>
export default {
render(h) {
const helloNode = h('p', 'hello')
// 在v2.5.18之前,使用两个相同的VNode是可以成功渲染的,但是后续页面更新只会更新最后一个节点
// 因为vue当前实现会将vnode会与其渲染出来的dom元素进行一对一关联。当同一个vnode渲染两次后,
// vnode最终只会与最后一个渲染出来的dom元素相关联,所以在patch阶段只有最后一个dom可以被更新
// 从v2.5.18开始,vue已经开始支持vnode复用了,可以成功渲染并更新
// 特别注意:使用数组包裹子节点时,不能使用()包裹,[(helloNode, helloNode)]这种写法就是错误的
// 因为使用括号包裹起来后,js会将两个helloNode当作一个整体,从而执行逗号运算符,永远取最后一个的值
return h('div', [helloNode, helloNode])
}
}
</script>
对应地,在vue2.5.18
之前,如果需要多次渲染相同dom节点
,可以先创建数组,然后在数组里面循环创建虚拟节点
:
<script>
export default {
data() {
return {
text: 123
}
},
render(h) {
const helloNode = h('p', this.text)
return h(
'div',
{
on: {
click: () => {
this.text = 456
}
}
},
Array.apply(null, { length: 2 }).map(() => helloNode)
)
}
}
</script>
5.render函数
的实际应用(render函数
比template模板
更加灵活的实例
):
-
案例1:
使用Vnode优雅地为element UI的$message添加自定义按钮
假设我们有这样一种需求,需要在消息提示右方自定义添加按钮
。通过查阅element UI的$message官方文档,发现message
属性其实是支持VNode
形式的,那我们就可以以VNode的形式
表示message
。
successCallBack() {
// 使用vue的api方法this.$createElement创建虚拟节点
const h = this.$createElement
const textVNode = h(
'span',
{ style: { color: '#13CE66', fontSize: '14px' } },
'组合产品已创建完成,下一步去创建自定义底板吧!'
)
const btnVNode = h(
'el-button',
{
props: { type: 'primary', size: 'mini' },
on: { click: () => this.$router.push('/customFloorDesign/editCustomFloorDesign') }
},
'去设置'
)
this.$message({
type: 'success',
message: h(
'div',
// 使用数组包裹div父级的两个子节点textVNode和btnVNode,因此可以一直递归嵌套写html
// 之所以需要使用一个textVNode,是为了显示文字,如果在div父级上添加domProps, 并绑定innerText
// 最终的结果会以父级的innerText为准,子集的内容会被覆盖掉
[textVNode, btnVNode]
)
})
}
效果如下:
-
案例2:
(动态调用h1 ~ h6标签展示标题文字)
假设有这样一个业务场景:我们需要动态调用h1 ~ h6标签
展示标题文字。如果用vue组件化
的思想,我们最先想到的会是,封装这么一个组件
:因为除去vue框架
的is属性
,template模板
是无法支持html动态标签
的。所以,只能通过传入不同的props
,分别处理h1 ~ h6
标题对应的组件逻辑。当需要处理的类似逻辑较多时,这样处理的坏处就是代码过于冗长,而且可读性较低
。
<template>
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</template>
<script>
export default {
props: {
level: Number
}
}
</script>
但是如果我们使用render函数
进行封装,会更加间接明了,而且可读性高。
在父组件中:
<template>
<div class="app-container">
<title :level="1">我是h1标题</home>
<title :level="2">我是h2标题</home>
</div>
</template>
<script>
import title from './title'
export default {
components: {
title
}
}
</script>
在js子组件中:
export default {
props: {
level: Number
},
render(h) {
`1. render函数的写法:`
return h(`h${this.level}`, this.$slots.default)
`2. JSX的写法:`
const tag = `h${this.level}`
return (
<tag>{this.$slots.default}</tag>
)
`3. 函数式组件的写法`
const { props, children} = context
const tag = `h${props.level}`
return <tag>{ children }</tag>
}
}
效果如下:
-
案例3:
展示不同样式的按钮
假设有这样一个业务场景:我们需要展示不同样式的按钮。
// customButton.vue
<script>
export default {
props: {
type: String
},
render(h) {
return h(
'span',
{
`class可以是具有判断条件的对象类型,也可以是数组类型`
class: {
btn: true,
'btn-success': this.type === 'success',
'btn-danger': this.type === 'danger',
'btn-warning': this.type === 'warning',
'btn-primary': this.type === 'primary'
},
on: {
click: this.clickHandler
},
------分割线(注释)
// 此处可以使用innerText来代替this.$slots.default,但是要多引入一个名为text的prop
// 因此会比较麻烦,所以不推荐
domProps: {
innerText: this.text
}
------
},
this.$slots.default
)
},
methods: {
clickHandler() {
this.$emit('clickHandler')
}
}
}
</script>
<style lang="scss" scoped>
.btn {
display: inline-block;
font-size: 14px;
color: #495060;
padding: 8px;
margin: 0 10px;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
}
.btn-primary,
.btn-warning,
.btn-success,
.btn-danger {
color: white;
}
.btn-primary {
background: #384edb;
}
.btn-warning {
background: #e6a23c;
}
.btn-success {
background: #67c23a;
}
.btn-danger {
background: #f56c6c;
}
</style>
// 父vue组件
<template>
<div class="app-container">
<customButton type="default" @clickHandler="clickHandler('default')">默认按钮</customButton>
<customButton type="primary" @clickHandler="clickHandler('primary')">主按钮</customButton>
<customButton type="warning" @clickHandler="clickHandler('warning')">警告按钮</customButton>
<customButton type="success" @clickHandler="clickHandler('success')">成功按钮</customButton>
<customButton type="danger" @clickHandler="clickHandler('danger')">危险按钮</customButton>
</div>
</template>
<script>
import customButton from './customButton'
export default {
components: {
customButton
},
methods: {
clickHandler(type) {
switch (type) {
case 'default':
// 对默认按钮做点击事件处理,因为是伪代码,就不在方面下定义了
// 在此仅仅是为了展示点击事件的处理,所以使用了五个不同类型的按钮进行展示
// 其实对相同类型的按钮,也可以使用具有不同type的字符串进行区分,处理不同的逻辑
this.doSomethingForDefaultButton()
break
case 'primary':
this.doSomethingForPrimaryButton()
break
case 'warning':
this.doSomethingForWarningButton()
break
case 'success':
this.doSomethingForSuccessButton()
break
case 'danger':
this.doSomethingForDangerButton()
break
}
}
}
}
</script>
效果如下:
6. 利用vue
调试工具可视化render函数
- 在谷歌浏览器上安装
vue-devtools
调试工具 - 选中需要解析的
vue组件
- 点击转换为
render函数
vue-devtools
调试工具就会自动帮你转换为render函数
JSX
1. JSX出现的意义以及如何让vue识别JSX语言:
当html页面
嵌套较少,且结构较为简单时,使用createElement
方法创建组件还是挺简单的。但随着html页面
嵌套的逐步加深,你会发现需要嵌套多层createElement
方法,代码写起来比较繁琐,可读性较差,而且维护成本还高。为解决这一痛点,JSX
横空出世。
JSX
语法虽然很好用,但是vue
无法识别出这种语法结构。如果希望在vue
框架中成功使用JSX
语法,需要在项目中安装插件:
npm i @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D
2. JSX在vue中的语法规范(具体细节详见第3小点
):
a. 插值表达式需使用{}
。区别于模板语法
,如果未在render
函数中提前解构变量,插值表达式必须使用this
才能访问到定义在data
中的变量和methods
中的方法;
b. 点击事件需要使用onClick
;
c. props
前不需要使用:
;
d. 父组件接收子组件的自定义事件,需要使用on
+ 自定义事件的名称
;
e. render函数
中return
的JSX代码
。包含return那一行
,如果有多行,需要使用()
包裹。如果只有一行,可以省略()
;
f. 如果要设置dom
中元素的值, 需要使用domProps + dom属性
的形式挂载dom属性
g. render
函数中支持直接定义变量
3. JSX与vue2
常用语法之间的转换
-
a. 自定义组件:
// 子组件customButton.vue
<script>
export default {
props: {
count: Number
},
methods: {
clickHandler() {
this.$emit('clickHandler', this.count)
}
},
render() {
return (
<div>
<el-button type="primary" onclick={this.clickHandler}>
点我数量加1
</el-button>
<p style="color: red; margin-top: 10px">当前数量为:{this.count}</p>
</div>
)
}
}
</script>
// 父组件: index.vue
<script>
import customButton from './customButton'
export default {
components: { customButton },
data() {
return {
count: 0
}
},
methods: {
clickHandler() {
this.count = this.count + 1
}
},
render() {
return <customButton count={this.count} onclickHandler={this.clickHandler}></customButton>
}
}
</script>
效果浏览:
-
b. class与style的碰撞:
`class与动态class的应用:`
<script>
export default {
data() {
return {
isRed: true,
isBold: true
}
},
render() {
return <p class="is-red is-bold">我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
// 1. 如果要使用插值表达式, 可以用[]包裹需要添加的类。
// 切记不能使用()包裹或者不使用任何符号包裹, 这样都会直接触发逗号运算符, 从而取到最后一个类
return <p class={['is-red','is-bold']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
// 2. 动态class的使用:
return <p class={[this.isRed && 'is-red', this.isBold && 'is-bold']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
return <p class={[this.isBold ? 'is-bold' : '', this.isRed ? 'is-red' : '']}>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
}
}
</script>
<style lang="scss" scoped>
.is-red {
color: red;
}
.is-bold {
font-weight: bold;
}
</style>
效果浏览:
`style与动态style的应用:`
<script>
export default {
data() {
return {
isRed: true,
isBold: true
}
},
render() {
// 1. style的常规写法:
return <p style="color: red; font-weight: bold">我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
// 2. 动态style的写法:
return <p style={ this.isRed && 'color: red' }>我是cxk, 我今年18啦, 我的爱好是 sing, dance and rap</p>
}
}
</script>
效果浏览:
-
c. v-html的写法:
`v-html的实现: 通过对传入的props指定domPropsInnerHTML属性即可达到v-html的作用`
`使用domProps + dom属性的形式挂载dom属性:`
`a. domPropsId: 指定id`
`b. domPropsInnerHTML: 指定innerHTML`
`c. domPropsClass: 指定class`
`d. domPropsInnerText: 指定InnerText。当div标签有文字时,以传入的msg,即domPropsInnerText为准。`
`1. 子组件`
<script>
export default {
props: {
msg: String
},
render() {
return <div domPropsInnerHTML={ this.msg }></div>
}
}
</script>
`2. 父组件`
<template>
<customDom msg=
"<div style='color: red;font-weight: bold'>
我是cxk, 我今年18啦,我的爱好是sing, dance and rap
</div>"
/>
</template>
<script>
import customDom from './customDom'
export default {
components: { customDom }
}
</script>
效果浏览:
-
d. v-for 的写法:
`v-for的使用:借助map来实现`
<script>
export default {
data() {
return {
hobby: ['增', '删', '改', '查']
}
},
render() {
return (
<div>
{this.hobby.map((item) => (
<el-button>{item}</el-button>
))}
</div>
)
}
}
</script>
效果浏览:
-
e. v-if 的写法:
`v-if的使用:借助条件判断语句实现`
<script>
export default {
data() {
return {
show: true
}
},
render() {
return (
<div>
爱我还是他:{this.show ? '我' : '他'}
</div>
)
}
}
</script>
-
f. v-model的写法:
`v-model双向绑定的使用:
方法一: 使用v-model={ 需要绑定的data变量 }
方法二: 使用监听时间来实现v-model
`
`1. 使用v-model直接绑定:`
<script>
export default {
data() {
return {
name: 'cxk'
}
},
watch: {
name(val) {
console.log('我变化啦:', val)
}
},
render() {
return (
<div>
<el-input style="width: 240px" v-model={this.name} />
</div>
)
}
}
</script>
`2. 使用监听事件来实现v-model:`
`方式一:在点击事件中调用定义好的方法`
<script>
export default {
data() {
return {
name: 'cxk'
}
},
methods: {
inputHandler(e) {
this.name = e
}
},
render() {
return (
<div>
<el-input style="width: 240px" type="text" value={this.name} onInput={this.inputHandler} />
</div>
)
}
}
</script>
`方式二:直接在点击事件中使用箭头函数`
<script>
export default {
data() {
return {
name: 'cxk'
}
},
render() {
return (
<div>
<el-input style="width: 240px" type="text" value={this.name} onInput={(e) => this.name = e} />
</div>
)
}
}
</script>
效果浏览:
-
g. 事件监听的写法:
`事件监听:`
`事件名称首字母需大写,使用驼峰式命名`
`1. 单个事件的监听:
方法一:使用on事件名称={this.方法}
方法二:使用{...{on: {input: this.handleInput}}}
`
`2. 多个事件的监听:
方法一:使用on事件名称1={this.方法1} on事件名称2={this.方法2}
方法二:使用{...{on: {事件名称1: this.方法1, 事件名称2: this.方法2 }}}`
<input type="text" value={this.value} {...{ on: { input: this.handleInput, focus: this.handleFocus } }} />
`3. 监听自定义组件的事件:
方法一:使用nativeOn事件名称={this.方法}
方法二:使用 {...{nativeOn:{事件名称: this.方法}}}
`
`4.重点:如果监听事件的方法中有传参,如果直接调用,会在render的时候自动执行一次事件方法`
<script>
import { Button } from 'element-ui'
export default {
methods: {
tell(data) {
console.log("我被调用了")
return `我是${data}`
}
},
render() {
`不能直接调用事件方法,这样会在render的时候就自动执行一次事件方法,并在后续点击时报错`
return <el-button type="primary" onClick={this.tell('cxk')}>自我介绍</el-button>
`解决方法一: 使用bind方法`
return (
<el-button type="primary" onClick={this.tell.bind(null, 'cxk')}>
自我介绍
</el-button>
)
`解决方法二:定义箭头函数传递参数`
return (
<el-button type="primary" onClick={() => this.tell('cxk')}>
自我介绍
</el-button>
)
}
}
</script>
-
h. 事件修饰符的写法:
`事件修饰符:`
`1. .stop: 阻止事件冒泡,在JSX的函数中使用event.stopPropagation()来代替`
`2. .prevent: 阻止默认行为,在JSX的函数中使用event.preventDefault()来代替`
`3. .self: 使用if(event.target !== event.currentTarget) return, 写后续逻辑`
`4. .enter与keyCode的结合:if(event.keyCode === 13) { ...dosomething }`
<script>
export default {
methods: {
handleClick(e) {
console.log('click事件:' + e.target)
},
handleInput(e) {
console.log('input事件:' + e.target)
},
handleMouseDown(e) {
console.log('mousedown事件:' + e.target)
},
handleMouseUp(e) {
console.log('mouseup事件' + e.target)
}
},
render() {
return (
<div
{...{
on: {
// 相当于 :click.capture
'!click': this.handleClick,
// 相当于 :input.once
'~input': this.handleInput,
// 相当于 :mousedown.passive
'&mousedown': this.handleMouseDown,
// 相当于 :mouseup.capture.once
'~!mouseup': this.handleMouseUp
}
}}
>
点击模块
</div>
)
}
}
</script>
-
i. JSX与el组件的写法:
`JSX与el组件的花火:`
<script>
export default {
data() {
return {
visible: false
}
},
methods: {
renderFooter() {
return (
<div>
<el-button type="primary" onClick={this.closeDialog}>
保存
</el-button>
<el-button onClick={this.closeDialog}>取消</el-button>
</div>
)
},
showDialog() {
this.visible = true
},
closeDialog() {
this.visible = false
}
},
render() {
const footerHTML = this.renderFooter()
return (
<div>
<el-button type="primary" onClick={this.showDialog}>
显示弹框
</el-button>
<el-dialog visible={this.visible} title="自我介绍" width="380px" before-close={this.closeDialog}>
<div>我是cxk, 我今年18啦,我的爱好是sing, dance and rap</div>
<template slot="footer">{footerHTML}</template>
</el-dialog>
</div>
)
}
}
</script>
效果浏览:
-
j. 默认插槽与具名插槽的写法:
`默认插槽与具名插槽:
子组件使用this.$slots.插槽名称创建插槽
父组件使用slot="插槽名称",接收子组件定义的默认插槽和具名插槽
`
`1. 子组件:`
<script>
export default {
render() {
return (
<div>
<p>{this.$slots.default}</p>
<p>{this.$slots.footer}</p>
</div>
)
}
}
</script>
`2. 父组件:`
`a. 父组件render函数的写法:`
<script>
import testSlot from './testSlot'
export default {
components: { testSlot },
render() {
return (
<testSlot>
<template slot="default">我是cxk</template>
<template slot="footer">我今年18啦</template>
</testSlot>
)
}
}
</script>
`b. 父组件template模板的写法:`
<template>
<testSlot>
<template #default>我是cxk</template>
<template #footer>我今年18啦</template>
</testSlot>
</template>
<script>
import testSlot from './testSlot'
export default {
components: { testSlot }
}
</script>
效果浏览:
-
k. 作用域插槽的写法:
`作用域插槽:
子组件使用this.$scopedSlots.插槽名称(需要传给父组件的对象数据),创建作用域插槽
父组件使用scopedSlots={{ 插槽名称: 定义箭头函数,返回需要渲染的html }}接收子组件创建的作用域插槽
`
`1. 子组件:`
<script>
export default {
render() {
return (
<div>
// this.$scopedSlots.插槽名称(需要传给父组件的对象数据),创建作用域插槽
<p>{this.$scopedSlots.name({ name: 'cxk' })}</p>
<p>{this.$scopedSlots.age({ age: 18 })}</p>
<p>{this.$scopedSlots.hobby({ hobby: 'sing, dance and rap' })}</p>
</div>
)
}
}
</script>
`2. 父组件:`
<script>
import testSlot from './testSlot'
export default {
components: { testSlot },
render() {
return (
<testSlot
scopedSlots={{
// 插槽名称: 定义箭头函数,返回需要渲染的html
name: ({ name }) => <p>我是{name}</p>,
age: ({ age }) => <p>我今年{age}啦</p>,
hobby: ({ hobby }) => <p>我的爱好是{hobby}</p>
}}
>
</testSlot>
)
}
}
</script>
效果浏览:
函数式组件
1. 函数式组件的特点和优点:
- a. 组件自身没有实例,即没有
this
,参数全凭render函数
中的context
参数来传递 - b. 没有
生命周期方法
- c. 只接收一些
prop
的函数
- d. 渲染开销低,因为它只是
函数
- e. 渲染速度快,因为组件内部没有状态,不需要经过
额外的响应式初始化
- f. 适用于页面逻辑比较简单、较多静态文本展示的情形,比如详情说明页面
2. 函数式组件的写法(template
模板 + render
函数)
a. vue2官网的context参数介绍:
b. 函数式组件template模板
写法总结: 需要在最外层template标签
上添加functional
字段
`1. 基础写法:`
<template functional>
...
</template>
`2. 实现v-bind="$attrs"和v-on="$listeners": `
`a. 子组件:`
<template functional>
<el-button v-bind="data.attrs" v-on="data.on">
<slot></slot>
</el-button>
</template>
`b. 父组件:`
<script>
import test from './test'
export default {
components: { test },
methods: {
clickHandler() {
console.log("我被调用了")
}
},
render() {
return <test type="primary" onClick={this.clickHandler}>自我介绍</test>
}
}
</script>
`3. class与style的动态绑定: 用于向父组件抛出自定义class, 修改样式`
`
格式为:
a. 在jsx中的显示:
<div class={ [data.class, data.staticClass] } style={data.staticStyle} >
<h1>{{ props.title }}</h1>
</div>
b. 在template中的显示:
<template functional>
<div :class="[data.class, data.staticClass]" :style="data.staticStyle">
<h1>{{ props.title }}</h1>
</div>
</template>
`
具体应用:
`子组件:`
<template functional>
<div>
<div :class="[data.class, data.staticClass]">自我介绍</div>
<slot></slot>
</div>
</template>
`父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return <test class="red" staticClass="bold" onClick={this.clickHandler}>我是cxk, 我今年18啦, 我的爱好是sing, dance and rap</test>
}
}
</script>
<style lang="scss" scoped>
.red {
color: red;
}
.bold {
font-weight: bold;
}
</style>
`4. 巧用$options做过滤器:`
`1. 子组件:`
<template functional>
<div>{{ $options.updateName(props.name) }}</div>
</template>
<script>
export default {
props: {
name: String
},
updateName(name) {
return `我是${name}`
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return <test name="cxk"></test>
}
}
</script>
`5. (重点)在模板的插值表达式中,可以直接使用parent拿到父组件的数据`
动态class效果浏览:
c. 函数式组件render函数
写法总结:
js模块
中必须添加functional: true
- 由于函数式组件中没有
this
。因此,如果需要展示props
的值,需要使用render
函数中的context
形参来解构
获取props
的值 - 同理,对于默认插槽和具名插槽,需要使用
context
形参解构出slots
,利用slots().插槽名称
来创建具名插槽 - 对于作用域插槽,需要使用
context
形参解构出scopedSlots
,利用scopedSlots.插槽名称(需要传递的对象数据)
来创建作用域插槽 - 函数式组件的事件传递,需要使用
context
形参解构出的listeners
,利用render
函数中的{on: {click: listeners.click}}
向父组件传递事件,父组件利用@click
进行接收 - 函数式组件同样支持
JSX
语法 - 特别重要:切记在函数式组件中,如果有使用到默认插槽和context形参的children属性,一定不要乱用template标签,这点会在下文第e和f点中详细指明
context
形参解构出的children
参数,用来接收引用的函数式组件除具名插槽以外的所有信息
context
形参解构出的data
参数, 作用在createElement
函数上,相当于为创建的组件绑定v-bind="data.attrs"
和v-on="listeners"
。因此,又常有
<script>
export default {
functional: true,
render (h, {data, children}) {
`1. data是对象`
`2. children为VNode子节点`
`以上两点完美符合render函数createElement参数的用法`
return h('el-button', data, children)
}
}
</script>
d. props
的用法:
特别注意:在vue2.3.0
之前,如果函数式组件想要使用prop
, 必须在组件中提供props
。在vue2.3.0
及之后版本,在组件中可以省略props
,组件上的某些attribute
(会排除class
之类的属性)会自动隐式解析为prop
。建议最好写上,因为这样结构会比较清晰。
`在vue2.3.0及之后版本,会自动将组件上的attribute转换为prop: `
`未定义props的情况:`
`1. 子组件: `
<script>
export default {
functional: true,
render(h, ctx) {
console.log('ctx', ctx)
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return <test name="cxk" age={18} hobby="sing, dance and rap" />
}
}
</script>
效果浏览:
`定义props的情况:`
`1. 子组件:`
<script>
export default {
functional: true,
props: {
obj: Object
},
render(h, { props: { obj } }) {
return <h1>{JSON.stringify(obj)}</h1>
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
data() {
return {
obj: {
name: 'cxk',
age: 18,
hobby: 'sing, dance and rap'
}
}
},
render() {
return <test obj={this.obj} />
}
}
</script>
效果浏览:
`子组件:`
<script>
export default {
functional: true,
render(h, { props: { customClass, showLabel }, scopedSlots }) {
const show = showLabel || showLabel == undefined
return (
<div class={[show && 'title-bar', customClass]}>
<p class="text">{scopedSlots.default()}</p>
</div>
)
}
}
</script>
<style scoped lang="scss">
.title-bar {
height: 20px;
.text {
font-size: $text-medium;
color: $color-content;
border-left: 3px solid $--color-primary;
padding-left: 6px;
line-height: 20px;
}
}
</style>
`父组件:`
<titleBar>开关</titleBar>
e. children
的用法:children
记录引用的函数式组件的所有子节点信息,包含各类插槽
信息
`使用children属性,在遇到template标签时,哪怕加上插槽,也不会渲染template标签里面的内容
因为在经过vue框架的内部处理后,text属性被置为undefined`
`1. 子组件:`
<script>
export default {
functional: true,
render(h, { children }) {
console.log('children', children)
return <div>{children}</div>
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return (
<test>
<h1>自我介绍</h1>
<template>template——准备好表演了吗?</template>
<p slot="default">带默认插槽的p标签——准备好表演了吗?</p>
<template slot="default">带默认插槽的template标签——准备好表演了吗?</template>
<template slot="name">我是cxk</template>
<template slot="age">我今年18啦</template>
<template slot="hobby">我的爱好是sing, dance and rap</template>
</test>
)
}
}
</script>
效果浏览:
打印结果展示:
将父组件中JSX
中的template
标签全部替换为块级元素,比如div
标签,就会全部展示,效果如下:
f. slots
的用法:和children
类似,但两者在遇到template
标签时,结果会有细微差别
`使用slots属性,在遇到template标签时,如果有加上插槽并且有调用,则会渲染标签里面的内容,否则不会渲染里面的内容。
a. 对于带有插槽的template标签,vue框架会在内部将其转换为text文本
b. 对于未佩戴插槽的template标签,vue框架会在内部将template标签的text内容置为undefined
`
`1. 子组件:`
<script>
export default {
functional: true,
render(h, { slots }) {
console.log('slots()', slots())
return (
<div>
{slots().default}
<div>{slots().name}</div>
<div>{slots().age}</div>
<div>{slots().hobby}</div>
</div>
)
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return (
<test class="red">
<h1>自我介绍</h1>
<template>template——准备好表演了吗?</template>
<p slot="default">带默认插槽的p标签——准备好表演了吗?</p>
<template slot="default">带默认插槽的template标签——准备好表演了吗?</template>
<template slot="name">我是cxk</template>
<template slot="age">我今年18啦</template>
<template slot="hobby">我的爱好是sing, dance and rap</template>
</test>
)
}
}
</script>
效果浏览:
打印截图:
将父组件中JSX
中的template
标签全部替换为块级元素,比如div
标签,就会全部展示,效果如下:
g. scopedSlots
的用法:和JSX使用作用域插槽的方式极度相似
`1. 子组件:`
<script>
export default {
functional: true,
render(h, { scopedSlots }) {
return (
<div>
{scopedSlots.name({ name: 'cxk' })}
{scopedSlots.age({ age: 18 })}
{scopedSlots.hobby({ hobby: 'sing, dance and rap' })}
</div>
)
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return (
<test
scopedSlots={{
// 插槽名称: 定义箭头函数,返回需要渲染的html
name: ({ name }) => <p>我是{name}</p>,
age: ({ age }) => <p>我今年{age}啦</p>,
hobby: ({ hobby }) => <p>我的爱好是{hobby}</p>
}}
>
</test>
)
}
}
</script>
效果浏览:
特别注意: 与上面相同地,如果将父组件中调用插槽的标签改为template
,同样不会展现标签里面的内容
h. data
的用法:
- 用于在组件上绑定
v-bind="$attrs"
和v-on="$listeners"
(譬如el-button
),函数式组件事件传递的另一种方法
`1. 子组件:直接把data属性绑定在组件上`
<script>
export default {
functional: true,
render(h, { data, children }) {
return <el-button {...data}>{children}</el-button>
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
methods: {
clickHandler() {
console.log("我被点击啦")
}
},
render() {
return <test type="primary" onClick={this.clickHandler}>自我介绍</test>
}
}
</script>
效果浏览:
- 在父组件上绑定需要传入的样式名称,用于样式传递(
个人感觉意义不大,因为直接在需要的地方定义样式,同样也会生效
)
`1. 子组件:`
<script>
export default {
functional: true,
render(h, { slots, data }) {
return <div class={data.class}>{slots().name}</div>
}
}
</script>
<style lang="scss" scoped>
.red {
color: red;
}
</style>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
render() {
return (
<test class="red">
<h1>自我介绍</h1>
<div slot="name">我是cxk</div>
</test>
)
}
}
</script>
效果浏览:
i. parent
的用法:相当于父组件的实例
利用好这个属性,我们可以在函数式组件中进行路由跳转
`子组件:`
<script>
`一个路由下可能存在多级子路由,此处最好用路由名称判断,因为是例子,所以就随意了。`
`注意: currentIndex需要作为公共变量放在外面,因为render里有路由跳转方法。路由跳转后,又会重新走render。`
`所以,如果将这个变量放render里面,则每次render渲染都永远为0,侧边栏菜单点击后不会有背景的变化。`
`但是如果放在外面,这个变量就成为了公共变量,因此会有背景变化。`
let currentIndex = 0
export default {
functional: true,
render(h, ctx) {
const { title, routerList, customClass } = ctx.props
const routerItems = routerList.map(({ name, path }, index) => {
return h(
'div',
{
key: path,
class: [index === currentIndex && 'active', 'name'],
on: {
click: () => {
currentIndex = index
ctx.parent.$router.push({ name: path })
}
}
},
name
)
})
return h('div', { class: ['personal-wrapper', customClass] }, [
h('div', { class: 'title' }, title),
h('div', { class: 'router-warpper mt10' }, routerItems)
])
}
}
</script>
<style lang="scss" scoped>
.personal-warpper {
font-size: $text-small;
}
.title {
color: $color-title;
}
.router-warpper {
width: 200px;
color: $color-content;
border: 1px solid $color-background--extensive;
}
.name {
padding-left: 48px;
height: 40px;
line-height: 40px;
cursor: pointer;
&:hover {
color: $--color-primary;
}
}
.active {
background: $color-background--extensive;
}
</style>
`父组件:`
<personalLayout customClass="mt40" title="账户管理" :routerList="routerList" />
routerList: [
{ name: '个人信息', path: '/personalCenter' },
{ name: '收货地址', path: '/address' },
{ name: '我的收藏', path: '/collect' },
{ name: '操作日志', path: '/record' }
]
j. listeners
的用法:用来接收父组件传递过来的事件,详见第l点
k. injections
的用法:用于祖先组件向后代组件传值
`注意: 必须得先在祖先组件中提供需要传给后代组件的值,然后在子组件中使用inject接收,不然无法获取到injections。
祖先组件传入的值如果是引用数据类型,则会多引入其它两个属性,具体见打印截图
祖先组件传入的值如果是基本数据类型,则会原封不动地展示对应值
`
`传入的值如果是引用数据类型:`
`1. 子组件:`
<script>
export default {
functional: true,
inject: ['obj'],
render(h, { injections: { obj } }) {
console.log('obj', obj)
return (
<div>
我是{obj.name}, 我今年{obj.age}啦, 我的爱好是{obj.hobby}
</div>
)
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
provide() {
return {
obj: {
name: 'cxk',
age: 18,
hobby: 'sing, dance and rap'
}
}
},
render() {
return <test />
}
}
</script>
`传入的值如果是基本数据类型:`
`a. 子组件:`
<script>
export default {
functional: true,
inject: ['description'],
render(h, { injections: { description } }) {
console.log('description', description)
return <div>{description}</div>
}
}
</script>
`b. 父组件:`
<script>
import test from './test'
export default {
components: { test },
provide() {
return {
description: '我是cxk, 我今年18啦, 我的爱好是sing, dance and rap'
}
},
render() {
return <test />
}
}
</script>
效果浏览:
引用数据类型打印截图:
基本数据类型打印截图:
l. 函数式组件事件传递:
`1. 子组件:`
<script>
export default {
functional: true,
`在子组件中使用listeners.click,接收父组件的点击事件`
render(h, { listeners: { click }, children }) {
return (
<el-button type="primary" onClick={click}>
{children}
</el-button>
)
}
}
</script>
`2. 父组件:`
<script>
import test from './test'
export default {
components: { test },
methods: {
clickHandler() {
console.log('我是cxk, 我今年18啦,我的爱好是sing, dance and rap')
}
},
render() {
return <test onClick={this.clickHandler}>自我介绍</test>
}
}
</script>
效果浏览:
m. 函数式组件实战:
`子组件:`
<script>
export default {
functional: true,
render(h, { props: { title, pie, data } }) {
// 函数式组件没有this, 直接将需要定义的data放到render函数中
const piesClass = {
3: 'w33',
4: 'w25',
5: 'w20'
}
return (
<div class="detail-warpper">
{title ? <h4>{title}</h4> : ''}
<div class="flex-wrap">
{data.map(({ description, prop }) => (
<div key={prop} class={['mb20', piesClass[pie]]}>
{description} : { data[prop] || '暂无' }
</div>
))}
</div>
</div>
)
}
}
</script>
`父组件:`
<flexDetail title="任务详情" :data="list" :pie="5" />
list: [
{
description: '任务编号',
prop: 'taskNo'
},
{
description: '报文类型',
prop: 'type'
},
{
description: '电商企业',
prop: 'businessEnterprise'
},
{
description: '物流企业',
prop: 'freightEnterprise'
},
{
description: '监管场所',
prop: 'inspectPlace'
},
{
description: '订单总数',
prop: 'totalAmount'
},
{
description: '状态',
prop: 'status'
},
{
description: '创建时间',
prop: 'createTime'
}
]
效果截图:
参考文献
结语
本文只是简单介绍了render函数
、函数式组件
与JSX
的基础用法, 帮助大家构建气海山田。熟能生巧, 更高阶的用法和写法,还需要大家不断去敲代码尝试和摸索。同时也欢迎大家在评论区提出你的高见,大概就这样吧~