Vue2 + JSX使用總結

什么是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

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容