Vue3 Composition API


什么是組合式 API?

https://www.vue3js.cn/docs/zh/guide/composition-api-introduction.html

通過創建 Vue 組件,我們可以將接口的可重復部分及其功能提取到可重用的代碼段中。僅此一項就可以使我們的應用程序在可維護性和靈活性方面走得更遠。然而,我們的經驗已經證明,光靠這一點可能是不夠的,尤其是當你的應用程序變得非常大的時候——想想幾百個組件。在處理如此大的應用程序時,共享和重用代碼變得尤為重要。

假設在我們的應用程序中,我們有一個視圖來顯示某個用戶的倉庫列表。除此之外,我們還希望應用搜索和篩選功能。處理此視圖的組件可能如下所示:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { type: String }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 獲取用戶倉庫
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

該組件有以下幾個職責:

  1. 從假定的外部 API 獲取該用戶名的倉庫,並在用戶更改時刷新它
  2. 使用 searchQuery 字符串搜索存儲庫
  3. 使用 filters 對象篩選倉庫

用組件的選項 (datacomputedmethodswatch) 組織邏輯在大多數情況下都有效。然而,當我們的組件變得更大時,邏輯關注點的列表也會增長。這可能會導致組件難以閱讀和理解,尤其是對於那些一開始就沒有編寫這些組件的人來說。

這種碎片化使得理解和維護復雜組件變得困難。選項的分離掩蓋了潛在的邏輯問題。此外,在處理單個邏輯關注點時,我們必須不斷地“跳轉”相關代碼的選項塊。

如果我們能夠將與同一個邏輯關注點相關的代碼配置在一起會更好。而這正是組合式 API 使我們能夠做到的。

為什么需要Vue對組合API

隨着Vue的日益普及,人們也開始在大型和企業級應用程序中采用Vue。 由於這種情況,在很多情況下,此類應用程序的組件會隨着時間的推移而逐漸增長,並且有時使用單文件組件人們很難閱讀和維護。 因此,需要以邏輯方式制動組件,而使用Vue的現有API則不可能。

代替添加另一個新概念,提議使用Composition API,該API基本將Vue的核心功能(如創建響應式數據)公開為獨立功能,並且該API有助於在多個組件之間編寫簡潔且可復用的代碼。

Vue已有的替代方案缺點是什么?

在引入新的API之前,Vue還有其他替代方案,它們提供了諸如mixin,HOC(高階組件),作用域插槽之類的組件之間的可復用性,但是所有方法都有其自身的缺點,由於它們未被廣泛使用。

  1. Mixins:一旦應用程序包含一定數量的mixins,就很難維護。 開發人員需要訪問每個mixin,以查看數據來自哪個mixin。
  2. HOC:這種模式不適用於.vue 單文件組件,因此在Vue開發人員中不被廣泛推薦或流行。
  3. 作用域插槽一通用的內容會進行封裝到組件中,但是開發人員最終擁有許多不可復用的內容,並在組件模板中放置了越來越多的插槽,導致數據來源不明確。

簡而言之,組合API有助於

  1. 由於API是基於函數的,因此可以有效地組織和編寫可重用的代碼。
  2. 通過將共享邏輯分離為功能來提高代碼的可讀性。
  3. 實現代碼分離。
  4. 在Vue應用程序中更好地使用TypeScript。

組合API(常用部分)

setup

  • 新的 option, 所有的組合 API 函數都在此使用,只在初始化時執行一次
  • 函數如果返回對象,對象中的屬性或方法,模板中可以直接使用
<template>
    <div>哈哈 我又變帥了</div>
    <h1>{{number}}</h1>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const number = Number.MAX_SAFE_INTEGER;

    return {
      number,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ref

作用:定義一個數據的響應式

語法:const xxx = ref(initValue):

  • 創建一個包含響應式數據的引用(reference)對象
  • js/ts 中操作數據:xxx.value
  • HTML模板中操作數據:不需要.value

一般用來定義一個基本類型的響應式數據

Vue2寫法

<template>
  <h2>setup和ref的基本使用</h2>
  <h2>{{ count }}</h2>
  <hr/>
  <button @click="update">更新</button>
</template>

<script lang="js">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    update() {
      this.count += 1;
    },
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Vue3寫法實現同一功能

<template>
  <h2>setup和ref的基本使用</h2>
  <h2>{{ count }}</h2>
  <hr/>
  <button @click="update">更新</button>
</template>

<script lang="js">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 變量,此時的數據不是響應式的數據
    // let count = 0;
    // ref是一個函數,定義的是一個響應式的數據,返回的是一個一個包含響應式數據的**引用(reference)對象**
    // 對象中有一個value屬性,如果需要對數據進行操作,需要使用該ref對象調用value屬性的方式進行數據的操作
    // html模板中是不需要.value的
    const count = ref(0);
    console.log(count);

    // 方法
    function update() {
      // count++;
      count.value += 1;
    }

    // 返回的是一個對象
    return {
      count,
      update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

reactive

作用:定義多個數據的響應式

const proxy = reactive(obj):接收一個普通對象然后返回該普通對象的響應式代理器對象

響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性

內部基於 ES6 的 Proxy 實現,通過代理對象操作源對象內部數據都是響應式的

<template>
  <h2>reactive的使用</h2>
  <h3>名字:{{user.name}}</h3>
  <h3>年齡:{{user.age}}</h3>
  <h3>媳婦:{{user.wife}}</h3>
  <h3>性別:{{user.gender}}</h3>

  <hr/>
  <button @click="updateUser">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:顯示用戶的相關信息,點擊按鈕,可以更新用戶的相關信息數據
  setup() {
    // 把數據變成響應式的數據
    // 返回的是一個Proxy(代理對象),被代理的是里面的obj對象
    // user現在是代理對象,obj是目標對象
    const obj:any = {
      name: '小明',
      age: 20,
      wife: {
        name: '小甜甜',
        age: 18,
        car: ['斯堪尼亞', '奔馳', 'DAF'],
      },
    };
    const user = reactive<any>(obj);

    const updateUser = () => {
      user.name = '小紅';
      user.gender = '男';
    };

    return {
      user,
      updateUser,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

比較 Vue2 與 Vue3 的響應式

Vue2 的響應式

核心:

  • 對象: 通過 defineProperty 對對象的已有屬性值的讀取和修改進行劫持(監視/攔截)
  • 數組: 通過重寫數組更新數組一系列更新元素的方法來實現元素修改的劫持
Object.defineProperty(data, 'count', {
  get() {},
  set() {}
})

問題

  • 對象直接新添加的屬性或刪除已有屬性,界面不會自動更新
  • 直接通過下標替換元素或更新 length,界面不會自動更新 arr[1] = {}

Vue3 的響應式

核心:

new Proxy(data, {
  // 攔截讀取屬性值
  get(target, prop) {
    return Reflect.get(target, prop);
  },
  // 攔截設置屬性值或添加新屬性
  set(target, prop, value) {
    return Reflect.set(target, prop, value);
  },
  // 攔截刪除屬性
  deleteProperty(target, prop) {
    return Reflect.deleteProperty(target, prop);
  },
});

proxy.name = 'tom';
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  const user = {
    name: 'John',
    age: 12,
  };

  // proxyUser是代理對象, user是被代理對象
  // 后面所有的操作都是通過代理對象來操作被代理對象內部屬性
  const proxyUser = new Proxy(user, {
    get(target, prop) {
      console.log('劫持get()', prop);
      return Reflect.get(target, prop);
    },

    set(target, prop, val) {
      console.log('劫持set()', prop, val);
      return Reflect.set(target, prop, val); // (2)
    },

    deleteProperty(target, prop) {
      console.log('劫持delete屬性', prop);
      return Reflect.deleteProperty(target, prop);
    },
  });
  // 讀取屬性值
  console.log(proxyUser === user);
  console.log(proxyUser.name, proxyUser.age);
  // 設置屬性值
  proxyUser.name = 'bob';
  proxyUser.age = 13;
  console.log(user);
  // 添加屬性
  proxyUser.sex = '男';
  console.log(user);
  // 刪除屬性
  delete proxyUser.sex;
  console.log(user);
</script>
</body>
</html>

setup 細節

setup 執行的時機

在 beforeCreate 之前執行(一次),此時組件對象還沒有創建

this 是 undefined,不能通過 this 來訪問 data/computed/methods / props

其實所有的 composition API 相關回調函數中也都不可以

setup 的返回值

一般都返回一個對象:為模板提供數據,也就是模板中可以直接使用此對象中的所有屬性/方法

返回對象中的屬性會與 data 函數返回對象的屬性合並成為組件對象的屬性

返回對象中的方法會與 methods 中的方法合並成功組件對象的方法

如果有重名,setup 優先

注意:

  • 一般不要混合使用:methods 中可以訪問 setup 提供的屬性和方法, 但在 setup 方法中不能訪問 data 和 methods
  • setup 不能是一個 async 函數:因為返回值不再是 return 的對象,而是 promise, 模板看不到 return 對象中的屬性數據

setup 的參數

  • setup(props, context) / setup(props, {attrs, slots, emit})
  • props:包含 props 配置聲明且傳入了的所有屬性的對象
  • attrs:包含沒有在 props 配置中聲明的屬性的對象,相當於 this.$attrs
  • slots:包含所有傳入的插槽內容的對象,相當於 this.$slots
  • emit:、用來分發自定義事件的函數,相當於 this.$emit
<template>
  <h2>App父級組件</h2>
  <div>msg: {{ msg }}</div>
  <button @click="msg += '====='">更新數據</button>
  <hr>
  <child :msg="msg" msg2="真香" @xxx="xxx"/>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const msg = ref('what are you nong sha lei');

    function xxx(text: string) {
      console.log('=============xxx in');
      msg.value += text;
    }

    return {
      msg,
      xxx,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子級組件</h2>
  <div>msg: {{ msg }}</div>
  <div>count: {{ count }}</div>
  <button @click="emitXxx">分發事件</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Child',
  props: ['msg'],
  // 數據初始化周期的回調
  // beforeCreate() {
  //   console.log('beforeCreate執行了');
  // },
  // mounted() {
  //   console.log('mounted執行了');
  //   console.log(this);
  // },
  // setup細節問題
  // setup是在beforeCreate生命周期回調之前就執行了,而且就執行一次
  // 由此可以推斷出:setup在執行的時候,當前的組件還沒被創建出來,也就意味着:組件實例對象this根本就不能用

  // setup中的對象內部的屬性和data函數中的return對象的屬性都可以在html模板中使用
  // setup中的對象中的屬性和data函數中的屬性會合並為組件對象的屬性
  // setup和methods中的方法會合並為組件對象的方法
  // 在vue3中盡量不要混合使用data和setup及methods和setup
  // setup 不能是一個 async 函數:因為返回值不再是 return 的對象,而是 promise, 模板看不到 return 對象中的屬性數據
  setup(props, context) {
    console.log('setup執行了', this);
    // props參數是一個對象,里面有服及組件向自己組建傳遞的數據,並且是在子級組件中使用props接收到的所有屬性
    // 包含props配置聲明且傳入的所有屬性的對象
    console.log(props);
    // context參數,是一個對象,里面有attrs對象(獲取當前組件上的屬性,但是該屬性實在props中沒有聲明接受的對象),
    // emit方法(分發事件的),slots對象(插槽)
    console.log(context.attrs);
    // const showMsg1 = () => {
    //   console.log('setup中的showMsg1方法');
    // };

    function emitXxx() {
      console.log('=============emitXxx in');
      context.emit('xxx', '++');
    }

    return {
      // setup一般返回一個對象,對象中的屬性和方法可以在html模板中直接使用
      // showMsg1,
      emitXxx,
    };
  },
  // data() {
  //   return {
  //     count: 10,
  //   };
  // },
  // methods: {
  //   showMsg() {
  //     console.log('methods中的showMsg方法');
  //   },
  // },
});
</script>

<style scoped>

</style>

reactive 與 ref細節

是 Vue3 的 composition API 中 2 個最重要的響應式 API

ref 用來處理基本類型數據,reactive 用來處理對象(遞歸深度響應式)

如果用 ref 對象/數組,內部會自動將對象/數組轉換為 reactive 的代理對象

ref 內部:通過給 value 屬性添加 getter/setter 來實現對數據的劫持

reactive 內部:通過使用 Proxy 來實現對對象內部所有數據的劫持,並通過 Reflect 操作對象內部數據

ref 的數據操作:在 js 中要.value,在模板中不需要(內部解析模板時會自動添加.value)

<template>
  <h2>ref和reactive更新數據的問題</h2>
  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <hr>
  <button @click="update">更新數據</button>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  name: 'App',
  // 是 Vue3 的 composition API 中 2 個最重要的響應式 API
  // ref 用來處理基本類型數據, reactive 用來處理對象(遞歸深度響應式)
  // 如果用 ref 對象/數組, 內部會自動將對象/數組轉換為 reactive 的代理對象
  // ref 內部: 通過給 value 屬性添加 getter/setter 來實現對數據的劫持
  // reactive 內部: 通過使用 Proxy 來實現對對象內部所有數據的劫持, 並通過 Reflect 操作對象內部數據
  // ref 的數據操作: 在 js 中要.value, 在模板中不需要(內部解析模板時會自動添加.value)
  setup() {
    const m1 = ref('abc');
    const m2 = reactive({
      name: '小明',
      wife: {
        name: '小紅',
      },
    });
    const m3 = ref({
      name: '小明',
      wife: {
        name: '小紅',
      },
    });
    const update = () => {
      console.log(m3);
      m1.value += '===';
      m2.wife.name += '===';
      m3.value.wife.name += '===';
    };
    return {
      m1, m2, m3, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

計算屬性與監視

computed 函數:

  • 與 computed 配置功能一致
  • 只有 getter
  • 有 getter 和 setter

watch 函數

  • 與 watch 配置功能一致
  • 監視指定的一個或多個響應式數據,一旦數據變化,就自動執行監視回調
  • 默認初始時不執行回調,但可以通過配置 immediate 為 true,來指定初始時立即執行第一次
  • 通過配置 deep 為 true,來指定深度監視

watchEffect 函數

  • 不用直接指定要監視的數據,回調函數中使用的哪些響應式數據就監視哪些響應式數據
  • 默認初始時就會執行第一次,從而可以收集需要監視的數據
  • 監視數據發生變化時回調
<template>
  <h2>計算屬性和監視</h2>
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:<input type="text" placeholder="請輸入姓氏" v-model="user.firstName"> <br>
    名字:<input type="text" placeholder="請輸入名字" v-model="user.lastName"> <br>
  </fieldset>
  <fieldset>
    <legend>計算屬性和監視的演示</legend>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName1"> <br>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName2"> <br>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName3"> <br>
  </fieldset>
</template>

<script lang="ts">
import {
  computed, defineComponent, reactive, ref, watchEffect,
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 定義一個響應式的對象
    const user = reactive({
      firstName: '東方',
      lastName: '不敗',
    });

    // 通過計算屬性的方式,實現第一個姓名的顯示
    // vue3的計算屬性
    // 計算屬性中的函數中如果只傳入一個回調函數,表示的是get

    // 第一個姓名
    const fullName1 = computed(() => `${user.firstName}_${user.lastName}`);

    // 第二個姓名
    const fullName2 = computed({
      get() {
        return `${user.firstName}_${user.lastName}`;
      },
      set(val:string) {
        const names:Array = val.split('_');
        // eslint-disable-next-line prefer-destructuring
        user.firstName = names[0];
        // eslint-disable-next-line prefer-destructuring
        user.lastName = names[1];
      },
    });

    // 監視指定的數據
    const fullName3 = ref('');
    // watch(user, ({ firstName, lastName }) => {
    //   fullName3.value = `${firstName}_${lastName}`;
    // }, { immediate: true, deep: true });

    // 監視,不需要配置immediate,本身默認就會進行監視,(默認執行一次)
    watchEffect(() => {
      fullName3.value = `${user.firstName}_${user.lastName}`;
    });
    return {
      user,
      fullName1,
      fullName2,
      fullName3,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>計算屬性和監視</h2>
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:<input type="text" placeholder="請輸入姓氏" v-model="user.firstName"> <br>
    名字:<input type="text" placeholder="請輸入名字" v-model="user.lastName"> <br>
  </fieldset>
  <fieldset>
    <legend>計算屬性和監視的演示</legend>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName1"> <br>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName2"> <br>
    姓名:<input type="text" placeholder="顯示姓名" v-model="fullName3"> <br>
  </fieldset>
</template>

<script lang="ts">
import {
  computed, defineComponent, reactive, ref, watch, watchEffect,
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 定義一個響應式的對象
    const user = reactive({
      firstName: '東方',
      lastName: '不敗',
    });

    // 通過計算屬性的方式,實現第一個姓名的顯示
    // vue3的計算屬性
    // 計算屬性中的函數中如果只傳入一個回調函數,表示的是get

    // 第一個姓名
    const fullName1 = computed(() => `${user.firstName}_${user.lastName}`);

    // 第二個姓名
    const fullName2 = computed({
      get() {
        return `${user.firstName}_${user.lastName}`;
      },
      set(val:string) {
        const [firstName, lastName] = val.split('_');
        user.firstName = firstName;
        user.lastName = lastName;
      },
    });

    // 監視指定的數據
    const fullName3 = ref('');
    watch(user, ({ firstName, lastName }) => {
      fullName3.value = `${firstName}_${lastName}`;
    }, { immediate: true, deep: true });

    // 監視,不需要配置immediate,本身默認就會進行監視,(默認執行一次)
    // watchEffect(() => {
    //   fullName3.value = `${user.firstName}_${user.lastName}`;
    // });

    // 監視fullName3的數據,改變firstName和lastName
    watchEffect(() => {
      const [firstName, lastName] = fullName3.value.split('_');
      user.firstName = firstName;
      user.lastName = lastName;
    });

    // watch---可以監控多個數據的
    // 當我們使用watch監控非響應式的數據是,代碼需要改一下
    watch([() => user.firstName, () => user.lastName, fullName3], () => {
      console.log('===============');
    });

    return {
      user,
      fullName1,
      fullName2,
      fullName3,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

生命周期

Vue2.x 的生命周期

lifecycle_2

與 2.x 版本生命周期相對應的組合式 API

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

新增的鈎子函數

組合式 API 還提供了以下調試鈎子函數:

  • onRenderTracked
  • onRenderTriggered
<template>
  <h2>App父級組件</h2>
  <button @click="isShow = !isShow">切換顯示</button>
  <hr>
  <child v-if="isShow" />
</template>

<script lang="ts">
import {
  defineComponent, ref,
} from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const isShow = ref(true);

    return {
      isShow,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子級組件</h2>
  <h4>msg: {{ msg }}</h4>
  <hr>
  <button @click="update">更新數據</button>
</template>

<script lang="ts">
import {
  defineComponent,
  onBeforeMount,
  onBeforeUnmount,
  onBeforeUpdate,
  onMounted,
  onUnmounted,
  onUpdated,
  ref,
} from 'vue';

export default defineComponent({
  name: 'Child',
  // vue2.x的生命周期鈎子
  beforeCreate() {
    console.log('2.x中的 beforeCreate');
  },
  created() {
    console.log('2.x中的 created');
  },
  beforeMount() {
    console.log('2.x中的 beforeMount');
  },
  mounted() {
    console.log('2.x中的 mounted');
  },
  beforeUpdate() {
    console.log('2.x中的 beforeUpdate');
  },
  updated() {
    console.log('2.x中的 updated');
  },
  beforeUnmount() {
    console.log('2.x中的 beforeUnmount');
  },
  unmounted() {
    console.log('2.x中的 unmounted');
  },
  setup() {
    console.log('3.0中的 setup');
    const msg = ref('msg');

    const update = () => {
      msg.value += '===';
    };

    onBeforeMount(() => {
      console.log('3.0中的 onBeforeMount');
    });

    onMounted(() => {
      console.log('3.0中的 onMounted');
    });

    onBeforeUpdate(() => {
      console.log('3.0中的 onBeforeUpdate');
    });

    onUpdated(() => {
      console.log('3.0中的 onUpdated');
    });

    onBeforeUnmount(() => {
      console.log('3.0中的 onBeforeUnmount');
    });

    onUnmounted(() => {
      console.log('3.0中的 onUnmounted');
    });

    return {
      msg, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

自定義 hook 函數

  • 使用 Vue3 的組合 API 封裝的可復用的功能函數
  • 自定義 hook 的作用類似於 vue2 中的 mixin 技術
  • 自定義 Hook 的優勢:很清楚復用功能代碼的來源, 更清楚易懂

需求1

收集用戶鼠標點擊的頁面坐標

<template>
  <h2>自定義hook函數操作</h2>
  <h2>x:{{ x }}, y: {{ y }}</h2>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import useMousePosition from '@/hooks/useMousePosition';

export default defineComponent({
  name: 'App',
  // 需求1:用戶在頁面中點擊頁面,把點擊位置的橫縱坐標展示出來
  setup() {
    const { x, y } = useMousePosition();
    return {
      x, y,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
import { onBeforeUnmount, onMounted, ref } from 'vue';

export default function () {
  const x = ref(-1);
  const y = ref(-1);

  // 點擊事件的回調函數
  const clickHandler = (event:MouseEvent) => {
    x.value = event.pageX;
    y.value = event.pageY;
  };

  // 頁面接在完畢了,再進行點擊的操作
  // 頁面接在完畢的生命周期
  onMounted(() => {
    window.addEventListener('click', clickHandler);
  });

  onBeforeUnmount(() => {
    window.removeEventListener('click', clickHandler);
  });

  return {
    x, y,
  };
}

需求2

封裝發 ajax 請求的 hook 函數

利用 TS 泛型強化類型檢查

<template>
  <h2>自定義hook函數操作</h2>
  <h2>x:{{ x }}, y: {{ y }}</h2>
  <hr>
  <h3 v-if="loading">正在加載中</h3>
  <h3 v-else-if="errorMsg">錯誤信息:{{ errorMsg }}</h3>
  <ul v-else v-for="(item, index) in data" :key="index">
    <li>firstName: {{ item.firstName }}</li>
    <li>lastName: {{ item.lastName }}</li>
    <li>email: {{ item.email }}</li>
  </ul>
</template>

<script lang="ts">
import { defineComponent, watch } from 'vue';
import useMousePosition from '@/hooks/useMousePosition';
import useRequest from '@/hooks/useRequest';

interface IProgrammerData {
  firstName: string,
  lastName: string,
  email: string,
}

export default defineComponent({
  name: 'App',
  // 需求1:用戶在頁面中點擊頁面,把點擊位置的橫縱坐標展示出來
  setup() {
    const { x, y } = useMousePosition();

    const { loading, data, errorMsg } = useRequest<IProgrammerData[]>('/data/example.json');

    watch(data, () => {
      if (data.value) {
        console.log(data.value.length);
      }
    });

    return {
      x, y, loading, data, errorMsg,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
// 引入axios
// 發送ajax請求
import { ref } from 'vue';
import axios from 'axios';

export default function <T> (url: string) {
  // 加載的狀態
  const loading = ref(true);
  const data = ref<T | null>(null);
  const errorMsg = ref('');

  axios.get(url)
    .then((response) => {
      loading.value = false;
      data.value = response.data;
    })
    .catch((error) => {
      loading.value = false;
      errorMsg.value = error.message || '未知錯誤';
    });

  return {
    loading,
    data,
    errorMsg,
  };
}

toRefs

把一個響應式對象轉換成普通對象,該普通對象的每個 property 都是一個 ref

應用:當從合成函數返回響應式對象時,toRefs 非常有用,這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用

問題:reactive 對象取出的所有屬性值都是非響應式的

解決:利用 toRefs 可以將一個響應式 reactive 對象的所有原始屬性轉換為響應式的 ref 屬性

<template>
  <h2>toRefs的使用</h2>
  <h3>name: {{ name }}</h3>
  <h3>age: {{ age }}</h3>
</template>

<script lang="ts">
import {
  defineComponent, reactive, toRefs, 
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      name: '自來也',
      age: 47,
    });

    const state2 = toRefs(state);

    // toRefs可以吧reactive包裹的數據變成普通對象的每一個屬性包裹的ref對象
    // 定時器,更新數據
    setInterval(() => {
      state2.name.value += '==';
    }, 2000);

    return {
      // ...state, // 不是響應式的數據
      ...state2,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ref 獲取元素

利用 ref 函數獲取組件中的標簽元素

功能需求:讓輸入框自動獲取焦點

<template>
  <h2>ref的另一個作用:可以獲取頁面中的元素</h2>
  <input type="text" ref="inputRef">
</template>

<script lang="ts">
import {
  defineComponent, onMounted, ref,
} from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:當頁面加載完畢之后,頁面中的文本框可以直接獲取焦點(自動獲取焦點)
  setup() {
    // 默認是空的,頁面加載完畢,說明組件已經存在了,獲取文本框元素
    const inputRef = ref<HTMLElement | null>(null);

    // 頁面加載后的生命周期API
    onMounted(() => {
      // eslint-disable-next-line no-unused-expressions
      inputRef.value && inputRef.value.focus();
    });
    return {
      inputRef,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Composition API(其它部分)

shallowReactive 與 shallowRef

shallowReactive:只處理了對象內最外層屬性的響應式(也就是淺響應式)

shallowRef:只處理了 value 的響應式,不進行對象的 reactive 處理

什么時候用淺響應式呢?

  • 一般情況下使用 ref 和 reactive 即可
  • 如果有一個對象數據,結構比較深,但變化時只是外層屬性變化 ===> shallowReactive
  • 如果有一個對象數據,后面會產生新的對象來替換 ===> shallowRef
<template>
  <h2>ShallowReactive和ShallowRef</h2>
  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <h3>m4: {{ m4 }}</h3>
  <hr>
  <button @click="update">更新數據</button>
</template>

<script lang="ts">
import {
  defineComponent, reactive, ref, shallowReactive, shallowRef,
} from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:當頁面加載完畢之后,頁面中的文本框可以直接獲取焦點(自動獲取焦點)
  setup() {
    // 深度劫持、深度監視
    const m1 = reactive({
      name: '鳴人',
      age: 20,
      car: {
        name: '奔馳',
        color: 'red',
      },
    });
    // 淺劫持
    const m2 = shallowReactive({
      name: '鳴人',
      age: 20,
      car: {
        name: '奔馳',
        color: 'red',
      },
    });
    const m3 = ref({
      name: '鳴人',
      age: 20,
      car: {
        name: '奔馳',
        color: 'red',
      },
    });
    const m4 = shallowRef({
      name: '鳴人',
      age: 20,
      car: {
        name: '奔馳',
        color: 'red',
      },
    });

    const update = () => {
      // m1
      // m1.name += '==';
      // m1.car.name += '==';

      // m2
      // m2.name += '==';
      // m2.car.name += '==';

      // m3
      // m3.value.name += '==';
      // m3.value.car.name += '==';

      // m4
      m4.value.name += '==';
      m4.value.car.name += '==';
    };

    return {
      m1, m2, m3, m4, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Readonly 與 ShallowReadonly

Readonly:

  • 深度只讀數據
  • 獲取一個對象(響應式或純對象) 或 ref 並返回原始代理的只讀代理。
  • 只讀代理是深層的:訪問的任何嵌套 property 也是只讀的。

ShallowReadonly

  • 淺只讀數據
  • 創建一個代理,使其自身的 property 為只讀,但不執行嵌套對象的深度只讀轉換

應用場景

  • 在某些特定情況下, 我們可能不希望對數據進行更新的操作,那就可以包裝生成一個只讀代理對象來讀取數據,而不能修改或刪除
<template>
  <h2>ShallowReadonly和Readonly</h2>
  <h3>state: {{ state }}</h3>
  <hr>
  <button @click="update">更新數據</button>
</template>

<script lang="ts">
import { defineComponent, reactive, shallowReadonly } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      name: '佐助',
      age: 20,
      car: {
        name: '奔馳',
        color: 'yellow',
      },
    });

    // 只讀的數據,深度只讀
    // const state2 = readonly(state);
    const state2 = shallowReadonly(state);

    const update = () => {
      state2.name += '===';
      state2.car.name += '===';
    };

    return {
      state, state2, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRaw 與 markRaw

toRaw

  • 返回由 reactivereadonly 方法轉換成響應式代理的普通對象。
  • 這是一個還原方法,可用於臨時讀取,訪問不會被代理/跟蹤,寫入時也不會觸發界面更新。

markRaw

  • 標記一個對象,使其永遠不會轉換為代理。返回對象本身
  • 應用場景:
    • 有些值不應被設置為響應式的,例如復雜的第三方類實例或 Vue 組件對象。
    • 當渲染具有不可變數據源的大列表時,跳過代理轉換可以提高性能。
<template>
  <h2>toRaw和markRaw</h2>
  <h3>state: {{ state }}</h3>
  <hr>
  <button @click="testToRaw">測試toRaw</button>
  <button @click="testMarkRaw">測試markRaw</button>
</template>

<script lang="ts">
import {
  defineComponent, markRaw, reactive, toRaw,
} from 'vue';

interface UserInfo {
  name: string;
  age: number;
  likes?: string[];
}

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive<UserInfo>({
      name: '小明',
      age: 20,
    });

    const testToRaw = () => {
      // 把代理對象變成了普通對象
      const user = toRaw(state);
      user.name += '===';
    };

    const testMarkRaw = () => {
      // state.likes = ['吃', '喝'];
      // state.likes[0] += '==';
      const likes = ['吃', '喝'];
      // 標記之后無法成為代理對象
      state.likes = markRaw(likes);
      setInterval(() => {
        if (state.likes) {
          state.likes[0] += '==';
        }
      }, 2000);
    };

    return {
      state, testToRaw, testMarkRaw,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRef

  • 為源響應式對象上的某個屬性創建一個 ref 對象, 二者內部操作的是同一個數據值, 更新時二者是同步的
  • 區別 ref: 拷貝了一份新的數據值單獨操作, 更新時相互不影響
  • 應用: 當要將 某個 prop 的 ref 傳遞給復合函數時,toRef 很有用
<template>
  <h2>toRef使用及特點</h2>
  <h3>state: {{ state }}</h3>
  <h3>age: {{ age }}</h3>
  <h3>money: {{ money }}</h3>
  <hr>
  <button @click="update">更新數據</button>
  <hr>
  <child :age="age"/>
</template>

<script lang="ts">

import {
  defineComponent, reactive, ref, toRef,
} from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const state = reactive({
      age: 5,
      money: 100,
    });

    // 把響應式數據的state對象中的某個屬性age變成了ref對象了
    const age = toRef(state, 'age');
    // 把響應式對象中的某個屬性使用ref進行包裝,變成了一個ref對象
    const money = ref(state.money);

    console.log(age);
    console.log(money);

    const update = () => {
      // 更新數據的
      // state.age += 2;
      age.value += 3;

      money.value += 10;
    };

    return {
      state, age, money, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子級組件</h2>
  <h3>age: {{ age }}</h3>
  <h3>length: {{ length }}</h3>
</template>

<script lang="ts">

import {
  computed, defineComponent, Ref, toRef,
} from 'vue';

function useGetLength(age: Ref) {
  return computed(() => age.value.toString().length);
}

export default defineComponent({
  name: 'Child',
  props: {
    age: {
      type: Number,
      required: true,
    },
  },
  setup(props) {
    const length = useGetLength(toRef(props, 'age'));
    return {
      length,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

customRef

  • 創建一個自定義的 ref,並對其依賴項跟蹤和更新觸發進行顯式控制
  • 需求: 使用 customRef 實現 debounce 的示例
<template>
  <h2>CustomRef的使用</h2>
  <input type="text" v-model="keyword">
  <p>{{ keyword }}</p>
</template>

<script lang="ts">

import { customRef, defineComponent } from 'vue';

// 自定義hook防抖的函數
// value傳入的數據,將來的數據類型不確定,所以,用泛型,delay防抖的間隔事件,默認是200毫秒
function useDebounceRef<T>(value: T, delay = 200) {
  let timeoutId: number;
  return customRef((track, trigger) => ({
    // 返回數據的
    get() {
      // 告訴vue追蹤數據
      track();
      return value;
    },
    // 設置數據的
    set(newValue: T) {
      // 清理定時器
      clearInterval(timeoutId);
      // 開啟定時器
      setTimeout(() => {
        // eslint-disable-next-line no-param-reassign
        value = newValue;
        // 告訴vue更新界面
        trigger();
      }, delay);
    },
  }));
}

export default defineComponent({
  name: 'App',
  setup() {
    // const keyword = ref('abc');
    const keyword = useDebounceRef('abc', 500);

    return {
      keyword,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

provide 與 inject

  • provide和inject提供依賴注入,功能類似 2.x 的provide/inject
  • 實現跨層級組件(祖孫)間通信
<template>
  <h2>provide 和 inject</h2>
  <p>當前的顏色: {{ color }}</p>
  <button @click="color = 'red'">紅色</button>
  <button @click="color = 'yellow'">黃色</button>
  <button @click="color = 'green'">綠色</button>
  <hr>
  <son />
</template>

<script lang="ts">

import { defineComponent, provide, ref } from 'vue';
import Son from '@/components/Son.vue';

export default defineComponent({
  name: 'App',
  components: { Son },
  setup() {
    const color = ref('red');
    provide('color', color);
    return {
      color,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h3>兒子組件</h3>
  <hr>
  <grand-son />
</template>

<script>
import { defineComponent } from 'vue';
import GrandSon from './GrandSon.vue';

export default defineComponent({
  name: 'Son',
  components: { GrandSon },
});
</script>

<style scoped>

</style>
<template>
  <h3 :style="{color}">孫子組件</h3>
</template>
<script>
import { defineComponent, inject } from 'vue';

export default defineComponent({
  name: 'GrandSon',
  setup() {
    const color = inject('color');

    return {
      color,
    };
  },
});
</script>

<style scoped>

</style>

響應式數據的判斷

  • isRef: 檢查一個值是否為一個 ref 對象
  • isReactive: 檢查一個對象是否是由 reactive 創建的響應式代理
  • isReadonly: 檢查一個對象是否是由 readonly 創建的只讀代理
  • isProxy: 檢查一個對象是否是由 reactive 或者 readonly 方法創建的代理


免責聲明!

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



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