Composition API

501 阅读8分钟

vue2每次都把整个vue导入,例如vue2的 main.js 文件中的代码:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')

很明显项目中不可能用到Vue所有的API,因此很多模块其实是没有用的,在Vue3中,对外暴露了很多的API供开发者使用,我们可以根据自己的需求,将所需要的API从Vue中导入。例如 main.js 中的代码:

import { createApp } from 'vue';
import App from './App.vue'

createApp(App).mount('#app')

利用了 importexport 的导入导出语法,实现了按需打包模块的功能,项目打包后的文件体积明显小了很多,这也是我们本文需要对 Vue3 API 进行详细了解的原因。

1. setup

setup 函数也是 Compsition API 的入口函数,我们的变量、方法都是在该函数里定义的,来看一下使用方法:

<template>
    <div id="app">
        <p>{{ number }}</p>
        <button @click="add">增加</button>
    </div>
</template>

<script>
// 1. 从 vue 中引入 ref 函数
import {ref} from 'vue'
export default {
    name: 'App',
    setup() {
        // 2. 用 ref 函数包装一个响应式变量 number
        let number = ref(0)
        // 3. 设定一个方法
        function add() {
            // number是被ref函数包装过了的,其值保存在.value中
            number.value ++
        }
        // 4. 将 number 和 add 返回出去,供template中使用
        return {number, add}
    }
}
</script>

上述代码中用到了 ref 函数,下面会详细讲解,在这里你只需要理解它的作用是包装一个响应式的数据即可,并且你可以将 ref 函数包装过的变量看作是Vue2 data 中的变量,这样就简单实现了一个点击按钮数字加1的功能。

Vue2中,我们访问 dataprops 中的变量,都是通过类似 this.number 这样的形式去获取的,但要特别注意的是,在setup中,this 指向的是 undefined,也就是说不能再向Vue2一样通过 this 去获取变量了。

那么到底该如何获取到 props 中的数据呢?

其实 setup 函数还有两个参数,分别是 props 、context,前者存储着定义当前组件允许外界传递过来的参数名称以及对应的值;后者是一个上下文对象,能从中访问到 attr 、emit 、slots。其中 emit 就是我们熟悉的Vue2中与父组件通信的方法,可以直接拿来调用。

1.1 setup函数的第1个参数props

setup(props,context){}

第一个参数props:

  • props是一个对象,包含父组件传递给子组件的所有数据。
  • 在子组件中使用props进行接收。
  • 包含配置声明并传入的所有的属性的对象。也就是说:如果你想通过props的方式输出父组件传递给子组件的值。你需要使用props进行接收配置。即props:{......},如果你未通过props进行接收配置,则输出的值是undefined。
<template>
  <div class="box" @click="sonHander">
    我是子组件中的数据
  </div>
</template>
<script>

export default {
    //未进行接收
    // props:{
    //     mytitle:{
    //         type: Object
    //     }
    // },
    setup(props, context) {
       const sonHander =  function(){
           context.emit('sonclick', '子组件传递给父组件');
       }
      console.log(props.mytitle);
      return {sonHander}
    }
};
</script>
<template>
  <div class="box" @click="sonHander">
    我是子组件中的数据
  </div>
</template>
<script>

export default {
    //未进行接收
    // props:{
    //     mytitle:{
    //         type: Object
    //     }
    // },
    setup(props, context) {
       const sonHander =  function(){
           context.emit('sonclick', '子组件传递给父组件');
       }
      console.log(props.mytitle);//输出的值是 undefined
      return {sonHander}
    }
};
</script>
为什么通过props.mytitle输出的值是undefined呢?
因为我们没有使用props进行接收配置。即
props:{
    mytitle:{
        type:Object
    }
}
如果我们添加上接收配置,则会接收从父组件传递下来的数据

1.2 参数context的讲解

第2个参数:context,是一个对象。

  • 里面有attrs(获取当前标签上的所有属性的对象),但是该属性是props中没有声明接收的所有的对象。
  • 如果你使用props去获取值,同时props中你声明了你要获取的值,则:attrs获取的值是undefined。
  • 注意点:attrs获取值是不需要props中声明接收。第1个参数props获取值是需要props中声明接收的。
  • 里面有emit事件分发,(传递给父组件需要使用该事件)。
  • 里面有slots插槽。
<template>
  <div class="box" @click="sonHander">我是子组件中的数据</div>
</template>
<script>
export default {
  //未进行接收
  props: {
    mytitle: {
      type: Object,
    },
  },
  setup(props, context) {
    //输出title:父组件传递的值
    console.log(props.mytitle);
    //输出别人的标题:使用context获取值,不需要使用props去接收
    console.log(context.attrs.othertitle);
    //输出undefined,因为context不需要使用props去接收
    console.log(context.attrs.mytitle);
    const sonHander = function () {
      context.emit("sonclick", "子组件传递给父组件");
    };
    return { sonHander };
  },
};
</script>

1.3 子组件向父组件派发事件

<template>
  <div class="box" @click="sonHander">我是子组件中的数据</div>
</template>
<script>
export default {
  setup(props, context) {
    const sonHander = function () {
      context.emit("sonclick", "子组件传递给父组件");
    };
    return { sonHander };
  },
};
</script>

1.4 优化事件派发

我们知道第2个参数context是一个对象,并且对象中有三个属性attrs,slots,emit,在事件派发的时候,直接使用emit就ok了。

<template>
  <div class="box" @click="sonHander">我是子组件中的数据</div>
</template>
<script>
export default {
  setup(props, { emit }) {
    //直接使用emit进行事件派发
    const sonHander = function () {
      emit("sonclick", "子组件传递给父组件");
    };
    return { sonHander };
  },
};
</script>
<style>
</style>

1.5 获取父组件传递的值

我们将使用props参数获取值,以及使用attrs获取值。

<template>
  <div class="box" @click="sonHander">我是子组件中的数据</div>
  <h2>使用props声明接收:{{props.mytitle}}</h2>
  <h2>使用参数attrs获取:{{attrs.othertitle}}</h2>
</template>
<script>
export default {
  //未进行接收
  props: {
    mytitle: {
      type: Object,
    },
  },
  setup(props, {emit, attrs}) {
    const sonHander = function () {
      emit("sonclick", "子组件传递给父组件");
    };
    return { sonHander, attrs, props };
  },
};
</script>

2. 生命周期

Vue2中有 beforeCreatecreatedbeforeMountmountedbeforeUpdate 等生命周期函数,而在Vue3中,这些生命周期部分有所变化,并且调用的方式也有所改变,下面放上一张变化图来简单了解一下。

Vue2Vue3
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updated  onUpdated
beforeDestoryonBeforeUnmount
destoryedonUnmounted     

2.1 组合API的创建钩子

对于使用 组合API 的 Vue3 生命周期钩子,使用setup()方法替换beforecatecreated。这意味着,在这些方法中放入的任何代码现在都只在setup方法中。

// 组合AP
import { ref } from 'vue'

export default {
   setup() {    
     const val = ref('hello') 
     console.log('Value of val is: ' + val.value)       
     return {         
       val
     }
   }
}

2.2 beforeMount() and onBeforeMount()

在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。在选项API中,可以使用this.$els来访问。在组合API中,为了做到这一点,必须在根元素上使用ref

// 组合 API
<template>
   <div ref='root'>
     Hello World
   </div>
</template> 

import { ref, onBeforeMount } from 'vue'

export default {
   setup() {
      const root = ref(null) 
      onBeforeMount(() => {   
         console.log(root.value) 
      }) 
      return { 
         root
      }
    }
 }

因为app.$el还没有创建,所以输出将是undefined

2.3 mounted() and onMounted()

在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问。同样,在 选项API中,我们可以使用this.$el来访问我们的DOM,在组合API中,我们需要使用ref来访问Vue生命周期钩子中的DOM。

import { ref, onMounted } from 'vue'
export default {
  setup() {    /* 组合 API */
    const root = ref(null)
    onMounted(() => {
      console.log(root.value)
    })
     return {
       root
     }
   }
} 

2.4 beforeUpdate() and onBeforeUpdate()

数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。

beforeUpdate对于跟踪对组件的编辑次数,甚至跟踪创建“撤消”功能的操作很有用。

2.5 updated() and onUpdated()

DOM更新后,updated的方法即会调用。

<template>
    <div>
      <p>{{val}} | edited {{ count }} times</p>
      <button @click='val = Math.random(0, 100)'>Click to Change</button>
    </div>
 </template>
<script>
import { onBeforeUpdate,onUpdated, ref} from 'vue'
export default {
   setup () {
     const count = ref(0)
     const val = ref(0)
 
     onBeforeUpdate(() => {
       count.value++;
       console.log("beforeUpdate");
     })
 
     onUpdated(() => {
       console.log("updated() val: " + val.value)
     })
     return {
       count, val
     }
   }
};
</script>

这些方法很有用,但是对于更多场景,我们需要使用的watch方法检测这些数据更改。 watch 之所以好用,是因为它给出了更改后的数据的旧值和新值。

另一种选择是使用计算属性来基于元素更改状态。

2.6 beforeUnmount() 和 onBeforeUnmounted()

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。

在 组合API中,删除事件侦听器的示例如下所示。

import { onMounted, onBeforeUnmount } from 'vue' 
export default {
   setup () { 
     const someMethod = () => {
       // do smth
     } 
     onMounted(() => {
       console.log('mount')
       window.addEventListener('resize', someMethod);
     })
 
     onBeforeUnmount(() => {
       console.log('unmount')
       window.removeEventListener('resize', someMethod);
     })
   }
}

实际操作是在Vite,vue-cli或任何支持热重载的开发环境中,更新代码时,某些组件将自行卸载并安装。

2.7 unmounted() 和 onUnmounted()

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

import { onUnmounted } from 'vue'
export default {
  setup () { /* 组合 API */
    onUnmounted(() => {
      console.log('unmounted')
    })
  }
}

2.8 activated() and onActivated()

 keep-alive 缓存的组件激活时调用。

例如,如果我们使用keep-alive组件来管理不同的选项卡视图,每次在选项卡之间切换时,当前选项卡将运行这个 activated 钩子。

假设我们使用keep-alive包装器进行以下动态组件。

<template>
   <div>
     <span @click='tabName = "Tab1"'>Tab 1 </span>
     <span @click='tabName = "Tab2"'>Tab 2</span>
     <keep-alive>
       <component :is='tabName' class='tab-area'/>
     </keep-alive>
   </div>
</template>
<script>
import Tab1 from './Tab1.vue'
import Tab2 from './Tab2.vue'
import { ref } from 'vue'
export default {
  components: {
    Tab1,
    Tab2
  },
  setup () { /* 组合 API */
    const tabName = ref('Tab1')
    return {
      tabName
    }
  }
}
</script>

在Tab1.vue组件内部,我们可以像这样访问activated钩子。

<template>
 <div>
 <h2>Tab 1</h2>
 <input type='text' placeholder='this content will persist!'/>
 </div>
</template>
<script>
import { onActivated } from 'vue'
export default {
 setup() {
    onActivated(() => {
       console.log('Tab 1 Activated')
    })
 }
} 
</script>

2.9 deactivated() 和 onDeactivated()

被 keep-alive 缓存的组件停用时调用。

这个钩子在一些用例中很有用,比如当一个特定视图失去焦点时保存用户数据和触发动画。

import { onActivated, onDeactivated } from 'vue'

export default {
  setup() {
    onActivated(() => {
       console.log('Tab 1 Activated')
    })
    onDeactivated(() => {
       console.log('Tab 1 Deactivated')
    })
  }
}

3. reactive

reactive 方法是用来创建一个响应式的数据对象。

用法很简单,只需将数据作为参数传入即可,代码如下

<template>
    <div id="app">
    <!-- 4. 访问响应式数据对象中的 count -->
    {{ state.count }}
    </div>
</template>
<script>
    // 1. 从 vue 中导入 reactive 
    import {reactive} from 'vue'
    export default {
        name: 'App',
        setup() {
            // 2. 创建响应式的数据对象
            const state = reactive({count: 3})
            // 3. 将响应式数据对象state return 出去,供template使用
            return {state}
        }
    }
</script>    

4. ref

在介绍 setup 函数时,我们使用了 ref 函数包装了一个响应式的数据对象,这里表面上看上去跟 reactive 好像功能一模一样啊,确实差不多,因为 ref 就是通过 reactive 包装了一个对象 ,然后是将值传给该对象中的 value 属性,这也就解释了为什么每次访问时我们都需要加上 .value

我们可以简单地把 ref(obj) 理解为这个样子 reactive({value: obj})

这里我们写一段代码来具体看一下

<script>
import { ref, reactive } from "vue";
export default {
  setup() {
    const obj = { count: 3 };
    const state1 = ref(obj);
    const state2 = reactive(obj);
    console.log(state1.value);
    console.log(state2);
  },
};
</script>

注意: 这里指的 .value 是在 setup 函数中访问 ref 包装后的对象时才需要加的,在 template 模板中访问时是不需要的,因为在编译时,会自动识别其是否为 ref 包装过的 那么我们到底该如何选择 ref 和 reactive 呢?

建议:

  • 基本类型值(String 、Nmuber 、Boolean 等)或单值对象(类似像 {count: 3} 这样只有一个属性值的对象)使用 ref。
  • 引用类型值(Object 、Array)使用 reactive。

5. toRef

toRef 是将某个对象中的某个值转化为响应式数据,其接收两个参数,第一个参数为 obj 对象;第二个参数为对象中的属性名

代码如下:

<script>
// 1. 导入 toRef
import { toRef } from "vue";
export default {
  setup() {
    const obj = { count: 3 };
    // 2. 将 obj 对象中属性count的值转化为响应式数据
    const state = toRef(obj, "count");
    // 3. 将toRef包装过的数据对象返回供template使用
    return { state };
  },
};
</script>

但其实表面上看上去 toRef 这个API好像非常的没用,因为这个功能也可以用 ref 实现,代码如下

<script>
    // 1. 导入 ref
    import {ref} from 'vue'
    export default {
        setup() {
        const obj = {count: 3}
        // 2. 将 obj 对象中属性count的值转化为响应式数据
        const state = ref(obj.count)

        // 3. 将ref包装过的数据对象返回供template使用
        return {state}
    }
}
</script>

乍一看好像还真是,其实这两者是有区别的,我们可以通过一个案例来比较一下,代码如下

<template>
  <p>{{ state1 }}</p>
  <button @click="add1">增加</button>

  <p>{{ state2 }}</p>
  <button @click="add2">增加</button>
</template>

<script>
import { ref, toRef } from "vue";
export default {
  setup() {
    const obj = { count: 3 };
    const state1 = ref(obj.count);
    const state2 = toRef(obj, "count");

    function add1() {
      state1.value++;
      console.log("原始值:", obj);
      console.log("响应式数据对象:", state1);
    }

    function add2() {
      state2.value++;
      console.log("原始值:", obj);
      console.log("响应式数据对象:", state2);
    }

    return { state1, state2, add1, add2 };
  },
};
</script>    

我们分别用 ref 和 toRef 将 obj 中的 count 转化为响应式,并声明了两个方法分别使 count 值增加,每次增加后打印一下原始值 obj 和被包装过的响应式数据对象,同时还要看看视图的变化。 ref:

  • 可以看到,在对响应式数据的值进行 +1 操作后,视图改变了,原始值未改变,响应式数据对象的值也改变了,这说明 ref 是对原数据的一个拷贝,不会影响到原始值,同时响应式数据对象值改变后会同步更新视图。

toRef:

  • 可以看到,在对响应式数据的值进行 +1 操作后,视图未发生改变,原始值改变了,响应式数据对象的值也改变了,这说明 toRef 是对原数据的一个引用,会影响到原始值,但是响应式数据对象值改变后会不会更新视图。

总结:

  • ref 是对传入数据的拷贝;toRef 是对传入数据的引用
  • ref 的值改变会更新视图;toRef 的值改变不会更新视图

6. toRefs

了解完 toRef 后,就很好理解 toRefs 了,其作用就是将传入的对象里所有的属性的值都转化为响应式数据对象,该函数支持一个参数,即 obj 对象。

我们来看一下它的基本使用

<script>
// 1. 导入 toRefs
import { toRefs } from "vue";
export default {
  setup() {
    const obj = {
      name: "前端印象",
      age: 22,
      gender: 0,
    }; // 2. 将 obj 对象转化为响应式数据
    const state = toRefs(obj); // 3. 打印查看一下
    console.log(state);
  },
};
</script>

打印结果如下:

返回的是一个对象,对象里包含了每一个包装过后的响应式数据对象。

7. toRaw

toRaw 方法是用于获取 ref 或 reactive 对象的原始数据的.

先来看一段代码

<template>
  <p>{{ state.name }}</p>
  <p>{{ state.age }}</p>
  <button @click="change">改变</button>
</template>

<script>
import { reactive } from "vue";
export default {
  setup() {
    const obj = {
      name: "前端印象",
      age: 22,
    };

    const state = reactive(obj);

    function change() {
      state.age = 90;
      console.log(obj); // 打印原始数据obj
      console.log(state); // 打印 reactive对象
    }

    return { state, change };
  },
};
</script>

我们改变了 reactive 对象中的数据,于是看到原始数据 obj 和被 reactive 包装过的对象的值都发生了变化,由此我们可以看出,这两者是一个引用关系。

那么此时我们就想了,如果直接改变原始数据 obj 的值,会怎么样呢?答案是: reactive 的值也会跟着改变,但是视图不会更新。

由此可见,当我们想修改数据,但不想让视图更新时,可以选择直接修改原始数据上的值,因此需要先获取到原始数据,我们可以使用 Vue3 提供的 toRaw 方法

toRaw 接收一个参数,即 ref 对象或 reactive 对象

<script>
import { reactive, toRaw } from "vue";
export default {
  setup() {
    const obj = {
      name: "前端印象",
      age: 22,
    };

    const state = reactive(obj);
    const raw = toRaw(state);

    console.log(obj === raw); // true
  },
};
</script>

上述代码就证明了 toRaw 方法从 reactive 对象中获取到的是原始数据,因此我们就可以很方便的通过修改原始数据的值而不更新视图来做一些性能优化了。 注意: 补充一句,当 toRaw 方法接收的参数是 ref 对象时,需要加上 .value 才能获取到原始数据对象。

8. provide && inject

与 Vue2中的 provide 和 inject 作用相同,只不过在Vue3中需要手动从 vue 中导入。

这里简单说明一下这两个方法的作用:

  • provide :向子组件以及子孙组件传递数据。接收两个参数,第一个参数是 key,即数据的名称;第二个参数为 value,即数据的值
  • inject :接收父组件或祖先组件传递过来的数据。接收一个参数 key,即父组件或祖先组件传递的数据名称

假设有三个组件,分别是 A.vue 、B.vue 、C.vue,其中 B.vue 是 A.vue 的子组件,C.vue 是 B.vue 的子组件

// A.vue
<script>
import { provide } from "vue";
export default {
  setup() {
    const obj = {
      name: "前端印象",
      age: 22,
    }; // 向子组件以及子孙组件传递名为info的数据
    provide("info", obj);
  },
};
</script>

// B.vue
<script>
import { inject } from "vue";
export default {
  setup() {
    // 接收A.vue传递过来的数据
    inject("info"); // {name: '前端印象', age: 22}
  },
};
</script>

// C.vue
<script>
import { inject } from "vue";
export default {
  setup() {
    // 接收A.vue传递过来的数据
    inject("info"); // {name: '前端印象', age: 22}
  },
};
</script>

9. watch && watchEffect

watch 和 watchEffect 都是用来监视某项数据变化从而执行指定的操作的,但用法上还是有所区别。 watch:watch(source, cb, [options] )
参数说明:

  • source:可以是表达式或函数,用于指定监听的依赖对象
  • cb:依赖对象变化后执行的回调函数
  • options:可选参数,可以配置的属性有 immediate(立即触发回调函数)、deep(深度监听)

当监听 ref 类型时:

<script>
import { ref, watch } from "vue";
export default {
  setup() {
    const state = ref(0);

    watch(state, (newValue, oldValue) => {
      console.log(`原值为${oldValue}`);
      console.log(`新值为${newValue}`); /* 1秒后打印结果:
    原值为0
    新值为1
  */
    });

    // 1秒后将state值+1
    setTimeout(() => {
      state.value++;
    }, 1000);
  },
};
</script>

当监听 reactive 类型时:

<script>
import { reactive, watch } from "vue";
export default {
  setup() {
    const state = reactive({ count: 0 });
    watch(
      () => state.count,
      (newValue, oldValue) => {
        console.log(`原值为${oldValue}`);
        console.log(`新值为${newValue}`); /* 1秒后打印结果:
      原值为0
      新值为1
    */
      }
    ); // 1秒后将state.count的值+1
    setTimeout(() => {
      state.count++;
    }, 1000);
  },
};
</script>

当同时监听多个值时:

<script>
import { reactive, watch } from "vue";
export default {
  setup() {
    const state = reactive({ count: 0, name: "zs" });
    watch(
      [() => state.count, () => state.name],
      ([newCount, newName], [oldvCount, oldvName]) => {
        console.log(oldvCount); // 旧的 count 值
        console.log(newCount); // 新的 count 值
        console.log(oldName); // 旧的 name 值
        console.log(newvName); // 新的 name 值
      }
    );
    setTimeout(() => {
      state.count++;
      state.name = "ls";
    }, 1000);
  },
};
</script>

因为 watch 方法的第一个参数我们已经指定了监听的对象,因此当组件初始化时,不会执行第二个参数中的回调函数,若我们想让其初始化时就先执行一遍,可以在第三个参数对象中设置 immediate: true

watch 方法默认是渐层的监听我们指定的数据,例如如果监听的数据有多层嵌套,深层的数据变化不会触发监听的回调,若我们想要其对深层数据也进行监听,可以在第三个参数对象中设置 deep: true

补充: watch方法会返回一个stop方法,若想要停止监听,便可直接执行该stop函数

接下来再来聊聊 watchEffect,它与 watch 的区别主要有以下几点:

  • 不需要手动传入依赖
  • 每次初始化时会执行一次回调函数来自动获取依赖
  • 无法获取到原值,只能得到变化后的值 来看一下该方法如何使用:
<script>
import { reactive, watchEffect } from "vue";
export default {
  setup() {
    const state = reactive({ count: 0, name: "zs" });
    watchEffect(() => {
      console.log(state.count);
      console.log(state.name); /* 初始化时打印:
    0
    zs
    1秒后打印:
    1
    ls
  */
    });
    setTimeout(() => {
      state.count++;
      state.name = "ls";
    }, 1000);
  },
};
</script>

从上述代码中可以看出,我们并没有像 watch 方法一样先给其传入一个依赖,而是直接指定了一个回调函数。
当组件初始化时,将该回调函数执行一次,自动获取到需要检测的数据是 state.count 和 state.name。根据以上特征,我们可以自行选择使用哪一个监听器。

10. computed

computed(计算属性)传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象,使用和 vue 2.0 区别不大。

computed 默认的使用方式

<template>
  <div class="home">
    <p>
      年龄:
      <button type="button" @click="changeAge(-1)">-</button>
      {{ age }}
      <button type="button" @click="changeAge(1)">+</button>
    </p>
    <p>出生年份:{{ year }}</p>
  </div>
</template>

<script>
import { computed, ref } from "vue";
export default {
  name: "Home",
  setup() {
    const age = ref(19);
    function changeAge(value) {
      age.value += value;
    }
    const year = computed(() => {
      return 2020 - age.value;
    });

    return {
      year,
      age,
      changeAge,
    };
  },
};
</script>

computed getter 和 setter

<template>
  <div class="home">
    <p>
      年龄:
      <button type="button" @click="changeAge(-1)">-</button>
      {{ age }}
      <button type="button" @click="changeAge(1)">+</button>
    </p>
    <p>
      出生年份:
      <button type="button" @click="changeYear(-1)">-</button>
      {{ year }}
      <button type="button" @click="changeYear(1)">+</button>
    </p>
  </div>
</template>

<script>
import { computed, ref } from "vue";
export default {
  name: "Home",
  components: {},
  setup() {
    const age = ref(19);
    function changeAge(value) {
      age.value += value;
    }
    const year = computed({
      get() {
        return 2020 - age.value;
      },
      set(val) {
        age.value = 2020 - val;
      },
    });

    function changeYear(value) {
      year.value = year.value + value;
    }

    return {
      year,
      age,
      changeYear,
      changeAge,
    };
  },
};
</script>