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

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

Teleport


我們?nèi)粘i_發(fā)中經(jīng)常會遇到這樣一個場景,比如我們封裝一個彈層 msk 組件,但是它包裹在一個 position: relative 定位的父元素中,此時它將被局限在父元素的大小中,我們很難將彈層鋪滿整個窗口。而 Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個父節(jié)點(diǎn)下渲染了 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

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

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

emits


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

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

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` 中方便管理維護(hù)
  emits: ['closeMsk'], 
  template: `
    <div class="msk" v-show="isOpen">
      <button @click="closeMsk">關(guān)閉彈層</button>
    </div>
  `,
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk')
    }
    return { closeMsk }
  }
})
app.mount('#root')

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

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

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

Suspense


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

動態(tài)組件

vue 2.0vue3.0 動態(tài)組件的使用方式基本差不多,都是根據(jù)數(shù)據(jù)的變化,結(jié)合 component 這個標(biāo)簽,來隨時動態(tài)切換組件的實(shí)現(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 助手方法中來顯式地定義,其實(shí)也很簡單,看栗子就知道了:

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ù)。從服務(wù)器檢索組件定義后,應(yīng)調(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>
`
})

當(dāng)然 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


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

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

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

父組件像孫子組件傳遞固定值
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)聽到改變后的值的。此時如果我們想對祖先組件中的更改做出響應(yīng),我們需要為 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>
`
})

當(dāng)然此時我們還沒有用到 Composition API 來實(shí)現(xiàn),我們在使用 Composiiton API 來重構(gòu)下上面的代碼:

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 實(shí)現(xiàn)了父組件和子孫組件中的數(shù)據(jù)傳遞,如果兩個毫無關(guān)聯(lián)的組件,那么我們應(yīng)該如何建立數(shù)據(jù)通訊呢?除了 vuex 你最先能想到什么,在 2.x 中,Vue實(shí)例可用于觸發(fā)通過事件觸發(fā) API 強(qiáng)制附加的處理程序 ($on$off$once),這用于創(chuàng)建 event hub,以創(chuàng)建在整個應(yīng)用程序中使用的全局事件偵聽器,因?yàn)槲仪懊鎸戇^相關(guān)文章,關(guān)于 vue2 的知識就不在這里過多贅述了,詳情可以點(diǎn)擊 Vue 常見 API 及問題。

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

Mixin


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

1、混入過程中,組件 datamethods、優(yōu)先級高于 mixin datamethods 優(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 自定義選項(xiàng)合并策略:

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 框的焦點(diǎn)事件,我們一般會這樣寫:

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

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

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

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

// 定義指令集合,因?yàn)榭梢允嵌鄠€指令,所以是復(fù)數(shù)
const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}
const app = Vue.createApp({
  directives,
  template: `
    <input ref="input" v-focus />
  `
})
動態(tài)指令參數(shù)

例如我們想通過一個指令實(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ù)字,那該如何進(jìn)行改造呢?

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

// 場景一:指令接收傳值
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'
  }
})

其實(shí)上面的小栗子還有個缺點(diǎn),就是我們將 v-pos 的值定義在 data 中,但是我們實(shí)時改變 data 中的值,頁面并不會產(chǎn)生對應(yīng)的響應(yīng)式變化。那是因?yàn)槲覀冎噶钭缘倪^程中 mounted 生命周期只會執(zhí)行一遍,所以如果我們希望對應(yīng)變化的產(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)聽指令值的實(shí)時變化
  updated(el, binding) {
    el.style.top = binding.value + 'px'
  }
})
const vm = app.mount('#root')

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

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

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

// 場景二:動態(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 項(xiàng)目中經(jīng)常會使用別人寫好的插件,例如 vue-routervue-touch 等,那么我們?nèi)绾巫约壕帉懸粋€插件呢?看官網(wǎng)的介紹:插件是自包含的代碼,通常向 Vue 添加全局級功能。它可以是公開 install() 方法的 object ,也可以是 function 。光看這句話可能有點(diǎn)懵,其實(shí)就傳達(dá)給了我們兩點(diǎn)訊息:

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

舉個栗子:

// 對象寫法:
const myPlugin = {
  install(app, options) {
    console.log(app, options) // vue 實(shí)例,{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 對應(yīng)的實(shí)例。我們既然能得到實(shí)例,我們就可以對其做很多拓展,例如:

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 這個語法,其實(shí)就是對 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 框輸入檢測,對輸入的值進(jìn)行一些基礎(chǔ)的校驗(yàn),如下栗子:

當(dāng)然,這個簡單的小栗子肯定難不倒聰明的我們,其實(shí)我們可以使用一個全局 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 歲'
    }
  }
})
// 校驗(yàn)插件
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ù)來校驗(yàn)值是否滿足條件,當(dāng)然判斷的過程中我們知道 watch 是有惰性的,所以我們在 watch 的配置中要加上 immediate: true ,這樣就可以在頁面加載完成時立即執(zhí)行。這樣我們就完成了一個迷你版的 input 校驗(yàn)功能。

自定義 v-model

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

1、比較繁瑣,要添加一個 model 屬性
2、組件上只能有一個 v-model,如果組件上出現(xiàn)多個 v-model,實(shí)現(xiàn)就比較困難
3、對初學(xué)者比較不友好,看的云里霧里

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

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

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

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  template: `
    <div>{{count}}</div>
    <child v-model="count"></child>
  `
})
app.component('child', {
  // props 中默認(rèn)的 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 的應(yīng)用:

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))
    }
  }
})

當(dāng)我們在 v-model 后面不接入任何參數(shù)時,就可以直接在子組件中使用默認(rèn)的 modelValue 與父組件中 v-model 的值進(jìn)行綁定,而當(dāng)我們在 v-model:age 傳入 age 參數(shù)之后,對應(yīng)的子組件的 props 中也需要改成 age,而更新值的時候組件中的 emit 中的方法也要改成對應(yīng)的 update: age 。其實(shí)新版本中的 v-model 使用更簡單更方便,同時可以綁定多個互不干擾。

非 Prop 的 Attribute


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

Attribute 繼承

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

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

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

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

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

如果你不希望組件的根元素繼承 attribute,你可以在組件的選項(xiàng)中設(shè)置 inheritAttrs: false。例如:禁用 attribute 繼承的常見情況是需要將 attribute 應(yīng)用于根節(jié)點(diǎn)之外的其他元素。

通過將 inheritAttrs 選項(xiàng)設(shè)置為 false,你可以訪問組件的 $attrs property,該 property 包括組件 propsemits property 中未包含的所有屬性 (例如,classstylev-on 監(jiān)聽器等)。還是上面的栗子,我們需要 child 組件中的 input 去渲染對應(yīng)的 classtype ,我們就可以將代碼改寫一下:

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

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

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

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

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" 控制臺就會給我們報(bào)錯,我們在其中一個根節(jié)點(diǎn)上使用之后父組件上對應(yīng)的 attribute 就會被繼承到這個根組件上。

當(dāng)然我們也可以禁止掉根節(jié)點(diǎn)上的繼承,直接在 header 結(jié)構(gòu)下的 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>
  `
})

查漏補(bǔ)缺


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

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

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

此時我們就要進(jìn)行 DOM 元素節(jié)點(diǎn)判斷,結(jié)合 setup 我們應(yīng)該如何去使用 ref 來獲取 dom 元素節(jié)點(diǎn)呢?代碼比較簡單就直接上結(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('點(diǎn)擊的是 index 里面的元素')
        } else {
          console.log('點(diǎn)擊的不是 index 里面的元素')
        }
      })
    })
    return { index }
  }
})

因?yàn)?setup 的執(zhí)行是在 beforeCreatecreated 之間,所以我們?nèi)绻肽玫綄?yīng)的 dom 元素節(jié)點(diǎn),最好在其內(nèi)部的生命周期中進(jìn)行獲取。

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

其實(shí)很簡單,就是一個 form 表單提交驗(yàn)證,不過封裝基本用的是 vue3 + ts ,小伙伴可以自己獨(dú)立實(shí)現(xiàn)一個類似 element-ui 中的表單效驗(yàn)插件,其實(shí)組件開發(fā)更多的是學(xué)習(xí)思路以及代碼的擴(kuò)展性,優(yōu)雅性。最近 github 經(jīng)常打不開,源代碼就放在 gitee 上了。本文多為自己學(xué)習(xí)筆記記錄,如有錯誤,歡迎指正!!!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

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