响应式API
- ref
- unref
- toRef
- toRefs
- isRef
- customRef
- shallowRef
- triggerRef
- computed
- watch
- watchEffect
1、解构带来的响应式陷阱
我们习惯了ES6的对象解构风格,但这在composition- api
里可能会有陷阱。因为结构可能会让你的响应式对象失去预期中的响应特性。
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive } from '@vue/runtime-dom'
export default {
setup() {
const data = reactive({
count: 0
})
function addCount() {
data.count += 1
}
return {
data,
count: data.count,
addCount
}
}
};
</script>
比如这里,button的click时候,不会得到预期的count
的增加。因为setup执行返回的count
并不是响应式的。
也就是说,虽然你的click事件确实的改变了data.count
的值,但是这个值并没有响应式的去改变其他引用这个值的地方。怎么去验证我们这个解释呢?
我们可以开着chrome的vue插件,定位到你的组件。然后你可以点击一下button,可以看到count
并没有变化,data
呢?看起来好像也没有变化?这不是不符合逻辑吗?甚至我们在click的回调函数里打印一下发现是有执行data.count+1 这个操作的。
事实是,data.count
确实是执行了的,但是因为不是reactive
的,所以插件里没有及时更新这个新数据。如果你把插件先切到别的组件上去,再切回来。你就会发现,data.count是符合预期的!
click操作前的插件看到的数据:
我做了2次click后,现在插件里把光标切到别的组件上,再切回来。就可以看到data的变化:
只是引用了data.count
的地方没有被更新,这就说明引用data.count
的地方是非reactive
的。
我们要要做的,就是改造一下引用data.count
的地方。我们在setup
里的返回,可以用computed
和toRefs
来改造一下返回值。
再看一下toRefs改造后的demo:
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive } from '@vue/runtime-dom'
export default {
setup() {
const data = reactive({
count: 0
})
function addCount() {
data.count += 1
}
return {
...toRefs(data),
addCount
}
}
};
</script>
第二个问题来了:toRefs
一定是安全应对解构的方案么?
不是的,因为toRefs
的结构是浅解构的,对于我们demo里的这种简单的对象是work的。但是如果是一个嵌套很深的复杂Object,还是会有解构后响应式断裂的问题。如果数据的层级比较复杂,建议使用computed
。
2、watch 和 watchEffect
watchEffect
很像React里的useEffect
,是一个副作用函数。用法也基本一致。
watch
的话,接收2个参数。第一个参数是watch
的target,看ts结构,必须是一个ref
对象或者computedRef
对象;第二个参数是watch的回调函数。
踩坑:
目前@vue/composition-api里的watch有bug。在watch一个数组的时候,触发不了回调函数,从v1.1版本开始就有这个问题。已经有人提了issue,暂未解决(no longer works for multiple sources after v1.1)。
3、composition-api模仿React的useContext
React的hooks出来之后,有个很好用的东西就是Context,很像一个微型Redux,可以很好的跨组件传值,尤其是在组件粒度很细的时候,我们的组件间通信频率也会升高。
之前vue2的时代,其实一直有provide
和inject
可以用。但是provide
和inject
的对象一般是非响应式的。官网是这么记载的:
vue2的时候,我们一般不太注重如何把一个数据变成响应式的(也不是没有办法,比如Vue.observable(obj)
可以把一个对象变成响应式的。如果我们把这个对象provide出去,那么传递的数据也就一直是响应式的了)。
vue3(或者vue2 + @vue/composition-api
)后,我们更多的关注到了数据的reactive
特性。比如用ref
或者reactive
关键字来构造一个响应式的对象。我们如果再用provide
直接传递一个reactive
的对象,岂不是可以模拟出类似React的useContext
这样的结构?
外层Context层构造:
import { createApp, defineComponent, provider, inject, reactive, readonly, toRefs } from 'vue';
// Provider 包装组件
const MyConfigProvider = defineComponent({
name: 'MyConfigProvider',
props: ['prefixCls', 'title'],
setup (props, { slots }: SetupContext) {
const { prefixCls, title } = toRefs(props);
const context = reactive({
prefixCls,
title
});
provide('myConfig', readonly(context));
return () => slots.default?.();
}
});
// 测试用子组件
const ChildComp = defineComponent({
name: 'ChildComp',
setup () {
const myConfig = inject('myConfig', {});
return () => (
<>
<p>{myConfig.prefixCls}</p>
<p>{myConfig.title}</p>
</>
)
}
});
// 调用Context层和子组件
const App = defineComponent({
name: 'App',
setup () {
const state = reactive({
prefixCls: 'myui',
title: 'MyApp',
i18n: (key: string) => key
});
return () => (
<div id="#app">
<MyConfigProvider {...state}>
<ChildComp />
</MyConfigProvider>
</div>
)
}
});
本身vue3(or vue2
+ @vue/composition-api
)也是支持hooks的。
这样我们就可以按照React hooks的开发习惯去给vue抽hooks了。