计算属性的set函数

假设兄弟组件共享数据 msg ,我们一般用 Vuex 处理,如下:

  1. import Vue from 'vue';
  2. import Vuex from 'vuex';
  3. Vue.use(Vuex);
  4. const types = {
  5. UPDATE_MSG: 'UPDATE_MSG',
  6. };
  7. const mutations = {
  8. [types.UPDATE_MSG](state, payload) {
  9. state.msg = payload.msg;
  10. },
  11. };
  12. const actions = {
  13. [types.UPDATE_MSG]({ commit }, payload) {
  14. commit(types.UPDATE_MSG, payload);
  15. },
  16. };
  17. export default new Vuex.Store({
  18. state: {
  19. msg: 'Hello world',
  20. },
  21. mutations,
  22. actions,
  23. });

需求:兄弟组件共享数据,且数据实时同步,比如在组件 comp1 中使用它,通过改变 comp1msg 值引起 comp2msg 变化,监控数据变化的时候,我们一般是使用watch处理

  1. <template>
  2. <div class="comp1">
  3. <h1>Component 1</h1>
  4. <input type="text" v-model="msg">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'comp1',
  10. data() {
  11. const msg = this.$store.state.msg;
  12. return {
  13. msg,
  14. };
  15. },
  16. watch: {
  17. msg(val) {
  18. this.$store.dispatch('UPDATE_MSG', { msg: val });
  19. },
  20. },
  21. };
  22. </script>

同样对 comp2 做相同修改。当然还得在 src/main.js 中引入:

  1. // main.js
  2. import Vue from 'vue';
  3. import App from './App';
  4. import store from './store';
  5. Vue.config.productionTip = false;
  6. /* eslint-disable no-new */
  7. new Vue({
  8. store,
  9. el: '#app',
  10. template: '<App/>',
  11. components: { App },
  12. });

但是你会发现修改 comp1 中的输入框,通过 vue-devtools 也可查看到 Vuex 中的的 state.msg 的确也跟着变了,但是 comp2 中输入框并没有发生改变,当然这因为我们初始化 msg 时,是直接变量赋值,并未监听 $store.state.msg 的变化,所以两个组件没法实现同步。

解决方案:再添加个 watch 属性,监听 $store.state.msg 改变,重新赋值组件中的 msg 不就行了,确实可以实现,但是这样代码是不是不太优雅,为了一个简单的 msg 同步,我们需要给 data 添加属性,外加两个监听器,这样的实现方式不太优雅,我们对此再次优化,我们直接定义计算属性 msg,然后返回就可以了。
修改 comp1 comp2 如下:

  1. <template>
  2. <div class="comp1">
  3. <h1>Component 1</h1>
  4. <input type="text" v-model="msg">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'comp1/comp2',
  10. computed: {
  11. msg: {
  12. get() {
  13. return this.$store.state.msg;
  14. },
  15. set(val) {
  16. this.$store.dispatch('UPDATE_MSG', { msg: val });
  17. },
  18. },
  19. },
  20. };
  21. </script>

可以发现 comp1 输入框的值comp2 输入框的值store 中的值 实现同步更新了。

可配置的 watch

先看一段代码:

  1. // ...
  2. watch: {
  3. username() {
  4. this.getUserInfo();
  5. },
  6. },
  7. methods: {
  8. getUserInfo() {
  9. const info = {
  10. username: 'yugasun',
  11. site: 'yugasun.com',
  12. };
  13. /* eslint-disable no-console */
  14. console.log(info);
  15. },
  16. },
  17. created() {
  18. this.getUserInfo();
  19. },
  20. // ...

组件创建的时候,获取用户信息,然后监听用户名,一旦发生变化就重新获取用户信息,这个场景在实际开发中非常常见。那么能不能再优化下呢?

答案是肯定的。其实,我们在 Vue 实例中定义 watcher 的时候,监听属性可以是个对象的,它含有三个属性: deepimmediatehandler,我们通常直接以函数的形式定义时,Vue 内部会自动将该回调函数赋值给 handler,而剩下的两个属性值会默认设置为 false
这里的场景就可以用到 immediate 属性,将其设置为 true 时,表示创建组件时 handler 回调会立即执行,这样我们就可以省去在 created 函数中再次调用了,实现如下:

  1. <script>
  2. export default {
  3. watch: {
  4. username: {
  5. immediate: true,
  6. handler: 'getUserInfo',
  7. },
  8. },
  9. methods: {
  10. getUserInfo() {
  11. const info = {
  12. username: 'yugasun',
  13. site: 'yugasun.com',
  14. };
  15. /* eslint-disable no-console */
  16. console.log(info);
  17. },
  18. },
  19. }
  20. </script>

computed和watch对比

image.png

Url改变但组件未变时,created 无法触发的问题

首先默认项目路由是通过 vue-router 实现的,其次我们的路由是类似下面这样的:

  1. // ...
  2. const routes = [
  3. {
  4. path: '/',
  5. component: Index,
  6. },
  7. {
  8. path: '/:id',
  9. component: Index,
  10. },
  11. ];

公用的组件 src/views/index.vue 代码如下:

  1. <template>
  2. <div class="index">
  3. <router-link :to="{path: '/1'}">挑战到第二页</router-link><br/>
  4. <router-link v-if="$route.path === '/1'" :to="{path: '/'}">返回</router-link>
  5. <h3>{{ username }} </h3>
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'Index',
  11. data() {
  12. return {
  13. username: 'Loading...',
  14. };
  15. },
  16. methods: {
  17. getName() {
  18. const id = this.$route.params.id;
  19. // 模拟请求
  20. setTimeout(() => {
  21. if (id) {
  22. this.username = 'Yuga Sun';
  23. } else {
  24. this.username = 'yugasun';
  25. }
  26. }, 300);
  27. },
  28. },
  29. created() {
  30. this.getName();
  31. },
  32. };
  33. </script>

两个不同路径使用的是同一个组件 Index,然后 Index 组件中的 getName 函数会在 created 的时候执行,你会发现,让我们切换路由到 /1 时,我们的页面并未改变,created 也并未重新触发


这是因为 vue-router 会识别出这两个路由使用的是同一个组件,然后会进行复用,所以并不会重新创建组件,那么 created 周期函数自然也不会触发。

方案一:通常解决办法就是添加 watcher 监听 $route 的变化,然后重新执行 getName 函数。代码如下:

  1. watch: {
  2. $route: {
  3. immediate: true,
  4. handler: 'getName',
  5. },
  6. },
  7. methods: {
  8. getName() {
  9. const id = this.$route.params.id;
  10. // 模拟请求
  11. setTimeout(() => {
  12. if (id) {
  13. this.username = 'Yuga Sun';
  14. } else {
  15. this.username = 'yugasun';
  16. }
  17. }, 300);
  18. },
  19. },

方案二:给 router-view 添加一个 key 属性,这样即使是相同组件,但是如果 url 变化了,Vuejs就会重新创建这个组件。我们直接修改 src/App.vue 中的 router-view 如下:

  1. <router-view :key="$route.fullPath"></router-view>

$attrs 多层组件数据传递

大多数情况下,从父组件向子组件传递数据的时候,我们都是通过 props 实现的,比如下面这个例子:

  1. <!-- 父组件中 -->
  2. <Comp3
  3. :value="value"
  4. label="用户名"
  5. id="username"
  6. placeholder="请输入用户名"
  7. @input="handleInput"
  8. >
  9. <!-- 子组件中 -->
  10. <template>
  11. <label>
  12. {{ label }}
  13. <input
  14. :id="id"
  15. :value="value"
  16. :placeholder="placeholder"
  17. @input="$emit('input', $event.target.value)"
  18. />
  19. </label>
  20. </template>
  21. <script>
  22. export default {
  23. props: {
  24. id: {
  25. type: String,
  26. default: 'username',
  27. },
  28. value: {
  29. type: String,
  30. default: '',
  31. },
  32. placeholder: {
  33. type: String,
  34. default: '',
  35. },
  36. label: {
  37. type: String,
  38. default: '',
  39. },
  40. },
  41. }
  42. </script>

但是如果子组件又包含了子组件,而且同样需要传递 id, value, placeholder... 呢?甚至三阶、四阶…呢?那么就需要我们在 props 中重复定义很多遍了。
这个时候就需要 vm.$attrs,官方解释:

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件—— 在创建高级别的组件时非常有用

修改代码如下:

  1. <!-- 父组件中 -->
  2. <Comp3
  3. :value="value"
  4. label="用户名"
  5. id="username"
  6. placeholder="请输入用户名"
  7. @input="handleInput"
  8. >
  9. <!-- 子组件中 -->
  10. <template>
  11. <label>
  12. {{ $attrs.label }}
  13. <input
  14. v-bind="$attrs"
  15. @input="$emit('input', $event.target.value)"
  16. />
  17. </label>
  18. </template>
  19. <script>
  20. export default {
  21. }
  22. </script>

$emit 处理回调

有时候我们会遇到一种场景:子组件向父组件通信,传递事件, vm.$emit (函数)处理完成之后,再处理后面的逻辑。但是 vm.$emit 不支持回调,默认返回的this,也就是组件实例对象。解决方式就是 $parent

父组件代码如下:

  1. // tempalte
  2. <component
  3. :is="componentId"
  4. ></component>
  5. //methods
  6. async handleRefresh () {
  7. await this.getDelayRepaymentInfo()
  8. }

子组件代码如下:

  1. async handleRefreshChild() {
  2. // await this.$emit('handleRefresh')
  3. await this.$parent.handleRefresh()
  4. // dosomething...
  5. console.log('2、担保人邀请完成 刷新数据', this.guarantorInfo)
  6. }

$refs 使用详解

注意:
【使用范围】$refs不能在created生命周期中使用 因为在组件创建时候 该ref还没有绑定元素,
【适用场景】它是非响应的,所以应该避免在模板或计算属性中使用 $refs ,它仅仅是一个直接操作子组件的应急方案

应用一:在DOM元素上使用$refs可以迅速进行dom定位,类似于$(“selectId”),如下

  1. <template>
  2. <div class="parent">
  3. <p ref="testTxt">{{oldMsg}}</p>
  4. <button @click="changeMsg()">点击修改段落内容</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. data(){
  10. return {
  11. oldMsg:'这是原有段落数据内容',
  12. newMsg:'hello,这是新的段落数据内容!!',
  13. }
  14. },
  15. methods:{
  16. changeMsg(){
  17. this.$refs.testTxt.innerHTML=this.newMsg;
  18. },
  19. }
  20. }
  21. </script>

应用二:通过$refs实现对子组件操作,代码如下:


①使用this.$refs.paramsName能更快的获取操作子组件属性值或函数
parentone.vue 如下:

<template>
    <div class="parent">
        <Childone ref="childItemId"></Childone>
        <p style="color:blue;">{{msgFromChild}}</p>
        <button @click="getChildMsg()">使用$refs获取子组件的数据</button>
    </div>
</template>

<script>
import Childone from './childone'
export default {
    components:{Childone},
    data(){
        return {
            msgFromChild:'',
        }
    },
    methods:{
        getChildMsg(){
            this.msgFromChild=this.$refs.childItemId.childMsg;
        },
    }
}
</script>

childone.vue 如下:


<template>
    <div class="child"></div>
</template>

<script>
export default {
    data(){
        return {
            childMsg:'这是子组件的参数'
        }
    }
}
</script>

扩展到$parent 、$children的使用:

②我们可以使用$children[i].paramsName 来获取某个子组件的属性值或函数,$children返回的是一个子组件数组
这里就只写父组件的代码了,parentone.vue如下:


<template>
    <div class="parent">
        <Childone></Childone>
        <Childtwo></Childtwo>
        <p style="color:blue;">{{msgFromChild}}</p>
        <button @click="getChildMsg()">使用$children来获取子组件的数据</button>
    </div>
</template>

<script>
import Childone from './childone'
import Childtwo from './childtwo'
export default {
    components:{Childone,Childtwo},
    data(){
        return {
            msgFromChild:'',
        }
    },
    methods:{
        getChildMsg(){
            this.msgFromChild=this.$children[1].child2Msg;
        },
    }
}
</script>

③那么子组件怎么获取父组件的数据内容?这需要使用$parent
**

组件数据通信方案总结

组件间不同的使用场景可以分为 3 类,对应的通信方式如下:

  • 父子通信:Props / $emit,$emit / $on,Vuex,$attrs / $listeners,provide/inject,$parent / $children&$refs
  • 兄弟通信:$emit / $on,Vuex
  • 隔代(跨级)通信:$emit / $on,Vuex,provide / inject,$attrs / $listeners