[Vue深入組件]:v-model語法糖與自定義v-model


1. v-model 語法糖

當你希望一個自定義組件的值能夠實現雙向綁定。 那么就需要:

  1. 將值傳入組件;
  2. 將變化的值逆傳回父組件。

實際上,就可以利用 props 實現的父傳子 + 通過自定義事件this.$emit實現的子傳父。實現雙向的數據流傳遞。

下面是一個示例:

有這樣一個父組件:

<template>
  <div>
    <Child :cusProp="message" @cusEvent="message = $event" />
    文字:{{message}}
  </div>

</template>
<script>
import Child from "./comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      message: 'init default'
    }
  }
}
</script>

和這樣的一個子組件:

<template>
  <div>
    this is child comp
    <input type="text" :value="cusProp" @input="onInputChange">
  </div>
</template>
<script>
export default {
  props:["cusProp"],
  methods: {
    onInputChange(e) {
      this.$emit('cusEvent', e.target.value)
    }
  }
}
</script>

image-20210824220325859

我們自定義了一個組件,名為<Child /> , 我們通過 v-bind:cusProp<Child /> 傳遞了一個名為 "cusProp" 的prop , 即 <Child :cusProp="message" />

然后在<Child />組件內部,通過props接收到了這個值,並通過v-bind:cusProp 將值綁定給了<input /> 元素。

緊接着,我們給<input /> 元素設定了一個input 監聽事件, 當輸入時,觸發該事件,然后將當前值通過this.$emit('cusProp',e.target.value) 觸發了一個我們自定義命名為"cusProp"的自定義事件,以參數的形式,將變化后的值逆向傳遞(子傳父)給了父組件。 在父組件中接收到變化后的值,然后通過$event 將值賦給了綁定的 message

從而實現了自定義的雙向綁定。

實際上,上邊這個過程,可以簡化為一個vue為我們預定義實現的v-model, 但是不能直接替換,我們需要做一些簡單的處理。這就涉及到了自定義v-model

2. 自定義v-model

2.1 v-model 語法糖, 以及最簡單的自定義v-model

首先,我們仿照着vue文檔的舉例,嘗試去理解需要自定義v-model的使用場景。

文檔中有這樣一段描述很重要

一個組件上的 v-model 默認會利用名為 value 的 prop 和名為 input 的事件

文檔中的這段話極為概要,但是這句話蘊含了很重要的一些細節:

實際,當你使用v-model 的時候,默認是,是傳遞的名為valueprop ,且$emit觸發的自定義事件的事件名是input

而回頭看看我們剛才寫的組件:

父組件:

 <Child :cusProp="message" @cusEvent="message = $event" />

子組件:

<input type="text" :value="cusProp" @input="onInputChange">
...
props:["cusProp"]
...
onInputChange(e) {
    this.$emit('cusEvent', e.target.value)
}

我們默認傳遞的prop值名為"cusProp", 即v-bind:cusProp , 且$emit觸發的自定義事件名為cusEvent 。 並不滿足能直接寫作v-model 的形式其前提條件。 所以我們不能直接替換。

我們需要做一些簡單的變化:

父組件:

<template>
  <div>
    <Child :value="message" @input="message = $event" /> <!--改動行-->
    文字:{{message}}
  </div>

</template>
<script>
import Child from "./comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      message: 'init default'
    }
  }
}
</script>

子組件:

<template>
  <div>
    this is child comp
    <input type="text" :value="value" @input="onInputChange"> <!--改動行-->
  </div>
</template>
<script>
export default {
  props:["value"],// --改動行--
  methods: {
    onInputChange(e) {
      this.$emit('input', e.target.value) //--改動行--
    }
  }
}
</script>

我們把prop 值的改為了value, 把$emit 觸發的事件改為了input 現在,我們就能寫作v-model 的 形式了,保持子組件不變,直接替換父組件中即可:

<Child v-model="message" />

自此,我們便能夠理解,為什么說v-model 實際上就是props + $emit 自定義事件的語法糖 。

2.2 通用自定義v-model

上邊的示例中,我們由於不滿足先是利用了props 父傳子,和自定義事件的子傳父,手動實現了一個數據流的雙向綁定。

緊接着,我們介紹了v-model 的實質,就是props + 自定義事件 的語法糖。 然后我們期望將我們自己的手動實現,簡化成v-model語法糖的形式。

文檔告訴我們,需要滿足兩個基本的默認條件:

  1. prop 名默認須為value
  2. $emit 觸發的自定義事件名默認須為input

而我們的手動實現起初並不滿足要求(prop ---- cusProp, $emit ---- cusEvent), 所以我們做了部分修改,以滿足默認的條件。 從而實現了將手動實現,轉換成了v-model語法糖的形式。

但是,這里有一個問題,就是v-model 默認的兩個條件,會對我們有着很大的限制,這里封裝的是一個<input/>輸入框,以value prop值,以input 作為自定義事件名,本身是合乎習慣的,但是,日常開發中,我們不可能只封裝一個輸入框,可不能所有的自定義v-model 組件,都以value 傳遞,自定義事件一定名為input ,這顯然是不合理的,也有違“自定義事件” 。我們開發工作中,可能更多的需要自定義指定prop名,和自定義事件。 為了更好的說明這個問題,解決通用性,下面我們通過一個示例來加深了解:

這里之所以要着重強調,是因為很容易出錯,這個文檔中說的input 事件到底指的是,自定義子組件中元素的監聽事件名為input,還是說$emit觸發的事件名為input。 以上就是為了強調,是后者,是$emit觸發的事件名,默認情況下,必須為input。 盡管它是自定義事件名,這也是之所以容易出錯的地方。

這里我們同樣使用<input/ 這個元素,但是,不再用輸入框了(type="text") ,我們將其指定為一個checkbox 看看會怎么樣呢?

<!--Father Component-->
<template>
  <div>
    <Child :cusProp="status" @cusEvent="status = $event" />
    狀態:{{status}}
  </div>

</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      status: true
    }
  }
}
</script>
<!-- Child Component-->
<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="cusProp" @change="onChange">
  </div>
</template>
<script>
export default {
  props:["cusProp"],
  methods: {
    onChange(e) {
      this.$emit('cusEvent', e.target.checked)
    }
  }
}

image-20210824220413039

一樣的,如果此時,你想寫作v-model語法糖的形式。就需要想剛才那樣做一些改動:

父組件:

<template>
  <div>
    <Child v-model="status" /> <!--改動行-->
    狀態:{{status}}
  </div>

</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      status: true
    }
  }
}
</script>

子組件:

<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="value" @change="onChange"> <!--改動行-->
  </div>
</template>
<script>
export default {
  props:["value"], //--改動行--
  methods: {
    onChange(e) {
      this.$emit('input', e.target.checked) //--改動行--
    }
  }
}

現在,由於這是一個checkbox ,我們可以放大剛才所描述的限制了。 你莫名奇妙的加上接受了一個名為value 的prop, 以及通過$emit 觸發了一個莫名其妙的input自定義事件。 盡管它能夠如期的正常工作。 當代碼量多了之后, 你會發現這種組件異常難以維護。

那么到底該怎么解決這樣一種場景呢?

其實非常簡單 , 只需要指定一個model 對象屬性即可:

我們只需要在剛才的基礎上,在<Child/>組件中指定如下model配置加以稍微改動即可:

<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="cusProp" @change="onChange">	<!--改動行-->
  </div>
</template>
<script>
export default {
  props:["cusProp"], 	//改動行 //當然一般直接寫父組件v-model的變量名, 這里為了說明是任意名所以 寫了個cusProp
  model:{				//改動行
    prop:'cusProp', 	//改動行
    event:'cusEvent'	//改動行
  },//改動行
  methods: {
    onChange(e) {
      this.$emit('cusEvent', e.target.checked)	//改動行
    }
  }
}

vue 為我們提供了一個名為model的實例配置項, 它可以指定一個任意的變量名,用於接受父組件中v-model 的傳遞值, 還可以指定一個任意的事件名, 用以"代理", $emit的觸發事件.

這樣,就解決了難以后期維護的問題,使得有雙向綁定需求的組件封裝更加的通用.

3. 總結

所以,總結一下。

什么情況下需要自定義v-model

  1. 當有自定義組件的雙向數據流的需求的時候,都可以自定義v-model 來達成目的。

    1. 其中,什么時候需要配置 model 屬性?

      當默認通過v-bind prop傳遞到自定義組件的變量名不是默認的value或者 觸發自定義事件的事件名不為input 的時候。

    2. 什么時候不需要配置model 屬性?

      當滿足默認的v-model規則時,即 prop傳遞到自定義組件的變量名為value 觸發自定義事件的事件名為input 的時候,不需要指定model屬性配置。可直接使用v-model 這種情況比較少見,基本僅當自定義組件是為了擴展type="text"<input/> 元素時才符合條件。

特別注意的一點:

自定義事件內部,可以通過任意事件去觸發$emit ,但是一般是通過DOM監聽事件,例如@change@input@click,等等。 但是默認情況下,如果不配置model實例配置,加以指明,$emit 觸發的事件名須是"input" 。 主要是不要混淆,這么默認情況下的約束規則,input 事件,指的是$emit觸發的事件名,而不是自定義子組件內部觸發$emit 的事件。

通過model 實例配置,實際上幫我們解決的主要問題是日后的維護問題,和代碼易讀性。 它相當於背后幫我們自動將默認propvalue ,默認自定義事件為input 做了一層別名化處理(alias),從而讓我們能夠去自定義任何名稱。

4. 附加拓展,實踐一個常見的v-model業務需求

【需求:】

假設現在有這樣一個需求(基於antdv)

image-20210824222912833

有這樣一個區域級聯選擇器,我希望,我能從父組件中給它一個初始值cascaderSelected:"浙江/杭州"。 在級聯選擇器值變換以后,這個cascaderSelected值響應式的變化。 要求利用v-model 實現,從而讓代碼簡潔高效。

實現:

父組件:

<template>
  <div>
    <cus-area-cascader v-model="cascaderSelected"/>
    當前選中區域:{{cascaderSelected}}
  </div>

</template>
<script>
import CusAreaCascader from "../cusVModelPractice/comps/CusAreaCascader.vue"
export default {
  components: {
    CusAreaCascader
  },
  data() {
    return {
      cascaderSelected: ['zhejiang', 'hangzhou','xihu']
    }
  }
}
</script>

子組件:

<template>
  <a-cascader :options="options" :value="onPropHandle" placeholder="Please select" @change="onChange" />
</template>
<script>
export default {
  props:['onPropHandle'],
  model:{
    prop:'onPropHandle',
    event:'onChangeHandle'
  },
  data() {
    return {
      options: [
        {
          value: 'zhejiang',
          label: 'Zhejiang',
          children: [
            {
              value: 'hangzhou',
              label: 'Hangzhou',
              children: [
                {
                  value: 'xihu',
                  label: 'West Lake',
                },
              ],
            },
          ],
        },
        {
          value: 'jiangsu',
          label: 'Jiangsu',
          children: [
            {
              value: 'nanjing',
              label: 'Nanjing',
              children: [
                {
                  value: 'zhonghuamen',
                  label: 'Zhong Hua Men',
                },
              ],
            },
          ],
        },
      ],
    };
  },
  methods: {
    onChange(value) {
      this.$emit('onChangeHandle',value)
    },
  },
};
</script>

image-20210824224431745

目標達成。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM