vue3 学习笔记-响应式api
vue3 出来好一阵子,趁这个机会赶紧学习下
配置
vue3 + vite + typescript
安装
yarn create @vitejs/app my-vue-ts-app --template vue-ts
cd my-vue-app
yarn dev
目录结构
package.json
{
"name": "my-vue-ts-app",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"element-plus": "^1.0.2-beta.35",
"sass": "^1.32.8",
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.1.5",
"@vue/compiler-sfc": "^3.0.5",
"typescript": "^4.1.3",
"vite": "^2.1.0",
"vue-tsc": "^0.0.8"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}
shims-vue.d.ts
这个文件的作用主要是适配
.vue文件,告诉 ts 是vue哪种类型文件
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
对比 vue2 的main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
可以看到vue3和vue2的 main.js 文件内容不太一样,最显著的就是 **创建实例函数 **的变更。
composition api
setup
setup
为了开始使用组合式api, 我们需要一个入口来使用它,在vue中被称为
setup。
执行事件, 在创建组件之前执行
参数function setup(props: Data, context: SetupContext): Data
在setup中返回的对象的属性,可以直接在template中使用
<template>
<div class="container">
<div>fake: {{ fakeCount }} -- <button @click="fakeAdd">+</button></div>
<div>
real: {{ realCount }} --
<button @click="realAdd">+</button>
</div>
</div>
</template>
<script lang="ts">
import { ref } from "vue";
export default {
setup() {
// 普通的变量是非响应式的
let fakeCount = 0;
// 响应式的变量
const realCount = ref(0);
const fakeAdd = () => {
fakeCount++;
};
const realAdd = () => {
realCount.value++;
};
return {
fakeCount,
fakeAdd,
realCount,
realAdd,
};
},
};
</script>
result
另外你还可以在setup中返回一个渲染函数
<script lang="ts">
import { h, ref } from "vue";
export default {
setup() {
let fakeCount = 0;
const realCount = ref(0);
const fakeAdd = () => {
fakeCount++;
};
const realAdd = () => {
realCount.value++;
};
return () =>
h("div", { class: "container" }, [
h("div", {}, [
fakeCount,
" -- ",
h("button", { onClick: fakeAdd }, ["+"]),
]),
h("div", {}, [
realCount.value,
" -- ",
h("button", { onClick: realAdd }, ["+"]),
]),
]);
},
};
</script>
setup script
有没有觉得这种script中还要
export default,最后setup中还要return的写法,有些麻烦? 这时候你就可以使用setup script了。
你只需要在设置 <script setup> 就能默认将变量导出去
<template>
<div class="container">
<div>fake: {{ fakeCount }} -- <button @click="fakeAdd">+</button></div>
<div>
real: {{ realCount }} --
<button @click="realAdd">+</button>
</div>
</div>
</template>
<script setup lang="ts">
import { h, ref } from "vue";
let fakeCount = 0;
const realCount = ref(0);
const fakeAdd = () => {
fakeCount++;
};
const realAdd = () => {
realCount.value++;
};
</script>
不过这也有缺点,就是编辑器中被template引用的变量不会高亮,所以要不要使用看你个人吧,接下来为了方便,我会一直使用setup script。
响应式api
ref
ref接受参数并返回它包装在具有valueproperty 的对象中,然后可以使用该 property访问或更改响应式变量的值
前面的例子中,想必已经注意到了 ref 这个api,还有 realCount.value++ 这个用法吧。实际上,ref 不仅可以对值类型的变量使用,还可以对引用类型的变量使用。
<template>
<div class="container">
<p>name: {{ Jack.name }}</p>
<p>age: {{ Jack.age }} -- <button @click="addAge">+</button></p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const Jack = ref({
name: "Jack",
age: 18,
});
const addAge = () => {
Jack.value.age++;
};
</script>
上面提到了,ref 会把参数包装在具有 value 属性的对象中,我们在控制台打开看一下我们的 Jack 里面的结构长什么样
可以看到, Jack.value 是一个代理,其中的get, set 都已经被 trap,实现了数据的拦截
reactive
返回对象的响应式副本。响应式转换是深层的——它影响所有嵌套 property。 为什么说这个响应式转换是深层的,接下来我们来看个例子。
<template>
<div class="container">
<p>name: {{ Jack.name }}</p>
<p>age: {{ Jack.age }} -- <button @click="addAge">+</button></p>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
interface Person {
name: string;
age?: number;
}
const Jack: Person = reactive({ name: "Jack" });
const addAge = () => {
Jack.age ? Jack.age++ : (Jack.age = 10);
};
</script>
注意到,我们定义 Jack 的时候并没有 age 属性,这个属性是我们后来加上去的,但是这个属性也具备 响应式。这在vue2 中是不可思议的:
因为在 vue2 中需要调用 Vue.set,更深层的原因是 vue2 在new Vue()的时候对 data 的每个属性进行 Object.defineProperty 进行数据拦截,于是后面加入的属性就无法进行数据拦截。
在 vue3 中,数据拦截的方式采用了es6中的Proxy + Reflect的方式,可以进行更加细致的操作。
proxy + reflect链接
computed
与
ref和watch类似,也可以使用从 Vue 导入的computed函数在 Vue 组件外部创建计算属性 。
使用 getter 函数,并为从 getter 返回的值返回一个不变的响应式
ref对象 注意,没有设置set属性的时候,与vue2中的computed相同,是只读的
<template>
<div class="container">
<p>{{ count }} -- <button @click="addCount">+</button></p>
<p>{{ double }}</p>
<p>{{ doubleAndDouble }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
const count = ref(10);
const double = computed(() => count.value * 2);
const doubleAndDouble = computed(() => double.value * 2);
const addCount = () => {
count.value++;
};
</script>
看到上面说明 computed 的 getter 属性可以返回一个只读的响应式对象ref,那么自然我们会想到再给它设置一个 setter 属性,是不是就能变成可读写的ref
<template>
<div class="container">
<p>{{ count }} -- <button @click="addCount">+</button></p>
<p>{{ double }} -- <button @click="addDouble">+</button></p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
const count = ref(10);
const double = computed({
get: () => count.value * 2,
set: (val) => {
count.value = val / 2;
},
});
const addCount = () => {
count.value++;
};
const addDouble = () => {
double.value++;
};
</script>
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
回想一下在 vue2 中,我们想要设置对某个变量进行实时的跟踪打印,需要指定变量的名称,然后重复写很多的 log, 这实在是很没有效率。在 vue3 中,我们可以使用 watchEffect 来作为一个通用的侦听器。
<template>
<div class="container">
<p>{{ count }} -- <button @click="addCount">+</button></p>
<p>{{ double }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from "vue";
const count = ref(10);
const double = computed(() => count.value * 2);
watchEffect(() => {
console.log("count", count.value);
console.log("double", double.value);
});
const addCount = () => {
count.value++;
};
</script>
可以看到,在我还没改变响应式对象的时候,watchEffect 就已经执行了,这是因为 watchEffect 在收集依赖项,以便于在依赖项变动时可以触发。
停止监听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听
<script setup lang="ts">
import { computed, ref, watchEffect } from "vue";
const count = ref(10);
const double = computed(() => count.value * 2);
const stop = watchEffect(() => {
console.log("count", count.value);
console.log("double", double.value);
});
const addCount = () => {
count.value++;
// 显式的调用以停止侦听
setTimeout(() => {
stop();
});
};
</script>
清除副作用
参考链接
watch
watch 和 vue2 中的完全等效
<template>
<div class="container">
<p>{{ count }} -- <button @click="addCount">+</button></p>
<div>
<p>name: {{ Jack.name }} <button @click="changeName">change</button></p>
<p>age: {{ Jack.age }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch, watchEffect } from "vue";
const count = ref(10);
const Jack = reactive({
name: "Jack",
age: count,
});
// 侦听器 data 源可以是 ref
watch(count, (count, prevCount) => {
console.log("count 变更, ", count, prevCount);
});
// 侦听器 data 源可以是返回值的 getter 函数
watch(
() => Jack.name,
(name, prevName) => {
console.log("Jack.name 变更, ", name, prevName);
}
);
const addCount = () => {
count.value++;
};
const changeName = () => {
Jack.name += "I";
};
</script>
当然,也可以同时监控多个源
watch(
[count, () => Jack.name],
([curCount, curName], [prevCount, prevName]) => {
console.log("count 变更", curCount, prevCount);
console.log("name 变更", curName, prevName);
}
);
refs
在这里主要介绍比较常用的,除了上述的ref,还有 toRef, toRefs
toRef
可以用来为源响应式对象上的 property 性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。
<template>
<div class="container">
<p>Jack {{ ageRef1 }} <button @click="addAgeRef1">+</button></p>
<p>Amy {{ ageRef2 }} <button @click="addAgeRef2">+</button></p>
</div>
</template>
<script setup lang="ts">
import { reactive, toRef } from "vue";
const Jack = {
name: "Jack",
age: 18,
};
const Amy = reactive({
name: "Amy",
age: 20,
});
const ageRef1 = toRef(Jack, "age");
const ageRef2 = toRef(Amy, "age");
console.log(ageRef1);
console.log(ageRef2);
const addAgeRef1 = () => {
ageRef1.value++;
};
const addAgeRef2 = () => {
ageRef2.value++;
};
</script>
</script>
在这里,可以看到,对非响应式的对象使用
toRef ,并不会让其变成ref。toRef 的作用是使响应式对象的属性创建一个 ref 并将其传递出去。
toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。
<template>
<div class="container">
<p>Jack {{ ageRef }} <button @click="addAgeRef">+</button></p>
</div>
</template>
<script setup lang="ts">
import { reactive, toRef, toRefs } from "vue";
const Jack = reactive({
name: "Jack",
age: 18,
});
// 创建了一个原来属性都变成ref的对象
const refs = toRefs(Jack);
const ageRef = refs.age;
const addAgeRef = () => {
ageRef.value++;
};
</script>
解构之后属性丢失响应式
如果是习惯es6的使用者,可能会经常的对对象进行解构,这时候要注意: 对象的解构会使属性失去响应式
<script setup lang="ts">
import { reactive, toRef, toRefs } from "vue";
const Jack = reactive({
name: "Jack",
age: 18,
});
const { age } = Jack;
console.log(age); // age = 18
</script>
这时候就可以使用 toRefs
<script setup lang="ts">
import { reactive, toRef, toRefs } from "vue";
const Jack = reactive({
name: "Jack",
age: 18,
});
// const { age } = Jack;
const { age } = toRefs(Jack);
console.log(age);
</script>
readonly防止对象被修改
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的。
<template>
<div class="container">
<p>Jack.age {{ Jack.age }} <button @click="addAge">+</button></p>
</div>
</template>
<script setup lang="ts">
import { reactive, readonly, toRef, toRefs } from "vue";
const Jack = reactive({
name: "Jack",
age: 18,
});
// 将对象进行
let originJack = readonly(Jack);
const { age } = toRefs(originJack);
const addAge = () => {
age.value++;
};
</script>
生命周期
在这里面大部分只是名字修改了,但是具体的触发时机没变,就不细说了。
依赖注入
provide 和 inject 启用依赖注入。只有在使用当前活动实例的 setup() 期间才能调用这两者
provide 函数允许你通过两个参数定义 property:property 的 name ( 类型) 和 property 的 value
<template>
<div class="container">
<test-1></test-1>
</div>
</template>
<script setup lang="ts">
import { provide } from "vue";
import Test1 from "./components/test1.vue";
provide("name", "Jack");
provide("age", 18);
</script>
<style scoped lang="scss">
.container {
width: 500px;
margin: auto;
text-align: center;
}
</style>
在 setup() 中使用 inject 时,还需要从 vue 显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。
inject 函数有两个参数:要注入的 property 的名称 和 一个默认的值 (可选)
<template>
<div>
<h1>This is Test</h1>
<p>name: {{ name }}</p>
<p>age: {{ age }}</p>
</div>
</template>
<script setup lang="ts">
import { inject } from "vue";
const name = inject("name");
const age = inject("age");
</script>
<style lang="scss" scoped></style>