乐趣区

Vue项目总结组件通信处理方案

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>

效果如下:

祖孙组件

有时候我们可能会碰到组件间的无限嵌套,这是我们使用 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>

效果如下:

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>

效果如下:

使用 observable 让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。

示例:

// Grand
provide() {
  this.read = Vue.observable({msg: ''})
  return {read: this.read};
}

效果如下:

兄弟组件

同级别组件相互间的通信,我们可以使用 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>

效果如下:

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"/>

效果如下:

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"/>

效果如下:

$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" />

通过在父组件中输入值可以看到子组件数据也同时更新了

$attrs$listeners

$attrs可以通过 v-bind="$attrs" 将组件上的特新都(class 和 style 除外)传入内部组件;传入的值与 inheritAttrs 的设置有关,通常封装高级组件。

当我们inheritAttrs 设置 true;组件渲染 DOM 时写在组件的特性会渲染上去;

$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.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};

总结

项目中组件的通信方式大概常用的是上面几种方案,我们可以通过不同的方式来实现组件通信,但是选择合适组件通信方式可以使我们事半功倍。写的不当之处,望指正~

退出移动版