vue3 学习笔记-响应式api

181 阅读6分钟

vue3 学习笔记-响应式api

vue3 出来好一阵子,趁这个机会赶紧学习下

配置

vue3 + vite + typescript

安装

yarn create @vitejs/app my-vue-ts-app --template vue-ts

cd my-vue-app
yarn dev

目录结构

image.png

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 文件内容不太一样,最显著的就是 **创建实例函数 **的变更。

createApp 详情地址

composition api

image.png

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

0.gif

另外你还可以在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>

0.gif

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>

0.gif

不过这也有缺点,就是编辑器中被template引用的变量不会高亮,所以要不要使用看你个人吧,接下来为了方便,我会一直使用setup script。

image.png

响应式api

ref

ref 接受参数并返回它包装在具有 value property 的对象中,然后可以使用该 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>

0.gif

上面提到了,ref 会把参数包装在具有 value 属性的对象中,我们在控制台打开看一下我们的 Jack 里面的结构长什么样

image.png
可以看到, Jack.value 是一个代理,其中的getset 都已经被 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>

0.gif

image.png
注意到,我们定义 Jack 的时候并没有 age 属性,这个属性是我们后来加上去的,但是这个属性也具备 响应式。这在vue2 中是不可思议的:
因为在 vue2 中需要调用 Vue.set,更深层的原因是 vue2new Vue()的时候对 data 的每个属性进行 Object.defineProperty 进行数据拦截,于是后面加入的属性就无法进行数据拦截
vue3 中,数据拦截的方式采用了es6中的Proxy + Reflect的方式,可以进行更加细致的操作。
proxy + reflect链接

computed

refwatch 类似,也可以使用从 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>

0.gif

看到上面说明 computedgetter 属性可以返回一个只读的响应式对象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>

0.gif

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>

0.gif

可以看到,在我还没改变响应式对象的时候,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>

0.gif

清除副作用

参考链接

vue3 中文文档

浅谈Vue3的watchEffect用途

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>

0.gif

当然,也可以同时监控多个源

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>

0.gif 在这里,可以看到,对非响应式的对象使用 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>

image.png

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>

0.gif

生命周期

image.png

在这里面大部分只是名字修改了,但是具体的触发时机没变,就不细说了。

依赖注入

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>