Vue3 新特性 + TypeScript 小實戰(zhàn)

上次將 Composition API 大致梳理了一遍 ,這次主要是想記錄一些 vue3 相較 vue2 新增出來的一些特性和一些方法上使用的變動,話不多說,直接開擼愤炸。

Teleport


我們?nèi)粘i_發(fā)中經(jīng)常會遇到這樣一個場景,比如我們封裝一個彈層 msk 組件掉奄,但是它包裹在一個 position: relative 定位的父元素中规个,此時它將被局限在父元素的大小中,我們很難將彈層鋪滿整個窗口姓建。而 Teleport 提供了一種干凈的方法诞仓,允許我們控制在 DOM 中哪個父節(jié)點下渲染了 HTML,而不必求助于全局狀態(tài)或?qū)⑵洳鸱譃閮蓚€組件速兔。舉個栗子:

<body>
  <div id="root"></div>
  <div id="center"></div>
</body>
<script>
const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <msk></msk>
    </div>
  `
})
app.component('msk', {
  template: `
    <div class="msk">111</div>
  `
})
app.mount('#root')
</script>

瀏覽器渲染結(jié)果如下:


未使用 teleport

這肯定不是我們想要實現(xiàn)的效果墅拭,我們希望蒙層是充滿整個窗口的,此時我們可以直接將蒙層組件通過 teleport 渲染到 body 下面或者我們指定的 dom 節(jié)點下面憨栽,teleport 上面有一個 to 的屬性帜矾,它接受一個 css query selector 作為參數(shù)翼虫。如下栗子:

<script>
const app = Vue.createApp({
  template: `
  <div class="mask-wrapper">
    // 使用 to 屬性將其掛載到 id = center 的 dom 節(jié)點下
    // 我們也可以直接使用 to = body 將其直接掛載到 body 中
    <teleport to="#center">
      <msk></msk>
    </teleport>
  </div>
`
})
</script>

emits


我們知道在 vue2 中父子組件傳值會用到 props$emit 屑柔,但是在 vue3 中新增了 emits ,它的主要作用是匯總該組件有哪些自定義事件珍剑,可以是一個數(shù)組寫法掸宛,也可以是一個對象寫法,同時在對象寫法中還支持自定義函數(shù)招拙,可以在運行時驗證參數(shù)是否正確唧瘾。

了解它的基礎用法之后我們將 teleport 中寫入的小栗子重寫,讓其組件通信完成最基本的顯示和隱藏的動態(tài)交互功能别凤。當然我們在子組件通過 $emit 觸發(fā)的事件要統(tǒng)一寫入 emits 數(shù)組中進行管理饰序。

const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <button @click="openMsk">打開彈層</button>
      <teleport to="#center">
        <msk :isOpen="isOpen" @closeMsk="closeMsk"></msk>
      </teleport>
    </div>
  `,
  setup() {
    const { ref } = Vue
    const isOpen = ref(false)
    const openMsk = () => {
      isOpen.value = true
    }
    const closeMsk = () => {
      isOpen.value = false
    }
    return { openMsk, isOpen, closeMsk }
  }
})
app.component('msk', {
  props: ['isOpen'],
  // 子組件中我們會向父組件觸發(fā) `closeMsk` 事件,所以將其統(tǒng)一寫入 `emits` 中方便管理維護
  emits: ['closeMsk'], 
  template: `
    <div class="msk" v-show="isOpen">
      <button @click="closeMsk">關閉彈層</button>
    </div>
  `,
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk')
    }
    return { closeMsk }
  }
})
app.mount('#root')

當然我們也可以在 emits 中使用對象寫法规哪,并且傳入驗證的自定義函數(shù):

app.component('msk', {
  props: ['isOpen'],
  emits: {
    // 'closeMsk': null, 無需驗證
    'closeMsk': (payload) => {
      return payload === 111 // 事件觸發(fā)時驗證傳入的值是否為 111
      // 驗證失敗求豫,因為我傳入的是 222
     // 無效的事件參數(shù):事件“closeMsk”的事件驗證失敗。
    }
  }诉稍,
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk', 222)
    }
    return { closeMsk }
  }
}

小伙伴們可以試一試蝠嘉,當然即使我傳入的值和驗證時的值不匹配但是并不會影響這個事件的正常執(zhí)行,只是會在瀏覽器中給出警告提示杯巨。

Suspense


teleport 組件一樣蚤告,這也是 vue3.0 新推出來的一個全新的組件,它的主要作用是和異步組件一起使用服爷,我們可以現(xiàn)在這里回憶一下 vue2.0 中我們是如何使用動態(tài)組件和異步組件的杜恰。

動態(tài)組件

vue 2.0vue3.0 動態(tài)組件的使用方式基本差不多获诈,都是根據(jù)數(shù)據(jù)的變化,結(jié)合 component 這個標簽箫章,來隨時動態(tài)切換組件的實現(xiàn)烙荷。這里簡單做個小回顧:

const app = Vue.createApp({
  setup() {
    const { ref, keepAlive } = Vue
    const currentItem = ref('first-item')
    const handleClick = () => {
      if (currentItem.value === 'first-item') {
        currentItem.value = 'second-item'
      } else {
        currentItem.value = 'first-item'
      }
    }
    return { currentItem, handleClick }
  },
  template: `
    <keep-alive>
      <component :is="currentItem"></component>
    </keep-alive>
    <button @click="handleClick">組件切換</button>
  `
})
app.component('first-item', {
  template: `
    <div>hello world</div>
  `
})
app.component('second-item', {
  template: `
    <input type="text" />
  `
})
app.mount('#root')
異步組件

以前,異步組件是通過將組件定義為返回 Promise 的函數(shù)來創(chuàng)建的檬寂,這里可以直接查看 vue2.0 中如何定義異步組件终抽。但是在 vue3.0 中現(xiàn)在,由于函數(shù)式組件被定義為純函數(shù)桶至,因此異步組件的定義需要通過將其包裝在新的 defineAsyncComponent 助手方法中來顯式地定義昼伴,其實也很簡單,看栗子就知道了:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
    <div>
      <async-show></async-show>
      <async-common-item></async-common-item>
    </div>
  `
})
app.component('asyncShow', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我將在 1s 之后被渲染出來</div>`
      })
    }, 1000)
  })
}))
app.component('async-common-item', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我將在 3s 之后被渲染出來</div>`
      })
    }, 3000)
  })
}))

defineAsyncComponent 接受返回 Promise 的工廠函數(shù)镣屹。從服務器檢索組件定義后圃郊,應調(diào)用 Promiseresolve 回調(diào)。你也可以調(diào)用 reject(reason)女蜈,來表示加載失敗持舆。

接下來就可以引入我們的主角 Suspense 組件,他可以用來接收一個或多個異步組件伪窖,它本身支持兩個具名插槽逸寓,一個承載異步插件返回等待狀態(tài)的插槽,一個承載異步插件返回成功狀態(tài)的插槽覆山。舉個小栗子:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
  <Suspense >
    <template #default> // 異步組件成功內(nèi)容包裹在 default 插槽中
      <async-show></async-show>
    </template>
    <template #fallback> // 異步組件未加載時顯示 fallback里的內(nèi)容
      <h1>loading !!!!</h1>
    </template>
  </Suspense>
`
})

當然 Suspense 組件也支持多個異步組件的插入竹伸,并且它會等待所有異步組件都返回才將其顯示出來,不過此時我們需要在其根上包一層簇宽,如下栗子:

const app = Vue.createApp({
  template: `
    <Suspense >
      <template #default>
        <div>
          <async-show></async-show>
          <async-common-item></async-common-item>
        </div>
      </template>
      <template #fallback>
        <h1>loading !!!!</h1>
      </template>
    </Suspense>
  `
})

Provide / Inject


通常勋篓,當我們需要從父組件向子組件傳遞數(shù)據(jù)時,我們使用 props魏割。想象一下這樣的結(jié)構:有一些深度嵌套的組件譬嚣,而深層的子組件只需要父組件的部分內(nèi)容。在這種情況下钞它,如果仍然將 prop 沿著組件鏈逐級傳遞下去拜银,可能會很麻煩。

對于這種情況须揣,我們可以使用一對 provideinject盐股。無論組件層次結(jié)構有多深,父組件都可以作為其所有子組件的依賴提供者耻卡。這個特性有兩個部分:父組件有一個 provide 選項來提供數(shù)據(jù)疯汁,子組件有一個 inject 選項來開始使用這些數(shù)據(jù)。

上面兩段話摘自官網(wǎng)卵酪,說的很明白幌蚊,基礎的用法其實和 vue2 中差不多谤碳,但是我們知道 vue2 中無法實現(xiàn)數(shù)據(jù)的響應式監(jiān)聽,但是 vue3 中我們使用 composition API 就可以完成對應響應式變化的監(jiān)聽溢豆。我們先來回顧一下 vue2 中的基礎用法:

父組件像孫子組件傳遞固定值
const app = Vue.createApp({
  provide: {
    count: 1
  },
  template: `
    <child />
  `
})
app.component('child', {
  template: `
    <child-child></child-child>
  `
})
app.component('child-child', {
  inject: ['count'],
  template: `
    <div>{{count}}</div>
  `
})

如果我們想使用 provide 傳遞數(shù)據(jù) data 中的值時蜒简,我們就不能用上面這種寫法,我們需要將 provide 轉(zhuǎn)換為返回對象的函數(shù)漩仙。栗子如下:

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  provide() {
    return {
      count: this.count
    }
  }
})
父組件像孫子組件動態(tài)傳值

如果此時我們新增一個按鈕改變父組件中 count 的值搓茬,子組件是無法繼續(xù)監(jiān)聽到改變后的值的。此時如果我們想對祖先組件中的更改做出響應队他,我們需要為 providecount 分配一個組合式API computed

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  methods: {
    handleClick() {
      this.count++
    }
  },
  provide() {
    return {
      count: Vue.computed(() => this.count)
    }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  inject: ['count'],
  template: `
  <div>{{count.value}}</div>
`
})

當然此時我們還沒有用到 Composition API 來實現(xiàn)卷仑,我們在使用 Composiiton API 來重構下上面的代碼:

const { ref, provide, inject } = Vue
const app = Vue.createApp({
  setup() {
    let count = ref(1)
    const handleClick = () => {
      count.value++
    }
    provide('count', count)
    return { handleClick }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  setup() {
    let count = inject('count')
    return { count }
  },
  template: `
  <div>{{count}}</div>
`
})

是不是感覺非常簡單,那么問題來了麸折,剛剛我們使用了 provide / inject 實現(xiàn)了父組件和子孫組件中的數(shù)據(jù)傳遞锡凝,如果兩個毫無關聯(lián)的組件,那么我們應該如何建立數(shù)據(jù)通訊呢垢啼?除了 vuex 你最先能想到什么窜锯,在 2.x 中,Vue實例可用于觸發(fā)通過事件觸發(fā) API 強制附加的處理程序 ($on芭析,$off$once)锚扎,這用于創(chuàng)建 event hub,以創(chuàng)建在整個應用程序中使用的全局事件偵聽器放刨,因為我前面寫過相關文章工秩,關于 vue2 的知識就不在這里過多贅述了尸饺,詳情可以點擊 Vue 常見 API 及問題进统。

但是在 vue3 中廢棄了 $on, $off,為什么會廢棄呢浪听,可以參考文章解讀Vue3中廢棄組件事件進行解讀螟碎。官方推薦我們使用第三方庫 mitt 進行全局事件的綁定和監(jiān)聽。大體用法其實和原來差不多迹栓,這里就不過多贅述了掉分。

Mixin


Mixin 應該算 vue2 中用的比較多的,用法其實和以前大體相差不大克伊,我在 Vue 常見 API 及問題 中記錄過 Mixin 的基礎用法酥郭,官網(wǎng)寫的也挺詳細的,當然現(xiàn)在關于組件公共邏輯的抽離其實更推薦使用 組合式 API 愿吹。這里還是簡單總結(jié)下 Mixin 的幾個特點:

1不从、混入過程中,組件 data犁跪、methods椿息、優(yōu)先級高于 mixin data歹袁,methods 優(yōu)先級。
2寝优、生命周期函數(shù)条舔,先執(zhí)行 mixin 里面的,在執(zhí)行組件里面的乏矾。
3孟抗、自定義的屬性,組件中的屬性優(yōu)先級高于 mixin 屬性優(yōu)先級钻心。

什么叫自定義屬性呢夸浅?我們來看個小栗子:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{number}}</div>
  `
})

我們直接定義了一個屬性 number,它既不在 data 中扔役,也不在 setup 中帆喇,而是直接掛載在 app 上,那么它就是 app 上的自定義屬性亿胸。此時我們無法在模板中直接使用 this 訪問到這個 number 屬性坯钦,必須要通過 this.$options 才能訪問到它:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

如果我們此時在 mixin 中也定義一個 number 屬性:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

前面我們說過,mixin 的優(yōu)先級低于組件優(yōu)先級侈玄,所以此時肯定輸出的是 3婉刀,但是如果我們希望 mixin 的優(yōu)先級高于組件優(yōu)先級,我們就可以使用 app.config.optionMergeStrategies 自定義選項合并策略:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
  <div>{{this.$options.number}}</div>
`
})
// 接收兩個參數(shù)序仙,配置優(yōu)先返回第一個參數(shù)突颊,如找不到在返回第二個參數(shù)
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
  return mixinValue || appValue
}

自定義指令


我們先假想一個使用場景,如果我們希望在頁面加載的時候自動獲取 input 框的焦點事件潘悼,我們一般會這樣寫:

const app = Vue.createApp({
  template: `
    <input ref="input" />
  `,
  mounted() {
    this.$refs.input.focus()
  }
})

假如另一個組件中又有一個 input 律秃,那么我們就又需要在那個組件的 dom 元素節(jié)點處定義 ref,然后在組件的生命周期中調(diào)用一遍 this.$refs.input.focus() 治唤。如果我們可以定義一個全局的 autofocus 事件棒动,只要遇到 input 我們就通過給定的指令直接觸發(fā)那應該怎么辦呢?此時我們就可以用到自定義指令了:

const app = Vue.createApp({
  template: `
    <input ref="input" v-focus />
  `
})
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

通過過 app.directive 我們定義了一個全局的 focus 指令宾添,指令的使用只需要在前面加上 v- 即可船惨;當然指令也和組件一樣,有著生命周期缕陕,我們在 mounted 的時候可以拿到使用指令的 dom 元素節(jié)點粱锐,然后操作這個節(jié)點完成對應的功能。當然上面我們使用 app.directive 將指令定義到了全局扛邑,日常開發(fā)中我們可能更多的是使用局部指令:

// 定義指令集合怜浅,因為可以是多個指令,所以是復數(shù)
const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}
const app = Vue.createApp({
  directives,
  template: `
    <input ref="input" v-focus />
  `
})
動態(tài)指令參數(shù)

例如我們想通過一個指令實時改變 input 框的位置鹿榜,那么此時我們寫下代碼:

const app = Vue.createApp({
  template: `
    <input class="input" ref="input" v-pos />
  `
})
app.directive('pos', {
  mounted(el) {
    el.style.top = '200px'
  }
})

上面這個栗子雖然我們每次使用 v-pos 都會改變輸入框的 top 值海雪,但是如果我們希望這個值不是固定的 200 锦爵,而是指令中傳給我們的數(shù)字,那該如何進行改造呢奥裸?

官網(wǎng)的文檔資料告訴了我們险掀,指令中的參數(shù)可以是動態(tài)的,例如湾宙,在 v-mydirective:[argument]="value" 中樟氢,argument 參數(shù)可以根據(jù)組件實例數(shù)據(jù)進行更新!這使得自定義指令可以在應用中被靈活使用侠鳄。光看文字可能有點糊埠啃,我們來使用實際的栗子:

// 場景一:指令接收傳值
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos="100" />
`
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.value 是我們傳遞給指令的值——在這里是 200
    el.style.top = binding.value + 'px'
  }
})

其實上面的小栗子還有個缺點,就是我們將 v-pos 的值定義在 data 中伟恶,但是我們實時改變 data 中的值碴开,頁面并不會產(chǎn)生對應的響應式變化。那是因為我們指令注冊的過程中 mounted 生命周期只會執(zhí)行一遍博秫,所以如果我們希望對應變化的產(chǎn)生就可以使用 updated 生命周期:

const app = Vue.createApp({
  data() {
    return {
      top: 100
    }
  },
  template: `
    <input class="input" v-pos="top" />
  `
})
app.directive('pos', {
  mounted(el, binding) {
    el.style.top = binding.value + 'px'
  },
  // 通過 updated 監(jiān)聽指令值的實時變化
  updated(el, binding) {
    el.style.top = binding.value + 'px'
  }
})
const vm = app.mount('#root')

當然如果我們在 mountedupdated 時觸發(fā)相同行為潦牛,而不關心其他的鉤子函數(shù)。那么你可以通過將這個回調(diào)函數(shù)傳遞給指令來實現(xiàn):

app.directive('pos', (el, binding) => {
  el.style.top = binding.value + 'px'
})

如果應用場景升級挡育,我們不僅希望它只是是在 top 上的偏移巴碗,而是通過我們指定傳入的方向值進行偏移,那么應該如何實現(xiàn)呢即寒?這時使用動態(tài)參數(shù)就可以非常方便地根據(jù)每個組件實例來進行更新橡淆。

// 場景二:動態(tài)指令參數(shù)
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos:[direction]="100" />
`,
  data() {
    return {
      direction: 'bottom'
    }
  }
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.arg 是我們傳遞給指令的參數(shù)
    const direction = binding.arg || 'top'
    el.style[direction] = binding.value + 'px'
  }
})

你可以試著使用自定義組件完成一個這樣的功能?

插件


我們在 vue 項目中經(jīng)常會使用別人寫好的插件母赵,例如 vue-router 逸爵、vue-touch 等,那么我們?nèi)绾巫约壕帉懸粋€插件呢市咽?看官網(wǎng)的介紹:插件是自包含的代碼痊银,通常向 Vue 添加全局級功能抵蚊。它可以是公開 install() 方法的 object 施绎,也可以是 function 。光看這句話可能有點懵贞绳,其實就傳達給了我們兩點訊息:

1谷醉、編寫插件可以是一個對象寫法,也可以是一個函數(shù)寫法
2冈闭、插件有一個公開的 install() 默認方法俱尼,它接收 vue 實例和你自定義的屬性兩個形參。

舉個栗子:

// 對象寫法:
const myPlugin = {
  install(app, options) {
    console.log(app, options) // vue 實例萎攒,{name: "cc"}
  }
}
app.use(myPlugin, { name: 'cc' })

//函數(shù)寫法:
const myPlugin = (app, options) => {
  console.log(app, options)
}
app.use(myPlugin, { name: 'cc' })

插件一般怎么寫呢遇八?我們使用插件的時候額外的參數(shù)會放到 options 中矛绘,而 app 是使用這個插件的時候 vue 對應的實例。我們既然能得到實例刃永,我們就可以對其做很多拓展货矮,例如:

const app = Vue.createApp({
  template: `
    <child></child>
  `
})
// 子組件就可以通過 inject 接收到我們寫的插件里的 `name`
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`
})
// 自己寫插件,在上面通過 `provide` 拓展一個 name 屬性
const myPlugin = (app, options) => {
  app.provide('name', 'cc')
}
app.use(myPlugin, { name: 'cc' })

官網(wǎng)栗子中給出了 app.config.globalProperties 這個語法斯够,其實就是對 vue 全局的屬性做一些拓展囚玫,比如我們想在全局上加入 sayHello 這樣一個屬性,我們一般會使用 app.config.globalProperties.$sayHello 這樣去寫读规,在屬性名前加入 $ 符號代表這是我們自己在 vue 全局添加的一個私有屬性抓督,更方便我們管理。此時我們就可以在組件中直接訪問到這個全局私有屬性:

app.config.globalProperties.$sayHello = 'hello cc'
// 子組件直接通過 this 使用 $sayHello 屬性
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`,
  mounted() {
    console.log(this.$sayHello)
  }
})

結(jié)合官網(wǎng)束亏,我們是否可以簡單的寫一個小插件铃在,例如表單中的 input 框輸入檢測,對輸入的值進行一些基礎的校驗碍遍,如下栗子:

當然涌穆,這個簡單的小栗子肯定難不倒聰明的我們,其實我們可以使用一個全局 mixin 就可以完成這個功能:

const app = Vue.createApp({
  data() {
    return {
      name: 'cc',
      age: '18'
    }
  },
  template: `
    <div>
      姓名: <input type="text" v-model="name" />
      <span class="hint" v-if="this.$options.rules.name.error">
        {{this.$options.rules.name.message}}
      </span>
    </div>
    <div>
      年齡: <input type="number" v-model="age" />
      <span class="hint" v-if="this.$options.rules.age.error">
        {{this.$options.rules.age.message}}
      </span>
    </div>
  `,
  rules: {
    name: {
      validate: name => name.length > 3,
      error: false,
      message: '用戶名最少為4個字符'
    },
    age: {
      validate: age => age > 20,
      error: false,
      message: '年齡不能小于 20 歲'
    }
  }
})
// 校驗插件
const validatorPlugin = (app, options) => {
  app.mixin({
    created() {
      const rules = this.$options.rules
      for (let key in rules) {
        let item = rules[key]
        this.$watch(key, (value) => {
          if (!item.validate(value)) {
            item.error = true
          } else {
            item.error = false
          }
        }, {
          immediate: true
        })
      }
    }
  })
}
app.use(validatorPlugin)

我們在組件中定義了 rules 屬性雀久,所以我們可以通過 this.$options.rules 直接訪問到這個屬性宿稀,然后我們通過 watch 監(jiān)聽 nameage 的變化,通過回調(diào)函數(shù)來校驗值是否滿足條件赖捌,當然判斷的過程中我們知道 watch 是有惰性的祝沸,所以我們在 watch 的配置中要加上 immediate: true ,這樣就可以在頁面加載完成時立即執(zhí)行越庇。這樣我們就完成了一個迷你版的 input 校驗功能罩锐。

自定義 v-model

vue2 中自定義 v-model 的實現(xiàn)及雙向綁定的原理我已經(jīng)寫過對應的文章了,Vue 中如何自定義 v-model 及雙向綁定的實現(xiàn)原理 卤唉,老版本的 v-model 有幾個痛點:

1蟀悦、比較繁瑣,要添加一個 model 屬性
2芽丹、組件上只能有一個 v-model褥赊,如果組件上出現(xiàn)多個 v-model,實現(xiàn)就比較困難
3熬的、對初學者比較不友好痊硕,看的云里霧里

所以在 vue3 中對 v-model 也是進行了大刀闊斧的改革,在 vue3 中實現(xiàn) v-model 不需要再給組件添加一個 model 屬性押框,只需要:

1岔绸、在組件的 props 中添加一個 modelValue 的屬性
2、更新值的時候組件中的 emit 時有一個 update:modelValue 的方法

我們直接通過一個栗子來認識 vue3 中的自定義 v-model

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  template: `
    <div>{{count}}</div>
    <child v-model="count"></child>
  `
})
app.component('child', {
  // props 中默認的 modelValue 接收父組件中 count 的值
  props: {
    modelValue: String
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
  `,
  methods: {
    handleClick() {
      // 組件更新值的時候使用規(guī)定的 `update: modelValue` 方法
      this.$emit('update:modelValue', this.modelValue + 3)
    }
  }
})

前面說過,vue3 中可以使用多個 v-model盒揉,那么我們在來看看多個 v-model 的應用:

const app = Vue.createApp({
  data() {
    return {
      count: 1,
      age: 18
    }
  },
  template: `
    <div>父組件的值</div>
    <div>{{count}}</div>
    <div>{{age}}</div>
    <div>子組件 v-model 綁定的值</div>
    <child v-model="count" v-model:age="age"></child>
  `
})

app.component('child', {
  props: {
    modelValue: Number,
    age: Number
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
    <input :value="age" @input="handleInput" type="number" />
  `,
  methods: {
    handleClick() {
      this.$emit('update:modelValue', this.modelValue + 3)
    },
    handleInput(event) {
      this.$emit('update:age', +(event.target.value))
    }
  }
})

當我們在 v-model 后面不接入任何參數(shù)時晋被,就可以直接在子組件中使用默認的 modelValue 與父組件中 v-model 的值進行綁定,而當我們在 v-model:age 傳入 age 參數(shù)之后刚盈,對應的子組件的 props 中也需要改成 age墨微,而更新值的時候組件中的 emit 中的方法也要改成對應的 update: age 。其實新版本中的 v-model 使用更簡單更方便扁掸,同時可以綁定多個互不干擾翘县。

非 Prop 的 Attribute


官網(wǎng)給出的定義為一個非 propattribute 是指傳向一個組件,但是該組件并沒有相應 propsemits 定義的 attribute谴分。常見的示例包括 class锈麸、styleid 屬性。咋看這段解釋可能有點懵牺蹄,其實我們可以通過一些栗子來看問題

Attribute 繼承

當組件返回單個根節(jié)點時忘伞,非 prop attribute 將自動添加到根節(jié)點的 attribute 中。例如下列栗子:

const app = Vue.createApp({
  template: `
    <child type="number" class="parent"></child>
  `
})

app.component('child', {
  template: `
    <div class="child">
      <input />
    </div>
  `
})

被渲染的 child 組件實際代碼結(jié)構如下:

// class 和 type 都被渲染到根節(jié)點上去了
<div class="child parent" type="number">
  <input>
</div>
禁用 Attribute 繼承

如果你不希望組件的根元素繼承 attribute沙兰,你可以在組件的選項中設置 inheritAttrs: false氓奈。例如:禁用 attribute 繼承的常見情況是需要將 attribute 應用于根節(jié)點之外的其他元素。

通過將 inheritAttrs 選項設置為 false鼎天,你可以訪問組件的 $attrs property舀奶,該 property 包括組件 propsemits property 中未包含的所有屬性 (例如,class斋射、style育勺、v-on 監(jiān)聽器等)。還是上面的栗子罗岖,我們需要 child 組件中的 input 去渲染對應的 classtype 涧至,我們就可以將代碼改寫一下:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="child">
      <input v-bind="$attrs" />
    </div>
  `
})

此時我們再從瀏覽器中查看 DOM 元素節(jié)點就可以看到如下結(jié)構:

<div class="child">
  <input type="number" class="parent">
</div>
多個根節(jié)點上的 Attribute 繼承

如果我們的子組件存在多個根節(jié)點怎么辦,例如:

const app = Vue.createApp({
  template: `
    <child class="child"></child>
  `
})
app.component('child', {
  template: `
    <div class="header" >
      <input />
    </div>
    <div class="main" v-bind="$attrs">main</div>
    <div class="footer">footer</div>
  `
})

如果我們不在其中一個根組件使用 v-bind = "$attrs" 控制臺就會給我們報錯桑包,我們在其中一個根節(jié)點上使用之后父組件上對應的 attribute 就會被繼承到這個根組件上南蓬。

當然我們也可以禁止掉根節(jié)點上的繼承,直接在 header 結(jié)構下的 input 框加入 v-bind = "$attrs" 即可哑了。如下栗子:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="header" >
      <input v-bind="$attrs" />
    </div>
    <div class="main">main</div>
    <div class="footer">footer</div>
  `
})

查漏補缺


我們知道在 vue2 模板中可以通過在 DOM 結(jié)構中指定 ref 屬性赘方,然后在邏輯代碼中通過 this.$refs. 去操作 DOM,那么在 vue3 中我們應該如何操作 DOM 元素呢垒手?

我們先來一個簡單的場景蒜焊,判斷點擊的 dom 元素是否在 id = 'index'dom 結(jié)構中,場景代碼如下:

// 判斷點擊的 dom 元素節(jié)點是不是在 index 中
const app = Vue.createApp({
    template: `
      <div id="index">
        <div id="index-list">555</div>
      </div>
      <div id="demo">666</div>
    `,
  })

此時我們就要進行 DOM 元素節(jié)點判斷科贬,結(jié)合 setup 我們應該如何去使用 ref 來獲取 dom 元素節(jié)點呢?代碼比較簡單就直接上結(jié)果了:

const app = Vue.createApp({
  template: `
    <div id="index" ref="index">
      <div id="index-list">555</div>
    </div>
    <div id="demo">666</div>
  `,
  setup() {
    const { ref, onMounted } = Vue
    const index = ref(null)
    onMounted(() => {
      document.addEventListener('click', function (e) {
        if (index.value.contains(e.target)) {
          console.log('點擊的是 index 里面的元素')
        } else {
          console.log('點擊的不是 index 里面的元素')
        }
      })
    })
    return { index }
  }
})

因為 setup 的執(zhí)行是在 beforeCreatecreated 之間,所以我們?nèi)绻肽玫綄?dom 元素節(jié)點榜掌,最好在其內(nèi)部的生命周期中進行獲取优妙。

vue3 中相較 vue2 大體的改動和日常開發(fā)中經(jīng)常會遇到的問題基本都已經(jīng)整理的差不多了,由于 vue3 代碼基本都是 ts 寫的憎账,所以學習 ts 其實已經(jīng)迫在眉睫的套硼。結(jié)尾綜合做個小栗子吧:

其實很簡單,就是一個 form 表單提交驗證胞皱,不過封裝基本用的是 vue3 + ts 邪意,小伙伴可以自己獨立實現(xiàn)一個類似 element-ui 中的表單效驗插件,其實組件開發(fā)更多的是學習思路以及代碼的擴展性反砌,優(yōu)雅性雾鬼。最近 github 經(jīng)常打不開,源代碼就放在 gitee 上了宴树。本文多為自己學習筆記記錄策菜,如有錯誤,歡迎指正>票帷S趾!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锭吨,一起剝皮案震驚了整個濱河市蠢莺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌零如,老刑警劉巖浪秘,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異埠况,居然都是意外死亡耸携,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門辕翰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夺衍,“玉大人,你說我怎么就攤上這事喜命」瞪常” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵壁榕,是天一觀的道長矛紫。 經(jīng)常有香客問我,道長牌里,這世上最難降的妖魔是什么颊咬? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任务甥,我火速辦了婚禮,結(jié)果婚禮上喳篇,老公的妹妹穿的比我還像新娘敞临。我一直安慰自己,他們只是感情好麸澜,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布挺尿。 她就那樣靜靜地躺著,像睡著了一般炊邦。 火紅的嫁衣襯著肌膚如雪编矾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天馁害,我揣著相機與錄音窄俏,去河邊找鬼。 笑死蜗细,一個胖子當著我的面吹牛裆操,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炉媒,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼踪区,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吊骤?” 一聲冷哼從身側(cè)響起缎岗,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎白粉,沒想到半個月后传泊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡鸭巴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年眷细,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹃祖。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡溪椎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恬口,到底是詐尸還是另有隱情校读,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布祖能,位于F島的核電站歉秫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏养铸。R本人自食惡果不足惜雁芙,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一轧膘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧却特,春花似錦扶供、人聲如沸筛圆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽太援。三九已至闽晦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間提岔,已是汗流浹背仙蛉。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碱蒙,地道東北人荠瘪。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像赛惩,于是被迫代替她去往敵國和親哀墓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容