什么是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也可以有多種形式
測試樣式效果圖
樣式
.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>
代碼運行后結果如圖:
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>
上面這段代碼看似沒問題,但是運行后,我們發現結果如下:
發現多了個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)
就是說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>
代碼運行結果界面
現在我們在父級組件引用標簽處加上我們組件內并沒有定義的屬性,addon-before,雖然我們沒有定義,但是a-inpu攜帶該屬性,且my-input組件使用了v-bind="$attrs"
<my-input v-model="inputValue" addon-before="Http://" style="width: 300px;margin:0 auto;"/>
加上后運行效果如下
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>
由于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>
運行結果
按照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