探索 Vue3 的高级用法:深入理解 provide 和 inject 的依赖注入
为了提供更方便的使用体验,Vue 引入了响应性的功能。然而,一旦将响应性从 Vue 中分离出来并集成到 Composition API 中,就出现了各种各样的细小问题。在这里,我们将对这些问题进行汇总。
目录
- reactive:以 reactive 为例,说清楚丢失响应性的根本原因,其他的丢失响应性也是这个原因。
- ref:表面上看挺好,其实也是一堆坑
- props:这是重灾区,稍不注意就中招
- 函数传参:其实 watch 是一个函数
事先约定
因为描述起来比较绕口,所以先做几个约定:
- ref.value:这是一个筐,啥都能往里装,小米、苹果、鸭梨、大象、轿车、大楼等都能装。
- ref.value 的值:筐里装的东东,我们选个代表:小米。
- props.xxx:这也是一个筐,父组件传啥就装啥,如果能把地球传来也能装。
- props.xxx 的值:父组件传过来的东东,比如小米。一般是基础类型,其实也可以传 reactive。
如果使用 ref 的话,父组件在默认的情况下,只会传小米,不会传筐。
reactive
reactive 非常好用,只是不能整体赋值,否则会失去响应性,官方不想想如何弥补,而是一刀切的推荐使用 ref,其实 ref 一样有坑。
整体赋值的情况
还是先看看 reactive 的情况,举例说明:
// ✔ 正确用法,必须使用 const 定义 const foo1 = reactive ({name:'mybj'}) const foo2 = reactive ({name:'mybj'}) console.log(foo1 === foo2) // false,两个不同对象的代理,地址(指针)不同 // ✘ 错误用法 ,不要使用 let、var,整体赋值也没个阻拦(js 会报错)。 let foo = reactive ({name:'mybj'}) const mychange = () => { foo = reactive ({name:'mybj999'}) // 不响应 }
整体赋值为啥不响应了呢?因为地址变更了呀。
看第一段,foo1 和 foo2 的地址是不同的,那么在事件里面,给 foo 整体赋值,这 foo 里面保存的就是另外一个地址了。
本质原因:setup 有一个 return,把各种响应性交给 template,这时候 template 会记录各种地址,在事件里面变更了地址,那么 template 呢?还在关注以前的地址呢,不知道新的地址,所以就无法响应了。
这一点很重要,vue 里面丢失响应性,大多都是这种原因。
多层的 reactive
reactive 是深层响应的,所以可以设计为多层对象,多层情况就更复杂了。
我们还是先看例子:
const more = reactive({ name: 'mybj', info: { name: '第二层' } }) // 默认深层监听,各种支持 watch(more, () => { console.log('watch more:', more) }) // 监听的是筐还是 【小米】?是【小米】! watch(more.info, () => { // 不会响应第二层的整体赋值 console.log('watch more.info:', more.info) }) // 监听的是筐的变化,换新筐才能监听 watch(() => more.info, () => { // 可以响应第二层的整体赋值 console.log('watch () => more.info:', more.info) }) // 手动深层监听,筐、**小米**都能监听 watch(() => more.info, () => { console.log('深层 watch () => more.info:', more.info) },{deep:true}) const myChange2 = () => { more.info = reactive({name:'mybj999'}) console.log('改变第二层后的 more:', more) }
- watch -> more:默认深层监听,支持各层的变化。
- watch -> more.xxx:表面上看是监听筐,但是实际上监听的是小米!为啥?别忘了 watch 是一个函数,这就涉及到函数传参的问题。筐里的 小米 换成 苹果 了,这个 watch 也就失效了
- watch -> () => more.xxx:这是啥?这是一个匿名函数!,这可不是小米,每次都会调用这个函数,所以可以得到最新的小米 。
- watch -> :手动深层监听,既能支持筐,又能支持小米。
会不会丢失响应性?要看赋值方式,也要看监听方式。
解构
这个就不多说了,尽量别解构就 OK 了。
ref
如果上面的原理都理解了,那么 ref 的问题就清楚了,还是筐和小米的问题。
- ref 是 class 的实例,也就是一个对象。
- ref.value 是对象的是个属性,相当于一个筐,啥都能往里装。
- 这个筐有响应性,但是筐里的东东有没有响应性,那就不一定了。
还是写代码举例:
const arr = ref([{ name: 'mybj' }]) // A 只监听筐的变化,不管小米如何变 watch(arr, () => { console.log('watch arr:', arr) }) // B 手动深层监听,可以监听筐和小米 watch(arr, () => { console.log('手动深层 watch arr:', arr) },{deep:true}) // C 只监听 ref 自己,和筐、小米都无关 watch(() => arr, () => { console.log('父组件: watch () => arr:', arr) }) // D 一开始能监听苹果,当使用 ref.value = [] 后,就不能监听苹果了。 watch(arr.value, () => { console.log('watch arr.value:', arr) }) // E 手动监听筐,不是深层,相当于 A watch(() => arr.value, () => { console.log('watch () => arr.value:', arr) }) // F 深层的监听,都有,相当于 B watch(() => arr.value, () => { console.log('手动深层 watch () => arr.value:', arr) },{deep:true})
好吧,我承认,我自己都写蒙了,各种情况都要测试一下,看看实际效果如何。
不知道各种 watch 方法是否符合你们的预期。
最安全的方式(对象的情况)
watch(arr, () => {}, {deep:true})
:
- 手动深层监听的方式,是最全面的,各种情况都能监听到。
watch(arr,...)
等效于watch(() => arr.value,...)
容易被误导的方式
watch(arr.value, () => {})
:
- 你以为监听的是筐?其实是苹果!
- 使用 arr.value = [] 之后,这个 watch 就无效了。
以为是深层,其实不是
watch(arr, () => {})
:是深层监听吗?不是!(和 reactive 的情况不同)
- 查看 watch 源码可知,相当于
() => ref.value
使用风格不统一
- ref 在 template 里面不需要使用
.value
,在 js 代码里面需要使用.value
。 - 但是 作为 watch 的第一个参数的时候,又可以不使用
.value
。 - watch 的第一个参数,ref 用不用
.value
,行为又不一致。
不看 watch 源码的话,晕不晕?
... if (isRef(source)) { getter = () => source.value; forceTrigger = isShallow(source); } else if (isReactive(source)) { getter = () => reactiveGetter(source); forceTrigger = true; // 等效于 深层监听 } else if (isArray(source)) { ... } else if (isFunction(source)) { ... }
对第一个参数(source
)的类型进行各种判断。
props
父子传值,父组件没事,子组件失去响应性了,为啥?还是因为地址变更了,子组件没能更新。
父组件
还是看看代码:
const arr = ref([{ name: 'mybj' }]) // 变更记录集 const change1 = () => { arr.value = [{name: 'mybj9999'}] } // 修改数组的一个元素 const change2 = () => { arr.value[0] = {name: 'mybj3333'} }
子组件
子组件里的 template 是肯定有响应性的,而 props 是 shallowReadonly,不是 Readonly,也不是 ref,所以还是有一点点不一样的情况,我们来看看 js 代码里的 watch 的响应情况:
// 方案一 watch(props.listRef, () => { console.log('----子组件: watch -- props.listRef:', props.listRef) }) // 方案二 watch(props.listRef, () => { console.log('----子组件: 手动深层:watch -- props.listRef:', props.listRef) },{deep:true}) // 方案三 watch(() => props.listRef, () => { console.log('----子组件: watch -- () => props.listRef:', props.listRef) }) // 方案四 watch(() => props.listRef, () => { console.log('----子组件: 手动深层:watch -- () => props.listRef:', props.listRef) },{deep:true})
大家猜猜各种 watch 会如何响应性?
测试结果
当父组件修改数组元素的时候,子组件的 watch 情况:
- 有响应性的
- 方案一:watch — props.listRef
- 方案二:手动深层:watch — props.listRef
- 方案四:手动深层:watch — () => props.listRef
- 没有响应性的
- 方案三:watch — () => props.listRef
当父组件替换数组的时候,子组件的 watch 情况:
- 有响应性的
- 方案三:
watch -- () => props.listRef
- 方案四:手动深层:
watch -- () => props.listRef
- 方案三:
- 没有响应性的
- 方案一:
watch -- props.listRef
- 方案二:手动深层:
watch -- props.listRef
- 方案一:
数组被替换后,再修改数组的元素,子组件的 watch 情况:
- 有响应性的
- 方案四:手动深层:
watch -- () => props.listRef
- 方案四:手动深层:
- 没有响应性的
- 方案一:
watch -- props.listRef
- 方案二:手动深层:
watch -- props.listRef
- 方案三:
watch -- () => props.listRef
- 方案一:
是不是有点乱?其实一开始我也没发现这么多种情况,因为我喜欢使用 reactive,还是前些日子有一位掘友问我,子组件的 watch 为啥没反应了?我问他具体写法,才发现这么多种情况。
最全面的还是方案四。
函数传参
写了半天才反应过来,watch 也是一个函数,响应性作为第一个参数,传递前有响应性,那么函数里面接受的是什么呢?其实挺复杂的,所以 watch 内部做了一个长串的判断。
函数的参数,就不得不说说一个很古老的问题:
- 传值:基础类型,string、number、boolean 等,对应小米
- 传地址:对象、数组、函数、正则、日期等,对应筐
码云笔记 » 探索 Vue3 的高级用法:深入理解 provide 和 inject 的依赖注入