Vue組件通信黑科技

Vue組件通信

組件可謂是 Vue框架的最有特點之一, 能夠將一大塊拆分爲小零件最後組裝起來。這樣的好處易於維護、擴展和複用等。javascript

提到 Vue的組件, 相必你們對Vue組件之間的數據流並不陌生。最常規的是父組件的數據經過 prop傳遞給子組件。子組件要進行數據更新,那麼須要經過自定義事件通知父組件。vue

那麼還有其餘方法, 方便父子組件乃至跨組件之間的通信嗎?java

props $emit

能夠經過 prop屬性從父組件向子組件傳遞數據webpack

// child.vue
Vue.component('child', {
    props: ['msg'],
    template: `<div>{{ msg }}</div>`
})


// parent.vue
<div>
    <child :msg="message"></child>
<div>
// 部分省略...
{
  data(){
    return {
      message: 'hello.child'
    }
  }
}

以上代碼父子組件通信是經過 prop的傳遞, Vue是單向數據流, 子組件不能直接修改父組件的數據。能夠經過自定義事件通知父組件修改,和要修改的內容ios

provide / inject

provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。可是咱們若是合理的運用, 那仍是很是方便的web

provide 應該是一個對象, 或者是一個函數返回一個對象。它主要的做用就是給應用provide的組件的子孫組件提供注入數據。就至關於 Vuex的store,用來存放數據。vue-router

inject: 給當前的組件注入 provide提供的數據。切記 provideinject 綁定並非可響應的。vuex

看一下這兩個怎麼使用axios

a組件給子組件b提供數據數組

// a.vue
  export default {
    provide: {
      name: 'qiqingfu'
    }
  }
  
  // b.vue
  export default {
    inject: ['name'],
    mounted(){
      console.log(this.name)  // qiqingfu
    }
  }

以上代碼父組件a提供了 name: qiqingfu的數據, 子組件和子孫組件若是須要就經過 inject注入進來就可使用了

provide / inject 替代 Vuex存儲用戶的登陸數據實例
這裏並非必定要替代 vuex, 介紹另外一種可行性

咱們在使用 webpack進行構建 Vue項目時, 都會有一個入口文件 main.js, 裏面一般都導入一些第三方庫如 vuexelementvue-router等。可是咱們也引入了一個 Vue的根組件 App.vue。簡單的app.vue是這樣子的

<template>
     <div>
       <router-view></router-view>
     </div>
  </template>
  <script>
    export default {
      
    }
    </script>

這裏的 App.vue 是一個最外層的組件, 能夠用來存儲一些全局的數據和狀態。由於組件的解析都是以App.vue做爲入口。引入你項目中全部的組件,它們的共同根組件就是App.vue。因此咱們能夠在 App.vue中將本身暴露出去

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    provide () {
      return {
        app: this
      }
    }
  }
</script>

以上代碼把整個 app.vue實例的 this對外提供, 而且命令爲 app。那麼子組件使用時只須要訪問或者調用 this.app.xxx或者訪問 app.vue的 datacomputed等。

由於 App.vue只會渲染一次, 很適合作一些全局的狀態數據管理, 好比用戶的登陸信息保存在 App.vuedata中。

app.vue

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    provide () {
      return {
        app: this
      }
    },
    data: {
      userInfo: null
    },
    mounted() {
      this.getUserInfo()
    },
    methods: {
      async getUserInfo() {
        const result = await axios.get('xxxxxx')
        if (result.code === 200) {
          this.userInfo = result.data.userinfo
        }
      }
    }
  }
</script>

以上代碼,經過接口獲取用戶對數據信息, 保存在 App.vue的data中。並且provide將實例提供給任何子組件使用。因此任何頁面和子組件均可以經過 inject注入 app。而且經過 this.app.userInfo來取得用戶信息。

那麼若是用戶要修改當前的信息怎麼辦? App.vue只初始化一次呀?

// header.vue
  <template>
    <div>
      <p>用戶名: {{ app.userInfo.username }}</p>
    </div>
  <template>

  <script>
    export default {
      name: 'userHeader',
      inject: ['app'],
      methods: {
        async updateUserName() {
          const result = await axios.post(xxxxx, data)
          if (result === 200) {
            this.app.getUserInfo()
          }
        }
      }
    }
  </script>

以上代碼, 在header.vue組件中用戶修改了我的信息, 只須要修改完成以後再調用一次 App.vue根組件的 getUserInfo方法, 就又會同步最新的修改數據。

dispatch / broadcast

父子組件通信的方法。能夠支持:

  • 父 - 子 傳遞數據 (廣播)
  • 子 - 父 傳遞數據 (派發)

先聊一下 Vue實例的方法 $emit()$on()

$emit: 觸發當前實例上的事件。附加參數都會傳給監聽器回調。

$on: 監聽當前實例上的事件

也就是一個組件向父組件經過 $emit自定義事件發送數據的時候, 它會被本身的 $on方法監聽到

// child.vue
   export default {
     name: 'child',
     methods: {
       handleEvent() {
         this.$emit('test', 'hello, 這是child.vue組件')
       }
     },
     mounted() {
       // 監聽自定義事件
       this.$on('test', data => {
         console.log(data)  // hello, 這是child.vue組件
       })
     }
   }

   // parent
   <template>
    <div>
      <child v-on:test="handleTest"></child>
    </div>
   <template>
   
   <script>
    export default {
      methods: {
        handleTest(event) {
          console.log(event) // hello, 這是child.vue組件
        }
      }
    }
   </script>

以上代碼, $on 監聽本身觸發的$emit事件, 由於不知道什麼時候會觸發, 因此會在組件的 createdmounted鉤子中監聽。

看起來畫蛇添足, 沒有必要在本身組件中監聽本身調用的 $emit。 可是若是當前組件的 $emit在別的組件被調用, 而且傳遞數據的話那就不同了。

舉個例子

// child.vue 
   export default {
     name: 'iChild',
     methods: {
       sayHello() {
         // 若是我在子組件中調用父組件實例的 $emit
         this.$parent.$emit('test', '這是子組件的數據')
       }
     }
   }

   // parent.vue
   export default = {
     name: 'iParent',
     mounted() {
       this.$on('test', data => {
         console.log(data) // 這是子組件的數據
       })
     }
   }

以上代碼, 在子組件調用父組件實例的 $emit方法, 而且傳遞響應的數據。那麼在父組件的 mounted鉤子中能夠用 $on監聽事件。

若是這樣寫確定不靠譜, 因此咱們要封裝起來。哪一個子組件須要給父組件傳遞數據就將這個方法混入(mixins)到子組件

dispatch 封裝

// emitter.js
   export default {
     methods: {
       dispatch(componentName, eventName, params) {
         let parent = context.$parent || context.$root
         let name = parent.$options.name

         while(parent && (!name || name !== componentNamee)) {
           parent = parent.$parent
           if (parent) {
             name = parent.$options.name
           }
         }
         if (parent) {
           parent.call.$emit(parent, eventName, params)
         }
       }
     }
   }

以上代碼對 dispatch進行封裝, 三個參數分別是 接受數據的父組件name自定義事件名稱傳遞的數據

對上面的例子從新修改

import Emitter from './emitter'
  // child.vue 
   export default {
     name: 'iChild',
     mixins: [ Emitter ],  // 將方法混入到當前組件
     methods: {
       sayHello() {
         // 若是我在子組件中調用父組件實例的 $emit
         this.dispatch('iParent', 'test', 'hello,我是child組件數據')
       }
     }
   }

   // parent.vue
   export default = {
     name: 'iParent',
     mounted() {
       this.$on('test', data => {
         console.log(data) // hello,我是child組件數據
       })
     }
   }

以上代碼, 子組件要向父組件傳遞數據。能夠將先 Emitter混入。而後再調用 dispatch方法。第一個參數是接受數據的父組件, 也能夠是爺爺組件, 只要 name值給的正確就能夠。而後接受數據的組件須要經過 $on來獲取數據。

broadcast

廣播是父組件向全部子孫組件傳遞數據, 須要在父組件中注入這個方法。實現以下:

// emitter.js
  export default {
    methods: {
      broadcast(componentName, eventName, params) {
       this.$children.forEach(child => {
         let name = child.$options.name
         if (name === componentName) {
           child.$emit.apply(child, [eventName].concat(params))
         } else {
           // 遞歸
           broadcast.apply(child, [componentName, eventName].concat([params]))
         }
       })
      }
    }
  }

以上代碼是經過遞歸匹配子組件的 name, 若是沒有找到那麼就遞歸尋找。找到以後調用子組件實例的 $emit()方法而且傳遞數據。

使用:

// 兒子組件 child.vue
   export default {
     name: 'iChild',
     mounted() {
       this.$on('test', data => {
         console.log(data)
       })
     }
   }

   // 父親組件 parent.vue 
   <template>
      <div>
        <child></child>  
      </div>
   <template>
   export default {
     name: 'iParent',
     components: {
       child
     }
   }

   // 爺爺組件 root.vue
   <template>
    <div>
      <parent></parent>
    </div>
   <template>
   import Emitter from './emitter'
   export default {
     name: 'iRoot',
     mixin: [ Emitter ],
     components: {
       parent
     },
     methods: {
       this.broadcast('iChild', 'test', '爺爺組件給孫子傳數據')
     }
   }

以上代碼, root根組件給孫組件(child.vue)經過調用 this.broadcast找到對應name的孫組件實例。child.vue只須要監聽 test事件就能夠獲取數據。

找到任意組件實例的方法

這些方法並不是 Vue組件內置, 而是經過自行封裝, 最終返回你要找的組件的實例,進而能夠調用組件的方法和函數等。

使用場景:

  • 由一個組件,向上找到最近的指定組件
  • 由一個組件,向上找到全部的指定組件
  • 由一個組件,向下找到最近的指定組件
  • 由一個組件,向下找到全部的指定組件
  • 由一個組件,找到指定組件的兄弟組件

找到最近的指定組件

findComponentUpwarp(context, componentName)

export function findComponentUpwarp(context, componentName) {
    let parent = context.$parent
    let name = parent.$options.name

    while(parent && (!name || name !== componentName)) {
      parent = parent.$parent
      if (parent) {
        name = parent.$options.name
      }
    }
    return parent
  }

這個函數接受兩個參數,分別是當前組件的上下文(this),第二個參數是向上查找的組件 name。最後返回找到組件的實例。

向上找到全部的指定組件

findComponentsUpwarp(context, componentName)
return Array

export function findComponentsUpwarp(context, componentName) {
    let parent = context.$parent
    let result = []
    if (parent) {
      if (parent.$options.name === componentName) result.push(parent)
      return result.concat(findComponentsUpwarp(parent, componentName))
    } else {
      return []
    }
  }

這個函數接受兩個參數,分別是當前組件的上下文(this),第二個參數是向上查找的組件 name。最後返回一個全部組件實例的數組

向下找到最近的指定組件

findComponentDownwarp(context, componentName)

export function findComponentDownwarp(context, componentName) {
    let resultChild = null
    context.$children.forEach(child => {
      if (child.name === componentName) {
         resultChild = child
         break
      } else {
        resultChild = findComponentDownwarp(child, componentName)
        if (resultChild) break
      }
    })
    
    return resultChild
  }

以上代碼接受兩個參數, 當前組件的上下文(this)和向下查找的組件name。返回第一個name和componentName相同的組件實例

向下找到全部的指定組件

findComponentsDownwarp(context, componentName)

export function findComponentsDownwarp(context, componentName) {
    return context.$children.reduce((resultChilds, child) => {
      if (child.$options.name === componentName) resultChilds.push(child)
      // 遞歸迭代
      let foundChilds = findComponentsDownwarp(child, componentName)
      return resultChilds.concat(foundChilds)
    }, [])
  }

以上代碼接受兩個參數, 當前組件的上下文(this)和向下查找的組件name。返回一個全部組件實例的數組

找到當前組件的其餘兄弟組件

findBrothersComponents(context, componentName, exceptMe?)

exceptMe默認爲true, 排除它本身, 若是設置爲false則包括當前組件

export function findBrothersComponents(context, componentName, exceptMe = true) {
    let res = context.$parent.$children.filter(child => child.$options.name === componentName)

    // 根據惟一表示_uid找到當前組件的下標
    let index = res.findIndex(item => item._uid === context._uid)
    if (exceptMe) res.splice(index, 1)
    return res
  }

以上代碼經過第一個參數的上下文, 拿到它父組件中的全部子元素,最後根據 exceptMe決定是否排除本身。

相關文章
相關標籤/搜索