vue3的Composition API,你好!

269 阅读15分钟

mixin

如果组件之间存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取

mixin提供一种灵活的方式,来分发vue组件中可复用的功能

一个mixin对象可以包含任何组件选项(option)

局部混入

a组件

data() {
    return {
        msg: 'hhh'
    }
}

b组件

data() {
    return {
        msg: 'hhh'
    }
}

存在可复用逻辑

混入

export const a_b_minxin = {
    data() {    
        return {        
            msg: 'hhh'    
        }
    }
}

a组件、b组件可以这样写

import { a_b_minxin } from './mixins'mixin: ['a_b_minxin']

不止data选项,其它选项和生命周期也可以抽取。

全局混入

如果希望混入的对象放进所有组件呢?

使用app的api

import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)
app.mixin({
    想混入的的内容
})
app.mount('#app')

合并规则

当混入的对象中的选项和组件对象中的选项出现冲突

分情况:

  • 如果是data函数返回值对象,会保留组件自身的数据
  • 如果是生命周期钩子函数,会被合并到数组中,都会被调用
  • 其它选项,比如methods、components等等,将会合并为同一个对象,如果对象的key相同,会取组件对象的键值对

options API的弊端

同一个逻辑分散

  • 实现某个功能时,这个功能对应的代码逻辑会被拆分到各个选项
  • 当组件变得更大,更复杂时,逻辑关注点的列表会增长,同一个功能的逻辑就会被拆分的很分散

比如一个计数器功能

<h2>{{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
data() {
    return {
        counter: 0
    }
},
methods: {
    increment() {
        counter++
    },
    decrement() {
        counter--
    }
}

你看,计数器功能的逻辑被拆分到data和methods里面了。一个功能的逻辑被拆分了你可觉得没什么,但是如果多个功能的逻辑都被拆分了,可读性非常不好

如果某一天你想修改某个逻辑,那你可有的忙啦,不是吗?

setup()

如果我们能将同一个逻辑关注点相同的代码收集在一起,不是更好么?

这就是Composition API想做的事

Vue Composition API也有人叫VCA

setup()是组件的一个选项,用来替代(methods、computed、watch、data、生命周期等等)

参数

setup(props, context) {}

props

父组件传过来的属性

如果setup中需要使用,拿到props参数即可

context

也叫SetupContext,包含3个属性:

  • attrs:所有非prop的属性(如id、class等等这些属性)
  • slots:父组件传递过来的插槽(这以渲染函数返回时才有用)
  • emit:组件内部需要发射事件时会用到(因为不能访问this,所以不可以通过this.$emit发射事件)

返回值

setup既然是个函数,那它也可以有返回值。它的返回值用来做什么呢?

可以在模板template中使用

可以通过setup返回值来替代data选项

<h2>{{ msg }}</h2>
setup() {
    return {
        msg: 'hhh'
    }
}

你就会看到hhh的标题~

不可以使用this

setup并没有绑定this,this并没有指向当前组件实例(之前官网说明的原因是:执行setup之前,组件实例还没有被创建出来。这个说法是错误的,后来已修改。组件实例创建出来后,才执行setup的,但是this并没有绑定当前组件实例

setup的调用发生在data、property、computed或methods被解析之前,所以他们无法在setup中被获取

(vue2在methods、生命周期中可以拿到this,指向的是当前组件实例)

获取ref

在vue2,我们要想获取一个元素或组件的信息,是这样的

通过this.$refs

<h2 ref="h">哈哈哈</h2>
methods: {
    foo() {
        console.log(this.$refs.h)
    }
}

但是vue3的setup里面是不能通过this获取当前组件实例的,所以不能通过this.$refs获取组件或元素的信息

那怎么获取?

<h2 ref="title">哈哈哈</h2>
import { ref } from 'vue'
setup() {
    const title = ref(null)
    return {
        title
    }
}

当h2节点挂载完成,就可以通过title.value获取到h2的信息,组件亦是如此

顶层写法

后面补充~

Reactive API

响应式

在setup里面定义的变量,不是响应式的,尽管你修改该变量的值,在界面上也不会响应。

date里的变量可以做到响应式的原因是:vue内部将data里面内部的变量经过reactive() 函数的处理

其实界面上拿到的变量时reactive()处理过的返回值

<h2>{{ counter }}</h2>
<button @click="increment">+</button>
setup() {
    let counter = 100
    const increment = () => {
        counter++
    }
    return {
        counter,
        increment
    }
}

这样虽然可以在界面上显示,但是当你点击+号时,界面并没有响应counter的变化(实际上counter已经变了)

你需要将counter传进reactive函数

<h2>{{ state.counter }}</h2>
<button @click="increment">+</button>
import { reactive } from 'vue'
setup() {
    const state = reactive({
        counter: 100
    })
    const increment = () => {
        state.counter++
    }
    return {
        state,
        increment
    }
}

其它API

isProxy()

检查对象是否是由reactive或readonly创建的proxy

isReactive()

  • 检查对象是否由reactive创建的proxy
  • 如果该proxy是由readonly创建的,但是包裹了由reactive创建的另一个proxy,它也会返回true
const readonlyObj = readonly(reactiv(obj1))
isRactive(obj)// true

isReadonly()

检查对象是否是由readonly创建的proxy

toRaw()

返回reactive或readonly代理的原始对象(不建议保留对原始对象的持久引用,谨慎使用)

shallowReactive()

创建一个proxy,它跟踪其自身property的响应式,但不执行嵌套对象的深层响应式转换(深层还是原生对象)

什么意思呢?

const obj = reactive({
    name: 'zsf',
    friend: {
        name: 'hhh'
    }
})
obj.friend.name = 'lll'

reactive() 对一个对象进行响应式转换彻底的:如果该对象内部嵌套着对象(不管多少层),都会一律变成响应式

但是如果你希望只是对象最外层的属性是响应式的,就可以使用shadowReactive()啦

shallowReadonly()

创建一个proxy,使其自身的property为只读,但不执行嵌套对象的深度只读转换(深度还是可读,可写的)

类似shallowReactive() ,只有对象最外层的属性是只读

Ref API

reactive()对传入的类型是有限制的,它要求我们必须传入的是一个对象或数组类型,如果传基本数据类型(String、Number、Boolean)会报一个警告

如果你觉得要通过state.counter使用有点麻烦,只是想单纯地对counter这个基本类型实现响应式

那就使用Ref API

ref自动解包

ref()会返回一个可变的响应式对象,该对象作为一个响应式的引用维护这它内部的值

它内部的值是在ref的value属性中被维护的

<h2>{{ value }}</h2>
<button @click="increment">+</button>
import { ref } from 'vue'
setup() {
    let counter = ref(100)
    const increment = () => {
        counter.value++
    }
    return {
        counter,
        increment
    }
}

这样counter就变成了一个ref的可响应式引用

理论上使用counter.value才可以使用counter的值,但vue为了我们开发方便,它做了这么一件事:

当我们在template模板中使用ref对象,它会自动进行解包

换句话说:template模板中使用时直接counter就行

这时你可能想问:setup中可不可以也这样使用呢?

不可以。上面说到,当我们在template模板中使用ref对象,它会自动进行解包

所以这样的用法只能在template模板中使用,别的地方想拿到原本counter的值(100),就要counter.value

浅层解包

ref对象的解包是浅层的

普通对象

如果最外层包裹的是普通对象(假设叫obj,obj包裹counter),然后在template中通过obj.counter使用,是不会解包的。

<h2>{{ obj.counter }}</h2>
<button @click="increment">+</button>
import { ref } from 'vue'
setup() {
    let counter = ref(100)
    const obj = {
        counter
    }
    const increment = () => {
        counter.value++
    }
    return {
        counter,
        increment
    }
}

如果这样写,你会发现报错了。你得obj.counter.value才行

reactive可响应式对象

但是,如果最外层包裹reactive可响应式对象,那么内容的ref是可以解包的

<h2>{{ reactiveObj.counter }}</h2>
<button @click="increment">+</button>
import { ref, reactive } from 'vue'
setup() {
    let counter = ref(100)
    const reactiveObj = reactive({
        counter
    })
    const increment = () => {
        counter.value++
    }
    return {
        counter,
        reactiveObj,
        increment
    }
}

这样写是没什么问题的,但是开发中不推荐

其它API

toRefs()

如果使用es6的解构语法,对reactive() 返回的对象进行解构赋值,不再是响应式

<h2>{{ name }}-{{ age }}</h2>
<button @click="increment">+</button>
import { reactive } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 100
    })
    let { name, age } = info
    const increment = () => {
        age++
    }
    return {
        name,
        age,
        increment
    }
}

当你点击+号,你会发现屏幕上age没变(解构后不再是响应式啦)

这里的let { name, age } = info就相当于

let name = 'zsf'
let age = 100

我们已经知道,在setup中只是简单的声明变量,就不是响应式的。

但是开发中要是这么一个需求:解构后依然是响应式,怎么办?

使用toRefs()

<h2>{{ name }}-{{ age }}</h2>
<button @click="increment">+</button>
import { reactive, toRefs } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 100
    })
    let { name, age } = toRefs(info)
    const increment = () => {
        age.value++
    }
    return {
        name,
        age,
        increment
    }
}

let { name, age } = toRefs(info)做的事情是:name = ref.name, age = ref.age

相当于用name和age分别指向是响应式的ref.name、ref.age

注意:age.value才能拿到age的值,template里只使用age是因为vue自动解包了(上面讲过了)

toRefs()使info.age和ref.age建立了链接任何一个修改都会引起另外一个变化,所以上面increment函数里面的age.value换成info.age也行

当你再点击+号,屏幕上的age就会变啦

toRef()

与toRefs()不同,toRefs()是将所有的属性变成对应的ref对象

而toRef()是转换一个属性

import { reactive, toRef } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 100
    })
    let age = toRef(info, 'age')
    return {
        age
    }
}

unref()

如果想获取一个ref引用中的value,也可以通过unref()

如果参数是个ref,则返回内部值,否则返回参数本身

unref()其实是 val = isRef(val) ? val.value : val语法糖函数

isRef()

判断是否是一个ref对象

shalldowRef()

创建一个浅层的ref对象

triggerRef()

手动触发和shalldowRef() 相关联的副作用

<h2>{{ name }}</h2>
<button @click="increment">+</button>
import { shallowRef, triggerRef } from 'vue'
setup() {
    const info = shallowRef({
        name: 'zsf',
        friend: {
            name: 'hhh'
        }
    })
    const changeInfo = () => {
        info.value.name = 'lll'
        triggerRef(info)
    }
    return {
        info,
        changeInfo
    }
}

shalldow()产生了浅层ref对象这个副作用,如果想消去这个副作用,使用triggerRef()

shalldow()和triggerRef()结合使用,其实就是ref() 的功能

customRef()

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制

  • 它需要一个工厂函数,该函数接受track函数trigger函数作为参数
  • 并且返回一个带有get和set对象
function my_ref() {
    return customRef((track, trigger) => {
        return {
            get() {
                track()
                return value
            },
            set(newValue) {
                value = newValue
                trigger()
            }
        }
    })
}

一个对象,当使用时track() 收集依赖,设置时trigger() 更新

老人言

能用ref就用ref,reactive不方便代码抽离~

readonly()

通过reactive或者ref可以获取到一个响应式的对象,但是在某些情况下,可以将该响应式的对象传给其它地方使用,但是不希望被修改

vue3提供了readonly()

readonly()会返回原生对象的只读代理(依然是个Proxy,proxy的set()被劫持,且不能对其进行修改)

原理

它内部的原理大概是这样的

const obj = {
    name: 'zsf'
}
const objProxy = new Proxy(obj, {
    get(target, key) {
        return target[key]
    },
    set(taget, key, value) {
        警告:不允许修改!
    }
})
objProxy.name = 'hhh'// 警告:不允许修改!

基本使用

<button @click="update">修改</button>
import { readonly } from 'vue'
setup() {
    const obj = {
        name: 'zsf'
    }
    const proxyObj = readonly(obj)
    const update = () => {
        proxyObj.name = 'hhh' 
    }
    return {
        update
    }
}

当你点击修改,就会出现警告!

computed()

setup是如何替换掉option API中的computed的呢?

使用computed()

computed() 返回值是一个ref对象

<h2>{{ fullName }}</h2>
<button @click="changeName">改名</button>
import { ref, computed } from 'vue'
setup() {
    const firstName = ref('z')
    const lastName = ref('sf')
    cosnt fullName = computed(() => {
        return firstName.value + '' + lastName.value
    })
    const changeName = () => {
        firstName.value = 'h'
    }
    return {
        fullName,
        changeName
    }
}

watch

setup是如何替换掉option API中的watch的呢?

  • watchEffect() 自动收集响应式数据的依赖
  • watch() 需要手动指定侦听的数据源

watchEffect()

  • 参数:接收一个函数,默认立即执行一次
  • 返回值: 是个函数
<h2>{{ name }}-{{ age }}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
    const name = ref('zsf')
    const age = ref(18)
    const changeName = () => name.value = 'hhh'
    const changeAge = () => age.value++
    watchEffect(() => {
        console.log(name.vlaue, age.value)
    })
    return {
        name,
        age,
        changeName,
        changeAge
    }
}

当你点击这两个按钮,使收集的依赖发生变化,会执行watchEffect()

原理

默认立即执行一次的过程中,它会收集用了哪些可响应式的对象(也就是收集依赖

只有收集的依赖发生变化时,watchEffect() 传入的函数才会再次执行

停止侦听

如果想在某些情况下停止侦听呢?

比如上面的例子,当年龄到25不再侦听,可以使用watchEffect()的返回值函数,调用它即可

<h2>{{ age }}</h2>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
    const age = ref(18)
    const changeAge = () => {
        age.value++
        if(age.value > 25) {
            stop()
        }
    }
    const stop = watchEffect(() => {
        console.log(name.vlaue, age.value)
    })
    return {
        age,
        changeAge
    }
}

清除副作用

什么是清除副作用呢?

在开发中我们需要在侦听函数中发起网络请求,但在网络请求还没到达时,停止了侦听器,或者侦听器侦听函数再次被执行了

那么上一次的网络请求应该被取消掉,这个时候就可以清除上一次的副作用

watchEffect第一个参数接收一个函数(假设叫foo),而foo的参数,又接收一个函数foo1,取消操作一般foo1里面执行

<h2>{{ age }}</h2>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
    const age = ref(18)
    const changeAge = () => {
        age.value++
    }
    const stop = watchEffect((foo) => {
        foo(() => {
            // 取消操作
        })
        console.log(name.vlaue, age.value)
    })
    return {
        age,
        changeAge
    }
}

执行时机

<h2 ref="title">哈哈哈</h2>
import { ref } from 'vue'
setup() {
    const title = ref(null)
    return {
        title
    }
}

当h2节点挂载完成,就可以通过title.value获取到h2的信息,组件亦是如此

但是,setup执行发生在节点挂载之前,现在的title.value是null

要是想拿到title.value的值呢?

使用watchEffect()的第二个参数,等节点挂载完成再执行watchEffect()的回调。这样,title.value就有值啦

import { ref, watchEffect } from 'vue'
setup() {
    const title = ref(null)
    watchEffect(() => {
        console.log(title.value)
    }, {
        flush: 'post'
    })
    return {
        title
    }
}

watch()

与watchEffect相比,watch允许我们:

  • 惰性执行副作用(第一次不会直接执行);
  • 具体说明当哪些状态发生改变时,才触发侦听器的执行
  • 可以访问侦听器变化前后的值

侦听单个数据源

参数1-数据源,有两种类型:

  • 一个getter函数,但是该函数必须引用一个reactive或ref对象
  • 一个可响应式对象,ref或者是reactive对象(常用的是ref对象)

参数2-新旧值

传入getter函数

<h2>{{ info.name }}</h2>
<button @click="changeName">修改name</button>
import { reactive, watch } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 18
    })
    watch(() => info.name, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        info.name = 'hhh'
    }
    return {
        changeName,
        info
    }
}

传入可响应式对象

import { reactive, watch } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 18
    })
    watch(info, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        info.name = 'hhh'
    }
    return {
        changeName
    }
}

传入reactive对象的话,oldValue和newValue拿到的值是reactive创建出来的Proxy对象的value

传入ref对象的话,oldValue和newValue拿到的值才是value值的本身(原生对象的value)

import { ref, watch } from 'vue'
setup() {
    const name = ref('zsf')
    watch(name, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        name.value = 'hhh'
    }
    return {
        changeName
    }
}

如果你想让传入reactive对象时,oldValue和newValue也是普通对象的值

你可以对info解构(变成普通对象了),然后再return,变成一个getter函数

import { reactive, watch } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 18
    })
    watch(() => {
        return {...info}
    }, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        info.name = 'hhh'
    }
    return {
        changeName
    }
}

侦听多个数据源

import { ref, reactive, watch } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 18
    })
    const name = ref('zsf')
    watch([info, name], (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        info.name = 'hhh'
    }
    return {
        changeName
    }
}

这样oldValue和newValue分别就会多个值而已

深度侦听

默认

将可响应式对象解构之后不是深度侦听了

import { reactive, watch } from 'vue'
setup() {
    const info = reactive({
        name: 'zsf',
        age: 18
    })
    watch(() => {
        return {...info}
    }, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    })
    const changeName = () => {
        info.name = 'hhh'
    }
    return {
        changeName
    }
}

要想解构之后继续深度侦听,得传第三个参数

watch(() => {
        return {...info}
    }, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    }, {
    deep: true
})

立即执行

如果想一开始就执行一次侦听器

同样在第三个参数,immediate: true就行

watch(() => {
        return {...info}
    }, (newValue, oldValue) => {
        console.log(oldValue, newValue)
    }, {
    deep: true,
    immediate: true
})

生命周期钩子

setup是如何替换声明周期钩子的呢?

通过onX() 这种API,其中X就是updated、mounted等等这些生命周期

import { onCreated, onUpdated, onMounted } from 'vue'
setup() {
    onUpdated(() => {
        console.log('updated')
    })
    其它同理
}

注意:

  • created和beforeCreated没有对应的API,如果想在这两个生命周期执行回调函数,直接放setup里即可;
  • 同一个生命周期可以出现多次

provide/inject

使用

父组件

import { provide } from 'vue'
setup() {
    const name = 'zsf'
    let counter = 100
    provide('name', name)
    provide('counter', counter)
}

子孙组件

<h2>{{ name }}-{{ counter }}</h2>
import { inject } from 'vue'
setup() {
    const name = inject('name')
    const counter = inject('counter')
    return {
        name,
        counter
    }
}

用法其实和vue2差不多,不过是通过api的方式在setup里面执行

搭配readonly

当父组件传过来的是可响应式对象,那子孙组件改变该对象,父组件也会受影响,这不符合单向数据流

所以父组件在提供数据时,应该限制只读

这不就是readonly() 的发挥作用的时候了嘛~

import { ref, provide, readonly } from 'vue'
setup() {
    const name = ref('zsf')
    let counter = ref(100)
    provide('name', readonly(name))
    provide('counter', readonly(counter))
}

如果父组件的某个数据确实需要改,子孙组件应该是通过发射事件的形式,通知父组件去修改

hook案例

计数器

vue2写法

<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
data() {
    return {
        counter: 0
    }
},
computed: {
  doubleCounter() {
      return this.counter * 2
  }  
},
methods: {
    increment() {
        this.counter++
    },
    decrement() {
        this.counter--
    }
}

计数器逻辑被分散到data、methods、computed里面了

想利用mixin复用这逻辑时,是不方便的,而且也有可能出现命名冲突的问题

vue3写法

Home.vue

<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
import { ref, computed } from 'vue'
setup() {
    const counter = ref(0)
    const doubleCounter = computed(() => counter.value * 2)
    const increment = () => counter.value++
    const decrement = () => counter.value--
    return {
        counter,
        doubleCounter,
        increment,
        decrement
    }
}

你看,同一个逻辑的代码都聚集在一起,维护起来不是方便许多吗?

还可以将这部分抽离出来

建一个hook目录,目录下新建一个useCounter.js

import { ref, computed } from 'vue'
export default function() {
    const counter = ref(0)
    const doubleCounter = computed(() => counter.value * 2)
    const increment = () => counter.value++
    const decrement = () => counter.value--
    return {
        counter,
        doubleCounter,
        increment,
        decrement
    }
}

Home.vue就可以这样写了

<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
import useCounter from './hook/useCounter.js'

setup() {
    const { counter, doubleCounter, increment,decrement } = useCounter()
    return {
        counter,
        doubleCounter,
        increment,
        decrement
    }
}

这样,一个逻辑的代码都聚集在一起,可读性好,而且,要是想复用useCounter() 不是很方便了吗?

修改网页标签页名字

需求

  • 参数 网页名
  • 返回值 网页名的ref对象,当网页名再次修改时,document.title = 网页名重新执行

Home.vue

import useTitle from './hook/useTitle.js'

setup() {
    const titleRef = useTitle('zsf')
    setTimeout(() => {
        titleRef.value = 'hhh'
    }, 2000)
}

useTitle.js

import { ref, watch } from 'vue'
export default function(title = 'default') {
    const titleRef = ref(title)
    watch(titleRef, (newValue) => {
        document.title = newValue
    }, {
        immediate: true
    })
}
return titleRef

这样你会发现,一开始网页名为zsf,2秒后变成hhh

监听滚动位置

需求

  • 右下角实时显示滚动位置

Home.vue

<div class="scroll">
    <div class="scroll-x">scrollX:{{ scrollX }}</div>
    <div class="scroll-y">scrollY:{{ scrollY }}</div>
</div>
import useScrollPosition from './hook/useScrollPosition.js'

setup() {
    const { scrollX, scrollY } = useScrollPosition()
    return {
        scrollX,
        scrollY
    }
}
.scroll {
    position: fixed;
    right: 30px;
    bottom: 30px;
}

为了能滚动,尽量让容器宽高超过设备,这里就不写了~

useScrollPosition.js

import { ref } from 'vue'
export default function() {
    const scrollX = ref(0)
    const scrollY = ref(0)
    document.addEventListener('scroll', () => {
        scrollX.value = window.scrollX
        scrollY.value = window.scrollY
    })
    return {
        scrollX,
        scrollY
    }
}

使用缓存

需求

  • 只传key,取value
  • 传key-vlaue,保存
  • 当value变化,重新执行保存

useLocalStorage.js

import { ref, watch } from 'vue'
export default function(key, value) {
    const data = ref(value)
    // 传key-value,保存到缓存;只传key,取value
    if(value) {
        window.localStorage.setItem(key, JSON.stringify(value))
    } else {
        data.value = JSON.parse(window.localStorage.getItem(key))
    }
    // 当value变化,重新执行保存到缓存
    watch(data, (newValue) => {
        window.localStorage.setItem(key, JSON.stringify(newValue))
    })
    
    return data
}

优秀的编码习惯

Home.vue

import useScrollPosition from './hook/useScrollPosition.js'
import useTitle from './hook/useTitle.js'
import useCounter from './hook/useCounter.js'
setup() {
    ...
}

当需要导入的模块很多时,可能可读性不够好

这时需要在hook目录下新建一个index.js(统一的导出出口)

index.js

import useScrollPosition from './useScrollPosition.js'
import useTitle from './useTitle.js'
import useCounter from './useCounter.js'
export {
	useScrollPosition,
    useTitle,
    useCounter
}

Home.vue就可以这样写了

import {
    useScrollPosition,
    useTitle,
    useCounter
} from './hook'

利用webpack对路径的解析特点(导入省略目录下的index.js),并且某种程度抽取了路径(现在在Home.vue只要写一份),代码看起来更简洁~