Vue 3.0 function-based API尝鲜(六):组件间通信
关键词:state、Vuex、context.root
从广义上来说,我觉得组件间通信的应用场景,无非就是父子组件间的通信和非父子组件间(兄弟、祖孙……)的通信。其实说白了,所谓的通信,就是控制数据的流动,也就是控制“状态”;我倒是挺认可有限状态机模型的。如果进一步细分,大致是这样几种:
- 父子组件间通信,这个是最简单也是最常用的场景。父子组件间的通信可以直接通过
props
和$emit
完成,如果放在3.0,就是props
和context.emit
。从直观感受上说,父子组件之间的关系,有点CSS那种一层层叠加的感觉;如果从面向对象理论的角度说,有点类似于组合委托,拥有共同的生命周期。 - 总线通信,我觉得store模式也算总线的一种。这个适用于比较简单的组件间通信。有时候需要兄弟组件传值,或者一个父组件需要管理多个子组件(这个时候用props就会臃肿,而且不好维护),并且整个项目规模不大,完全可以考虑用一个总线代替Vuex来管理整个项目的状态。当然缺陷也是很明显的,扩展性明显不足,同时对没有办法很好地追踪数据的流动(至少很不优雅),给调试带来了困难。说到底是一个工程问题,要在成本和效率上进行权衡。
- Vuex通信,这个单独拿出来,适用于比较复杂的项目。有时候会有一些全局共享的数据,比如token和一些登录信息。虽然这些数据也可以存在sessionStorage、localStorage或者cookie里,但刷新后丢失这个特性有时候还是很有用的,比如我可以检测这个来更新token。我很喜欢Dan Abramov那句话:“Flux libraries are like glasses: you’ll know when you need them.”需要的时候,你自然就会想到它。Vuex可以更好地在组件外部管理状态,而且是那种受控的全局变量,避免了无法维护的公共耦合。
- router通信。不要忘了,router也是可以携带数据的。有时候,我们需要在两个页面间传递数据(比如id),但是这个数据只在这里出现,并不值得放进Vuex里。这个时候,我们就可以让router来传递数据;可能还需要
activated
和deactivated
钩子的配合。router的params和query都可以承担这个职责。
说起总线,要么就是创建一个空的Vue实例,然后用$emit
和$on
进行传递:
// bus.js
import Vue from 'vue'
export const bus = new Vue()
// parent.vue
created () {
bus.$on('foo', (bar) => {// do sth...})
}
// child.vue
bus.$emit('foo', bar)
但是这种写法的限制比较多,要求监听的对象(在这里是父组件)一定要先于被监听的对象创建;而且中途销毁子组件也会影响监听。
或者像官方文档上写的那样,创建一个store:
var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered')
this.state.message = ''
}
}
不过在这种简单的应用场景下,还有更简单的方案;我们只是需要一个状态的容器而已,不是吗?所以……还记得2.6的新API,Vue.observable
吗?
让一个对象可响应。Vue 内部会用它来处理
data
函数返回的对象。返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景。
在 Vue 2.x 中,被传入的对象会直接被
Vue.observable
改变,它和被返回的对象是同一个对象。在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行修改仍然是不可响应的。因此,为了向前兼容,我们推荐始终操作使用Vue.observable
返回的对象,而不是传入源对象。
我们先设想一个最简单的非父子组件间通信的场景。这个时候,我们可以用Vue.observable
创建一个容器,然后就可以代替之前的bus了:
import Vue from 'vue'
export const bus = Vue.observable({
foo: 'bar'
})
在3.0里,state
,从某种意义上,是可以用来取代Vue.observable
的:
import Vue from 'vue'
import { plugin, state } from 'vue-function-api'
Vue.use(plugin)
export const bus = state({
foo: 'bar'
})
需要注意的是,这里一定要有一个Vue.use
的过程。这个不同于函数式的组件。不过,在3.0里,state
的作用并不仅止于这些。它的真正意义,大概是作为包装对象的另一面,作为共享状态的容器只是其中一个应用场景罢了:
如果你依然想创建一个没有包装的响应式对象,可以使用
state
API(和 2.x 的Vue.observable()
等同)。
说完这个,说说Vuex的用法。我们都知道,Vuex和router是挂载在根实例上的,所以我们可以用$store
、$router
、$route
来调用它们。这一点在main里体现得很明显:
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
但是到了3.0,$已经被setup代替了。那么,我们应该怎么使用呢?可以看一个例子,这个是来自vue-function-api的issue的写法:
setup (props, { root }) {
const fields = computed(() => root.$store.state.fields)
}
不过,为了支持3.0,Vuex和router的API显然是会有变动的。按照作者的说法,未来的API可能会类似于这样:
import { useStore, useComputed } from 'vuex';
setup (props, { root }) {
const store = useStore();
const filed = useComputed('state.field');
}
目前为了达到这个效果(也就是所谓的FOP,面向未来编程),可以按照作者的说法,搞一个tricky的适配;这个我也在demo里写了:
const store = new Vuex.Store({//...})
export function userStore() {
return store;
}
当然,也有dalao自己实现了一个版本。虽然我并不认识他……但是我觉得很有意思,就分享给大家:传送门。
目录
Vue 3.0 function-based API尝鲜(一):前言
Vue 3.0 function-based API尝鲜(二):配置与启动
Vue 3.0 function-based API尝鲜(三):包装对象
Vue 3.0 function-based API尝鲜(四):值得一提的watch
上一篇: vue学习之计算属性和侦听器、过滤器
下一篇: Leetcode 775