Vue元件之間的資料傳遞(通訊、互動)詳解

YXi發表於2019-08-12

預設情況下資料不能在元件之間共享,但是可以通過以下幾種方法實現元件之間的資料傳遞:

props

父子元件之間的資料傳遞:

  • 父傳子
  • 子傳父

父傳子

  • 確定父中有資料
  • 在父元件的模板中通過屬性繫結,把資料綁到子元件上
  • 在子元件中定義props屬性,用來接收父傳遞過來的資料
  • 在子元件的模板就可以直接使用接收過來的資料

父繫結資料,子接收資料

詳細的格式:

Props:{
	資料項名字:{
		type:型別。指明從父元件中傳遞過來的資料必須是什麼型別。它的取值是:Object,Array,String,Number,Boolean 都是構造器。不要寫成字串  
		default://預設值。當父元件沒有傳資料時,就用這個值  
		required:true/false 。是否必須一定要傳遞過來
	}
}
複製程式碼

子傳父

  • 在父元件的模板中,給子元件新增一個事件監聽
  • 在子元件中,某個時間通過this.$emit發出這個事件,發出事件的同時可以攜帶資料 (this.$emit("事件名",附加的資料))
  • 當父中的方法觸發,資料作用與這個方法的第一個引數

父傳子傳孫,只能一級一級的傳,不能跨級傳

示例:

<body>
    <!-- ******************************************************************** -->
    <!-- 父傳子 -->
    <!-- <div id="app">
        <h1>父元件 ->資料:{{num}}</h1>
        <hr>
        <son :sonnum="num" :sonname="name"></son>
    </div>

    <template id="son">
        <div>
            子元件 -> 資料:{{mysum}} -> {{sonname}}
            <button @click="sonnum=200">修改資料為200</button>
            <h2>{{mysum}}</h2>
            <button @click="mysum=200">修改資料為200</button>
        </div>
    </template> -->
    <!-- ******************************************************************** -->

    <!-- 子傳父 -->
    <div id="app">
        <h1>父元件</h1>

        <son @submitmsg="addmsg"></son>
        <h2>{{a}}</h2>
    </div>

    <template id="son">
        <div>
            <h3>子元件</h3>
            <button @click="fashe">發射</button>
        </div>
    </template>


    <script>
        //  子傳父************************************************************************
        let Son = {
            template: "#son",
            data() {
                return {
                    num:111,
                }
            },
            methods: {
                fashe() {
                    this.$emit("submitmsg", this.num)
                }
            }
        }
        let vm = new Vue({
            el: "#app",
            data: {
                a: 0,
            },
            methods: {
                addmsg(info) {
                    this.a = info
                }
            },
            components: {
                Son,
            }
        })

//  父傳子*************************************************************************       
        // let Son = {
        //     template: "#son",
        //     data() {
        //         return {
        //             mysum: this.sonnum,

        //         }
        //     },
        //     props: {
        //         sonnum: Number,
        //         sonname: {
        //             type: String,
        //             // required:true,
        //             default: "jun",
        //         }
        //     },
        //     methods: {

        //     }
        // }
        // let vm = new Vue({
        //     el: "#app",
        //     data: {
        //         num: 100,
        //         name: "fan"
        //     },
        //     methods: {

        //     },
        //     components: {
        //         Son,
        //     }
        // })
//********************************************************************************
    </script>
</body>
複製程式碼

這麼多東西,相信你也懶得看,你可以自己建一個檔案,複製到裡面測試一下

$attrs

如果想要把父元件的資料傳遞給子子元件,如果使用props繫結來進行資訊的傳遞,顯然是比較繁瑣的
為了解決該需求,引入了 $attrs

$attrs 可以收集父元件中的所有傳過來的屬性除了那些在元件中沒有通過 props 定義的

示例:
首先有三個元件A-B-C,然後想A中的屬性傳入C中,基本的做法是這樣的,一層一層通過 props 往下傳遞

<template>
  <div id="app">
    A{{msg}}
    <component-b :msg="msg"></component-b>
  </div>
</template>
<script>
let vm = new Vue({
  el: "#app",
  data: {
    msg: "100"
  },
  components: {
    ComponentB: {
      props: ["msg"],
      template: `<div>B<component-c :msg="msg"></component-c></div>`,
      components: {
        ComponentC: {
          props: ["msg"],
          template: "<div>C{{msg}}</div>"
        }
      }
    }
  }
});
</script>
複製程式碼

ComponentB 元件中並沒有使用到父元件傳遞過來的屬性 msg,但是這樣寫就是想把屬性再傳遞給ComponentC,那麼除了這種寫法還可以給ComponentC繫結$attrs屬性。

<script>
let vm = new Vue({
  el: "#app",
  data: {
    msg: "100"
  },
  components: {
    ComponentB: {
      inheritAttrs: false,
      template: `<div>B<component-c v-bind="$attrs"></component-c></div>`,
      components: {
        ComponentC: {
          props: ["msg"],
          template: "<div>C{{msg}}</div>"
        }
      }
    }
  }
});
</script>
複製程式碼

這樣就可以很方便的做到資料傳遞,使用起來也比較簡單,避免多寫 props 的痛苦

一般在使用 $attrs 時,都會加上 inheritAttrs:false 它的作用就是沒有用到的資料,就不會顯示在DOM結構上

$listeners

說完了 $attrs,知道了怎麼把資料從A傳遞給C,那麼此時我們又想到了一個問題,怎麼把C元件的資訊同步到A元件呢? 這時就用到了 $listeners

當元件的根元素不具備一些DOM事件,但是根元素內部元素具備相對應的DOM事件,那麼可以使用 $listeners 獲取父元件傳遞進來的所有事件函式,再通過v-on="xxx"繫結到相對應的內部元素上即可。 簡單的來說,就是父元件向子元件傳遞的所有方法都存在在$listeners

有時候我們會使用 .native 修飾符把原生事件繫結到元件上,但是這樣存在弊端,如果元件的根元素不能使用 某事件時,這個繫結就會失效,而且還不容易控制它的事件範圍,所以我們一般不用這個修飾符

示例:

//父元件  
<template>
  <div>
    ParentPage
    <button @click="handleClick">ParentClick</button>
    <Child @customClick="handleClick" />
  </div>
</template>

<script>
import Child from "./Child";
export default {
  name: "ParentPage",

  components: {
    Child
  },

  methods: {
    handleClick() {
      alert("hello");
    }
  }
};
</script>
複製程式碼
//子元件
<template>
  <div>
    ChildPage
    <!-- <button @click="$emit('customClick')">ChildClick</button> -->
    <button @click="$listeners.customClick">ChildClick</button>
  </div>
</template>

<script>
import SubChild from "./SubChild.vue";
export default {
  name: "ChildPage",
  components: {
    SubChild
  },
  data() {
    return {};
  }
};
</script>
複製程式碼

當多層元件引用時,子元件傳遞父元件方法 v-on="$listeners" 至子子元件

// 子元件
<template>
  <div>
    ChildPage
    <!-- <button @click="$emit('customClick')">ChildClick</button> -->
    <button @click="$listeners.customClick">ChildClick</button>
    <SubChild v-on="$listeners" />
  </div>
</template>

<script>
import SubChild from "./SubChild.vue";
export default {
  name: "ChildPage",
  components: {
    SubChild
  },
  data() {
    return {};
  }
};
</script>
複製程式碼
// 子子元件
<template>
  <div>
    SubChildPage
    <button @click="$listeners.customClick">SubChildClick</button>
  </div>
</template>

<script>
export default {
  name: "SubChildPage",
  data() {
    return {};
  }
};
</script>
複製程式碼

相信如果好好看了以上的程式碼,你就會理解的

$emit

1.父元件可以使用props把資料傳給子元件
2.子元件可以使用 $emit 觸發父元件的自定義事件

vm.$emit( event, arg ) //觸發當前例項上的事件
vm.$on( event, fn );//監聽event事件後執行 fn;
複製程式碼

示例:

//父元件
<template>
  <div>
    <div>$emit子元件呼叫父元件的方法並傳遞資料</div>
    <h1>父元件資料:{{msg}}</h1>
    <emit-ch @updateInfo="updateInfo" :sendData="msg"></emit-ch>
  </div>
</template>
<script>
import emitCh from "./$emitCh";
export default {
  name: "emitFa",
  components: { emitCh },
  data() {
    return {
      msg: "北京"
    };
  },
  methods: {
    updateInfo(data) {
      // 點選子元件按鈕時觸發事件
      console.log(data);
      this.msg = data.city; // 改變了父元件的值
    }
  }
};
</script>
複製程式碼
<template>
  <div class="train-city">
    <h3>父元件傳給子元件的資料:{{sendData}}</h3>
    <br />
    <button @click="select()">點選子元件</button>
  </div>
</template>

<script>
export default {
  name: "emitCh", // 相當於一個全域性 ID,可以不寫,寫了可以提供更好的除錯資訊
  props: ["sendData"], // 用來接收父元件傳給子元件的資料
  data() {
    return {};
  },
  computed: {},
  methods: {
    select() {
      let data = {
        city: "杭州"
      };
      this.$emit("updateInfo", data); // select事件觸發後,自動觸發updateInfo事件
    }
  }
};
</script>
複製程式碼

總體來說,就是用子元件觸發父元件裡的方法,子元件裡面this.$emit裡的this,就指的是父元件

$refs

$refs 的使用方法就是在元素或元件標籤上新增ref屬性指定一個引用資訊,引用資訊將會註冊在父元件的$refs物件上,在js中使用$refs來指向DOM元素或元件例項;

  • 首先給你的子元件做標記:
    <firstchild ref="test"></firstchild>

  • 然後在父元件中,通過 this.$refs.test 就可以訪問這個子元件,包括訪問子元件裡的 data 裡的資料,並且還可以呼叫它的函式

$parent$children

說了上面的 $refs 接下來說說 $parent$children

  • 使用 this.$parent 可以查詢當前元件的父元件。
  • 使用 this.$children 可以查詢當前元件的直接子元件,可以遍歷全部子元件, 需要注意 $children 並不保證順序,也不是響應式的。

當然你也可以使用 this.$root 來查詢根元件,並可以配合$children遍歷全部元件。

注:這兩個都是不限制距離的,就是說可以直接查詢到最外層資料或者最內層資料,當然,如果你能很清楚的知道子元件的順序,你也可以用下標來操作

示例:

//父元件
<template>
  <div class="game">
    <h2>{{ msg }}</h2>
    <LOL ref="lol"></LOL>
    <DNF ref="dnf"></DNF>
  </div>
</template>
<script>
import LOL from "@/components/game/LOL";
import DNF from "@/components/game/DNF";
export default {
  name: "game",
  components: {
    LOL,
    DNF
  },
  data() {
    return {
      msg: "Game",
      lolMsg: "Game->LOL",
      dnfMsg: "Game->DNF"
    };
  },
  methods: {},
  mounted() {
    //注意 mounted

    //讀取子元件資料,注意$children子元件的排序是不安全的
    console.log(this.$children[0].gameMsg); //LOL->Game

    //讀取命名子元件資料
    console.log(this.$refs.dnf.gameMsg); //DNF->Game

    //從根元件查詢元件資料
    console.log(this.$root.$children[0].msg); //APP
    console.log(this.$root.$children[0].$children[0].msg); //Game
    console.log(this.$root.$children[0].$children[0].$children[0].msg); //Game->LOL
    console.log(this.$root.$children[0].$children[0].$children[1].msg); //Game->DNF
  }
};
</script>
複製程式碼
//子元件LOL  
<template>
  <div class="lol">
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
export default {
  name: "LOL",
  data() {
    return {
      msg: "LOL",
      gameMsg: "LOL->Game"
    };
  },
  methods: {},
  created() {
    //直接讀取父元件資料
    this.msg = this.$parent.lolMsg;
  }
};
</script>
複製程式碼
//子元件DNF
<template>
  <div class="dnf">
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
import Bus from "../../utils/bus.js";
export default {
  name: "DNF",
  data() {
    return {
      msg: "DNF",
      gameMsg: "DNF->Game"
    };
  },
  methods: {},
  created() {
    //從根元件向下查詢父元件資料
    this.msg = this.$root.$children[0].$children[0].dnfMsg;
    //this.msg = this.$children.dnfMsg;
  }
};
</script>
複製程式碼

上面的有些是使用下標的,當然也可以不使用下標,直接 $parent 獲取父元件的例項 $children獲取所有的子元件

provideinject

provideinject使用場景也是元件傳值,尤其是祖父元件--子子元件等有跨度的元件間傳值,單向傳值(由provide的元件傳遞給inject的元件)。 不推薦使用

  • provide 選項應該是一個物件或返回一個物件的函式。該物件包含可注入其子孫的屬性。
  • inject 通常是一個字串陣列。

示例:

//父元件
<template>
  <div>
    <father-dom>
    </father-dom>
  </div>
</template>
<script>
import sonDom from "./sonDom.vue";
export default {
  provide: {
    fooNew: "bar"
  },
  data() {
    return {};
  },
  components: { sonDom },
  methods: {}
};
</script>
複製程式碼
//子元件
<template>
  <div>
    <child-dom></child-dom>
  </div>
</template>
<script>
import childDom from "./childDom.vue";
export default {
  name: "son-dom",
  components: { childDom }
};
</script>
複製程式碼
//子子元件  
<template>
  <div>
    <p>fooNew:{{fooNew}}</p>
  </div>
</template>
<script>
export default {
  name: "childDom",
  inject: ["fooNew"],
  methods: {}
};
</script>
複製程式碼

Vuex

接下來說一下我們的壓軸好戲,最 6 的一種資料傳遞方式:Vuex

當我們的應用遇到多個元件共享狀態時,單向資料流的簡潔性很容易被破壞。

  • 多個檢視依賴於同一狀態。
  • 來自不同檢視的行為需要變更同一狀態。來自不同檢視的行為需要變更同一狀態。

對於問題一,傳參的方法對於多層巢狀的元件將會非常繁瑣,並且對於兄弟元件間的狀態傳遞無能為力。對於問題二,我們經常會採用父子元件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的程式碼。

因此,我們為什麼不把元件的共享狀態抽取出來,以一個全域性單例模式管理呢?在這種模式下,我們的元件樹構成了一個巨大的“檢視”,不管在樹的哪個位置,任何元件都能獲取狀態或者觸發行為!

另外,通過定義和隔離狀態管理中的各種概念並強制遵守一定的規則,我們的程式碼將會變得更結構化且易維護。

Vuex

  • state
    Vuex裡的state相當於一個全域性的state,你可以在component的任何地方獲取和修改它。使用 Vuex 並不意味著你需要將所有的狀態放入 Vuex。雖然將所有的狀態放到 Vuex 會使狀態變化更顯式和易除錯,但也會使程式碼變得冗長和不直觀。如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態。你應該根據你的應用開發需要進行權衡和確定。
//獲取state
this.$store.state.count

//vuex的輔助方法
import { mapState } from 'vuex'
computed:mapState([
  'count'
])
複製程式碼
  • getters
    Vuex裡的getters類似於computed,有時候我們需要從 store 中的 state 中派生出一些狀態,例如對列表進行過濾並計數,如果有多個元件需要用到此屬性,我們要麼複製這個函式,或者抽取到一個共享函式然後在多處匯入它——無論哪種方式都不是很理想。
    Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性)。就像計算屬性一樣,getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。
//直接使用
this.$store.getters.doneTodosCount

//使用輔助方法
import { mapGetters } from 'vuex'
computed:mapGetters({
  doneCount: 'doneTodosCount'
})
複製程式碼
  • mutations
    更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。使用常量替代 mutation 事件型別在各種 Flux 實現中是很常見的模式。這樣可以使 linter 之類的工具發揮作用,同時把這些常量放在單獨的檔案中可以讓你的程式碼合作者對整個 app 包含的 mutation 一目瞭然。用不用常量取決於你——在需要多人協作的大型專案中,這會很有幫助。但如果你不喜歡,你完全可以不這樣做。
    一條重要的原則就是要記住 mutation 必須是同步函式,mutation 中的非同步函式中的回撥,當 mutation 觸發的時候,回撥函式還沒有被呼叫,devtools 不知道什麼時候回撥函式實際上被呼叫——實質上任何在回撥函式中進行的狀態的改變都是不可追蹤的。(通常請求和計時器方法都是非同步函式)
//觸發mutations
this.$store.commit('xxx')

//輔助函式
import { mapMutations } from 'vuex'
methods:mapMutations(['increment' ])
複製程式碼
  • actions
    對於Action的官方解釋如下。
    類似於 mutation,不同在於:
  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意非同步操作。

個人理解如下:
如果有非同步的複雜邏輯並且可以重複呼叫就使用Action。

//觸發action
store.dispatch('increment')

//輔助函式
import { mapActions } from 'vuex'
methods:mapActions(['increment' ])
複製程式碼
  • Module
    由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,store 物件就有可能變得相當臃腫。

為了解決以上問題,Vuex 允許我們將 store 分割成模組(module)。每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組——從上至下進行同樣方式的分割。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
複製程式碼

相信看了本篇文章,你肯定會對元件之間的資料傳遞和通訊有了更深的理解


^_<

相關文章