Vue:数据与事件通信

Vue:数据与事件通信

Prop:父子组件间通信

类比于程序和函数,父组件是一个更大的抽象级别,往往作为数据的拥有者,而子组件则是作为数据处理的执行者。在父子组件间的数据传递方面,Vue 提供了一个属性:Prop

我们可以在组件实例中访问 prop,就像我们访问 data 那样。

通过 Prop,父组件得以将数据交由子组件处理或详细表示。

<!-- 父组件 -->
<template>
	<div id="main-view">
		<task v-for="task in tasks" :title="task.title"></task>
	</div>
</template>

<script>
	import Task from '../components/Task.vue'
	export default {
		name: 'main-view',
		data() {
			return {
				tasks: [
					{title: 'list-1'},
					{title: 'list-2'},
					{title: 'list-3'}
				]
			}
		},
		components: {
			Task
		}
	}
</script>
<!-- 子组件 -->
<template>
	<div class='task'>
		<span>{{ title }}</span>
	</div>
</template>

<script>
	export default {
		name: 'task',
		props: ['title']
	}
</script>

父子组件通过 prop 进行数据传递,但是这一个数据传递是单向下行绑定的。在 Vue 中,父组件的值发生了变化,那么子组件中对应的 prop 数据是能够响应变化的,但是反过来不行。我们并不希望在子组件中能够直接更改父组件中的数据信息——子组件不应该有这么大的权限。对于 prop 这一单向数据流,从某个方面来说,类似于函数中的参数值传递。和函数值传递所不同的是,当我们在子组件中尝试对 prop 进行修改的时候,Vue 会在控制台抛出警告。

但是对于数组和对象这一类引用类型,我们不改变其与 prop 的绑定,而是改变其“内部值”。例如,我们可以向 prop 传入一个对象,在子组件中对对象的属性值进行更改。这并不会引起 Vue 对你的警告,但是在子组件中改变父组件中的数据并不是一个好的做法。

在很多时候,我们确实需要在子组件响应某种事件时(比如点击子组件),对数据进行更改。当然,我们并不希望子组件拥有如此大的权限对父组件的数据进行修改。父组件的数据修改应由父组件完成,或者由“更高级者”来完成。

为了实现父子组件间的交互,Vue 提供了 v-on$emit 方法。

<!-- 子组件 -->
<div class='child-component'>
    <button v-on:click="$emit('clickFunc')">
        click me!
    </button>
</div>
<!-- 父组件 -->
<father-component v-on:clickFunc="...">
    <child-component></child-component>
</father-component>

父组件监听某一事件,如上方的 clickFunc ,当子组件抛出这一事件时,父级组件就会接收该事件并执行相关操作。

当然,对于组件要抛出的事件,你可以通过 emits 来显式声明:

export default {
  emits: ['clickFunc', 'submit']
}

需要注意的是,具有直接关系父子组件才能够实现“抛出”和“监听”。对于平级(兄弟)组件以及更深层次的嵌套组件,无法直接通过 $emitv-on 实现事件通信。我们可以通过全局事件总线来实现更为复杂的事件通信。

Provide 和 Inject:Prop 逐级透传问题的解决方案

实际应用中,页面结构不会仅仅有父子组件,深层次嵌套的组件组合是常见的:

<root>
    <first-level>
        <second-level>
            <third-level></third-level>
        </second-level>
    </first-level>
</root>

假设这么一个场景:数据 messageroot 所拥有,并被 third-level 所使用。数据 message 为局部数据,原则上平级组件不应该能够访问到该数据。Vue 中数据的传递通常由 prop 来完成,此处也可用 prop 实现数据的传递:rootmessage 传递给 first-level ,继而 first-level 传递给 second-level ,最终 messagesecond-level 交给 third-level 。虽然数据的传递过程略微冗长,但至少我们的目标实现了。不过除了传递过程冗长的问题外,似乎 first-levelsecond-level 不必要,也不应该接触到 message

Vue 给出的解决方式是:使用 ProvideInject 。“一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖”。

<!-- 依赖提供者 -->
export default {
	provide: {
		message: 'Hello World!'
	}
}
<!-- 接收者 -->
export default {
    inject: ['message']
}

对于 provide 对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。

如果接收者溯源找不到期望值,那么将会抛出错误。在解决这一个问题前,我们先了解关于注入时的别名。当我们使用数组形式的 inject,注入的属性将会以同名的 key 暴露到组件实例上。也就是我们可以在接收者中使用 this.message 访问到 inject 中的 message 。如果我们想要一个新的名字,我们可以这么做:

export default {
    inject: {
        localMessage: {
            from: 'message'
        }
    }
}

回到找不到期望值的问题。事实上,就如 prop 一般,我们可以为 inject 设置默认值:

export default {
    inject: {
        message: {
            from: 'message', //当与注入名同名时,这个选项是可选的
            default: 'default value'
        }
    }
}

当然,我们还可以提供当前组件实例的状态,如 data()中的数据。但需要进行修改,将 provide 以函数形式呈现:

export default {
    data() {
        return {
            message: 'Hello World!'
        }
    },
    provide() {
        return {
            message: this.message
        }
    }
}

但是这一方式并没有保持响应式。这就意味着当 data() 中的 message 由 'Hello World!' 变为 'Bye' 时,provide 中的 message 仍然是 'Hello World'。我们可以使用计算属性解决这一问题:

import { computed } from 'vue'

export default {
  data() {
    return {
      message: 'Hello World!'
    }
  },
  provide() {
    return {
      message: computed(() => this.message)
    }
  }
}

参考:依赖注入 | Vue.js (vuejs.org)

Bus:全局事件总线

关于 Bus ,其中文释义为:总线;公共汽车。“公共汽车”这一释义在此处也十分形象:对于所有人都是可见的,所有人都可以乘坐。

Bus 游走于各个组件,实现数据和事件的传递和分发。

首先我们需要绑定一个 Bus 实例:

<!-- main.js -->
Vue.prototype.$bus = new Vue()

这样,我们就可以在组件中使用 $busBus 打破了组件间事件通信的壁障,无论是平级组件还是深层次的嵌套组件都可以实现通信。当然,父子组件也可以使用 Bus 进行通信,不过父子组件可以直接使用 $emitv-on 进行事件通信。

在组件创建或挂载时即对事件进行监听:

created()/mounted() {
	this.$bus.$on('isCalled', this.isCalled)
}

而对于事件的抛出,只需要在事件发生处抛出即可:

<button @click="this.$bus.emit('isCalled')">
    click me!
</button>

对于所注册的总线事件,我们需要在组件销毁前对事件解除监听,否则会出现多次挂载,导致一次触发但多个响应:

beforeDestroyed() {
	this.$bus.$off('isCalled', this.Called)
}

需要注意的是,由于我们是在组件中监听事件,当一个组件被复用多次时,抛出事件后这些组件实例都会对事件进行响应——如果你正需要这一效果,那么可以充分利用这一性质。如果你只希望其中的某一个组件实例对此做出响应,那么就需要做出判断,或者以别的方式实现事件通信。

Store:仓库式管理状态

Vue 为项目状态管理提供了 Vuex ,但随着 Vue 的不断发展,现如今 Vue 更推荐使用 Pinia 对项目进行状态管理。

对 Vuex 而言,重要的是两个点:storemutation 。前者为状态,后者为改变状态的手段。当涉及到异步操作时,需要用到 action

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default {
	state: {
        count: 0
    }, // 在 Vuex 官网,采用的是函数返回对象式的写法
    mutations: {
        increment(state) {
            state.count++
        }
    }
}

Vuex 不希望你在组件中直接改变 state ,即意味着在组件中 state 是只读的。要改变 state ,应该显式提交 mutation

export default {
	data() {
        
    },
    computed: {
		count() {
            return this.$store.state.count
        }
    }
    methods: {
        test() {
            this.$store.commit('increment')
        }
    }
}

Vuex 使用单一状态树对项目的状态进行管理——意味着仅需要一个对象就可以对整个项目的各个状态进行访问。同时,为了不使状态管理臃肿和混乱,我们可以将状态划分到不同的子模块中。

// moduleA.js

export default {
    state: {
        a: 'a'
    },
    mutations: {
        ...
    }
}
// moduleB.js

export default {
    state: {
        b: 'b'
    },
    mutations: {
        ...
    }
}
// index.js

import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './moduleA.js'
import moudleB from './moduleB.js'

Vue.use(Vuex)
export default new Vuex.Store({
    modules: {
        moduleA,
        moduleB
    }
})

在访问时:

// 组件文件中

export default {
    data() {
        ...
    },
    computed: {
        one() {
            return this.$store.state.moduleA.a
        }
    }
}

参考: Vuex (vuejs.org)

posted @ 2022-08-28 00:28  轻轻一跃  阅读(26)  评论(0)    收藏  举报