VUE 3.0 學習探索入門系列 - Vue3.x 生命週期 和 Composition API 核心語法理解(6)

村口蹲一郎發表於2020-03-30

1 Vue2.x 生命週期回顧

  1. beforeCreate,在例項初始化之後,資料觀測 (data observer) 和 event/watcher 事件配置之前被呼叫。
  2. created,在例項建立完成後被立即呼叫。在這一步,例項已完成以下的配置:資料觀測 (data observer),屬性和方法的運算,watch/event 事件回撥。然而,掛載階段還沒開始,$el 屬性目前尚不可用。
  3. beforeMount,在掛載開始之前被呼叫:相關的 render 函式首次被呼叫。
  4. mounted,例項被掛載後呼叫,這時 el 被新建立的 vm.$el 替換了。 如果根例項掛載到了一個文件內的元素上,當mounted被呼叫時 vm.$el 也在文件內。
  5. beforeUpdate,資料更新時呼叫,發生在虛擬 DOM 打補丁之前。這裡適合在更新之前訪問現有的 DOM,比如手動移除已新增的事件監聽器。
  6. updated,由於資料更改導致的虛擬 DOM 重新渲染和打補丁,在這之後會呼叫該鉤子。
  7. activated,被 keep-alive 快取的元件啟用時呼叫。
  8. deactivated,被 keep-alive 快取的元件停用時呼叫。
  9. beforeDestroy,例項銷燬之前呼叫。在這一步,例項仍然完全可用。
  10. destroyed,例項銷燬後呼叫。該鉤子被呼叫後,對應 Vue 例項的所有指令都被解綁,所有的事件監聽器被移除,所有的子例項也都被銷燬。
  11. errorCaptured,當捕獲一個來自子孫元件的錯誤時被呼叫。

參考:cn.vuejs.org/v2/api/#選項-…

以下是整個生命週期圖示:

VUE 3.0 學習探索入門系列 - Vue3.x 生命週期 和 Composition API 核心語法理解(6)

參考:cn.vuejs.org/v2/guide/in…

2 Vue3.x 生命週期變化

被替換

  1. beforeCreate -> setup()
  2. created -> setup()

重新命名

  1. beforeMount -> onBeforeMount
  2. mounted -> onMounted
  3. beforeUpdate -> onBeforeUpdate
  4. updated -> onUpdated
  5. beforeDestroy -> onBeforeUnmount
  6. destroyed -> onUnmounted
  7. errorCaptured -> onErrorCaptured

新增的

新增的以下2個方便除錯 debug 的回撥鉤子:

  1. onRenderTracked
  2. onRenderTriggered

參考:vue-composition-api-rfc.netlify.com/api.html#li…

特別說明

由於 Vue3.x 是相容 Vue2.x 的語法的,因此為了保證 Vue2.x 的語法能正常在 Vue3.x 中執行,大部分 Vue2.x 的回撥函式還是得到了保留。比如:雖然 beforeCreatecreatedsetup() 函式替代了,也就是說在 Vue3.x 中建議使用 setup(),而不是舊的API,但是如果你要用,程式碼也是正常執行的。

但是,以下2個生命週期鉤子函式被改名後,在 Vue3.x 中將不會再有 beforeDestroydestroyed

  1. beforeDestroy -> onBeforeUnmount
  2. destroyed -> onUnmounted

另外,假如 Vue3.x 在 Q2 如期 Release 的話,大家一定要注意,在混合使用 Vue2.x 和 Vue3.x 語法的時候,特別要注意這2套API的回撥函式的執行順序。

3 Vue2.x + Composition API 對比 Vue3.x 生命週期執行順序

如果大家看了我上一篇文章 VUE 3.0 學習探索入門系列 - 糾結要不要升級到Vue3.0?該如何升級?(5),我說過當 Vue3.x 正式 Realease 以後,我可能會先使用 Vue2.x + Composition API,完了再使用 Vue3.x 搭建新的基礎框架。

那我今天主要關心的一個問題,一旦我使用 Vue2.x + Composition API 進入過渡期後,當我再使用 Vue3.x 搭建新的框架,那麼之前的基礎元件還能使用麼?

先測試下生命週期函式的執行順序。

3.1 Vue2.x + Composition API 生命週期執行順序

如下示例,在 Vue2.x 中引入相容包 Composition API,然後Vue2.x 和 Vue3.x 的生命週期函式混合使用。

<template>
    <div>
        <p> {{ id }} </p>
        <p> {{ name }} </p>
    </div>
</template>
<script>
    import {
        ref,
        onBeforeMount,
        onMounted,
        onBeforeUpdate,
        onUpdated,
        onBeforeUnmount,
        onUnmounted
    } from '@vue/composition-api';

    export default {
        setup() {
            const id = ref(1)

            console.log('setup')

            onBeforeMount(() => {
                console.log('onBeforeMount')
            })
            onMounted(() => {
                console.log('onMounted')
            })
            onBeforeUpdate(() => {
                console.log('onBeforeUpdate')
            })
            onUpdated(() => {
                console.log('onUpdated')
            })
            onBeforeUnmount(() => {
                console.log('onBeforeUnmount')
            })
            onUnmounted(() => {
                console.log('onUnmounted')
            })

            // 測試 update 相關鉤子
            setTimeout(() => {
                id.value = 2
            }, 2000)

            return {
                id
            }
        },
        data() {
            console.log('data')
            return {
                name: 'lilei'
            }
        },
        beforeCreate() {
            console.log('beforeCreate')
        },
        created() {
            console.log('created')
        },
        beforeMount() {
            console.log('beforeMount')
        },
        mounted() {
            console.log('mounted')
            setTimeout(() => {
                this.id = 3;
            }, 4000)
        },
        beforeUpdate() {
            console.log('beforeUpdate')
        },
        updated() {
            console.log('updated')
        },
        beforeUnmount() {

        },
        unmounted() {
            console.log('unmounted')
        },
        beforeDestroy() {
            console.log('beforeDestroy')
        },
        destroyed() {
            console.log('destroyed')
        }
    }
</script>

複製程式碼

執行結果:

1. beforeCreate
2. setup
3. data
4. created
5. beforeMount
6. onBeforeMount
7. mounted
8. onMounted
9. beforeUpdate
10. onBeforeUpdate
11. updated
12. onUpdated
13. beforeDestroy
14. onBeforeUnmount
15. destroyed
16. onUnmounted
複製程式碼

結論

Vue2.x 中通過補丁形式引入 Composition API,進行 Vue2.xVue3.x 的回撥函式混用時:Vue2.x 的回撥函式會相對先執行,比如:mounted 優先於 onMounted

3.2 Vue3.x 生命週期執行順序

以下直接使用 Vue3.x 語法,看看其在相容 Vue2.x 情況下,生命週期回撥函式混合使用的執行順序。

<template>
    <div>
        <p> {{ id }} </p>
        <p> {{ name }} </p>
    </div>
</template>

<script>
import {
    ref,
    onBeforeMount,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    onRenderTracked,
    onRenderTriggered
} from 'vue';

export default {
    setup() {
        const id = ref(1)

        console.log('setup')

        onBeforeMount(() => {
            console.log('onBeforeMount')
        })
        onMounted(() => {
            console.log('onMounted')
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
        })
        onUpdated(() => {
            console.log('onUpdated')
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount')
        })
        onUnmounted(() => {
            console.log('onUnmounted')
        })
        onRenderTracked(() => {
            console.log('onRenderTracked')
        })
        onRenderTriggered(() => {
            console.log('onRenderTriggered')
        })

        // 測試 update 相關鉤子
        setTimeout(() => {
            id.value = 2;
        }, 2000)

        return {
            id
        }
    },
    data() {
        console.log('data')
        return {
            name: 'lilei'
        }
    },
    beforeCreate() {
        console.log('beforeCreate')
    },
    created() {
        console.log('created')
    },
    beforeMount() {
        console.log('beforeMount')
    },
    mounted() {
        console.log('mounted')
        setTimeout(() => {
            this.id = 3;
        }, 4000)
    },
    beforeUpdate() {
        console.log('beforeUpdate')
    },
    updated() {
        console.log('updated')
    },
    beforeUnmount() {
        console.log('beforeUnmount')
    },
    unmounted() {
        console.log('unmounted')
    }
}
</script>

<style scoped>
</style>
複製程式碼

執行結果:

1. beforeCreate
2. data
3. created
4. onRenderTracked
5. onRenderTracked
6. onBeforeMount
7. beforeMount
8. onMounted
9. mounted
10. onRenderTriggered
11. onRenderTracked
12. onRenderTracked
13. onBeforeUpdate
14. beforeUpdate
15. onUpdated
16. updated
17. onBeforeUnmount
18. beforeUnmount
19. onUnmounted
20. unmounted
複製程式碼

結論

Vue3.x 中,為了相容 Vue2.x 的語法,所有舊的生命週期函式得到保留(除了 beforeDestroydestroyed),當生命週期混合使用時:Vue3.x 的生命週期相對優先於 Vue2.x 的執行,比如:onMountedmounted 先執行。

4 Vue2.x + Composition API 過度到 Vue3.x 生命週期總結

綜上所述:

  • Vue2.x 中通過補丁形式引入 Composition API,進行 Vue2.xVue3.x 的回撥函式混用時:Vue2.x 的回撥函式會相對先執行,比如:mounted 優先於 onMounted
  • Vue3.x 中,為了相容 Vue2.x 的語法,所有舊的生命週期函式得到保留(除了 beforeDestroydestroyed)。當生命週期混合使用時:Vue3.x 的生命週期相對優先於 Vue2.x 的執行,比如:onMountedmounted 先執行。

通過對比可以得出:當你的主版本是哪個,當生命週期混用時,誰的回撥鉤子就會相對優先執行。

所以,這裡就會有點坑!為了給減小以後不必要的麻煩,如果大家在 Vue2.x 中通過補丁形式引入 Composition API的使用的時候,建議:

  1. 不要混用Vue2.x和Vue3.x的生命週期。要麼你繼續使用 Vue2.x 的鉤子函式,要麼使用 Vue3.x 的鉤子函式,這樣就沒問題。
  2. 在原則1的情況下,建議原始碼從工程或者目錄就區分開新老版本。方便以後升級或者被引入到 Vue3.x 使用的時候,更有針對性相容測試。

5 Composition API 核心語法

以下內容,大部分參考官方: Vue Composition API

5.1 setup 主執行函式

setup 是 Composition API 的核心,可以說也是整個 Vue3.x 的核心。

  • setup 就是將 Vue2.x 中的 beforeCreatecreated 代替了,以一個 setup 函式的形式,可以靈活組織程式碼了。
  • setup 還可以 return 資料或者 template,相當於把 datarender 也一併代替了!

為什麼說 setup 靈活了呢?因為在這個函式中,每個生命週期可以是一個函式,在裡面執行,以函式的方式程式設計。

下面看看其幾個核心特點:

1 返回一組資料給template

<template>
  <div>{{ count }} {{ object.foo }}</div>
</template>

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

export default {
  setup() {
    const count = ref(0)
    const object = reactive({ foo: 'bar' })

    // expose to template
    return {
      count,
      object
    }
  }
}
</script>
複製程式碼

2 使用jsx語法

<script>
import { h, ref, reactive } from 'vue'
export default {
    setup() {
        const count = ref(0)
        const object = reactive({ foo: 'bar' })

        return () => h('div', [
            h('p', { style: 'color: red' }, count.value),
            h('p', object.foo)
        ])
    }
}
</script>
複製程式碼

3 Typescript Type

interface Data {
  [key: string]: unknown
}

interface SetupContext {
  attrs: Data
  slots: Slots
  emit: ((event: string, ...args: unknown[]) => void)
}

function setup(
  props: Data,
  context: SetupContext
): Data
複製程式碼

4 引數

需要注意的是,在 setup 函式中,取消了 this!兩方面的原因:

  1. 由於 setup 是一個入口函式,本質是面向函式程式設計了,而 this 是物件導向的一種體現!在完全基於函式的程式設計世界中,這個 this 就很難在能達到跟 Vue2.x 那種基於 OOP 思想的 Options 機制的實現的效果。

  2. 同樣是基於函數語言程式設計,那麼如果加上 this,對於新手而言,本來 this 就不好理解,這時候就更加懵逼了。比如:

    setup() {
      function onClick() {
        this // 如果有 this,那麼這裡的 this 可能並不是你期待的!
      }
    }
    複製程式碼

取消了 this,取而代之的是 setup 增加了2個引數:

  • props,元件引數
  • context,上下文資訊
setup(props, context) {
    // props
    // context.attrs
    // context.slots
    // context.emit
}
複製程式碼

也許你會有疑問,僅有這2個引數就夠了麼?夠了。你在 Vue2.x 的時候,this 無法就是獲取一些 data、props、computed、methods 等麼?

其實,這2個引數都是外部引入的,這個沒辦法只能帶入初始化函式中。除此之外,你元件上用到的所有 this 能獲取的資料,現在都相當於在 setup 中去定義了,相當於區域性變數一樣,你還要 this 幹嘛呢?

比如:

setup(props, context) {
    // data
    const count = ref(1)
    
    // 生命週期鉤子函式
    onMounted(() => {
      console.log('mounted!')
    })
    
    // 計算函式
    const plusOne = computed(() => count.value + 1)
    
    // methods 方法
    const testMethod = () => {
        console.log('methods');
    }
    
    // return to template
    return {
        count,
        testMethod
    }
}
複製程式碼

一切在 setup 中,都相當於變成了 區域性變數 了,你還要 this 幹嘛?

當然,如果你要講 Vue2.x 和 Vue3.x 混用!那就很彆扭了,以後用 this,以後又不能用,你自己也會懵逼,所以在此建議:雖然Vue3.x相容Vue2.x語法,但是不建議混合使用各自語法!

5.2 reactive 方法

reactive 方法包裹後的 物件 就變成了一個代理物件,相當於 Vue2.x 中的 Vue.observable()。也就可以實現頁面和資料之間的雙向繫結了。

這個包裹的方法是 deep 的,對所有巢狀的屬性都生效。

注意: 一般約定 reactive 的引數是一個物件,而下文提到的 ref 的引數是一個基本元素。但如果反過來也是可以的,reactive 其實可以是任意值,比如:reactive(123) 也是可以變成一個代理元素,可以實現雙向繫結。

比如:

<template>
    <div>
        <p>{{ obj1.cnt }}</p>
        <p>{{ obj2.cnt }}</p>
    </div>
</template>

<script>
import { reactive } from 'vue'
setup() {
    // 普通物件
    const obj1 = {
        cnt: 1
    }
    // 代理物件
    const obj2 = reactive({
        cnt: 1
    })
    obj1.cnt++
    obj2.cnt++
    return {
        obj1,
        obj2
    }
}
</script>
複製程式碼

頁面顯示結果:

1
2
複製程式碼

可以看到,普通物件屬性更新時,頁面是不會同步更新的。只有代理物件,才可以實現雙向繫結。

5.3 ref 方法

ref 方法包裹後的 元素 就變成了一個代理物件。一般而言,這裡的元素引數指 基本元素 或者稱之為 inner value,如:number, string, boolean,null,undefied 等,object 一般不使用 ref,而是使用上文的 reactive

也就是說 ref 一般適用於某個元素的;而 reactive 適用於一個物件。

ref 也就相當於把單個元素轉換成一個 reactive 物件了,物件預設的鍵值名是:value

比如:

setup() {
    const count = ref(100)
}
複製程式碼

ref 包裹後的元素變成一個代理物件,效果就相當於:

setup() {
    const count = reactive({
        value: 100
    })
}
複製程式碼

因為變成了一個代理物件,所以取值的時候需要 .value

setup() {
    const count = ref(100)
    console.log(count.value) // output: 100
}
複製程式碼

另外 ref 的結果在 template 上使用時,會自動開啟 unwrap,不需要再加 .value

<template>
  <div>{{ count }}</div>
</template>

<script>
export default {
  setup() {
    return {
      count: ref(0)
    }
  }
}
</script>
複製程式碼

以下是一些基本元素 ref 的結果:

setup() {
    console.log(ref(100).value) // output: 100
    console.log(ref('test').value) // output: test
    console.log(ref(true).value) // output: true
    console.log(ref(null).value) // output: null
    console.log(ref(undefined).value) // output: undefined
    console.log(ref({}).value) // output: {}
}
複製程式碼

5.3 isRef 方法

判斷一個物件是否 ref 代理物件。

const unwrapped = isRef(foo) ? foo.value : foo
複製程式碼

5.4 toRefs 方法

將一個 reactive 代理物件打平,轉換為 ref 代理物件,使得物件的屬性可以直接在 template 上使用。

看看下面的例子你可能就明白它的作用了。

<template>
  <p>{{ obj.count }}</p>
  <p>{{ count }}
  <p>{{ value }}
</template>

<script>
export default {
  setup() {
    const obj = reactive({
        count: 0,
        value: 100
    })
    return {
      obj,
      // 如果這裡的 obj 來自另一個檔案,
      // 這裡就可以不用包裹一層 key,可以將 obj 的元素直接平鋪到這裡
      // template 中可以直接獲取屬性
      ...toRefs(obj)
    }
  }
}
</script>
複製程式碼

5.5 computed 函式

與 Vue2.x 中的作用類似,獲取一個計算結果。當然功能有所增強,不僅支援取值 get(預設),還支援賦值 set

注意: 結果是一個 ref 代理物件,js中取值需要 .value

正常獲取一個計算結果:

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 報錯,因為未實現 set 函式,無法賦值操作!
複製程式碼

當 computed 引數使用 object 物件書寫時,使用 get 和 set 屬性。set 屬性可以將這個物件程式設計一個可寫的物件。

也就是說 computed 不僅可以獲取一個計算結果,它還可以反過來處理 ref 或者 reactive 物件!

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

plusOne.value = 1
console.log(count.value) // 0
console.log(plusOne.value) // 100
複製程式碼

plusOne.value = 1 相當於給計算物件賦值,會觸發 set 函式,於是 count 值被修改了。

5.5 readonly 函式

使用 readonly 函式,可以把 普通 object 物件reactive 物件ref 物件 返回一個只讀物件。

返回的 readonly 物件,一旦修改就會在 console 有 warning 警告。程式還是會照常執行,不會報錯。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 只要有資料變化,這個函式都會執行
  console.log(copy.count)
})

// 這裡會觸發 watchEffect
original.count++

// 這裡不會觸發上方的 watchEffect,因為是 readonly。
copy.count++ // warning!
複製程式碼

還有一些 API 諸如 watchwatchEffect 等,這裡就不說了,以上是一些比較重要的的語法,希望能對大家有所幫助。

(全文完)

相關文章