什么是JSX
摘自 React 官方:
它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模板语言,但它具有 JavaScript 的全部功能。
Vue 什么时候应当使用JSX
这里说的是应当,而不是必须。因为在绝大多数情况下,模板语法都能胜任,只不过写起来看着不太好看而已。或者使用模板语法,那写起来恐怕不是一般的长,而且阅读会费劲很多。
下面我们来看下应当使用JSX而不是模板语法的情况:
假设现在有如下需求:
封装一个机遇ant-design-vue的输入框组件,组件要求有如下功能
1.传入form属性,布尔值,组件自动套上a-form-model-item标签,并接收相应的属性
2.placeholderTip 属性,布尔值,组件自动挡套上a-tooltip,显示值为placeholder
3.传入span,数字,并且大于0小于24,自动套上a-col标签
4.如果都没传,那就只渲染a-input标签
5.如果同时传1,2,3中两个以上的属性,那么包裹顺序为,从外到里依次是a-col,a-form-model-item,a-tooltip
让我们先用模板语法实现下这个组件 input.vue
<!--input 输入框-->
<template>
<div>
<!-- 先判断是否有span-->
<a-col v-if="span > 0 && span<24" :span="span">
<!-- 先判断是否有form-->
<a-form-model-item v-if="form" :label="label" :prop="prop">
<!-- 先判断是否有placeholderTip-->
<a-tooltip v-if="placeholderTip" placement="topLeft" :title="placeholder">
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-tooltip>
<!-- 都没有,只渲染a-input额-->
<a-input v-else
v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-form-model-item>
<!-- 如果没有form,判断是否有placeholderTip-->
<a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-tooltip>
<!-- 都没有,只渲染a-input额-->
<a-input v-else
v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-col>
<!-- 先判断是否有form-->
<a-form-model-item v-else-if="form" :label="label" :prop="prop">
<!-- 先判断是否有placeholderTip-->
<a-tooltip v-if="placeholderTip" placement="topLeft" :title="placeholder">
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-tooltip>
<!-- 都没有,只渲染a-input额-->
<a-input v-else
v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-form-model-item>
<!-- 如果没有form,判断是否有placeholderTip-->
<a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-tooltip>
<!-- 都没有,只渲染a-input额-->
<a-input v-else
v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</div>
</template>
<script>
export default {
name: "Input",
props: {
value: [String, Number], // 值
placeholderTip: Boolean, // 输入框 placeholder提示
form: Boolean, // 是否使用form-item标签包裹
label: String, // 标签
prop: String, // 校验的prop
placeholder:String,//提示
span:Number,//span
},
methods: {
/**
* 输入框改变
* @param e
*/
inputChange(e) {
let v = e.target.value
this.$emit("input", v)
this.$emit("change", v)
},
},
}
</script>
从上面代码我们可以看出,有好几段看起来一样的代码,但是我们却不好抽离出来。或者说并不能完全剥离。
比如这段
<a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
</a-tooltip>
在代码中被用了4次,看着是可以剥离出去的,抽成一个新的子组件,然而接着你发现,他里面的
<a-input v-bind="$attrs"
:value="value"
@change="inputChange"
/>
这个却是在父组件有单独使用。而且就算剥离出去,模板里面的多个相同的判断,v-else-if="placeholderTip" 和 v-if="placeholderTip"也无法减少。
下面我们用jsx来实现一下
<script>
export default {
name: "Input",
props: {
value: [String, Number], // 值
placeholderTip: Boolean, // 输入框 placeholder提示
form: Boolean, // 是否使用form-item标签包裹
label: String, // 标签
prop: String, // 校验的prop
placeholder:String,//提示
span:Number,//span
},
methods: {
/**
* 输入框改变
* @param e
*/
inputChange(e) {
let v = e.target.value
this.$emit("input", v)
this.$emit("change", v)
},
/**
* 渲染form-item节点
* @param child
* @returns {JSX.Element}
*/
renderFormItem (child) {
const { label, prop} = this
return <a-form-model-item label={label} prop={prop}>
{child}
</a-form-model-item>
},
/**
* 渲染输入框组件
* @returns {JSX.Element}
*/
renderInputDom(){
return <a-input attrs={this.$attrs}
value={this.value}
onChange={this.inputChange}
/>
},
},
render (createElement, context) {
const { placeholderTip, form,span } = this
const hasSpan = typeof span === "number" && span>0 && span<24
const inputChild = placeholderTip ? <a-tooltip placement="topLeft" title={this.placeholder}>
{ this.renderInputDom()}
</a-tooltip> : this.renderInputDom()
if(form){
return hasSpan ? <a-col span={span}>{this.renderFormItem(inputChild)}</a-col> : this.renderFormItem(inputChild)
}
return hasSpan ? <a-col span={span}>{inputChild}</a-col> : inputChild
}
}
</script>
看下两者的不同:
首先就从代码行数来说,用模板91行,去掉模板里面的注释,那也还有80行,而用jsx,不到60行
其次,使用jsx,我们将渲染a-input和a-form-model-item抽离成渲染函数,是否有a-tooltip 和 a-col则使用三元运算符配合。在需要的地方调用相应的渲染函数,相比模板语法的直接复制标签,jsx维护性更好。
上面这个例子也许还不能看出jsx的重要性。下面说个复杂点的需求。
1.一个表单页面,表单项是动态的
2.页面渲染哪个表单组件(输入框还是下拉框,或者单选,复选框等),是根据服务器返回的数据指明的值渲染的
3.页面每行排几个表单元素,是动态的,根据服务器返回的值和表单元素本身一些特性来决定(比如多文本输入框,富文本,直接要求占整行)
4.表单元素排列的顺序先后,由数据数组的下标决定
这个需求,例子就不写了。如果用模板语法,那会更糟糕。
那我们什么时候应当使用jsx,而不是模板?
1.页面的渲染有比较多的条件,而且这些条件又不在同一层,有交叉,嵌套等情况
2.页面元素是动态的,元素间排列组合是动态的
当然,对于vue的开发者来说,一般的业务开发,还是模板为主,用起来更简单。至于jsx,除了上面说的动态表单外,组件封装可能会用的相对较多。
jsx用法总结
对于使用vue-cli创建的项目,jsx是自带的,我们不需要安装啥东西。如果么有使用vue-cli创建项目,这里假设已经安装了@vue/babel-preset-jsx插件
- 1 如何使用jsx替代template标签渲染dom?
使用模板
<template>
<div>
<div>测试</div>
</div>
</template>
使用jsx。render函数用于渲染html,在methods的方法里面,也可以直接return html标签。在.vue文件中,需要写在script标签里面,在js文件或者jsx文件中,则不用script标签
jsx返回标签,可以简单理解为拼串,在大括号{}里面可以写js代码
<script>
export default {
methods:{
/**
渲染子元素
**/
renderChild(){
return <div>测试</div>
}
},
render(){
return <div>{ this.renderChild() }</div>
}
}
</script>
2 如何书写属性,特别是值是动态变化的属性
使用模板
<template>
<div>
<div :title="$route.name">测试</div>
</div>
</template>
jsx。有变化的地方就是用大括号{},至于大括号里面,那就是js代码。所以下面的示例title={this.$route.name},也可写成 title={this.getName()},类似这样,在getName函数里面return 出值就好。
<script>
export default {
/**
渲染name
**/
getName(){
return this.$route.name
//假设组件最终需要的是组件或者标签,这里还可以类似这样
//return <div>{this.$route.name}</div>
}
render(){
return <div title={this.$route.name}>测试</div>
//return <div title={this.getName()}>测试</div>
}
}
</script>
这里重点说下class和style。由于class和style写法相对比较多,同样的jsx也可以有多种形式
测试样式效果图
image.png
样式
.default999{
color:#999999;
background: blueviolet;
}
.border-red{
border:solid 1px red;
margin-bottom: 20px;
}
.yellow-bk{
background: yellow;
}
.red{
color:red;
}
.green{
color:green;
}
.edit{
box-shadow: 1px 1px 1px #2b96ff;
}
.view{
box-shadow: 2px 2px 1px #8cc5ff;
}
.bold-font{
font-weight: bold;
}
.line-through{
text-decoration: line-through;
}
使用模板
<template>
<div>
<div :class="{'yellow-bk':$route.query.isEdit === 'true'}">yellow-bk</div>
<div class="default999" :class="{'yellow-bk':$route.query.isEdit === 'true'}">yellow-bk default999</div>
<div class="border-red" :class="$route.query.isEdit === 'true' ? 'yellow-bk' : 'default999'">yellow-bk | default / border-red</div>
<div class="border-red" :class="[$route.query.id === 'xxx' ? 'red' : 'green', editStyle]">red | green / bold-font | line-through /border-red</div>
<div class="border-red" :style="{
'margin-bottom':$route.query.isEdit === 'true' ? '8px' : '10px',
'display':$route.query.isEdit === 'true' ? 'block' : 'flex'
}">style 1 /border-red</div>
<div class="border-red" :style="errStyle">style 2 /border-red / errStyle</div>
</div>
</template>
<script>
export default {
data (){
return {
errStyle:{
color:"red",
background:"#85ce61"
}
}
},
computed:{
editStyle(){
return this.$route.query.isEdit === "false" ? "bold-font" : "line-through"
}
},
methods: {
/**
* 输入框改变
* @param e
*/
inputChange(e) {
let v = e.target.value
this.$emit("input", v)
this.$emit("change", v)
},
},
}
</script>
style其实就是一个对象,所以不管怎么变,只要得到一个对象或者返回一个对象即可
class花样要多点,模板有对象形式,三元运算符形式,数组形式,默认类名。当然也可以用函数返回
jsx
<script>
export default {
data (){
return {
errStyle:{
color:"red",
background:"#85ce61"
}
}
},
computed:{
editStyle(){
return this.$route.query.isEdit === "false" ? "bold-font" : "line-through"
}
},
methods: {
/**
* 输入框改变
* @param e
*/
inputChange(e) {
let v = e.target.value
this.$emit("input", v)
this.$emit("change", v)
},
},
render(){
return <div>
<div class={{'yellow-bk':this.$route.query.isEdit === 'true'}}>yellow-bk</div>
<div class={{ 'yellow-bk': this.$route.query.isEdit === 'true',default999:true }}>yellow-bk/default999</div>
<div class={this.$route.query.isEdit === 'true' ? 'yellow-bk border-red' : 'default999 border-red'}>yellow-bk /border-red</div>
<div class={[this.$route.query.id === 'xxx' ? 'red' : 'green', this.editStyle,'border-red']}>red | green / bold-font | line-through /border-red </div>
<div class="border-red" style={{
'margin-bottom':this.$route.query.isEdit === 'true' ? '8px' : '10px',
'display':this.$route.query.isEdit === 'true' ? 'block' : 'flex'
}}>style 1 /border-red</div>
<div class="border-red" style={this.errStyle}>style 2 /border-red / errStyle</div>
</div>
}
}
</script>
模板语法可以使用两个class,一个正常使用,一个变量形式 ,如
<span class="red" :class={blue:isEdit}></span>
jsx只能写一个class,上面需要写成
<span :class={blue:isEdit,red:true}></span>
3 指令
在jsx里面,指令变成小驼峰,例如v-model可变为vModel
3.1 自定义指令
模板语法
<template>
<div>
<div v-default></div>
<div v-default:指令参数默认值></div>
<div v-default="{name:'动态数据默认值'}"></div>
<div v-default:指令参数默认值="{name:'动态数据默认值'}"></div>
</div>
</template>
<script>
export default {
directives:{
default:{
// 当被绑定的元素插入到 DOM 中时……
bind: function (el, binding, vnode) {
const actionName = binding.arg
const value = binding.value
console.log("actionName",actionName)
console.log("value",value)
let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)): "")
el.innerHTML=innerHtml || "自定义指令"
}
}
}
}
</script>
代码运行后结果如图:
image.png
jsx
<script>
export default {
directives:{
default:{
// 当被绑定的元素插入到 DOM 中时……
bind: function (el, binding, vnode) {
const actionName = binding.arg
const value = binding.value
console.log("actionName",actionName)
console.log("value",value)
let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)): "")
el.innerHTML=innerHtml || "自定义指令"
}
}
},
render(createElement, context) {
return <div>
<div vDefault></div>
<div vDefault:指令参数默认值></div>
<div vDefault={{name:'动态数据默认值'}}></div>
<div vDefault:指令参数默认值={{name:'动态数据默认值'}}></div>
</div>
}
}
</script>
上面这段代码看似没问题,但是运行后,我们发现结果如下:
image.png
发现多了个true,也就是指令没有传值的时候,默认为true,想要实现模板的效果,还需更改下,vDefault={undefined},下面是更改后的代码
<script>
export default {
name:"Input",
directives:{
default:{
// 当被绑定的元素插入到 DOM 中时……
bind: function (el, binding, vnode) {
const actionName = binding.arg
const value = binding.value
console.log("actionName",actionName)
console.log("value",value)
let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)): "")
el.innerHTML=innerHtml || "自定义指令"
}
}
},
render(createElement, context) {
return <div>
<div vDefault={undefined}></div>
<div vDefault:指令参数默认值={undefined}></div>
<div vDefault={{name:'动态数据默认值'}}></div>
<div vDefault:指令参数默认值={{name:'动态数据默认值'}}></div>
</div>
}
}
</script>
3.2 内置指令
这些指令有 v-html,v-if,v-for,v-text,v-show,v-model,v-bind,v-on,v-slot等。其中只有少部分适用于驼峰形式
3.2.1 适用于驼峰形式的指令:v-show,v-model,v-on(在事件绑定处单独说明)
以表单双向数据绑定的v-model举例
模板语法
<template>
<a-input v-model="value">
</template>
<script>
export default{
data(){
return {
value:"",//值
}
}
}
</script>
使用jsx
<script>
export default{
data(){
return {
value:"",//值
}
},
render(){
return <a-input vModel={this.value}/>
}
}
</script>
修饰符
模板语法
<input v-model.trim="value"/>
jsx,使用_分隔修饰符
<input vModel_trim={this.value} />
3.2.2 不适用与驼峰形式的指令。内置指令大部分都不适用于驼峰形式,除v-slot放插槽处单独说明外,下面一一列举。
3.2.2.1 v-html
我们先用v-html来试下使用驼峰形式的例子
模板语法
<template>
<div v-html="'自定义html'"></div>
</template>
按照上面的写法使用jsx
<script>
export default {
render(createElement, context) {
return <div>
<div vHtml={"自定义html"}></div>
</div>
}
}
</script>
写好后,我们运行,发现报错
vue.runtime.esm.js?c320:619 [Vue warn]: Failed to resolve directive: html
(found in)
image.png
就是说html不是一个指令。@vue/babel-preset-jsx给出的标准写法是使用domPropsInnerHTML
<script>
export default {
name:"Input",
render(createElement, context) {
return <div>
<div domPropsInnerHTML={"自定义html"}></div>
</div>
}
}
</script>
3.2.2.2 v-text
模板语法
<template>
<div>
<div v-text="text"></div>
</div>
</template>
<script>
export default {
data(){
return{
text:"vText文字"
}
}
}
</script>
jsx语法,使用domPropsInnerText
<script>
export default {
name: 'JsxExample',
data(){
return{
text:"vText文字"
}
},
render() {
return <div>
<div domPropsInnerText={this.text}></div>
</div>
}
}
</script>
3.2.2.3 v-if
这恐怕是最简单的了,v-if就是if else 语法
使用模板
<template>
<div>
<div v-if="$route.query.id === 'xxx'">测试</div>
<div v-else>else渲染</div>
</div>
</template>
jsx
使用 domPropsInnerText
<script>
export default {
render(){
if(this.$route.query.id === 'xxx'){
return <div>测试</div>
}
return <div>else渲染</div>
}
}
</script>
或者
<script>
export default {
render(){
return <div>{this.$route.query.id === 'xxx' ? "测试" : "else渲染"}</div>
}
}
</script>
3.2.2.4 v-for
使用模板
<template>
<div>
<div v-for="(item,index) in list" :key="index">
<span>{{item + index}}</span>
</div>
</div>
</template>
<script>
export default {
data (){
return {
list:["测试","测试","测试","测试","测试","测试","测试"]
}
},
}
</script>
使用jsx
v-for 的jsx习惯使用map方法和reduce方法。最终的结果就是得到一个由dom节点组成的数组。所以除了习惯性的map和reduce方法以外,理论上可以遍历的方法都可以使用。下面分别使用map,reduce和for 循环来实现。
<script>
export default {
data (){
return {
list:["测试","测试","测试","测试","测试","测试","测试"],//数据列表
}
},
methods:{
/**
* 使用map
* @returns {JSX.Element}
*/
renderDomUseMap(){
return <div>
{
this.list.map((item,index)=><div><span>{item + index} use map</span></div>)
}
</div>
},
/**
* 使用reduce
* @returns {JSX.Element}
*/
renderDomUseReduce(){
return <div>
{
this.list.reduce((result,current,index)=>{
result.push(<div><span>{current + index} use reduce</span></div>)
return result
},[])
}
</div>
},
/**
* 使用for循环
* @returns {[]}
*/
renderDomUseFor(){
let listDom = []
for(let i=0;i<this.list.length;i++){
listDom.push(<p>{this.list[i] + i} use for</p>)
}
return <div>
{listDom}
</div>
}
},
render(createElement, context) {
// return this.renderDomUseMap()
// return this.renderDomUseFor()
return this.renderDomUseReduce()
}
}
</script>
3.2.2.5 v-bind="$attrs"
封装组件的时候,为了能全部集成我们组件内依赖的某个组件的属性,比如我们封装一个自定义功能的输入框,希望能全部基础a-input的属性,又不想去全部吧属性定义一遍。这时候会用到v-bind="$attrs"
我们先用模板语法定义一个输入框组件,组件名字my-input.vue。这里的输入框基于ant-design-vue 的a-input组件
<template>
<div>
<a-input :value="value" v-bind="$attrs" @change="inputChange"/>
</div>
</template>
<script>
export default {
name:"MyInput",
props:{
value:String,//值
},
methods:{
/**
* 点击
*/
inputChange(e){
console.log(e)
this.$emit("input",e.target.value)
this.$emit("change",e.target.value)
}
}
}
</script>
然后我们在父级页面引用,这里父级页面为home-view.vue
<template>
<div class="home">
<my-input v-model="inputValue" style="width: 300px;margin:0 auto;"/>
</div>
</template>
<script>
import MyInput from '@/components/my-input.vue'
export default {
name: 'HomeView',
components: {
MyInput
},
data(){
return{
inputValue:"",//值
}
}
}
</script>
代码运行结果界面
image.png
现在我们在父级组件引用标签处加上我们组件内并没有定义的属性,addon-before,虽然我们没有定义,但是a-inpu携带该属性,且my-input组件使用了v-bind="$attrs"
<my-input v-model="inputValue" addon-before="Http://" style="width: 300px;margin:0 auto;"/>
加上后运行效果如下
image.png
jsx语法
下面用jsx实现v-bind="$attrs"
<script>
export default {
name: 'MyInput',
props:{
value:String,//值
},
methods:{
/**
* 点击
*/
inputChange(e){
console.log(e)
this.$emit("input",e.target.value)
this.$emit("change",e.target.value)
}
},
render() {
return <div>
<a-input value={this.value} vOn:change={this.inputChange} attrs={this.$attrs} />
</div>
}
}
</script>
只需在a-input标签上加上 attrs={this.$attrs} 即可
4 如何绑定事件
4.1 普通事件绑定
模板语法
<template>
<div>
<div @click="bindEvent">绑定事件</div>
<a-input @change="inputChange" v-model="value"></a-input>
</div>
</template>
<script>
export default {
name:"Input",
data(){
return{
value:"",//值
}
},
methods:{
/**
* 绑定事件
*/
bindEvent(e){
console.log(e)
},
/**
* 输入事件
* @param e
*/
inputChange(e){
console.log(e)
}
}
}
</script>
jsx
<script>
export default {
name:"Input",
data(){
return{
value:"",//值
}
},
methods:{
/**
* 绑定事件
*/
bindEvent(e){
console.log(e)
},
/**
* 绑定事件
*/
bindEventByVon(e){
console.log(e)
},
/**
* 输入事件
* @param e
*/
inputChange(e){
console.log(e)
},
input(e){
this.value = e.target.value
}
},
render(){
return <div>
<div onClick={this.bindEvent}>绑定事件</div>
<div vOn:click={this.bindEventByVon}>v-on指令形式绑定事件</div>
<a-input onChange={this.inputChange} vModel={this.value} />
</div>
}
}
</script>
结合事件说下v-model。由于v-model是由属性 value和事件input组成,因此 v-model除了如上述示例使用vModel以外,还可以分开写,如下
<a-input onChange={this.inputChange} value={this.value} onInput={e=>this.value=e.target.value}/>
4.2 绑定事件时传递参数
在模板语法中,我们可以随意如下书写
<div>
<a-input @input="input()"/>
<a-button @click="submit('form')">按钮</a-button>
</div>
使用jsx时,按照模板语法的思路和习惯,我们可能会如下书写
<div>
<a-input onInput={this.input()}/>
<a-button onClick={this.submit('form')}>按钮</a-button>
</div>
这时候会发现,页面刚加载事件就被调用了。如果把模板语法看成是在页面写html的话,写jsx就是通过javascript创建页面元素,所以this.input()就是直接调用了该函数,所以不能写括号,需要写出this.input,也就是不需要调用,因为事件需要某些因素条件才能出发。那同理,我们也不能写成this.submit('form'),这样函数就会直接被调用了。但是事件确实需要传参的话,就需要套在一个匿名函数里面调用,如下
<div>
<a-input onInput={()=>this.input()}/>
<a-button onClick={()=>this.submit('form')}>按钮</a-button>
</div>
4.3 事件修饰符
在vue里面,有些很好用得事件修饰符,比如@click.stop @click.13等。jsx里面修饰符用_连接
模板语法
<template>
<div>
<input @click.stop.prevent="click" />
</div>
</template>
<script>
export default {
methods:{
/**
* 点击
*/
click(){
console.log("点击")
}
}
}
</script>
jsx
<script>
export default {
name: 'JsxExample',
methods:{
click(){
console.log("click")
}
},
render() {
return <div>
<input vOn:click_stop_prevent={this.click} />
</div>
}
}
</script>
4.4 v-on="$listeners"
和v-bind="$attrs"类似功能,v-on="$listeners"可以让子组件继承所有我们依赖的组件的事件
模板语法
<template>
<div>
<a-input :value="realValue" v-bind="$attrs" v-on="$listeners" @input="inputEvent" />
</div>
</template>
<script>
export default {
props:{
value:[InputEvent,String],//值
},
data(){
return{
realValue:"",//真实的值
}
},
watch:{
value:function (e){
this.realValue = (typeof e === "string" || !e) ? e : e.target.value
}
},
methods:{
inputEvent(e){
console.log(e)
this.$emit("input",e)
}
}
}
</script>
这里顺便讲下基于ant-design-vue和基于element-ui的输入框使用v-on="$listeners"时的一些小区别。
ant-design-vue 的 a-input 的 input事件反出的是event事件,但是value属性接收的是字符串或数字。因此不能直接将prop的value赋值给 a-input,需要单独做处理后,见上面代码的 watch监听。使用v-on="$listeners"的情况下,直接将prop的value赋值给 a-input,会重新触发$listeners里面的input或者change事件,造成接收值不准确,报错。
使用element-ui就不存在这个问题,因为element-ui的input事件直接返回value值,而不是event事件。使用element-ui可以如下:
<template>
<div>
<el-input :value="value" v-bind="$attrs" v-on="$listeners" @input="inputEvent" />
</div>
</template>
<script>
export default {
props:{
value:[String],//值
},
methods:{
inputEvent(value){
console.log(value)
this.$emit("input",value)
}
}
}
</script>
那如何验证v-on="$listeners"生效呢?我们在父级组件绑定一个没有直接声明的事件即可。这里以ant-design-vue 的 a-input举例。ant-design-vue的a-input组件有个回车事件pressEnter。
父级组件HomeView.vue代码
<template>
<div class="home">
<my-input v-model="inputValue" addon-before="Http://" style="width: 300px;margin:0 auto;" @pressEnter="pressEnter"/>
</div>
</template>
<script>
import MyInput from '@/components/jsx-example.vue'
export default {
name: 'HomeView',
components: {
MyInput
},
data(){
return{
inputValue:"",//值
}
},
methods:{
/**
* 按下回车键
*/
pressEnter(e){
console.log(e)
},
}
}
</script>
运行后在输入框按回车健,我们可以看到pressEnter事件成功打印了值
v-on="$listeners"的jsx语法。使用on监听
<script>
export default {
name: 'MyInput',
props:{
value:[String,InputEvent],//值
},
data(){
return{
realValue:"",//真实的值
}
},
watch:{
value:function (e){
this.realValue = (typeof e === "string" || !e) ? e : e.target.value
}
},
methods:{
/**
* 点击
*/
inputChange(e){
console.log(e)
this.$emit("input",e)
}
},
render() {
return <div>
<a-input value={this.realValue}
attrs={this.$attrs}
vOn:change={this.inputChange}
on={this.$listeners}
/>
</div>
}
}
</script>
既然可以用on属性,那我们在jsx监听事件时,也可以直接在on里面书写。如下
render() {
return <div>
<a-input value={this.realValue}
attrs={this.$attrs}
on={{
change:this.inputChange,
...this.$listeners
}}
/>
</div>
}
5 插槽
插槽包括父组件使用jsx和子组件使用jsx,默认插槽,具名插槽以及作用域插槽。
5.1 默认插槽与具名插槽
我们先从简单的例子开始,创建一个my-slot组件,使用模板语法,组件里面包括默认插槽和具名插槽
my-slot.vue
<template>
<div>
<div>
<slot name="top"></slot>
</div>
<slot></slot>
<div>
<slot name="bottom"></slot>
</div>
</div>
</template>
然后我们在父级组件,HomeView.vue同样使用模板语法使用插槽,代码如下
<template>
<div class="home">
<my-slot>
默认插槽
<template #top>
顶部插槽内容
</template>
<template #bottom>
底部部插槽内容
</template>
</my-slot>
</div>
</template>
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'HomeView',
components: {
MySlot
},
}
</script>
或者使用vue比较老的插槽使用语法slot属性,该属性在vue 2.6.0版本后被废弃
<template>
<div class="home">
<my-slot>
默认插槽
<div slot="top">
顶部插槽内容
</div>
<div slot="bottom">
底部部插槽内容
</div>
</my-slot>
</div>
</template>
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'HomeView',
components: {
MySlot
},
}
</script>
新建一个AboutView.vue,作为新的父级组件,使用jsx语法
按jsx-vue2示例的写法
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'AboutView',
components: {
MySlot
},
render() {
return <div class="home">
<my-slot>
<div slot="top">
顶部插槽内容
</div>
默认插槽
<div slot="bottom">
底部部插槽内容
</div>
</my-slot>
</div>
}
}
</script>
父级使用jsx语法使用插槽还是比较简单的,和模板语法没啥区别,甚至和模板语法被废弃的slot属性完全一样。
接下来我们对my-slot.vue进行jsx改造。jsx里面,子组件使用this.$slots接收插槽,默认插槽的名字是default。代码如下
<script>
export default {
name: 'MySlot',
render() {
const slots = this.$slots
console.log(slots)
return <div>
<div>
{slots.top}
</div>
{slots.default}
<div>
{slots.bottom}
</div>
</div>
}
}
</script>
5.2 作用域插槽
作用域插槽,就是父级组件可以使用子组件通过prop传递过来的变量的插槽。我们先将模板语法的my-slot定义的插槽改造成作用域插槽
<template>
<div>
子组件原本内容
<div>
<slot name="testScopeSlot" :user="user"></slot>
</div>
</div>
</template>
<script>
export default {
data(){
return{
user:{
name:"张三"
},//用户信息
}
}
}
</script>
相应的,对HomeView.vue做相应的改造,以便能够接收使用user
<template>
<div class="home">
<my-slot>
<template #testScopeSlot="{user}">
作用域插槽内容:{{user.name}}
</template>
</my-slot>
</div>
</template>
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'HomeView',
components: {
MySlot
},
}
</script>
若父级组件使用vue 2.6.0后废弃的语法,如下
<template>
<div class="home">
<my-slot>
<div slot="testScopeSlot" slot-scope="{user}">
作用域插槽内容:{{user.name}}
</div>
</my-slot>
</div>
</template>
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'HomeView',
components: {
MySlot
},
}
</script>
对AboutView.vue进行改造,以便能使用jsx语法接收和使用my-slot的user变量
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'AboutView',
components: {
MySlot
},
render() {
return <div class="home">
<my-slot
scopedSlots={{
testScopeSlot: ({user}) => {
return `作用域插槽内容:${user.name}`
}
}}
>
</my-slot>
</div>
}
}
</script>
这里相对之前的都比较难于理解,用slot slot-scope已经不管用了。父组件想要读到子组件通过插槽返出的变量,需要在子组件标签上挂载scopedSlots属性。scopedSlots是一个对象,里面包含了子组件定义的各个插槽,以名字为键名,键值是一个函数。默认插槽名字仍然是default。本示例定义的插槽名字是testScopeSlot,testScopeSlot的值是函数,函数的参数是对象,对象里包含user,即子组件返出的变量名。
下面我们使用jsx改造my-slot的作用域插槽
<script>
export default {
name: 'MySlot',
data(){
return{
user:{
name:"张三"
},//用户信息
}
},
render() {
const scopedSlots = this.$scopedSlots
console.log(scopedSlots)
// const testScopeSlotDom = scopedSlots.testScopeSlot({user:this.user})
// console.log(testScopeSlotDom)
return <div>
子组件原本内容
<div>
{scopedSlots.testScopeSlot({user:this.user})}
</div>
</div>
}
}
</script>
image.png
由于testScopeSlot是一个函数,因此我们只需要执行testScopeSlot函数即可,然后将use作为函数的参数传递就行。这里有点绕,可以这样反过来理解,父级组件定义了一个函数,函数接收一个对象参数,对象中包含user属性,将这个函数传递到子组件,子组件执行这个函数,并将子组件变量作为参数传递给函数,子组件执行函数后,函数将相应的结果return出去,被父组件接收,然后父组件处理,用于显示。
下面我们将最初定义的默认插槽和具名插槽都改成作用域插槽试试。更改后的my-slot
<script>
export default {
name: 'MySlot',
data(){
return{
topInfo:"我是顶部插槽数据",//顶部插槽
defaultInfo:"我是默认插槽数据",//顶部插槽
bottomInfo:"我是顶部插槽数据",//顶部插槽
}
},
render() {
const scopedSlots = this.$scopedSlots
const {topInfo,defaultInfo,bottomInfo} = this
return <div>
<div>
{scopedSlots.top({topInfo})}
</div>
{scopedSlots.default({defaultInfo})}
<div>
{scopedSlots.bottom({bottomInfo})}
</div>
</div>
}
}
</script>
相应的,我们更改AboutView.vue文件
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'AboutView',
components: {
MySlot
},
render() {
return <div class="home">
<my-slot
scopedSlots={{
top: ({topInfo}) => {
return `作用域插槽内容:${topInfo}`
},
default: ({defaultInfo}) => {
return `作用域插槽内容:${defaultInfo}`
},
bottom: ({bottomInfo}) => {
return `作用域插槽内容:${bottomInfo}`
}
}}
>
</my-slot>
</div>
}
}
</script>
运行结果
image.png
按照vue默认定义的作用域插槽数据,参数是一个对象形式。因此我们在子组件执行函数时,需要按对象形式传递,如 { topInfo } 。既然是我们自己传递参数,那我们是不是可以更改下参数传递形式,如下 my-slot.vue
<script>
export default {
name: 'MySlot',
data(){
return{
topInfo:"我是顶部插槽数据",//顶部插槽
defaultInfo:"我是默认插槽数据",//顶部插槽
bottomInfo:"我是顶部插槽数据",//顶部插槽
}
},
render() {
const scopedSlots = this.$scopedSlots
const {topInfo,defaultInfo,bottomInfo} = this
return <div>
<div>
{scopedSlots.top(topInfo)}
</div>
{scopedSlots.default(defaultInfo)}
<div>
{scopedSlots.bottom(bottomInfo)}
</div>
</div>
}
}
</script>
然后相应的 AboutView.vue做更改
<script>
import MySlot from '@/components/my-slot.vue'
export default {
name: 'AboutView',
components: {
MySlot
},
render() {
return <div class="home">
<my-slot
scopedSlots={{
top: (topInfo) => {
return `作用域插槽内容:${topInfo}`
},
default: (defaultInfo) => {
return `作用域插槽内容:${defaultInfo}`
},
bottom: (bottomInfo) => {
return `作用域插槽内容:${bottomInfo}`
}
}}
>
</my-slot>
</div>
}
}
</script>
这里把传递和接收参数都改成字符串,运行结果相同。这里也提现了jsx在某种情况下的优势,相比模板语法,jsx能更灵活的控制代码逻辑。
参考:vue2-jsx: https://github.com/vuejs/jsx-vue2
网友评论