vue 元件通訊看這篇就夠了

於是乎_發表於2019-06-17

vue 元件間的通訊是 vue 開發中很基礎也十分重要的部分,作為使用 vue 的開發者每天都在使用。 同時,vue 通訊也是面試中非常高頻的問題,有很多面試題,都是圍繞通訊展開。

本文會介紹常見的通訊方式,並分析每種方式的使用場景和注意點。

vue中提倡單向資料流,這是為了保證資料流向的簡潔性,使程式更易於理解。但對於一些邊界情況,vue也提供了隱性的通訊方式,這些通訊方式會打破單向資料流的原則,應該謹慎使用。

下面我們將元件通訊分為父子元件通訊 和 非父子元件通訊進行分析。

父子元件通訊

prop 和 events

propevents 最基礎也最常用,這裡不提供示例。
通過 prop 向下傳遞,通過事件向上傳遞是一個 vue 專案最理想的通訊狀態。
使用時有兩點需要注意:

第一,不應該在一個子元件內部改變 prop,這樣會破壞單向的資料繫結,導致資料流難以理解。如果有這樣的需要,可以通過 data 屬性接收或使用 computed 屬性進行轉換。
第二,如果 props 傳遞的是引用型別(物件或者陣列),在子元件中改變這個物件或陣列,父元件的狀態會也會做相應的更新,利用這一點就能夠實現父子元件資料的“雙向繫結”,雖然這樣實現能夠節省程式碼,但會犧牲資料流向的簡潔性,令人難以理解,最好不要這樣去做。想要實現父子元件的資料“雙向繫結”,可以使用 v-model.sync

v-model 指令

v-model 是用來在表單控制元件或者元件上建立雙向繫結的,他的本質是 v-bindv-on 的語法糖,在一個元件上使用 v-model,預設會為元件繫結名為 valueprop 和名為 input 的事件。
當我們元件中的某一個 prop 需要實現上面所說的”雙向繫結“時,v-model 就能大顯身手了。有了它,就不需要自己手動在元件上繫結監聽當前例項上的自定義事件,會使程式碼更簡潔。

下面以一個 input 元件實現的核心程式碼,介紹下 v-model 的應用。

<!--父元件-->
<template>
    <base-input v-model="input"></base-input>
</template>
<script>
    export default {
        data() {
            return {
                input: ''
            }
        },
    }
</script>
複製程式碼
<!--子元件-->
<template>
    <input type="text" :value="currentValue"  @input="handleInput">
</template>
<script>
    export default {
        data() {
            return {
                currentValue: this.value === undefined || this.value === null ? ''
            }
        },
        props: {
            value: [String, Number],
        },
        methods: {
            handleInput(event) {
                const value = event.target.value;
                this.$emit('input', value);
            },
        },
}
</script>
複製程式碼

有時,在某些特定的控制元件中名為 value 的屬性會有特殊的含義,這時可以通過 model 選項來回避這種衝突。

.sync 修飾符

.sync 修飾符在 vue 1.x 的版本中就已經提供,1.x 版本中,當子元件改變了一個帶有 .syncprop 的值時,會將這個值同步到父元件中的值。這樣使用起來十分方便,但問題也十分明顯,這樣破壞了單向資料流,當應用複雜時,debug 的成本會非常高。於是乎,在vue 2.0中移除了 .sync。 但是在實際的應用中,.sync 是有它的應用場景的,所以在 vue 2.3 版本中,又迎來了全新的 .sync

新的 .sync 修飾符所實現的已經不再是真正的雙向繫結,它的本質和 v-model 類似,只是一種縮寫。

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
複製程式碼

上面的程式碼,使用 .sync 就可以寫成

<text-document v-bind:title.sync="doc.title"></text-document>
複製程式碼

這樣,在子元件中,就可以通過下面程式碼來實現對這個 prop 重新賦值的意圖了。

this.$emit('update:title', newTitle)
複製程式碼

v-model 和 .sync 對比

.sync 從功能上看和 v-model 十分相似,都是為了實現資料的“雙向繫結”,本質上,也都不是真正的雙向繫結,而是語法糖。
相比較之下,.sync 更加靈活,它可以給多個 prop 使用,而 v-model 在一個元件中只能有一個。
從語義上來看,v-model 繫結的值是指這個元件的繫結值,比如 input 元件,select 元件,日期時間選擇元件,顏色選擇器元件,這些元件所繫結的值使用 v-model 比較合適。其他情況,沒有這種語義,個人認為使用 .sync 更好。

ref

ref 特性可以為子元件賦予一個 ID 引用,通過這個 ID 引用可以直接訪問這個子元件的例項。 當父元件中需要主動獲取子元件中的資料或者方法時,可以使用 $ref 來獲取。

<!--父元件-->
<template>
    <base-input ref="baseInput"></base-input>
</template>
<script>
    export default {
        methods: {
        focusInput: function () {
            this.$refs.usernameInput.focus()
        }
    }
}
</script>
複製程式碼
<!--子元件-->
<template>
    <input ref="input">
</template>
<script>
    export default {
    methods: {
        focus: function () {
            this.$refs.input.focus()
        }
    }
}
</script>
複製程式碼

使用 ref 時,有兩點需要注意

  1. $refs 是作為渲染結果被建立的,所以在初始渲染的時候它還不存在,此時無法無法訪問。
  2. $refs 不是響應式的,只能拿到獲取它的那一刻子元件例項的狀態,所以要避免在模板和計算屬性中使用它。

$parent 和 $children

$parent 屬性可以用來從一個子元件訪問父元件的例項,$children 屬性 可以獲取當前例項的直接子元件。
看起來使用 $parent 比使用prop傳值更加簡單靈活,可以隨時獲取父元件的資料或方法,又不像使用 prop 那樣需要提前定義好。但使用 $parent 會導致父元件資料變更後,很難去定位這個變更是從哪裡發起的,所以在絕大多數情況下,不推薦使用。
在有些場景下,兩個元件之間可能是父子關係,也可能是更多層巢狀的祖孫關係,這時就可以使用 $parent
下面是 element ui 中的元件 el-radio-group 和 元件 el-radio 使用示例:

<template>
  <el-radio-group v-model="radio1">
    <el-radio :label="3">備選項</el-radio>
    <component-1>
        <el-radio :label="3">備選項</el-radio>
    </component-1>
  </el-radio-group>
</template>

<script>
  export default {
    data () {
      return {
        radio2: 3
      };
    }
  }
</script>
複製程式碼

在 el-radio-group 和 元件 el-radio 通訊中, 元件 el-radio 的 value 值需要和 el-radio-group的 v-model 的值進行“繫結”,我們就可以在 el-radio 內藉助 $parent 來訪問到 el-radio-group 的例項,來獲取到 el-radio-group 中 v-model 繫結的值。
下面是獲取 el-radio 元件中獲取 el-radio-group 例項的原始碼:

// el-radio元件
    let parent = this.$parent;
    while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
        } else {
            this._radioGroup = parent; // this._radioGroup 為元件 el-radio-group 的例項
        }
    }
複製程式碼

非父子元件通訊

$attrs 和 $listeners

當要和一個巢狀很深的元件進行通訊時,如果使用 propevents 就會顯的十分繁瑣,中間的元件只起到了一箇中轉站的作用,像下面這樣:

<!--父元件-->
  <parent-component :message="message">我是父元件</parent-component>
<!--子元件-->
  <child-component :message="message">我是子元件</child-component>
<!--孫子元件-->
  <grand-child-component :message="message">我是孫子元件</grand-child-component>
複製程式碼

當要傳遞的資料很多時,就需要在中間的每個元件都重複寫很多遍,反過來從後代元件向祖先元件使用 events 傳遞也會有同樣的問題。使用 $attrs$listeners 就可以簡化這樣的寫法。

$attrs 會包含父元件中沒有被 prop 接收的所有屬性(不包含class 和 style 屬性),可以通過 v-bind="$attrs" 直接將這些屬性傳入內部元件。

$listeners 會包含所有父元件中的 v-on 事件監聽器 (不包含 .native 修飾器的) ,可以通過 v-on="$listeners" 傳入內部元件。

下面以父元件和孫子元件的通訊為例介紹它們的使用:

<!--父元件 parent.vue-->
<template>
    <child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
    inheritAttrs: false,
    data() {
        return {
            name: '通訊',
            message: 'Hi',
        }
    },
    methods: {
        sayHello(mes) {
            console.log('mes', mes) // => "hello"
        },
    },
}
</script>
複製程式碼
<!--子元件 child.vue-->
<template>
    <grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
    data() {
        return {}
    },
    props: {
        name,
    },
}
</script>
複製程式碼
<!--孫子元件 grand-child.vue-->
<template>
</template>
<script>
export default {
    created() {
        this.$emit('sayHello', 'hello')
    },
}
</script>
複製程式碼

provide 和 inject

provideinject 需要在一起使用,它可以使一個祖先元件向其所有子孫後代注入一個依賴,可以指定想要提供給後代元件的資料/方法,不論元件層次有多深,都能夠使用。

<!--祖先元件-->
<script>
export default {
    provide: {
        author: 'yushihu',
    },
    data() {},
}
</script>

複製程式碼
<!--子孫元件-->
<script>
export default {
    inject: ['author'],
    created() {
        console.log('author', this.author) // => yushihu
    },
}
</script>
複製程式碼

provideinject 繫結不是響應的,它被設計是為元件庫和高階元件服務的,平常業務中的程式碼不建議使用。

dispatch 和 broadcast

vue 在2.0版本就已經移除了 $dispatch$broadcast,因為這種基於元件樹結構的事件流方式會在元件結構擴充套件的過程中會變得越來越難維護。但在某些不使用 vuex 的情況下,仍然有使用它們的場景。所以 element ui 和 iview 等開源元件庫中對 broadcastdispatch 方法進行了重寫,並通過 mixin 的方式植入到每個元件中。
實現 dispatchbroadcast 主要利用我們上面已經說過的 $parent$children

//element ui 中重寫 broadcast 的原始碼
function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;
    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
複製程式碼

broadcast 方法的作用是向後代元件傳值,它會遍歷所有的後代元件,如果後代元件的 componentName 與當前的元件名一致,則觸發 $emit 事件,將資料 params 傳給它。

 //element ui 中重寫 dispatch 的原始碼
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
複製程式碼

dispatch 的作用是向祖先元件傳值,它會一直尋找父元件,直到找到元件名和當前傳入的元件名一致的祖先元件,就會觸發其身上的 $emit 事件,將資料傳給它。這個尋找對應的父元件的過程和文章前面講解 $parent 的例子類似。

eventBus

對於比較小型的專案,沒有必要引入 vuex 的情況下,可以使用 eventBus。相比我們上面說的所有通訊方式,eventBus 可以實現任意兩個元件間的通訊。
它的實現思想也很好理解,在要相互通訊的兩個元件中,都引入同一個新的vue例項,然後在兩個元件中通過分別呼叫這個例項的事件觸發和監聽來實現通訊。

//eventBus.js
import Vue from 'vue';  
export default new Vue(); 
複製程式碼
<!--元件A-->
<script>
import Bus from 'eventBus.js'; 
export default {
    methods: {  
        sayHello() {  
            Bus.$emit('sayHello', 'hello');   
        }  
    } 
}
</script>
複製程式碼
<!--元件B-->
<script>
import Bus from 'eventBus.js'; 
export default {
    created() {  
        Bus.$on('sayHello', target => {  
            console.log(target);  // => 'hello'
        });  
    } 
}
</script>
複製程式碼

通過 $root 訪問根例項

通過 $root,任何元件都可以獲取當前元件樹的根 Vue 例項,通過維護根例項上的 data,就可以實現元件間的資料共享。

//main.js 根例項
new Vue({
    el: '#app',
    store,
    router,
    // 根例項的 data 屬性,維護通用的資料
    data: function () {
        return {
            author: ''
        }
    }, 
    components: { App },
    template: '<App/>',
});
複製程式碼
<!--元件A-->
<script>
export default {
    created() {  
        this.$root.author = '於是乎'
    }
}
</script>
複製程式碼
<!--元件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>
複製程式碼

通過這種方式,雖然可以實現通訊,但在應用的任何部分,任何時間發生的任何資料變化,都不會留下變更的記錄,這對於稍複雜的應用來說,除錯是致命的,不建議在實際應用中使用。

Vuex

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。對一箇中大型單頁應用來說是不二之選。
使用 Vuex 並不代表就要把所有的狀態放入 Vuex 管理,這樣做會讓程式碼變的冗長,無法直觀的看出要做什麼。對於嚴格屬於元件私有的狀態還是應該在元件內部管理更好。

自己實現簡單的 Store 模式

對於小型的專案,通訊十分簡單,這時使用 Vuex 反而會顯得冗餘和繁瑣,這種情況最好不要使用 Vuex,可以自己在專案中實現簡單的 Store。

//store.js
var store = {
  debug: true,
  state: {
    author: 'yushihu!'
  },
  setAuthorAction (newValue) {
    if (this.debug) console.log('setAuthorAction triggered with', newValue)
    this.state.author = newValue
  },
  deleteAuthorAction () {
    if (this.debug) console.log('deleteAuthorAction triggered')
    this.state.author = ''
  }
}
複製程式碼

和 Vuex 一樣,store 中 state 的改變都由 store 內部的 action 來觸發,並且能夠通過 log 保留觸發的痕跡。這種方式十分適合在不需要使用 Vuex 的小專案中應用。
$root 訪問根例項的方法相比,這種集中式狀態管理的方式能夠在除錯過程中,通過 log 記錄來確定當前變化是如何觸發的,更容易定位問題。

歡迎關注我的公眾號「前端小苑」,我會定期在上面更新原創文章。

vue 元件通訊看這篇就夠了

相關文章