[程式] Vue 3 改版後與 Vue 2 的差異

紀錄 Vue 3 與 Vue 2 的差異.

Raellen
Raellen
Tags:
[程式] Vue 3 改版後與 Vue 2 的差異

學習資源

升級後的重大變革

新增功能

1. Composition API

  • 相關邏輯可以集中
  • 減少 this 指向的問題
  • Vue3 自定義Hook
    • 使用 Composition API 寫法取代舊版 mixins
    • hook是一個函式
    • 將程式碼拆解,能夠讓各個頁面引用
      • 可以當作公用的函式

2. useCssVars

  • 可以在組件加上自訂的 CSS 變數 var()

3. Teleport

  • Teleport 可以將定義的內容轉移到目標元素。

4. Fragments

  • 模板可以省略根元素,避免多出許多不必要的元素

5. Emits Component Option

  • Vue 3 提供 emits 的聲明,用法就像 props 一樣。

6. Suspense & Error Boundary

  • Suspense 是非同步載入的組件,在資料讀取完成以及渲染完成之前,會先顯示 #fallback 的內容。
  • 可以不用特地加一個 loading 變數來處理加載資料時的判斷

7. ::v-slotted & ::v-global

棄用的功能

1. KeyCode

2. $on, $off& $once

  • 移除 $on, $off& $once 也代表不能再使用EventBus

3. Inline Template Attribute

<!-- before -->
<my-comp inline-template :msg="parentMsg">
  {{ msg }} {{ childState }}
</my-comp>

<!-- after -->
<my-comp v-slot="{ childState }">
  {{ parentMsg }} {{ childState }}
</my-comp>
<!-- my-comp -->
<slot :childState="childState" />

4. Filters

5. $listeners

  • 在 Vue3 中 $listeners 已經整合到 $attrs
Vue2: 
<input type="text" v-bind="$attrs" v-on="$listeners" />
Vue3:
<input type="text" v-bind="$attrs" />

Composition API 簡介

Composition API 是什麼

Composition API 其實就是要取代 Vue2時,元件與元件間的程式碼與邏輯結構分散,難以重複使用的問題。 也可看作Function-based APIs,就是以函式作為邏輯的中心,將「該放一起的東西放在一起」。

Vue2 的 Options API 出了什麼問題?

原先,Vue2是將不同功能拆開來使用,但是隨著元件的增多。同一個邏輯操作的程式碼卻散佈在各個地方,不但不利於維護,也難以重複使用。

功能與邏輯的重複使用

  1. Mixins 不再建議繼續使用

  2. 自定義指令 透過app.directive() 來建立一個客製化的指令,假設叫它'img-fallback'

    當圖片載入錯誤時,用預設圖取代

    //建立新元件
    const app = vue.createApp({});
    
    //在 app 註冊自定義指令
    app.directive('img-fallback', {
        //mounted hook
        mounted(el){
        //當el觸發error事件的時候,把src換成預設圖
            el.add.EventListener('error',() => {
            el.src = 'default.png'
            })
        }    
    })
    
    

    此時便可在所有想設定預設圖的<img>標籤加上v-img-fallback的自訂指令

    <div-id="app">
    <img
        v-img-fallback
        src="noimage.png"
    </div>
    

    此時,當瀏覽器試圖取得不存在的圖檔(noimage.png)時,就會觸發error事件,並且將圖檔置換成預設圖片。

    除了 mounted鉤子外,還有其他的鉤子可以使用:

    • created
    • beforeMounted
    • mounted
    • beforeUpdate
    • updated
    • beforeUnmount
    • unmomunted

這裡的鉤子函式是「指令」所使用的鉤子函式,與生命週期鉤子沒有直接關係。

  1. Composition API 將跨元件共用的屬性(如data、computed、methods等)包裝起來,然後再將要使用的元件引入進去。 與mixins最大的差別在於,被import到元件內的屬性,必須要以「物件」或「函式」的形式來將它們引入到元件中。

Vue3 自定義Hook

  • 能夠將程式碼拆解,成為能夠引用的公用函式

範例程式碼:./mouse

import { ref, onMounted, onUnmounted } from "vue";

export function useMousePosition() {
  const x = ref(0);
  const y = ref(0);

  function update(e) {
    x.value = e.pageX;
    y.value = e.pageY;
  }

  onMounted(() => {
    window.addEventListener("mousemove", update);
  });

  onUnmounted(() => {
    window.removeEventListener("mousemove", update);
  });

  return { x, y };
}

在頁面或組件中引用的方式

// 引用整個函式
import { useMousePosition } from "./mouse";

export default {
  setup() {
      // 定義並return 函式所return的東西即可
    const { x, y } = useMousePosition();
    return { x, y };
  },
};

Composition API 核心

Compostion API 與 Options API 最大的差別:

  • 元件的實體物件內不會再有 data、computed、methods 與生命週期 Hooks 等屬性 禁止再將上述屬性放到實體物件中
  • 取代原先分散式的結構
  • 解決以 this 取得屬性所產生的問題
  • 以新的setup()函式取代

setup: 啟動元件的位置

  • 增加可讀性
  • 要將模板內會用到的部分 return 出去
setup(){
// 將程式邏輯定義在todoList()內
// 增加可讀性
    const {todo, items, add , remove } = todoList();

// 將模板會用到的部分 return 出去
    return {
    todo,
    items,
    add,
    remove
    };
}

props 與 context

當setup()啟動時,會帶入兩個參數:props與context 由於已經不再使用 this 來存取屬性,所以可以透過 setup 函式提供的 props 物件來取得 props 的內容

  • 取代原先以 this 存取屬性的 props
  • context名稱可自訂
export default {
props {
    defaultNum: {
    type: Number,
    defautl: 0
    }
},
//注意要加上props
    setup(props,context) {
        // 透過prop物件取的對應的屬性
        console.log(props.defaultNum);
    }
}

context物件

  • 提供了 attrs, slots, emit 三種屬性,分別對應到元件的實體物件上
  • 可以透過 context.attrs、context.slots、context.emit 來使用
<template>
    <div @click="clickInfo">
        子组件 == {{name1}}, {{title1}}
    </div>
</template>
<script>
import { reactive } from 'vue'
export default {
    name: 'child1',
    props: {
        name: String,
        title: String
    },
    setup(props, context){
        
        const name1 = reactive(props.name)
        const title1 = reactive(props.title)
        // 子组件点击事件
        const clickInfo = ()=>{
            // 抛出事件
            context.emit('itemclick', {name: 'emitClick'})
        }
        return { name1, title1, clickInfo };
    }
}
</script>

ref 與 reactive : 使值動態響應

在Compotision API 中,可以使用ref()來將數值進行包裝,並且回傳一個響應式的「物件」。 這個ref()函式:

  • 可以包裝原始型別(primitive),也可以包裝物件或陣列。
  • 必須後綴.value來讀取數值

在這個物件內,會提供一個value的屬性,可以透過這個value來更新或讀取狀態內的數值:

const count = ref(0);
console.log(count.value); //0
count.valut++;
console.log(count.value); //1

範例:count.vue

<template>
    <h1>{{ count }}</h1>
    <button @click="plus">Plus</button>
</template>

<script>
    // 透過import 引入 ref
    import { ref } from 'vue';
    
    export default {
        setup(){
            const count = ref(0)
        }
        
        return {
            // 透過 ref()包裝的數值可保有響應性
            count,
            plus,
            // 純數值在模板中同樣可以渲染,但不會有響應性追蹤
            nonReactiveCount: 0
        }
    }
</script>

ref 與 Dom 節點:替代$ref

由於setup()中已無this.$ref可用,要存取DOM中的元素,可利用先前提到的ref()

<template>
    <!-- 模板內要加上 ref 屬性 -->
    <div ref="root"></div>
</template>

<script>
    import { ref, onMounted } from 'vue'
    
    export default {
        setup(){
            // 這裡的 root 與 <div ref="root"> 配對
            const root = ref(null);
            
            //可以透過 root.value 取得實際的 DOM 元素
            onMounted(()=>{
                console.log(root.value);
            })
        return {
            root,
            }
        }
    }
</script>

v-for 與 ref 複數動態節點 : v-bind:ref

當 DOM 節點是透過 v-for 產生,同時又需要 ref 綁定模板時, 可以透過 v-bind:ref 綁定到某個陣列:

<div
    v-for="(item, i) in list"
    :ref=" el => { divs[i] = el }"
>
    {{ item }}
</div>

如此便能指定divs[0]、divs[1]、divs[2]所對應的<div>節點:

export default {
    setup(){
        const list = reactive([1,2,3]);
        const divs = ref ([]);
        
        //確保在每次更新前重置divs
        onBeforeUpdate(()=> {
            divs.value = [];;lll
        })
        
        return {
            list,
            divs,
        }
    }
}

reactive() : 包裝響應式物件的另一種函式

除了ref(),也可以使用reactive()來包裝響應式物件。 兩者最大的不同是:

  • ref()可以包裝原始型別(primitive),也可以包裝物件或陣列。
  • reactive內的參數只能是「物件」
  • reactive()存取內部屬性時,不需後綴.value
<template>
    <h1>{{ data.count }}</h1>
    <button @click="data.plus">plus</button>
</template>

<script>
import { reactive } from "vue"
export default {
    setup(){
        //reactive包裝物件
        const data = reactive({
        count:0,
        plus() => data.count++
        });
        
        return {
            data
        };
    }
};
</scrpit>
  • reactive()回傳一個被ES6 Proxy代理過的物件,才能做到響應式的更新。

toRefs 與 reactive:解決ES6解構語法造成的不響應問題

使用ES6的展開運算子「...」時,會將物件從響應式狀態抽離,變成普通物件。 因此需要改用 toRefs()

<template>
    <h1>{{ data.count }}</h1>
    <button @click="data.plus">plus</button>
</template>

<script>
import { toRefs } from "vue"
import counter from "./counter"

export default {
    setup(){
        //reactive包裝物件
        const count = counter();
        
        return {
            ...toRefs(count)
        };
    }
};
</scrpit>

computed

Composition API的computed,要改為函數式的寫法:

  • 要後綴 .value 來讀取值
  • computed 的參數為一個 getter 函式,並回傳一個 ref 物件
<template>
    <h1>{{ data.count }}</h1>
    <h1>{{ doubleCount }}</h1>
    <h1>{{ quadrupleCount }}</h1>
    
    <button @click="plus">
        Plus
    </button>
</template>

<script>
import { ref, computed } from "vue"
import counter from "./counter"

export default {
    setup(){
        const count = ref(0);
        
        // computed 的參數為一個 getter 函式,並回傳一個 ref 物件
        const doubleCount = computed(()=> count.value * 2);
        
        //使用 computed回傳的內容要後綴.value
        const quadrupleCount = computed(()=> doubleCount.value * 2);
        const plus = () => count.value++;
        
        return {
            count,
            doubleCount,
            quadrupleCount,
            plus
        };
    }
};
</scrpit>

computed 的 get 與 set

  • computed 可以傳入 getter
  • computed 可以傳入 get 與 set
const count = ref(0)

const plusOne = computed({
    get:() => count.value + 1,
    set:(val) => {
        count.value = val - 1
    }
})

readonly

將物件傳到readonly()中,會回傳一個被代理過的「唯獨」物件:

setup() {
    const original = reactive({ count: 0 });
    const copy = readonly(orignal);
    const plus = () => original.count++;
    
    return {
        original,
        copy,
        plus,
    }
}

執行plus()後,original.count 會從 0 變成 1 而透過readonly()包裝後回傳的copy物件,內部的.count 也會變成 1

methods

  • Composition API 不再提供methods
  • 直接將函式透過setup() return出去即可

watch & watchEffect

  • 在 Composition API中,要改為函數式語法
  • 可以觀察單一的 ref 物件
  • 也可以觀察響應的 reactive 物件

觀察單一 ref 物件

watch(count, (val, oldVal) => {
    console.log(`new count is $(val), preCount is $(oldVal)`)
})

觀察 reactive 物件

  • 將傳入 watch()的參數count 改為 ()=>data.count
watch(
    ()=>data.count,
    (val, oldVal) => { console.log(`new count is $(val), preCount is $(oldVal)`)}
)

透過陣列同時 watch 多個屬性

  • callback 是共用的
    watch(
        [() => data.count,() => data.doubleCount],
        ([newCount, newDoubleCount], [PreCount, PreDoubleCound]) => {
            console.log(`new count is $(newCount), prevCount is ${prevCount}` )
            console.log(`new doubleCount is $(newDoubleCount), prevDoubleCount is ${prevDoubleCount}` )
        }
    ) 

對不同屬性更新想執行不同的動作

  • 可以分別寫兩個 watch

觀察陣列或物件

  • 需要在watch()中加入第三個參數, {deep: true}

watchEffect

  • 當.value更新時,會呼叫callback
  • 與 watch的差別:
    • watchEffect會在 setup 剛建立時就執行一次
    • 不需要像watch一樣指定觀察的目標
    • 內部的callback函式對應數值更新後自動執行( 類似coumputed )
    • watchEffect 無法取得更新前的數值
watchEffect(()=> {
    console.log("watchEffect", count.value);
})

解除 watchEffect 觀察

  • 個別的watchEffect() 被呼叫後會回傳一個屬於它的停止函式。
  • 想要解除觀察時,可以呼叫該watchEffect() 所回傳的函式來停止觀察。
  • 只能在現存元件中使用,如果元件被銷毀,provide 與 inject 的連結就會失效
const stop1 = watchEffect(() => {
    console.log("watchEffect", count.value)
})

相依性注入 (Dependency Injection)

  • Composition API 版的 provide 與 inject store.js
    export {
        todoList: symbol()
    };
  • 在提供狀態的元件加上provide(),並將內容指定到對應的物件上: provide.vue
//提供者元件
import { ref, provide } from "vue";
import store from "./store";

export defautl {
    setup(){
        const todoList = ref([]);
        
        // 將 todoList 透過 provide 指定到 store.todoList
        provide(store.todoList, todoList);
        
        return {
            todoList,
        };
    },
};
  • 在取用的元件,透過inject取出: inject.vue
//取用者元件
import { ref, inject } from "vue";
import store from "./store";

export default {
    setup(){
        // 透過inject 取出 store.todoList
        const todoList = inject (store.todoList);
        
        return {
            todoList,
        }
    }
}

Composition API 的生命週期鉤子

  • 在 Composition API 中,生命鉤子改為函數式的語法:
//使用時先import
import { onMounted, onUpdated, onUnmounted } from 'vue';

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