【Vue專案總結】元件通訊處理方案

Mondo發表於2019-05-30

Vue元件之間的通訊是我們在專案中常常碰到的,而選擇合適的通訊方式尤為重要,這裡總結下作者在實際專案中所運用到的通訊方案,如有遺漏,請大家見諒。文章程式碼具體見DEMO;文章首發於imondo.cn

父子元件

Vue中常見的是父與子元件間的通訊,所要用到的關鍵欄位是props$emit

props接受父元件傳給子元件資訊的欄位,它的型別:Array<string> | Object;詳細解釋可以參考文件

$emit由子元件觸發事件向上傳播給父級訊息。

示例:

// Parent

<template>
  <div class="parent">
    我是父元件
    <p>來自子級的回答:{{ childMsg }}</p>
    <Child :msg="msg" @click="handleClick"/>
  </div>
</template>
<script>
import Child from "./Child";
export default {
  name: "Parent",
  components: {
    Child
  },
  data() {
    return {
      msg: "叫你吃飯了",
      childMsg: ''
    };
  },
  methods: {
    // 接收來自子級的事件訊息
    handleClick(val) {
      this.childMsg = val;
    } 
  }
};
</script>

// Child
<template>
  <div class="child">
    <p>我是子元件</p>
    <p>父級來的資訊: {{ msg }}</p>
    <button @click="handleClick">回答父級</button>
  </div>
</template>
<script>
export default {
  name: "Child",
  // 接收父級傳來的資訊
  props: {
    msg: String
  },
  methods: {
    // 向父級傳播事件訊息
    handleClick() {
      this.$emit('click', '我知道了');
    }
  },
};
</script>

複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

祖孫元件

有時候我們可能會碰到元件間的無限巢狀,這時我們使用props時無法向下無限極傳遞資料的,我們可以用到provide/injectprovide可以向其子孫元件傳遞資料,而不關子孫元件的層級有多深,使用inject都可以拿到資料。詳細解釋可以參考文件

示例:

// Grand
<template>
  <div class="grand">
    <p>我是祖父</p>
    <Parent />
  </div>
</template>

<script>
export default {
  name: "Grand",
  provide: {
    grandMsg: '都來吃飯'
  },
  components: {
    Parent
  }
};
</script>

// Parent
<template>
  <div class="parent">
    我是父元件
    <p>祖父的資訊:{{ grandMsg }}</p>
    <Child />
  </div>
</template>
<script>
import Child from "./Child";
export default {
  name: "Parent",
  components: {
    Child
  },
  inject: {
    grandMsg: {
      default: ''
    }
  }
};

// Child
<template>
  <div class="child">
    <p>我是子元件</p>
    <p>爺爺的資訊: {{ grandMsg }}</p>
  </div>
</template>
<script>
export default {
  name: "Child",
  inject: {
    grandMsg: {
      default: ''
    }
  }
};
</script>
複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

provideinject 繫結並不是可響應的。我們可以通過傳遞祖父級的例項this或著使用observable來使傳遞的資料是響應的。

// Grand
<template>
  <div class="grand">
    <p>我是祖父</p>
    <input type="text" v-model="msg" placeholder="輸入祖父的訊息"/>
    <Parent />
  </div>
</template>
<script>
import Parent from "./Parent";
export default {
  name: "Grand",
  provide() {
    return { // 利用函式 provide 返回物件
      grandVm: this // 傳遞例項
    };
  },
  ...
  data() {
    return {
      msg: ""
    };
  }
};
</script>

// Child
<template>
  <div class="child">
    <p>我是子元件</p>
    <p>爺爺的例項資訊: {{ grandVmMsg }}</p>
  </div>
</template>
<script>
export default {
  name: "Child",
  inject: {
    grandVm: {
      default: () => {
        "";
      }
    }
  },
  computed: {
    grandVmMsg() {
      return this.grandVm.msg;
    }
  }
};
</script>

複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

使用observable讓一個物件可響應。Vue 內部會用它來處理 data 函式返回的物件。

示例:

// Grand
provide() {
  this.read = Vue.observable({
    msg: ''
  })
  return {
    read: this.read
  };
}
複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

兄弟元件

同級別元件相互間的通訊,我們可以使用EventBus或著Vuex

簡單的EventBus示例:

// Bus.js
import Vue from "vue";
export default new Vue();

// Child
<div class="child">
  <p>我是子元件一</p>
  <button @click="handleClick">元件一事件</button>
</div>
<script>
import Bus from "./Bus";
export default {
  name: "Child",
  methods: {
    handleClick() {
      Bus.$emit("click", "嘿,老鐵");
    }
  }
};
</script>

// ChildOne
<div class="child">
  <p>我是子元件二</p>
  <p>兄弟叫我:{{ msg }}</p>
</div>
<script>
import Bus from "./Bus";
export default {
  name: "ChildOne",
  data() {
    return {
      msg: ""
    };
  },
  mounted() {
    Bus.$on("click", msg => {
      this.msg = msg;
    });
  }
};
</script>
複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

v-modelsync

v-model是我們用ElementUI常見的表單繫結值方式;可以直接修改子元件修改父元件傳入的值,簡化了我們元件通訊的邏輯。

示例:

// ModelCom
<div class="child">
  <input type="text" @input="handleInput">
</div>
<script>
export default {
  name: "ModelSync",
  methods: {
    // 通過繫結表單input中的input事件,向上觸發input事件來修改值
    handleInput(e) {
      const value = e.target.value;
      this.$emit('input', value);
    }
  }
};
</script>

// Home
<ModelSync v-model="msg"/>
複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

sync修飾符也可以是我們的prop進行雙向繫結

它需要我們在子元件內觸發this.$emit('update:prop', val)事件

// ModelCom
<input type="text" @input="handleChange">
...
props: ['value'],
methods: {
  handleChange(e) {
    const value = e.target.value;
    // 觸發更新
    this.$emit('update:value', value);
  }
}

// Home
<ModelSync :value.sync="syncMsg"/>
複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

$children$parent

我們可以在元件中通過當前的例項物件訪問到元件的$children$parent來找到各自元件的父級元件或子級元件例項。

示例:

// Child
<div class="child">
  <p>我是子元件</p>
  <p>來自父元件的msg: {{ msg }}</p>
</div>
...
<script>
export default {
  name: "ChildParent",
  data() {
    return {
      value: ''
    }
  },
  computed: {
    msg() {
      return this.$parent.value;
    }
  },
  created() {
    console.log(this.$parent); 
  }
}

// Parent
<input v-model="value" />

複製程式碼

通過在父元件中輸入值可以看到子元件資料也同時更新了

【Vue專案總結】元件通訊處理方案

$attrs$listeners

$attrs可以通過 v-bind="$attrs" 將元件上的特性都(class 和 style 除外)傳入內部元件;傳入的值與inheritAttrs的設定有關,通常封裝高階元件。

當我們inheritAttrs 設定 true;元件渲染DOM時寫在元件的特性會渲染上去;

【Vue專案總結】元件通訊處理方案

$listeners包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部元件。

具體詳細可見文件

示例:

// Attr
<div class="child">
  <p>Attr</p>
  <p>這是$attrs:{{ placeholder }}</p>
  <p>這是$listeners:{{ test }}</p>
  <button @click="$listeners.click">監聽了$listeners</button>
</div>
...
<script>
export default {
  name: "AttrListen",
  inheritAttrs: true,
  props: {
    test: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      placeholder: this.$attrs.placeholder
    }
  }
};
</script>

// Home
<AttrListen placeholder="這是個attr" :test="value" v-bind="$attrs" v-on="$listeners" @click="handleListen"/>

複製程式碼

效果如下:

【Vue專案總結】元件通訊處理方案

通過封裝查詢元件

通過封裝函式來向上或向下派發事件

參考見Vue.js元件精講

// emitter.js
function broadcast(componentName, eventName, params) {
    this.$children.forEach(child => {
        const name = child.$options.name;

        if (name === componentName) {
            child.$emit.apply(child, [eventName].concat(params));
        } else {
            broadcast.apply(child, [componentName, eventName].concat([params]));
        }
    });
}
export default {
    methods: {
        dispatch(componentName, eventName, params) {
            let parent = this.$parent || this.$root;
            let name = parent.$options.name;

            while (parent && (!name || name !== componentName)) {
                parent = parent.$parent;

                if (parent) {
                    name = parent.$options.name;
                }
            }
            if (parent) {
                parent.$emit.apply(parent, [eventName].concat(params));
            }
        },
        broadcast(componentName, eventName, params) {
            broadcast.call(this, componentName, eventName, params);
        }
    }
};

複製程式碼

通過封裝函式來查詢指定任意元件

參考見Vue.js元件精講

// 由一個元件,向上找到最近的指定元件
function findComponentUpward (context, componentName) {
    let parent = context.$parent;
    let name = parent.$options.name;

    while (parent && (!name || [componentName].indexOf(name) < 0)) {
        parent = parent.$parent;
        if (parent) name = parent.$options.name;
    }
    return parent;
}
export { findComponentUpward };

// 由一個元件,向上找到所有的指定元件
function findComponentsUpward (context, componentName) {
    let parents = [];
    const parent = context.$parent;

    if (parent) {
        if (parent.$options.name === componentName) parents.push(parent);
        return parents.concat(findComponentsUpward(parent, componentName));
    } else {
        return [];
    }
}
export { findComponentsUpward };

// 由一個元件,向下找到所有指定的元件
function findComponentsDownward (context, componentName) {
    return context.$children.reduce((components, child) => {
        if (child.$options.name === componentName) components.push(child);
        const foundChilds = findComponentsDownward(child, componentName);
        return components.concat(foundChilds);
    }, []);
}
export { findComponentsDownward };

// 由一個元件,找到指定元件的兄弟元件
function findBrothersComponents (context, componentName, exceptMe = true) {
    let res = context.$parent.$children.filter(item => {
        return item.$options.name === componentName;
    });
    let index = res.findIndex(item => item._uid === context._uid);
    if (exceptMe) res.splice(index, 1);
    return res;
}
export { findBrothersComponents };
複製程式碼

總結

專案中元件的通訊方式大概常用的是上面幾種方案,我們可以通過不同的方式來實現元件通訊,但是選擇合適元件通訊方式可以使我們事半功倍。寫的不當之處,望指正~

其他總結文章:

相關文章