> **寫在前面:** > > * 本篇內容內容主要講述了,在使用 `Konva` 進行開發過程中遇到的一些問題。(既然是組件載入順序,主要牽扯到的就是,父子組件的關係,父子組件的生命周期) > > * 眾所周知,`Vue`中父子組件生命周期的執行順序為: > > ```javascript > / ...
寫在前面:
本篇內容內容主要講述了,在使用
Konva
進行開發過程中遇到的一些問題。(既然是組件載入順序,主要牽扯到的就是,父子組件的關係,父子組件的生命周期)眾所周知,
Vue
中父子組件生命周期的執行順序為:// 掛載階段 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted // 更新階段 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated // 銷毀階段 父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
然而,在某些情況下我們有其他需求,例如我們不得不讓子組件的初始化在父組件初始化完成之後再進行(一般是針對
mounted
),下麵將進行詳細說明
1、引用關係說明
- 最終目的:使用
Konva
庫繪製組件,該組件由兩個按鈕、一個電平表、一個增益控制推桿,這些子組件組合起來構成所需組件,並將其繪製到Stage
中的Layer
上Stage
和Layer
只有一個,所以應當寫在App.vue中,使用時將Layer
傳遞給子組件(且這個Layer應當是響應式的);且由於要繪製所需組件,因此自然是要引用所需組件,即所需組件是App.vue的子組件- 所需組件應當引用各個子組件,它是各個子組件的父親
- 綜上,是一個三層的繼承關係,此外,由於有了
Stage
和Layer
才能繪製所需組件,有了所需組件才能繪製各個子組件,此時,各個控制項的初始化順序與生命周期剛好相反。
2、兩層繼承關係示例
假如說目前我只有兩層繼承關係,
App.vue
和所需組件Channel.vue
代碼在下方展示,詳細的內容我將在代碼中使用註釋詳細說明,請按照註釋編號順序進行閱讀和理解
-
App.vue
要點:
layer
依賴註入,使所有的子組件可以獲取- 父組件的
layer
進行依賴註入時需要使用響應式,以便於父組件知道layer
的改變(類比C語言的直接傳參和指針傳參) - 需要將所需組件引用、註冊並展示到頁面上
<template> <div id="app"> <div id="frame"> <!-- 7. 將所需子組件展示到頁面 --> <Channel /> </div> </div> </template> <script> import Konva from 'konva'; import { computed } from 'vue'; // 5. 引入所需組件用於繪製和頁面展示 import Channel from './components/Channel.vue'; export default { // 2. 父組件將 layer 傳遞給子組件,子組件沒有 layer 就無法繪製組件 provide() { // 依賴註入,所有子組件可獲取 return { // 3. 傳遞給子組件的 layer應當是響應式的,否則對子組件的修改無法同步到父組件的layer layer: computed(() => this.layer), // 4. 響應式的區別,類比C語言的直接傳參和指針傳參 } }, components: { // 6. 註冊子組件 Channel, }, mounted() { // 0.初始化組件 this.initializeKonva(); window.addEventListener("resize", this.handleResize); }, beforeUnmount() { window.removeEventListener("resize", this.handleResize); }, data() { return { stage: null, layer: null, }; }, methods: { initializeKonva() { this.stage = new Konva.Stage({ container: "frame", width: window.innerWidth, height: window.innerHeight, }); // 1. 這裡為瞭解耦和效率,全局使用一個layer this.layer = new Konva.Layer(); this.stage.add(this.layer); }, handleResize() { this.stage.width(window.innerWidth); this.stage.height(window.innerHeight); this.stage.batchDraw(); }, }, }; </script> <style scoped> /* 樣式細節不表 */ </style>
-
Channel.vue
要點:
-
接收
layer
-
使用
this.$nextTick(() => { 初始化代碼 })
,會使得初始化代碼在父組件的初始化完成後再執行-
this.$nextTick()
是Vue.js
提供的一個方法,用於在DOM更新之後執行回調函數。它的作用是確保在下次DOM
更新迴圈結束之後執行回調函數,以確保操作的準確性和可靠性。 -
在
Vue.js
中,當數據發生改變時,Vue
會非同步地更新DOM
。這意味著在修改數據後立即訪問更新後的DOM
可能無法得到正確的結果,因為此時DOM
可能尚未完成更新。通過使用
this.$nextTick()
方法,我們可以將回調函數延遲到下一次DOM
更新迴圈之後執行。在這個時候,Vue
已經完成了所有的非同步DOM
更新,我們可以放心地操作更新後的DOM
元素,確保獲取到準確的結果。
-
-
繪製完成後更新
layer
<template> <div> <div ref="container"></div> </div> </template> <script> import Konva from 'konva'; export default { // 1. 接收父組件依賴註入的 layer inject: ['layer'], components: {}, data() {return {};}, mounted() { // 2. 使用 this.$nextTick(() => {}),在DOM更新之後執行回調函數 this.$nextTick(() => { // 3. 初始化 this.initializeKonva(); }); }, methods: { initializeKonva() { this.group = new Konva.Group({ // ... }); const backgroundRect = new Konva.Rect({ // ... }); const textTop = new Konva.Text({ // ... }); this.textLevel = new Konva.Text({ // ... }); this.textGain = new Konva.Text({ // ... }); const textBottom = new Konva.Text({ // ... }); const line1 = new Konva.Line({ // ... }); const line2 = new Konva.Line({ // ... }); const line3 = new Konva.Line({ // ... }); this.group.add(backgroundRect, textTop, this.textLevel, this.textGain, textBottom, line1, line2, line3); // 4. layer 是通過依賴註入傳遞,inject接收的,使用 this 訪問 this.layer.add(this.group); // 5. 更新 layer this.layer.draw(); }, }, }; </script> <style></style>
-
3、三層及以上繼承關係示例
- 在上面的內容中,使用
this.$nextTick(() => { 回調 })
解決了兩層繼承關係中的反向初始化順序的問題。- 但是這本質上更像是一種小聰明,當到了三層以上繼承關係的時候這種方法不能有任何效果,因為子組件和孫子組件如果不同時使用
this.$nextTick(() => { 回調 })
總會有人在父組件之前初始化,而如果都用了this.$nextTick(() => { 回調 })
那麼它們兩個本身的初始化順序仍然是先子後父,一定會出問題。所以要使用其他的方式來解決這個問題代碼在下方展示,詳細的內容我將在代碼中使用註釋詳細說明,請按照註釋編號順序進行閱讀和理解
-
Channel.vue
要點:
- 接收
layer
等不再贅述 - 使用
this.$nextTick(() => { 初始化代碼 })
,會使得初始化代碼在父組件的初始化完成後再執行 - 設置
flag
用於判斷當前組件初始化是否完成,使用v-if="flag"
控制子組件初始化時機
<template> <div> <div ref="container"></div> <!-- 3. v-if="flag" 控制子組件的初始化時機 --> <SwitchButton :btnNameIndex="0" :x="0" :y="group.height() / 17 + group.height() / 17 / 4" :parent="this.group" v-if="flag" /> <SwitchButton :btnNameIndex="1" :x="0" :y="group.height() / 17 * 3 - group.height() / 17 / 3" :parent="this.group" v-if="flag" /> <!-- 4. :parent="this.group" 將this.group傳遞給子組件,命名為parent,這種傳遞方式預設為響應式,無需其他操作 --> <LevelMeter :x="0" :y="group.height() / 17 * 4 + group.height() / 17 / 2" :parent="this.group" v-if="flag" @levelChangeEvent="handleLevelChange" /> <Gain :x="0" :y="group.height() / 17 * 4 + group.height() / 17 / 4" :parent="this.group" v-if="flag" @dBChangeEvent="handleDBChange" /> </div> </template> <script> import Konva from 'konva'; import SwitchButton from './SwitchButton.vue'; import LevelMeter from './LevelMeter.vue'; import Gain from './Gain.vue'; export default { inject: ['layer'], components: { SwitchButton, LevelMeter, Gain, }, data() { return { // ... // 0. 準備一個flag用於確認初始化時機 flag: false, group: null, }; }, mounted() { // 1. 存在父親,切需要使用父親中的 layer ,等待父組件初始化完成 this.$nextTick(() => { this.initializeKonva(); // 2. 使用flag判斷是否已經初始化完成 this.flag = true; }); }, methods: { initializeKonva() { // ... this.layer.add(this.group); this.layer.draw(); }, handleDBChange(newDB) { // ... }, handleLevelChange(newLevel) { // ... }, }, }; </script> <style></style>
- 接收