1. 程式人生 > >嚐鮮 vue3.x 新特性 - CompositionAPI

嚐鮮 vue3.x 新特性 - CompositionAPI

原文地址 http://www.liulongbin.top:8085

0. 基礎要求

  1. 瞭解常見的 ES6 新特性
    • ES6 的匯入匯出語法
    • 解構賦值
    • 箭頭函式
    • etc...
  2. 瞭解 vue 2.x 的基本使用
    • 元件
    • 常用的指令
    • 生命週期函式
    • computed、watch、ref 等

1. 相關資源

  • 【知乎 - Vue Function-based API RFC】https://zhuanlan.zhihu.com/p/68477600
  • 【github - vuejs/composition-api】https://github.com/vuejs/composition-api
  • 【github - composition-api/CHANGELOG.md】https://github.com/vuejs/composition-api/blob/master/CHANGELOG.md
  • 【開源中國 - 尤雨溪公佈 Vue 3.0 開發路線:將從頭開始重寫 3.0】https://www.oschina.net/news/100515/plans-for-the-next-iteration-of-vue-js

2. 初始化專案

  1. 安裝 vue-cli3

    npm install -g @vue/cli
    # OR
    yarn global add @vue/cli
  2. 建立專案

    vue create my-project
    # OR
    vue ui
  3. 在專案中安裝 composition-api 體驗 vue3 新特性

    npm install @vue/composition-api --save
    # OR
    yarn add @vue/composition-api
  4. 在使用任何 @vue/composition-api 提供的能力前,必須先通過 Vue.use() 進行安裝

    import Vue from 'vue'
    import VueCompositionApi from '@vue/composition-api'
    
    Vue.use(VueCompositionApi)

    安裝外掛後,您就可以使用新的 Composition API 來開發元件了。

3. setup

setup() 函式是 vue3 中,專門為元件提供的新屬性。它為我們使用 vue3 的 Composition API 新特性提供了統一的入口。

3.1 執行時機

setup 函式會在 beforeCreate 之後、created 之前執行

3.2 接收 props 資料

  1. props 中定義當前元件允許外界傳遞過來的引數名稱:

    props: {
        p1: String
    }
  2. 通過 setup 函式的第一個形參,接收 props 資料:

    setup(props) {
        console.log(props.p1)
    }

3.3 context

setup 函式的第二個形參是一個上下文物件,這個上下文物件中包含了一些有用的屬性,這些屬性在 vue 2.x 中需要通過 this 才能訪問到,在 vue 3.x 中,它們的訪問方式如下:

const MyComponent = {
  setup(props, context) {
    context.attrs
    context.slots
    context.parent
    context.root
    context.emit
    context.refs
  }
}

注意:在 setup() 函式中無法訪問到 this

4. reactive

reactive() 函式接收一個普通物件,返回一個響應式的資料物件。

4.1 基本語法

等價於 vue 2.x 中的 Vue.observable() 函式,vue 3.x 中提供了 reactive() 函式,用來建立響應式的資料物件,基本程式碼示例如下:

import { reactive } from '@vue/composition-api'

// 建立響應式資料物件,得到的 state 類似於 vue 2.x 中 data() 返回的響應式物件
const state = reactive({ count: 0 })

4.2 定義響應式資料供 template 使用

  1. 按需匯入 reactive 函式:

    import { reactive } from '@vue/composition-api'
  2. setup() 函式中呼叫 reactive() 函式,建立響應式資料物件:

    setup() {
         // 建立響應式資料物件
     const state = reactive({count: 0})
    
         // setup 函式中將響應式資料物件 return 出去,供 template 使用
     return state
    }
  3. template 中訪問響應式資料:

    <p>當前的 count 值為:{{count}}</p>

5. ref

5.1 基本語法

ref() 函式用來根據給定的值建立一個響應式的資料物件,ref() 函式呼叫的返回值是一個物件,這個物件上只包含一個 .value 屬性:

import { ref } from '@vue/composition-api'

// 建立響應式資料物件 count,初始值為 0
const count = ref(0)

// 如果要訪問 ref() 創建出來的響應式資料物件的值,必須通過 .value 屬性才可以
console.log(count.value) // 輸出 0
// 讓 count 的值 +1
count.value++
// 再次列印 count 的值
console.log(count.value) // 輸出 1

5.2 在 template 中訪問 ref 建立的響應式資料

  1. setup() 中建立響應式資料:

    import { ref } from '@vue/composition-api'
    
    setup() {
     const count = ref(0)
    
         return {
             count,
             name: ref('zs')
         }
    }
  2. template 中訪問響應式資料:

    <template>
     <p>{{count}} --- {{name}}</p>
    </template>

5.3 在 reactive 物件中訪問 ref 建立的響應式資料

當把 ref() 創建出來的響應式資料物件,掛載到 reactive() 上時,會自動把響應式資料物件展開為原始的值,不需通過 .value 就可以直接被訪問,例如:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 輸出 0
state.count++            // 此處不需要通過 .value 就能直接訪問原始值
console.log(count)       // 輸出 1

注意:新的 ref 會覆蓋舊的 ref,示例程式碼如下:

// 建立 ref 並掛載到 reactive 中
const c1 = ref(0)
const state = reactive({
  c1
})

// 再次建立 ref,命名為 c2
const c2 = ref(9)
// 將 舊 ref c1 替換為 新 ref c2
state.c1 = c2
state.c1++

console.log(state.c1) // 輸出 10
console.log(c2.value) // 輸出 10
console.log(c1.value) // 輸出 0

6. isRef

isRef() 用來判斷某個值是否為 ref() 創建出來的物件;應用場景:當需要展開某個可能為 ref() 創建出來的值的時候,例如:

import { isRef } from '@vue/composition-api'

const unwrapped = isRef(foo) ? foo.value : foo

7. toRefs

toRefs() 函式可以將 reactive() 創建出來的響應式物件,轉換為普通的物件,只不過,這個物件上的每個屬性節點,都是 ref() 型別的響應式資料,最常見的應用場景如下:

import { toRefs } from '@vue/composition-api'

setup() {
    // 定義響應式資料物件
    const state = reactive({
      count: 0
    })
    
    // 定義頁面上可用的事件處理函式
    const increment = () => {
      state.count++
    }
    
    // 在 setup 中返回一個物件供頁面使用
    // 這個物件中可以包含響應式的資料,也可以包含事件處理函式
    return {
      // 將 state 上的每個屬性,都轉化為 ref 形式的響應式資料
      ...toRefs(state),
      // 自增的事件處理函式
      increment
    }
}

頁面上可以直接訪問 setup() 中 return 出來的響應式資料:

<template>
  <div>
    <p>當前的count值為:{{count}}</p>
    <button @click="increment">+1</button>
  </div>
</template>

8. computed

computed() 用來建立計算屬性,computed() 函式的返回值是一個 ref 的例項。使用 computed 之前需要按需匯入:

import { computed } from '@vue/composition-api'

8.1 建立只讀的計算屬性

在呼叫 computed() 函式期間,傳入一個 function 函式,可以得到一個只讀的計算屬性,示例程式碼如下:

// 建立一個 ref 響應式資料
const count = ref(1)

// 根據 count 的值,建立一個響應式的計算屬性 plusOne
// 它會根據依賴的 ref 自動計算並返回一個新的 ref
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 輸出 2
plusOne.value++            // error

8.2 建立可讀可寫的計算屬性

在呼叫 computed() 函式期間,傳入一個包含 getset 函式的物件,可以得到一個可讀可寫的計算屬性,示例程式碼如下:

// 建立一個 ref 響應式資料
const count = ref(1)

// 建立一個 computed 計算屬性
const plusOne = computed({
  // 取值函式
  get: () => count.value + 1,
  // 賦值函式
  set: val => { count.value = val - 1 }
})

// 為計算屬性賦值的操作,會觸發 set 函式
plusOne.value = 9
// 觸發 set 函式後,count 的值會被更新
console.log(count.value) // 輸出 8

9. watch

watch() 函式用來監視某些資料項的變化,從而觸發某些特定的操作,使用之前需要按需匯入:

import { watch } from '@vue/composition-api'

9.1 基本用法

const count = ref(0)

// 定義 watch,只要 count 值變化,就會觸發 watch 回撥
// watch 會在建立時會自動呼叫一次
watch(() => console.log(count.value))
// 輸出 0

setTimeout(() => {
  count.value++
  // 輸出 1
}, 1000)

9.2 監視指定的資料來源

監視 reactive 型別的資料來源:

// 定義資料來源
const state = reactive({ count: 0 })
// 監視 state.count 這個資料節點的變化
watch(() => state.count, (count, prevCount) => { /* ... */ })

監視 ref 型別的資料來源:

// 定義資料來源
const count = ref(0)
// 指定要監視的資料來源
watch(count, (count, prevCount) => { /* ... */ })

9.3 監視多個數據源

監視 reactive 型別的資料來源:

const state = reactive({ count: 0, name: 'zs' })

watch(
  [() => state.count, () => state.name],    // Object.values(toRefs(state)),
  ([count, name], [prevCount, prevName]) => {
    console.log(count)         // 新的 count 值
    console.log(name)          // 新的 name 值
    console.log('------------')
    console.log(prevCount)     // 舊的 count 值
    console.log(prevName)      // 新的 name 值
  },
  {
    lazy: true // 在 watch 被建立的時候,不執行回撥函式中的程式碼
  }
)

setTimeout(() => {
  state.count++
  state.name = 'ls'
}, 1000)

監視 ref 型別的資料來源:

const count = ref(0)
const name = ref('zs')

watch(
  [count, name], // 需要被監視的多個 ref 資料來源
  ([count, name], [prevCount, prevName]) => {
    console.log(count)
    console.log(name)
    console.log('-------------')
    console.log(prevCount)
    console.log(prevName)
  },
  {
    lazy: true
  }
)

setTimeout(() => {
  count.value++
  name.value = 'xiaomaolv'
}, 1000)

9.4 清除監視

setup() 函式內建立的 watch 監視,會在當前元件被銷燬的時候自動停止。如果想要明確地停止某個監視,可以呼叫 watch() 函式的返回值即可,語法如下:

// 建立監視,並得到 停止函式
const stop = watch(() => { /* ... */ })

// 呼叫停止函式,清除對應的監視
stop()

9.5 在 watch 中清除無效的非同步任務

有時候,當被 watch 監視的值發生變化時,或 watch 本身被 stop 之後,我們期望能夠清除那些無效的非同步任務,此時,watch 回撥函式中提供了一個 cleanup registrator function 來執行清除的工作。這個清除函式會在如下情況下被呼叫:

  • watch 被重複執行了
  • watch 被強制 stop

Template 中的程式碼示例如下:

/* template 中的程式碼 */
<input type="text" v-model="keywords" />

Script 中的程式碼示例如下:

// 定義響應式資料 keywords   
const keywords = ref('')

// 非同步任務:列印使用者輸入的關鍵詞
const asyncPrint = val => {
  // 延時 1 秒後列印
  return setTimeout(() => {
    console.log(val)
  }, 1000)
}

// 定義 watch 監聽
watch(
  keywords,
  (keywords, prevKeywords, onCleanup) => {
    // 執行非同步任務,並得到關閉非同步任務的 timerId
    const timerId = asyncPrint(keywords)

    // 如果 watch 監聽被重複執行了,則會先清除上次未完成的非同步任務
    onCleanup(() => clearTimeout(timerId))
  },
  // watch 剛被建立的時候不執行
  { lazy: true }
)

// 把 template 中需要的資料 return 出去
return {
  keywords
}

10. LifeCycle Hooks

新版的生命週期函式,可以按需匯入到元件中,且只能在 setup() 函式中使用,程式碼示例如下:

import { onMounted, onUpdated, onUnmounted } from '@vue/composition-api'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

下面的列表,是 vue 2.x 的生命週期函式與新版 Composition API 之間的對映關係:

  • beforeCreate -> use setup()
  • created -> use setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

11. provide & inject

provide()inject() 可以實現巢狀元件之間的資料傳遞。這兩個函式只能在 setup() 函式中使用。父級元件中使用 provide() 函式向下傳遞資料;子級元件中使用 inject() 獲取上層傳遞過來的資料。

11.1 共享普通資料

App.vue 根元件:

<template>
  <div id="app">
    <h1>App 根元件</h1>
    <hr />
    <LevelOne />
  </div>
</template>

<script>
import LevelOne from './components/LevelOne'
// 1. 按需匯入 provide
import { provide } from '@vue/composition-api'

export default {
  name: 'app',
  setup() {
    // 2. App 根元件作為父級元件,通過 provide 函式向子級元件共享資料(不限層級)
    //    provide('要共享的資料名稱', 被共享的資料)
    provide('globalColor', 'red')
  },
  components: {
    LevelOne
  }
}
</script>

LevelOne.vue 元件:

<template>
  <div>
    <!-- 4. 通過屬性繫結,為標籤設定字型顏色 -->
    <h3 :style="{color: themeColor}">Level One</h3>
    <hr />
    <LevelTwo />
  </div>
</template>

<script>
import LevelTwo from './LevelTwo'
// 1. 按需匯入 inject
import { inject } from '@vue/composition-api'

export default {
  setup() {
    // 2. 呼叫 inject 函式時,通過指定的資料名稱,獲取到父級共享的資料
    const themeColor = inject('globalColor')
    
    // 3. 把接收到的共享資料 return 給 Template 使用
    return {
      themeColor
    }
  },
  components: {
    LevelTwo
  }
}
</script>

LevelTwo.vue 元件:

<template>
  <div>
    <!-- 4. 通過屬性繫結,為標籤設定字型顏色 -->
    <h5 :style="{color: themeColor}">Level Two</h5>
  </div>
</template>

<script>
// 1. 按需匯入 inject
import { inject } from '@vue/composition-api'

export default {
  setup() {
    // 2. 呼叫 inject 函式時,通過指定的資料名稱,獲取到父級共享的資料
    const themeColor = inject('globalColor')

    // 3. 把接收到的共享資料 return 給 Template 使用
    return {
      themeColor
    }
  }
}
</script>

11.2 共享 ref 響應式資料

如下程式碼實現了點按鈕切換主題顏色的功能,主要修改了 App.vue 元件中的程式碼,LevelOne.vueLevelTwo.vue 中的程式碼不受任何改變:

<template>
  <div id="app">
    <h1>App 根元件</h1>
    
    <!-- 點選 App.vue 中的按鈕,切換子元件中文字的顏色 -->
    <button @click="themeColor='red'">紅色</button>
    <button @click="themeColor='blue'">藍色</button>
    <button @click="themeColor='orange'">橘黃色</button>

    <hr />
    <LevelOne />
  </div>
</template>

<script>
import LevelOne from './components/LevelOne'
import { provide, ref } from '@vue/composition-api'

export default {
  name: 'app',
  setup() {
    // 定義 ref 響應式資料
    const themeColor = ref('red')
    
    // 把 ref 資料通過 provide 提供的子元件使用
    provide('globalColor', themeColor)
    
    // setup 中 return 資料供當前元件的 Template 使用
    return {
      themeColor
    }
  },
  components: {
    LevelOne
  }
}
</script>

12. template refs

通過 ref() 還可以引用頁面上的元素或元件。

12.1 元素的引用

示例程式碼如下:

<template>
  <div>
    <h3 ref="h3Ref">TemplateRefOne</h3>
  </div>
</template>

<script>
import { ref, onMounted } from '@vue/composition-api'

export default {
  setup() {
    // 建立一個 DOM 引用
    const h3Ref = ref(null)

    // 在 DOM 首次載入完畢之後,才能獲取到元素的引用
    onMounted(() => {
      // 為 dom 元素設定字型顏色
      // h3Ref.value 是原生DOM物件
      h3Ref.value.style.color = 'red'
    })

    // 把建立的引用 return 出去
    return {
      h3Ref
    }
  }
}
</script>

12.2 元件的引用

TemplateRefOne.vue 中的示例程式碼如下:

<template>
  <div>
    <h3>TemplateRefOne</h3>
    
    <!-- 4. 點選按鈕展示子元件的 count 值 -->
    <button @click="showNumber">獲取TemplateRefTwo中的count值</button>

    <hr />
    <!-- 3. 為元件新增 ref 引用 -->
    <TemplateRefTwo ref="comRef" />
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'
import TemplateRefTwo from './TemplateRefTwo'

export default {
  setup() {
    // 1. 建立一個元件的 ref 引用
    const comRef = ref(null)

    // 5. 展示子元件中 count 的值
    const showNumber = () => {
      console.log(comRef.value.count)
    }

    // 2. 把建立的引用 return 出去
    return {
      comRef,
      showNumber
    }
  },
  components: {
    TemplateRefTwo
  }
}
</script>

TemplateRefTwo.vue 中的示例程式碼:

<template>
  <div>
    <h5>TemplateRefTwo --- {{count}}</h5>
    <!-- 3. 點選按鈕,讓 count 值自增 +1 -->
    <button @click="count+=1">+1</button>
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    // 1. 定義響應式的資料
    const count = ref(0)
    
    // 2. 把響應式資料 return 給 Template 使用
    return {
      count
    }
  }
}
</script>

13. createComponent

這個函式不是必須的,除非你想要完美結合 TypeScript 提供的型別推斷來進行專案的開發。

這個函式僅僅提供了型別推斷,方便在結合 TypeScript 書寫程式碼時,能為 setup() 中的 props 提供完整的型別推斷。

import { createComponent } from 'vue'

export default createComponent({
  props: {
    foo: String
  },
  setup(props) {
    props.foo // <- type: string
  }
})