14

Vue组件通信详解

 3 years ago
source link: https://zhuanlan.zhihu.com/p/242774231
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vue组件通信详解

Enjoy what you are doing!

使用Vue进行项目开发,碰到的比较多的问题就是如何进行组件之间的参数传递。为了能够更优雅的解决此类问题,笔者在这里总结了开发中经常用到的一些组件通信方式,并配合一些例子方便理解。

v2-80f42f5ec9bbf19169907f000ddf750a_720w.jpg

每一小节的例子都在src/views目录中,小伙伴可以结合对应的代码来阅读文章。

码字不易,如果文章内容对你有帮助的话,希望能点赞鼓励一下作者。

注:有些例子刻意为之,只是为了学习对应的知识点,对于实际使用场景刻意不必深究

props传参

Vue中,我们可以通过为子组件传入props,然后在子组件中接收,并通过this来直接访问

<!--demo-props-->
<template>
  <div class="demo-props">
    <h2>{{ count }}</h2>
    <demo-child
      :count="count"
      :add-count="addCount"
    >
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>
<!--demo-child-->
<template>
  <div class="demo-child">
    <demo-grandson :add-count="addCount"></demo-grandson>
    <button @click="addCount">child click</button>
  </div>
</template>

这里我们为demo-children传入了countadd-count属性,然后又将add-count传入到demo-grandson组件中。这样当我们分别点击父组件(demo-props)、子组件(demo-child)和孙子组件(demo-grandson)中的按钮时,都会更新count属性

当然我们也可以使用v-bind来直接绑定一个对象,Vue会帮我们将组件属性进行分发,类似于react中的{...props}

这张图的出处在这里,有兴趣的小伙伴可以去围观 :https://twitter.com/tannerlinsley/status/1300847251846176768

Vue中我们也可以利用这个对象简写的特性来少敲几下键盘:

<!--demo-props-->
<template>
  <div class="demo-props">
    <h2>{{ count }}</h2>
    <demo-child v-bind="{count,addCount}" >
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>

自定义事件

Vue中可以通过@符号来监听自定义事件,并在子组件中通过$emit方法来触发监听的事件。我们将上面的例子用自定义事件来进行改写:

<!-- 父组件 -->
<template>
  <div class="demo-custom-event">
    <h2>{{ count }}</h2>
    <demo-child
      :count="count"
      @add-count="addCount"
    >
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>
<!-- 子组件 -->
<template>
  <div class="demo-child">
    <demo-grandson @add-count="addCount"></demo-grandson>
    <button @click="addCount">child click</button>
  </div>
</template>
<script>
export default {
  // ...
  methods: {
    addCount () {
      // 通知父组件执行'add-count'对应的事件
      this.$emit('add-count');
    }
  }
};
</script>

完成上述代码后,我们依旧可以通过点击各个组件内的按钮来更新count属性

双向绑定v-model/.sync

Vue为了方便用户,提供了俩个可以实现双向绑定数据的语法糖。用户不再需要在父组件进行事件监听,便可以实现属性的自动更新。

<!-- 父组件 -->
<template>
  <div class="demo-two-way">
    <h2>count: {{ count }}</h2>
    <h2>count1: {{ count1 }}</h2>
    <h2>count2: {{ count2 }}</h2>
    <demo-child
      v-model="count"
      :count1.sync="count1"
    >
    </demo-child>
    <button @click="count++">parent click</button>
  </div>
</template>
<!-- 子组件 -->
<template>
  <div class="demo-child">
    <demo-grandson :add-count="addCount"></demo-grandson>
    <button @click="addCount">child click</button>
    <button @click="$emit('update:count1',count1+1)">child:update .sync count1</button>
  </div>
</template>
<script>
export default {
  name: 'DemoChild',
  props: {
    value: {
      type: Number,
      default: 0
    },
    count1: {}
  },
  components: {
    DemoGrandson
  },
  data () {
    return {};
  },
  methods: {
    addCount () {
      this.$emit('input', this.value + 1);
    }
  }
};
</script>

相比于之前的传参方式,我们不再需要在父组件中监听addCount事件来更新父组件中的countVue会帮我们自动监听对应的事件,并更新属性值。

这俩个语法糖的本质如下:

  • v-model: 自动绑定value属性 + input事件
  • xxx.sync: 自动绑定update:xxx事件

下面我们模拟实现下这俩个语法为我们简化的一些事情:

<!-- 父组件 -->
<template>
  <div class="demo-model">
    <h2>模拟实现v-model的count: {{ count }}</h2>
    <h2>模拟实现.sync指令的count: {{ count1 }}</h2>
    <demo-child
      :value="count"
      @input="count = $event"
      :count1="count1"
      @update:count1="count1 = $event"
    >
    </demo-child>
  </div>
</template>
<!--子组件-->
<template>
  <div class="demo-model-child">
    <button @click="addCount">child click</button>
    <button @click="$emit('update:count1',count1+1)">child: update .sync count1</button>
  </div>
</template>
<script>
export default {
  name: 'DemoModelChild',
  props: {
    value: {
      type: Number,
      default: 0
    },
    count1: {}
  },
  data () {
    return {};
  },
  methods: {
    addCount () {
      this.$emit('input', this.value + 1);
    }
  }
};
</script>

上例中的count属性,我们通过value来接收,并将其传到子组件。然后子组件中通过调用this.$emit('input',this.value+1)通知父组件调用@input指令监听的事件,并将最新值作为参数传入。

父组件收到通知后调用@input指令监听的事件,并通过传入的参数来更新count属性。

而对于使用.sync修饰符的count1,我们可以随意指定其要传递给子组件的属性名,而不只能是value(v-model中的value也可以更改),并且会通过监听@update:count1,在count1发生变化后通过调用@update:count1对应的内容来更新count1。(注意:这里@update:count1中的count1与子组件中props接收的属性相同)

当然,v-model也并不是一定只能监听value属性和input事件,Vue为我们提供了自定义属性及更新事件的功能: f="https://cn.vuejs.org/v2/guide/components-custom-events.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84-v-model">自定义组件的v-model

到这里,我们使用v-model/.sync更简单的实现了功能。

$parent/$children

Vue可以让我们通过$parent/$children来直接访问到父组件或子组件实例,这样就可以直接使用组件实例中的任意属性和方法。

<template>
  <div class="demo-access-instance">
    <h2>parent:{{ count }}</h2>
    <h2>child:{{ child.count }}</h2>
    <demo-child>
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>

<script>
import DemoChild from './demo-child';

export default {
  name: 'DemoAccessInstance',
  components: {
    DemoChild
  },
  data () {
    return {
      count: 0,
      child: {}
    };
  },
  computed: {},
  mounted () {
    this.child = this.$children[0];
  },
  methods: {
    addCount () {
      this.count++;
    }
  }
};
</script>

在父组件挂载完成后,通过this.$chilren[0]获取到了子组件实例,之后直接通过子组件实例来访问子组件的count属性。

<template>
  <div class="demo-child">
    <button @click="addCount">child click</button>
    <button @click="addParentCount">child:update parent count</button>
  </div>
</template>

<script>
export default {
  name: 'DemoChild',
  props: {},
  data () {
    return {
      count: 0
    };
  },
  methods: {
    addCount () {
      this.count++;
    },
    addParentCount () {
      this.$parent.count++;
    }
  }
};
</script>

在子组件中,也可以通过this.$parent来直接获取到父组件的count属性进行更新。

$attrs/$listeners

在很多情况下,我们并不需要重新封装一个组件,而是只需要在旧有组件的基础上再添加一些功能。这里我们就用到了$attrs$listenners属性,而$attrs又会与inheritAttrs属性一起使用。

先看一下这些属性的用途:

  • $attrs: 包含父作用域中绑定的没有被识别或提取为props的属性(classstyle除外)
  • inheritAttrs: 默认的,父作用域中没有被作为props识别的属性将会"回退",并且作为正常的HTML属性应用到子组件的根元素。设置inheritAttrsfalse,将会禁用这个默认行为。
  • $listenners: 包含父作用域中v-on绑定的监听器(不包括.native修饰符绑定的监听器)

假设我们有demo-grandson组件,可以接收count进行展示,并且接受addCount方法来更新count

而现在我们想要在不改变demo-grandson的基础上,再实现一个组件,它具有demo-grandson的所有功能,并且还可以展示标题。代码如下:

<!-- 父组件 -->
<template>
  <div class="demo-all-props">
    <demo-child
      :count="count"
      @add-count="addCount"
      title="访问所有属性"
    >
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>
<!-- 子组件 -->
<template>
  <div class="demo-child">
    <h1>{{ title }}</h1>
    <demo-grandson
      v-bind="$attrs"
      v-on="$listeners"
    >
    </demo-grandson>
  </div>
</template>

<script>
import DemoGrandson from './demo-grandson';

export default {
  name: 'DemoChild',
  inheritAttrs: false,
  props: {
    title: {
      type: String,
      required: true
    }
  },
  components: {
    DemoGrandson
  },
};
</script>

子组件中的$attrs为除title外的所有根元素中传入的属性组成的对象,配合inheritAttrs: false,并不会让其作为正常的HTML属性在element中展示。之后再配合v-bind将属性分发到demo-grandson上:

$listeners中包含v-on(即@)中绑定的所有事件监听函数,同理通过v-on分发到demo-grandson上:

这样demo-grandson中有再多的属性和事件,我们都可以通过v-bind=$attrsv-on=$linstenners进行传入。而不用每次都在props中定义,然后再单独在子组件标签上通过:@来进行绑定。

这俩个属性在对项目中用到的ui组件库进行二次封装时尤其好用,既可以保证使用原有组件所有的api,还可以额外封装一些项目中复用度高的功能。

依赖注入(provide/inject)

provide/inject通常用于跨层级传参,不管组件的层级有多深,都可以通过inject来获得父组件provide提供的内容。

provide/inject的数据传递思路如下:

通常情况下,我们会将父组件的实例通过provide传入,这样子组件通过inject就可以直接获取到父组件的实例,从而可以使用到父组件实例中定义的任意属性和方法,我们把之前的例子通过provide/inject来进行实现:

<!--parent-->
<template>
  <div class="demo-provide-inject">
    <demo-child></demo-child>
  </div>
</template>

<script>
import DemoChild from './demo-child';

export default {
  name: 'DemoProvideInject',
  provide () {
    return { top: this };
  },
  components: { DemoChild },
  data () {
    return { count: 0 };
  },
  methods: {
    addCount () {
      this.count++;
    }
  }
};
</script>

在子组件中调用addCount方法

<template>
  <div class="demo-child">
    <demo-grandson></demo-grandson>
    <button @click="top.addCount">child click</button>
  </div>
</template>

<script>
import DemoGrandson from './demo-grandson';

export default {
  name: 'DemoChild',
  inject: ['top'],
  components: {
    DemoGrandson
  },
};
</script>

在孙子组件中渲染count到页面中,并且通过按钮来更新count:

<template>
  <div class="demo-grandson">
    <h2>child: {{ top.count }}</h2>
    <button @click="top.addCount">grandson click</button>
  </div>
</template>

<script>
export default {
  name: 'DemoGrandson',
  inject: ['top'],
};
</script>

事件分发(dispatch)和广播(broadcast)

当我们的组件层级比较深的时候,我们需要一层一层向下传递事件,而当更新父组件中的某个属性时,又需要一层一层的将更新向上通知,大概的逻辑如下:

为了可以直接通过子组件更新父组件,而不再用经历中间的事件监听步骤,我们可以递归遍历找到父组件的子组件(demo-child),然后调用它的$emit('event-name')来更新父组件中的属性。这便是$dispatch方法的核心思路,代码如下:

Vue.prototype.$dispatch = function (ComponentName, event, ...args) {
  let parent = this.$parent;
  while (parent) { //
    const { name } = parent.$options;
    // 递归查找父组件,如果组件名满足要求的话,调用组件实例的 $emit方法
    if (name === ComponentName) {
      parent.$emit(event, ...args);
      break;
    }
    parent = parent.$parent;
  }
};

$broadcast方法可以帮我们在父组件中直接调用较深层的子组件的$emit('eventName')方法,从而通过子组件的父组件更改传入到子组件的值(在本例中为grandson传入到great-grandson中的name属性),代码如下:

Vue.prototype.$broadcast = function (ComponentName, event, ...args) {
  for (let i = 0; i < this.$children.length; i++) {
    const child = this.$children[i];
    const { name } = child.$options;
    // 如果找到满足的子组件,调用 $emit方法
    if (name === ComponentName) {
      child.$emit(event, ...args);
    } else {
      if (child.$children) {
        // 继续递归查找符合要求的子组件
        child.$broadcast(ComponentName, event, ...args);
      }
    }
  }
};

在原型上添加了对应的方法后,我们便可以在组件中通过组件实例来直接调用:

<!-- 父组件 -->
<template>
  <div class="demo-extend-proto">
    <h2>parent: {{ count }}</h2>
    <demo-child
      :count="count"
      @add-count="addCount"
    >
    </demo-child>
    <button @click="changeName">parent:change name</button>
  </div>
</template>

<script>
import DemoChild from './demo-child';

export default {
  name: 'DemoExtendProto',
  components: { DemoChild },
  data () {
    return {
      count: 0
    };
  },
  methods: {
    addCount (params) {
      this.count++;
    },
    changeName () {
      // 更新直接name属性
      this.$broadcast('DemoGreatGrandson', 'change-name', 'xxx');
    }
  }
};
</script>
<!-- 子组件 -->
<template>
  <div class="demo-child">
    <demo-grandson></demo-grandson>
    <button @click="$emit('add-count')">child click</button>
  </div>
</template>
<!-- grandson -->
<template>
  <div class="demo-grandson">
    <great-grandson :name="name" @change-name="changeName"></great-grandson>
    <button @click="addCount">grandson click</button>
  </div>
</template>

<script>
export default {
  name: 'DemoGrandson',
  // ... some code
  data () {
    return {
      name: '张三'
    };
  },
  methods: {
    addCount () {
      // 直接通知父组件更新count
      this.$dispatch('DemoChild', 'add-count', 'xxx');
    },
    changeName () {
      this.name = this.name + 1;
    }
  }
};
</script>
<template>
  <div class="demo-great-grandson">
    <h2>great grandson:{{ name }}</h2>
    <button @click="addCount">great-grandson: click</button>
  </div>
</template>

<script>
export default {
  // ... some code
  props: {
    name: {
      type: String,
    }
  },
  methods: {
    addCount () {
      // 直接通知父组件更新count
      this.$dispatch('DemoChild', 'add-count', 'xxx');
    },
  }
};
</script>

上边代码的逻辑大概如下:

现在我们便可以通过$dispatch/$broadcast来实现跨层级调用$emit方法,少写一些进行事件监听的@$emit代码。

上述代码参考element ui源码中$dispatch/$broadcast的相应实现:

elememnt ui并没有将方法挂载到Vue的原型上,而是定义了mixins中,最终可以通过mixins属性来混入到组件中进行使用:

截图中的代码地址,有兴趣的小伙伴可以点击链接直接查看:

事件总线(bus)

Vue通过$emit/$on实现了事件的发布订阅机制,通过$on来订阅事件,通过$emit来触发$on订阅的事件,并将需要的参数传入。我们也可以借助Vue$emit$on属性,来进行组件之间的函数调用以及参数传递。

首先我们需要在Vue的原型上扩展$bus属性,方便直接在组件中通过this.$bus来进行调用:

Vue.prototype.$bus = new Vue();

$bus的值是一个新的Vue实例,所以它可以调用Vue实例的$emit$on方法。

在父组件挂载完毕后,我们通过$bus.$on来订阅事件:

<template>
  <div class="demo-bus">
    <demo-child :count="count"></demo-child>
  </div>
</template>
<script>
import DemoChild from './demo-child';

export default {
  name: 'DemoBus',
  components: {
    DemoChild
  },
  data () {
    return {
      count: 0
    };
  },
  mounted () {
    this.initListeners();
  },
  methods: {
    initListeners () {
      this.$bus.on('add-count');
    },
    addCount () {
      this.count++;
    }
  }
};
</script>

在子组件和孙子组件中,可以通过$bus.$emit来通知执行对应的订阅事件来更新count属性:

<!-- 子组件 -->
<template>
  <div class="demo-child">
    <h2>child: {{ count }}</h2>
    <demo-grandson></demo-grandson>
    <button @click="$bus.$emit('add-count')">child click</button>
  </div>
</template>
<!-- 孙子组件 -->
<template>
  <div class="demo-grandson">
    <button @click="$bus.$emit('add-count')">grandson click</button>
  </div>
</template>

不管组件层级有多深,我们都可以通过约定好的名字(例子中是add-count)来直接调用父组件中的订阅函数。

Vuex

对于稍大规模一点的项目来说,通过Vuex来管理全局状态是比较好的选择。我们可以在任意组件使用Vuex中的state,并且可以通过commit一个mutation来更新状态。

下面我们用Vuex来再次实现count累加的例子。

首先在store中定义statemutation

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    addCount (state, count) {
      state.count++;
    }
  },
});

可以在任意组件中引入,并且更改state

每个文件中引入Vuex辅助函数的代码如下:

import { mapMutations, mapState } from 'vuex';

export default {
  // ... some code
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapMutations(['addCount'])
  }
};

HTML模板代码:

<!-- 父组件 -->
<template>
  <div class="demo-props">
    <h2>{{ count }}</h2>
    <demo-child>
    </demo-child>
    <button @click="addCount">parent click</button>
  </div>
</template>

<!-- 子组件 -->
<template>
  <div class="demo-child">
    <h2>child: {{ count }}</h2>
    <demo-grandson></demo-grandson>
    <button @click="addCount">child click</button>
  </div>
</template>

<!-- 孙子组件 -->
<template>
  <div class="demo-grandson">
    <h2>grandson count: {{ count }}</h2>
    <button @click="addCount">grandson click</button>
  </div>
</template>

现在我们可以通过Vuex辅助函数在实例上提供的count属性和addCount方法,就可以在任意组件使用和更新count

总结

当我们能够熟练掌握组件之间的各种传参技巧后,在实际的工作以及面试中便能够更加的游刃有余、从容不迫。

我们面临一个实际问题之后,所思考的不再是如何进行组件传参,而是如何能更好的选择和设计一种比较灵活优雅的传参方法,提高程序的可读性和可扩展性。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK