vue3之Composition API詳解

poetries發表於2021-10-18

Composition API也叫組合式API,是Vue3.x的新特性。

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

通俗的講:

沒有Composition API之前vue相關業務的程式碼需要配置到option的特定的區域,中小型專案是沒有問題的,但是在大型專案中會導致後期的維護性比較複雜,同時程式碼可複用性不高。Vue3.x中的composition-api就是為了解決這個問題而生的

compositon api提供了以下幾個函式:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命週期的hooks

一、setup元件選項

新的 setup 元件選項在建立元件之前執行,一旦 props 被解析,並充當合成 API 的入口點

提示:

由於在執行 setup 時尚未建立元件例項,因此在 setup 選項中沒有 this。這意味著,除了props 之外,你將無法訪問元件中宣告的任何屬性——本地狀態、計算屬性或方法。

使用 setup 函式時,它將接受兩個引數:

  1. props
  2. context

讓我們更深入地研究如何使用每個引數

1. Props

setup 函式中的第一個引數是 props。正如在一個標準元件中所期望的那樣,setup 函式中的 props 是響應式的,當傳入新的 prop 時,它將被更新
// MyBook.vue

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

注意:

但是,因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性。

如果需要解構 prop,可以通過使用 setup 函式中的 toRefs 來安全地完成此操作。

// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
    const { title } = toRefs(props)

    console.log(title.value)
}

2. 上下文

傳遞給 setup 函式的第二個引數是 contextcontext 是一個普通的 JavaScript 物件,它暴露三個元件的 property
// MyBook.vue

export default {
  setup(props, context) {
    // Attribute (非響應式物件)
    console.log(context.attrs)

    // 插槽 (非響應式物件)
    console.log(context.slots)

    // 觸發事件 (方法)
    console.log(context.emit)
  }
}
context 是一個普通的 JavaScript 物件,也就是說,它不是響應式的,這意味著你可以安全地對 context 使用 ES6 解構
// MyBook.vue
export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}
attrsslots 是有狀態的物件,它們總是會隨元件本身的更新而更新。這意味著你應該避免對它們進行解構,並始終以 attrs.xslots.x 的方式引用 property。請注意,與 props 不同,attrsslots響應式的。如果你打算根據 attrsslots 更改應用副作用,那麼應該在 onUpdated 生命週期鉤子中執行此操作。

3. setup元件的 property

執行 setup 時,元件例項尚未被建立。因此,你只能訪問以下 property:
  • props
  • attrs
  • slots
  • emit

換句話說,你將無法訪問以下元件選項:

  • data
  • computed
  • methods

4. ref reactive 以及setup結合模板使用

在看setup結合模板使用之前,我們首先得知道refreactive 方法。

如果 setup 返回一個物件則可以在模板中繫結物件中的屬性和方法,但是要定義響應式資料的時候可以使用ref, reactive方法定義響應式的資料

錯誤寫法:
<template>
{{msg}}
<br>

<button @click="updateMsg">改變etup中的msg</button>

<br>
</template>

<script>
export default {
    data() {
        return {

        }
    },
    setup() {
        let msg = "這是setup中的msg";
        let updateMsg = () => {
            alert("觸發方法")
            msg = "改變後的值"
        }
        return {
            msg,
            updateMsg
        }
    },

}
</script>

<style lang="scss">
.home {
    position: relative;
}
</style>
正確寫法一:
ref用來定義響應式的 字串、 數值、 陣列、Bool型別
import {  
    ref
} from 'vue'
<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改變etup中的msg</button>
<br>
<br>
<ul>
    <li v-for="(item,index) in list" :key="index">
        {{item}}
    </li>
</ul>

<br>
</template>

<script>
import {

    ref
} from 'vue'

export default {
    data() {
        return {

        }
    },
    setup() {
        let msg = ref("這是setup中的msg");

        let list = ref(["馬總", "李總", "劉總"])

        let updateMsg = () => {
            alert("觸發方法");
            msg.value = "改變後的值"
        }
        return {
            msg,
            list,
            updateMsg
        }
    },

}
</script>

<style lang="scss">
.home {
    position: relative;
}
</style>
正確寫法二:

reactive 用來定義響應式的物件

import {
    reactive   
} from 'vue'
<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改變setup中的msg</button>
<br>
<br>
<ul>
    <li v-for="(item,index) in list" :key="index">
        {{item}}
    </li>
</ul>
<br>
{{setupData.title}}
<br>
<button @click="updateTitle">更新setup中的title</button>
<br>
<br>
</template>

<script>
import {
    reactive,
    ref
} from 'vue'

export default {
    data() {
        return {

        }
    },
    setup() {
        let msg = ref("這是setup中的msg");

        let setupData = reactive({
            title: "reactive定義響應式資料的title",
            userinfo: {
                username: "張三",
                age: 20
            }

        })

        let updateMsg = () => {
            alert("觸發方法");
            msg.value = "改變後的值"
        }
        let updateTitle = () => {
            alert("觸發方法");
            setupData.title = "我是改變後的title"

        }
        return {
            msg,
            setupData,
            updateMsg,
            updateTitle
        }
    },

}
</script>

<style lang="scss">
.home {
    position: relative;
}
</style>

說明:要改變ref定義的屬性名稱需要通過 屬性名稱.value來修改,要改變reactive中定義的物件名稱可以直接

5. 使用 this

setup() 內部,this 不會是該活躍例項的引用,因為 setup() 是在解析其它元件選項之前被呼叫的,所以 setup() 內部的 this 的行為與其它選項中的 this 完全不同。這在和其它選項式 API 一起使用 setup() 時可能會導致混淆

二、toRefs - 解構響應式物件資料

把一個響應式物件轉換成普通物件,該普通物件的每個 property 都是一個 ref ,和響應式物件 property 一一對應
<template>
<div>
    <h1>解構響應式物件資料</h1>
    <p>Username: {{username}}</p>
    <p>Age: {{age}}</p>
</div>
</template>

<script>
import {
    reactive,
    toRefs
} from "vue";

export default {
    name: "解構響應式物件資料",
    setup() {
        const user = reactive({
            username: "張三",
            age: 10000,
        });

        return {
            ...toRefs(user)
        };
    },
};
</script>
當想要從一個組合邏輯函式中返回響應式物件時,用 toRefs 是很有效的,該 API 讓消費元件可以 解構 / 擴充套件(使用 操作符)返回的物件,並不會丟失響應性:
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  })

  // 對 state 的邏輯操作
  // ....

  // 返回時將屬性都轉為 ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以解構,不會丟失響應性
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar,
    }
  },
}

三、computed - 計算屬性

<template>
<div>
    <h1>解構響應式物件資料+computed</h1>

    <input type="text" v-model="firstName" placeholder="firstName" />
    <br>
    <br>
    <input type="text" v-model="lastName" placeholder="lastName" />

    <br>
    {{fullName}}
</div>
</template>

<script>
import {
    reactive,
    toRefs,
    computed
} from "vue";

export default {
    name: "解構響應式物件資料",
    setup() {
        const user = reactive({
            firstName: "",
            lastName: "",
        });

        const fullName = computed(() => {
            return user.firstName + " " + user.lastName
        })

        return {
            ...toRefs(user),
            fullName
        };
    },
};
</script>

四、readonly “深層”的只讀代理

傳入一個物件(響應式或普通)或 ref,返回一個原始物件的只讀代理。一個只讀的代理是“深層的”,物件內部任何巢狀的屬性也都是隻讀的
<template>
  <div>
    <h1>readonly - “深層”的只讀代理</h1>
    <p>original.count: {{original.count}}</p>
    <p>copy.count: {{copy.count}}</p>
  </div>
</template>

<script>
import { reactive, readonly } from "vue";

export default {
  name: "Readonly",
  setup() {
    const original = reactive({ count: 0 });
    const copy = readonly(original);

    setInterval(() => {
      original.count++;
      copy.count++; // 報警告,Set operation on key "count" failed: target is readonly. Proxy {count: 1}
    }, 1000);


    return { original, copy };
  },
};
</script>

五、watchEffect

在響應式地跟蹤其依賴項時立即執行一個函式,並在更改依賴項時重新執行它。
<template>
<div>
    <h1>watchEffect - 偵聽器</h1>
    <p>{{data.count}}</p>
    <button @click="stop">手動關閉偵聽器</button>
</div>
</template>

<script>
import {
    reactive,
    watchEffect
} from "vue";
export default {
    name: "WatchEffect",
    setup() {
        const data = reactive({
            count: 1,
            num: 1
        });
        const stop = watchEffect(() => console.log(`偵聽器:${data.count}`));
        setInterval(() => {
            data.count++;
        }, 1000);
        return {
            data,
            stop
        };
    },
};
</script>

六、watch 、watch 與watchEffect區別

對比watchEffectwatch允許我們

  • 懶執行,也就是說僅在偵聽的源變更時才執行回撥;
  • 更明確哪些狀態的改變會觸發偵聽器重新執行;
  • 訪問偵聽狀態變化前後的值

更明確哪些狀態的改變會觸發偵聽器重新執行

<template>
<div>
    <h1>watch - 偵聽器</h1>
    <p>count1: {{data.count1}}</p>
    <p>count2: {{data.count2}}</p>
    <button @click="stopAll">Stop All</button>
</div>
</template>

<script>
import {
    reactive,
    watch
} from "vue";
export default {
    name: "Watch",
    setup() {
        const data = reactive({
            count1: 0,
            count2: 0
        });
        // 偵聽單個資料來源
        const stop1 = watch(data, () =>
            console.log("watch1", data.count1, data.count2)
        );
        // 偵聽多個資料來源
        const stop2 = watch([data], () => {
            console.log("watch2", data.count1, data.count2);
        });
        setInterval(() => {
            data.count1++;
        }, 1000);
        return {
            data,
            stopAll: () => {
                stop1();
                stop2();
            },
        };
    },
};
</script>

訪問偵聽狀態變化前後的值

<template>
<div>
    <h1>watch - 偵聽器</h1>
    <input type="text" v-model="keywords" />
</div>
</template>

<script>
import {
    ref,
    watch
} from "vue";
export default {
    name: "Watch",
    setup() {
        let keywords = ref("111");
        // 偵聽單個資料來源
        watch(keywords, (newValue, oldValue) => {
            console.log(newValue, oldValue)
        });

        return {
            keywords
        };
    },
};
</script>

懶執行,也就是說僅在偵聽的源變更時才執行回撥

<template>
<div>
    <h1>watch - 偵聽器</h1>
    <p>num1={{num1}}</p>
    <p>num2={{num2}}</p>
</div>
</template>

<script>
import {
    ref,
    watch,
    watchEffect
} from "vue";
export default {
    name: "Watch",
    setup() {
        let num1 = ref(10);
        let num2 = ref(10);
        // 偵聽單個資料來源
        watch(num1, (newValue, oldValue) => {
            console.log(newValue, oldValue)
        });

        watchEffect(() => console.log(`watchEffect偵聽器:${num2.value}`));

        return {
            num1,
            num2
        };
    },
};
</script>

七、組合式api生命週期鉤子

你可以通過在生命週期鉤子前面加上 “on” 來訪問元件的生命週期鉤子。

下表包含如何在 setup () 內部呼叫生命週期鉤子:

選項式 APIHook inside setup
beforeCreate不需要*
created不需要*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
因為 setup 是圍繞 beforeCreatecreated 生命週期鉤子執行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何程式碼都應該直接在 setup 函式中編寫
export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

八、Provider Inject

通常,當我們需要將資料從父元件傳遞到子元件時,我們使用 props。想象一下這樣的結構:你有一些深巢狀的元件,而你只需要來自深巢狀子元件中父元件的某些內容。在這種情況下,你仍然需要將 prop 傳遞到整個元件鏈中,這可能會很煩人

對於這種情況,我們可以使用 provideinject 對父元件可以作為其所有子元件的依賴項提供程式,而不管元件層次結構有多深。這個特性有兩個部分:父元件有一個 provide 選項來提供資料,子元件有一個 inject 選項來開始使用這個資料

1. 非組合式api中的寫法

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  provide: {
    location: 'North Pole',
    geolocation: {
      longitude: 90,
      latitude: 135
    }
  }
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
export default {
  inject: ['location', 'geolocation']
}
</script>

2. 組合式api中的寫法

Provider:
setup() 中使用 provide 時,我們首先從 vue 顯式匯入 provide 方法。這使我們能夠呼叫 provide 時來定義每個 property

provide 函式允許你通過兩個引數定義 property

  1. propertyname (<String> 型別)
  2. propertyvalue

使用 MyMap 元件,我們提供的值可以按如下方式重構:

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>
Inject:
setup() 中使用 inject 時,還需要從 vue 顯式匯入它。一旦我們這樣做了,我們就可以呼叫它來定義如何將它暴露給我們的元件。

inject 函式有兩個引數:

  1. 要注入的 property 的名稱
  2. 一個預設的值 (可選)

使用 MyMarker 元件,可以使用以下程式碼對其進行重構:

<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

Provider Inject 響應性

父元件:

import {
    provide,
    ref,
    reactive
} from 'vue'

setup() {
        const location = ref('北京')
        const geolocation = reactive({
            longitude: 90,
            latitude: 135
        })
        const updateLocation = () => {
            location.value = '上海'
        }
        provide('location', location);
        provide('geolocation', geolocation);
        return {
            updateLocation
        }
    }
<button @click="updateLocation">改變location</button>

子元件:

import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

相關文章