问题场景:我们想要组织代码的方式更加简洁。希望代码会进行如下转换,并且效果一致,同时希望我们创建的组合式函数具有更多的灵活性。现在具有优化前的代码和优化后的代码如下:
优化前
js
const isDark = useDark() // 切换主题时,isDark的value会变化
const title = useTitle('hello') // 这个函数修改document.title的值
console.log(document.title) // hello
// 一个再常见不过的处理方式。
watch(isDark, ()=>{
// title是ref
title.value = isDark.value? 'good evening' : 'good morning'
})
优化后
js
const isDark = useDark()
const title = useTitle (()=>isDark.value? 'good evening' : 'good morning')
但是问题是优化后的代码传入的是一个getter函数,这需要一些特殊的处理,而且如果是传递响应式对象属性参数,又应该怎么保持跟响应式对象之间的连接,而不因为reactive方法失去响应性呢? 所以我们这里分成两个情况讨论,一个是如何将一个函数改造成支持getter函数的组合式函数。另一个是这种编码的方式的如何迭代形成的。
问题:创建更有灵活性的组合式函数
问题场景,假如我们有些通用的工具函数,比如useDouble。函数的参数是数值类型,返回值同样也是数值类型。现在由于是一个compositionApi,我希望返回的是一个响应式对象
now:
js
function useDouble(t){
return t*2
}
const count = ref(0)
const result = computed(()=>{
return useDouble(count.value)
})
</script>
<template>
<button @click="count++">count+</button>
<h1>{{ result }}</h1>
</template>
after: 现在我们返回一个计算属性来获取响应性变量ref,但是我们需要参数必须是一个ref,如果传递的是一个原始number值,那么将会出现NaN
ts
// 现在我们返回的是一个计算属性,但是我们需要参数必须是一个ref
function useDouble(t:Ref<T>){
return computed(()=>t.value*2)
}
const count = ref(0)
const result = useDouble(count)
</script>
<template>
<button @click="count++">count+</button>
<h1>{{ result }}</h1>
</template>
我们希望我们能传递一个原始值,以增强这个函数的灵活性,所以需要再次进行一个改进
ts
//现在支持原始值传入,会返回一个计算属性ref。
function useDouble(t:Ref<T>|T){
// 这里可以替换为computed(()=>(unref(t)*2)
return computed(()=>(isRef(t)?t.value:t)*2)
}
由于我们不清楚计算属性中的t是ref还是普通的原始值类型。我们希望可以更统一的进行操作。于是我们可以将t规范化为ref
ts
//现在支持原始值传入,会返回一个计算属性ref。
function useDouble(t:Ref<T>|T){
const r = ref(t)
return computed(()=>r.value*2)
}
这样仍然有一个缺点,就是我们使用reactive的属性传入该函数的时候,由于reactive方法的局限性,我们将失去响应式对象跟返回的计算属性的连接。这样页面只有方法传递响应式对象第一次的值作为参数,获取到返回的计算属性,从而渲染出来的结果。对目前的结果来说,就是2,且无法通过button增加(失去连接)。
js
<script setup>
function useDouble(t){
const r = ref(t)
return computed(()=>r.value*2)
}
const count = reactive({data:1})
const result = useDouble(count.data)
</script>
<template>
<button @click="count.data++">count+</button>
<h1>{{ result }}</h1>
</template>
进行改造,让函数内部处理getter函数,这样能够让参数在传递reactive对象的属性的时候,也能保持连接,所以目前的useDouble函数就能支持原始值、ref、getter函数传参,跟watch函数一样。
js
<script setup>
function useDouble(t){
// const r = ref(t)
const r = typeof t === 'function' ? computed(t) : ref(t) // after
return computed(()=>r.value*2)
}
const count = reactive({data:1})
const result = useDouble(()=>count.data) // after
</script>
<template>
<button @click="count.data++">count+</button>
<h1>{{ result }}</h1>
</template>
接着我们再进行refactor,将转换的部分抽取出来,使其成为一个函数: resolveRef
js
const resolveRef = (t) => typeof t === 'function' ? computed(t) : ref(t) // after
现在我们就可以根据resolveRef创建一个ref或者computedRef,始终对依赖的响应式对象有连接。
改造完成之后的好处是这个useDouble函数的灵活性,在这样改造之后获得了很大的提升。无论是原始值,或者是ref,或者是响应式对象的属性相关的getter函数,都可以通过这个函数获取到一个ref,从而获得响应性。
编码方式的迭代
回到我们刚接触问题的时候。之前我们大概会这么进行编码,当用户切换主题时,isDark的值将发生变化。
js
const isDark = useDark() // 切换主题时,isDark的value会变化
const title = useTitle('hello') // 这个函数修改document.title的值
console.log(document.title) // hello
watch(isDark, ()=>{
// title是ref
title.value = isDark.value? 'good evening' : 'good morning'
})
那么我们从上面的代码知道,title的值需要依赖isDark的值,如果useTitle的值支持ref传参,那我们将直接通过以下这种方式进行传参,于是useTitle方法内部获取了一个计算属性,直接通过该ref的变化,可以修改传出来的title的值。
js
const isDark = useDark()
const title = useTitle(computed(()=>isDark.value? 'good evening' : 'good morning'))
缺点是我们需要手动调用computed进行包装
所以我们可以应用resolveRef来改造。于是在函数内部通过resolveRef保持了对isDark的连接。此时即使使用reactive也能够同样使用相同的编码。并且去除了computed的包装。在函数内部通过computed来收集依赖。
js
const isDark = useDark()
const title = useTitle (()=>isDark.value? 'good evening' : 'good morning')
总结
我们通过resolveRef来统一化参数,并且在方法内部对ref进行了统一的处理。增加了组合式函数的灵活性。同时使用一种新的写法,来提高开发效率,减少出错的情况。
tips 缺点则是组合式函数依赖的ref,如果使用getter的话,就无法更新了,因为会被resolveRef转换为computedRef了。