Vue3 Composition API

什么是组合式 API?

https://www.vue3js.cn/docs/zh/guide/composition-api-introduction.html

通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要。

假设在我们的应用程序中,我们有一个视图来显示某个用户的仓库列表。除此之外,我们还希望应用搜索和筛选功能。处理此视图的组件可能如下所示:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { type: String }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

该组件有以下几个职责:

  1. 从假定的外部 API 获取该用户名的仓库,并在用户更改时刷新它
  2. 使用 searchQuery 字符串搜索存储库
  3. 使用 filters 对象筛选仓库

用组件的选项 (datacomputedmethodswatch) 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。

这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。而这正是组合式 API 使我们能够做到的。

为什么需要Vue对组合API

随着Vue的日益普及,人们也开始在大型和企业级应用程序中采用Vue。 由于这种情况,在很多情况下,此类应用程序的组件会随着时间的推移而逐渐增长,并且有时使用单文件组件人们很难阅读和维护。 因此,需要以逻辑方式制动组件,而使用Vue的现有API则不可能。

代替添加另一个新概念,提议使用Composition API,该API基本将Vue的核心功能(如创建响应式数据)公开为独立功能,并且该API有助于在多个组件之间编写简洁且可复用的代码。

Vue已有的替代方案缺点是什么?

在引入新的API之前,Vue还有其他替代方案,它们提供了诸如mixin,HOC(高阶组件),作用域插槽之类的组件之间的可复用性,但是所有方法都有其自身的缺点,由于它们未被广泛使用。

  1. Mixins:一旦应用程序包含一定数量的mixins,就很难维护。 开发人员需要访问每个mixin,以查看数据来自哪个mixin。
  2. HOC:这种模式不适用于.vue 单文件组件,因此在Vue开发人员中不被广泛推荐或流行。
  3. 作用域插槽一通用的内容会进行封装到组件中,但是开发人员最终拥有许多不可复用的内容,并在组件模板中放置了越来越多的插槽,导致数据来源不明确。

简而言之,组合API有助于

  1. 由于API是基于函数的,因此可以有效地组织和编写可重用的代码。
  2. 通过将共享逻辑分离为功能来提高代码的可读性。
  3. 实现代码分离。
  4. 在Vue应用程序中更好地使用TypeScript。

组合API(常用部分)

setup

  • 新的 option, 所有的组合 API 函数都在此使用,只在初始化时执行一次
  • 函数如果返回对象,对象中的属性或方法,模板中可以直接使用
<template>
    <div>哈哈 我又变帅了</div>
    <h1>{{number}}</h1>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const number = Number.MAX_SAFE_INTEGER;

    return {
      number,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ref

作用:定义一个数据的响应式

语法:const xxx = ref(initValue):

  • 创建一个包含响应式数据的引用(reference)对象
  • js/ts 中操作数据:xxx.value
  • HTML模板中操作数据:不需要.value

一般用来定义一个基本类型的响应式数据

Vue2写法

<template>
  <h2>setup和ref的基本使用</h2>
  <h2>{{ count }}</h2>
  <hr/>
  <button @click="update">更新</button>
</template>

<script lang="js">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    update() {
      this.count += 1;
    },
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Vue3写法实现同一功能

<template>
  <h2>setup和ref的基本使用</h2>
  <h2>{{ count }}</h2>
  <hr/>
  <button @click="update">更新</button>
</template>

<script lang="js">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 变量,此时的数据不是响应式的数据
    // let count = 0;
    // ref是一个函数,定义的是一个响应式的数据,返回的是一个一个包含响应式数据的**引用(reference)对象**
    // 对象中有一个value属性,如果需要对数据进行操作,需要使用该ref对象调用value属性的方式进行数据的操作
    // html模板中是不需要.value的
    const count = ref(0);
    console.log(count);

    // 方法
    function update() {
      // count++;
      count.value += 1;
    }

    // 返回的是一个对象
    return {
      count,
      update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

reactive

作用:定义多个数据的响应式

const proxy = reactive(obj):接收一个普通对象然后返回该普通对象的响应式代理器对象

响应式转换是“深层的”:会影响对象内部所有嵌套的属性

内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的

<template>
  <h2>reactive的使用</h2>
  <h3>名字:{{user.name}}</h3>
  <h3>年龄:{{user.age}}</h3>
  <h3>媳妇:{{user.wife}}</h3>
  <h3>性别:{{user.gender}}</h3>

  <hr/>
  <button @click="updateUser">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:显示用户的相关信息,点击按钮,可以更新用户的相关信息数据
  setup() {
    // 把数据变成响应式的数据
    // 返回的是一个Proxy(代理对象),被代理的是里面的obj对象
    // user现在是代理对象,obj是目标对象
    const obj:any = {
      name: '小明',
      age: 20,
      wife: {
        name: '小甜甜',
        age: 18,
        car: ['斯堪尼亚', '奔驰', 'DAF'],
      },
    };
    const user = reactive<any>(obj);

    const updateUser = () => {
      user.name = '小红';
      user.gender = '男';
    };

    return {
      user,
      updateUser,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

比较 Vue2 与 Vue3 的响应式

Vue2 的响应式

核心:

  • 对象: 通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截)
  • 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
  get() {},
  set() {}
})

问题

  • 对象直接新添加的属性或删除已有属性,界面不会自动更新
  • 直接通过下标替换元素或更新 length,界面不会自动更新 arr[1] = {}

Vue3 的响应式

核心:

new Proxy(data, {
  // 拦截读取属性值
  get(target, prop) {
    return Reflect.get(target, prop);
  },
  // 拦截设置属性值或添加新属性
  set(target, prop, value) {
    return Reflect.set(target, prop, value);
  },
  // 拦截删除属性
  deleteProperty(target, prop) {
    return Reflect.deleteProperty(target, prop);
  },
});

proxy.name = 'tom';
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  const user = {
    name: 'John',
    age: 12,
  };

  // proxyUser是代理对象, user是被代理对象
  // 后面所有的操作都是通过代理对象来操作被代理对象内部属性
  const proxyUser = new Proxy(user, {
    get(target, prop) {
      console.log('劫持get()', prop);
      return Reflect.get(target, prop);
    },

    set(target, prop, val) {
      console.log('劫持set()', prop, val);
      return Reflect.set(target, prop, val); // (2)
    },

    deleteProperty(target, prop) {
      console.log('劫持delete属性', prop);
      return Reflect.deleteProperty(target, prop);
    },
  });
  // 读取属性值
  console.log(proxyUser === user);
  console.log(proxyUser.name, proxyUser.age);
  // 设置属性值
  proxyUser.name = 'bob';
  proxyUser.age = 13;
  console.log(user);
  // 添加属性
  proxyUser.sex = '男';
  console.log(user);
  // 删除属性
  delete proxyUser.sex;
  console.log(user);
</script>
</body>
</html>

setup 细节

setup 执行的时机

在 beforeCreate 之前执行(一次),此时组件对象还没有创建

this 是 undefined,不能通过 this 来访问 data/computed/methods / props

其实所有的 composition API 相关回调函数中也都不可以

setup 的返回值

一般都返回一个对象:为模板提供数据,也就是模板中可以直接使用此对象中的所有属性/方法

返回对象中的属性会与 data 函数返回对象的属性合并成为组件对象的属性

返回对象中的方法会与 methods 中的方法合并成功组件对象的方法

如果有重名,setup 优先

注意:

  • 一般不要混合使用:methods 中可以访问 setup 提供的属性和方法, 但在 setup 方法中不能访问 data 和 methods
  • setup 不能是一个 async 函数:因为返回值不再是 return 的对象,而是 promise, 模板看不到 return 对象中的属性数据

setup 的参数

  • setup(props, context) / setup(props, {attrs, slots, emit})
  • props:包含 props 配置声明且传入了的所有属性的对象
  • attrs:包含没有在 props 配置中声明的属性的对象,相当于 this.$attrs
  • slots:包含所有传入的插槽内容的对象,相当于 this.$slots
  • emit:、用来分发自定义事件的函数,相当于 this.$emit
<template>
  <h2>App父级组件</h2>
  <div>msg: {{ msg }}</div>
  <button @click="msg += '====='">更新数据</button>
  <hr>
  <child :msg="msg" msg2="真香" @xxx="xxx"/>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const msg = ref('what are you nong sha lei');

    function xxx(text: string) {
      console.log('=============xxx in');
      msg.value += text;
    }

    return {
      msg,
      xxx,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子级组件</h2>
  <div>msg: {{ msg }}</div>
  <div>count: {{ count }}</div>
  <button @click="emitXxx">分发事件</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Child',
  props: ['msg'],
  // 数据初始化周期的回调
  // beforeCreate() {
  //   console.log('beforeCreate执行了');
  // },
  // mounted() {
  //   console.log('mounted执行了');
  //   console.log(this);
  // },
  // setup细节问题
  // setup是在beforeCreate生命周期回调之前就执行了,而且就执行一次
  // 由此可以推断出:setup在执行的时候,当前的组件还没被创建出来,也就意味着:组件实例对象this根本就不能用

  // setup中的对象内部的属性和data函数中的return对象的属性都可以在html模板中使用
  // setup中的对象中的属性和data函数中的属性会合并为组件对象的属性
  // setup和methods中的方法会合并为组件对象的方法
  // 在vue3中尽量不要混合使用data和setup及methods和setup
  // setup 不能是一个 async 函数:因为返回值不再是 return 的对象,而是 promise, 模板看不到 return 对象中的属性数据
  setup(props, context) {
    console.log('setup执行了', this);
    // props参数是一个对象,里面有服及组件向自己组建传递的数据,并且是在子级组件中使用props接收到的所有属性
    // 包含props配置声明且传入的所有属性的对象
    console.log(props);
    // context参数,是一个对象,里面有attrs对象(获取当前组件上的属性,但是该属性实在props中没有声明接受的对象),
    // emit方法(分发事件的),slots对象(插槽)
    console.log(context.attrs);
    // const showMsg1 = () => {
    //   console.log('setup中的showMsg1方法');
    // };

    function emitXxx() {
      console.log('=============emitXxx in');
      context.emit('xxx', '++');
    }

    return {
      // setup一般返回一个对象,对象中的属性和方法可以在html模板中直接使用
      // showMsg1,
      emitXxx,
    };
  },
  // data() {
  //   return {
  //     count: 10,
  //   };
  // },
  // methods: {
  //   showMsg() {
  //     console.log('methods中的showMsg方法');
  //   },
  // },
});
</script>

<style scoped>

</style>

reactive 与 ref细节

是 Vue3 的 composition API 中 2 个最重要的响应式 API

ref 用来处理基本类型数据,reactive 用来处理对象(递归深度响应式)

如果用 ref 对象/数组,内部会自动将对象/数组转换为 reactive 的代理对象

ref 内部:通过给 value 属性添加 getter/setter 来实现对数据的劫持

reactive 内部:通过使用 Proxy 来实现对对象内部所有数据的劫持,并通过 Reflect 操作对象内部数据

ref 的数据操作:在 js 中要.value,在模板中不需要(内部解析模板时会自动添加.value)

<template>
  <h2>ref和reactive更新数据的问题</h2>
  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <hr>
  <button @click="update">更新数据</button>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';

export default defineComponent({
  name: 'App',
  // 是 Vue3 的 composition API 中 2 个最重要的响应式 API
  // ref 用来处理基本类型数据, reactive 用来处理对象(递归深度响应式)
  // 如果用 ref 对象/数组, 内部会自动将对象/数组转换为 reactive 的代理对象
  // ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持
  // reactive 内部: 通过使用 Proxy 来实现对对象内部所有数据的劫持, 并通过 Reflect 操作对象内部数据
  // ref 的数据操作: 在 js 中要.value, 在模板中不需要(内部解析模板时会自动添加.value)
  setup() {
    const m1 = ref('abc');
    const m2 = reactive({
      name: '小明',
      wife: {
        name: '小红',
      },
    });
    const m3 = ref({
      name: '小明',
      wife: {
        name: '小红',
      },
    });
    const update = () => {
      console.log(m3);
      m1.value += '===';
      m2.wife.name += '===';
      m3.value.wife.name += '===';
    };
    return {
      m1, m2, m3, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

计算属性与监视

computed 函数:

  • 与 computed 配置功能一致
  • 只有 getter
  • 有 getter 和 setter

watch 函数

  • 与 watch 配置功能一致
  • 监视指定的一个或多个响应式数据,一旦数据变化,就自动执行监视回调
  • 默认初始时不执行回调,但可以通过配置 immediate 为 true,来指定初始时立即执行第一次
  • 通过配置 deep 为 true,来指定深度监视

watchEffect 函数

  • 不用直接指定要监视的数据,回调函数中使用的哪些响应式数据就监视哪些响应式数据
  • 默认初始时就会执行第一次,从而可以收集需要监视的数据
  • 监视数据发生变化时回调
<template>
  <h2>计算属性和监视</h2>
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:<input type="text" placeholder="请输入姓氏" v-model="user.firstName"> <br>
    名字:<input type="text" placeholder="请输入名字" v-model="user.lastName"> <br>
  </fieldset>
  <fieldset>
    <legend>计算属性和监视的演示</legend>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName1"> <br>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName2"> <br>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName3"> <br>
  </fieldset>
</template>

<script lang="ts">
import {
  computed, defineComponent, reactive, ref, watchEffect,
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 定义一个响应式的对象
    const user = reactive({
      firstName: '东方',
      lastName: '不败',
    });

    // 通过计算属性的方式,实现第一个姓名的显示
    // vue3的计算属性
    // 计算属性中的函数中如果只传入一个回调函数,表示的是get

    // 第一个姓名
    const fullName1 = computed(() => `${user.firstName}_${user.lastName}`);

    // 第二个姓名
    const fullName2 = computed({
      get() {
        return `${user.firstName}_${user.lastName}`;
      },
      set(val:string) {
        const names:Array = val.split('_');
        // eslint-disable-next-line prefer-destructuring
        user.firstName = names[0];
        // eslint-disable-next-line prefer-destructuring
        user.lastName = names[1];
      },
    });

    // 监视指定的数据
    const fullName3 = ref('');
    // watch(user, ({ firstName, lastName }) => {
    //   fullName3.value = `${firstName}_${lastName}`;
    // }, { immediate: true, deep: true });

    // 监视,不需要配置immediate,本身默认就会进行监视,(默认执行一次)
    watchEffect(() => {
      fullName3.value = `${user.firstName}_${user.lastName}`;
    });
    return {
      user,
      fullName1,
      fullName2,
      fullName3,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>计算属性和监视</h2>
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:<input type="text" placeholder="请输入姓氏" v-model="user.firstName"> <br>
    名字:<input type="text" placeholder="请输入名字" v-model="user.lastName"> <br>
  </fieldset>
  <fieldset>
    <legend>计算属性和监视的演示</legend>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName1"> <br>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName2"> <br>
    姓名:<input type="text" placeholder="显示姓名" v-model="fullName3"> <br>
  </fieldset>
</template>

<script lang="ts">
import {
  computed, defineComponent, reactive, ref, watch, watchEffect,
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    // 定义一个响应式的对象
    const user = reactive({
      firstName: '东方',
      lastName: '不败',
    });

    // 通过计算属性的方式,实现第一个姓名的显示
    // vue3的计算属性
    // 计算属性中的函数中如果只传入一个回调函数,表示的是get

    // 第一个姓名
    const fullName1 = computed(() => `${user.firstName}_${user.lastName}`);

    // 第二个姓名
    const fullName2 = computed({
      get() {
        return `${user.firstName}_${user.lastName}`;
      },
      set(val:string) {
        const [firstName, lastName] = val.split('_');
        user.firstName = firstName;
        user.lastName = lastName;
      },
    });

    // 监视指定的数据
    const fullName3 = ref('');
    watch(user, ({ firstName, lastName }) => {
      fullName3.value = `${firstName}_${lastName}`;
    }, { immediate: true, deep: true });

    // 监视,不需要配置immediate,本身默认就会进行监视,(默认执行一次)
    // watchEffect(() => {
    //   fullName3.value = `${user.firstName}_${user.lastName}`;
    // });

    // 监视fullName3的数据,改变firstName和lastName
    watchEffect(() => {
      const [firstName, lastName] = fullName3.value.split('_');
      user.firstName = firstName;
      user.lastName = lastName;
    });

    // watch---可以监控多个数据的
    // 当我们使用watch监控非响应式的数据是,代码需要改一下
    watch([() => user.firstName, () => user.lastName, fullName3], () => {
      console.log('===============');
    });

    return {
      user,
      fullName1,
      fullName2,
      fullName3,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

生命周期

Vue2.x 的生命周期

lifecycle_2

与 2.x 版本生命周期相对应的组合式 API

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

新增的钩子函数

组合式 API 还提供了以下调试钩子函数:

  • onRenderTracked
  • onRenderTriggered
<template>
  <h2>App父级组件</h2>
  <button @click="isShow = !isShow">切换显示</button>
  <hr>
  <child v-if="isShow" />
</template>

<script lang="ts">
import {
  defineComponent, ref,
} from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const isShow = ref(true);

    return {
      isShow,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子级组件</h2>
  <h4>msg: {{ msg }}</h4>
  <hr>
  <button @click="update">更新数据</button>
</template>

<script lang="ts">
import {
  defineComponent,
  onBeforeMount,
  onBeforeUnmount,
  onBeforeUpdate,
  onMounted,
  onUnmounted,
  onUpdated,
  ref,
} from 'vue';

export default defineComponent({
  name: 'Child',
  // vue2.x的生命周期钩子
  beforeCreate() {
    console.log('2.x中的 beforeCreate');
  },
  created() {
    console.log('2.x中的 created');
  },
  beforeMount() {
    console.log('2.x中的 beforeMount');
  },
  mounted() {
    console.log('2.x中的 mounted');
  },
  beforeUpdate() {
    console.log('2.x中的 beforeUpdate');
  },
  updated() {
    console.log('2.x中的 updated');
  },
  beforeUnmount() {
    console.log('2.x中的 beforeUnmount');
  },
  unmounted() {
    console.log('2.x中的 unmounted');
  },
  setup() {
    console.log('3.0中的 setup');
    const msg = ref('msg');

    const update = () => {
      msg.value += '===';
    };

    onBeforeMount(() => {
      console.log('3.0中的 onBeforeMount');
    });

    onMounted(() => {
      console.log('3.0中的 onMounted');
    });

    onBeforeUpdate(() => {
      console.log('3.0中的 onBeforeUpdate');
    });

    onUpdated(() => {
      console.log('3.0中的 onUpdated');
    });

    onBeforeUnmount(() => {
      console.log('3.0中的 onBeforeUnmount');
    });

    onUnmounted(() => {
      console.log('3.0中的 onUnmounted');
    });

    return {
      msg, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

自定义 hook 函数

  • 使用 Vue3 的组合 API 封装的可复用的功能函数
  • 自定义 hook 的作用类似于 vue2 中的 mixin 技术
  • 自定义 Hook 的优势:很清楚复用功能代码的来源, 更清楚易懂

需求1

收集用户鼠标点击的页面坐标

<template>
  <h2>自定义hook函数操作</h2>
  <h2>x:{{ x }}, y: {{ y }}</h2>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import useMousePosition from '@/hooks/useMousePosition';

export default defineComponent({
  name: 'App',
  // 需求1:用户在页面中点击页面,把点击位置的横纵坐标展示出来
  setup() {
    const { x, y } = useMousePosition();
    return {
      x, y,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
import { onBeforeUnmount, onMounted, ref } from 'vue';

export default function () {
  const x = ref(-1);
  const y = ref(-1);

  // 点击事件的回调函数
  const clickHandler = (event:MouseEvent) => {
    x.value = event.pageX;
    y.value = event.pageY;
  };

  // 页面接在完毕了,再进行点击的操作
  // 页面接在完毕的生命周期
  onMounted(() => {
    window.addEventListener('click', clickHandler);
  });

  onBeforeUnmount(() => {
    window.removeEventListener('click', clickHandler);
  });

  return {
    x, y,
  };
}

需求2

封装发 ajax 请求的 hook 函数

利用 TS 泛型强化类型检查

<template>
  <h2>自定义hook函数操作</h2>
  <h2>x:{{ x }}, y: {{ y }}</h2>
  <hr>
  <h3 v-if="loading">正在加载中</h3>
  <h3 v-else-if="errorMsg">错误信息:{{ errorMsg }}</h3>
  <ul v-else v-for="(item, index) in data" :key="index">
    <li>firstName: {{ item.firstName }}</li>
    <li>lastName: {{ item.lastName }}</li>
    <li>email: {{ item.email }}</li>
  </ul>
</template>

<script lang="ts">
import { defineComponent, watch } from 'vue';
import useMousePosition from '@/hooks/useMousePosition';
import useRequest from '@/hooks/useRequest';

interface IProgrammerData {
  firstName: string,
  lastName: string,
  email: string,
}

export default defineComponent({
  name: 'App',
  // 需求1:用户在页面中点击页面,把点击位置的横纵坐标展示出来
  setup() {
    const { x, y } = useMousePosition();

    const { loading, data, errorMsg } = useRequest<IProgrammerData[]>('/data/example.json');

    watch(data, () => {
      if (data.value) {
        console.log(data.value.length);
      }
    });

    return {
      x, y, loading, data, errorMsg,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
// 引入axios
// 发送ajax请求
import { ref } from 'vue';
import axios from 'axios';

export default function <T> (url: string) {
  // 加载的状态
  const loading = ref(true);
  const data = ref<T | null>(null);
  const errorMsg = ref('');

  axios.get(url)
    .then((response) => {
      loading.value = false;
      data.value = response.data;
    })
    .catch((error) => {
      loading.value = false;
      errorMsg.value = error.message || '未知错误';
    });

  return {
    loading,
    data,
    errorMsg,
  };
}

toRefs

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

应用:当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用

问题:reactive 对象取出的所有属性值都是非响应式的

解决:利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性

<template>
  <h2>toRefs的使用</h2>
  <h3>name: {{ name }}</h3>
  <h3>age: {{ age }}</h3>
</template>

<script lang="ts">
import {
  defineComponent, reactive, toRefs, 
} from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      name: '自来也',
      age: 47,
    });

    const state2 = toRefs(state);

    // toRefs可以吧reactive包裹的数据变成普通对象的每一个属性包裹的ref对象
    // 定时器,更新数据
    setInterval(() => {
      state2.name.value += '==';
    }, 2000);

    return {
      // ...state, // 不是响应式的数据
      ...state2,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

ref 获取元素

利用 ref 函数获取组件中的标签元素

功能需求:让输入框自动获取焦点

<template>
  <h2>ref的另一个作用:可以获取页面中的元素</h2>
  <input type="text" ref="inputRef">
</template>

<script lang="ts">
import {
  defineComponent, onMounted, ref,
} from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:当页面加载完毕之后,页面中的文本框可以直接获取焦点(自动获取焦点)
  setup() {
    // 默认是空的,页面加载完毕,说明组件已经存在了,获取文本框元素
    const inputRef = ref<HTMLElement | null>(null);

    // 页面加载后的生命周期API
    onMounted(() => {
      // eslint-disable-next-line no-unused-expressions
      inputRef.value && inputRef.value.focus();
    });
    return {
      inputRef,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Composition API(其它部分)

shallowReactive 与 shallowRef

shallowReactive:只处理了对象内最外层属性的响应式(也就是浅响应式)

shallowRef:只处理了 value 的响应式,不进行对象的 reactive 处理

什么时候用浅响应式呢?

  • 一般情况下使用 ref 和 reactive 即可
  • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化 ===> shallowReactive
  • 如果有一个对象数据,后面会产生新的对象来替换 ===> shallowRef
<template>
  <h2>ShallowReactive和ShallowRef</h2>
  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>
  <h3>m3: {{ m3 }}</h3>
  <h3>m4: {{ m4 }}</h3>
  <hr>
  <button @click="update">更新数据</button>
</template>

<script lang="ts">
import {
  defineComponent, reactive, ref, shallowReactive, shallowRef,
} from 'vue';

export default defineComponent({
  name: 'App',
  // 需求:当页面加载完毕之后,页面中的文本框可以直接获取焦点(自动获取焦点)
  setup() {
    // 深度劫持、深度监视
    const m1 = reactive({
      name: '鸣人',
      age: 20,
      car: {
        name: '奔驰',
        color: 'red',
      },
    });
    // 浅劫持
    const m2 = shallowReactive({
      name: '鸣人',
      age: 20,
      car: {
        name: '奔驰',
        color: 'red',
      },
    });
    const m3 = ref({
      name: '鸣人',
      age: 20,
      car: {
        name: '奔驰',
        color: 'red',
      },
    });
    const m4 = shallowRef({
      name: '鸣人',
      age: 20,
      car: {
        name: '奔驰',
        color: 'red',
      },
    });

    const update = () => {
      // m1
      // m1.name += '==';
      // m1.car.name += '==';

      // m2
      // m2.name += '==';
      // m2.car.name += '==';

      // m3
      // m3.value.name += '==';
      // m3.value.car.name += '==';

      // m4
      m4.value.name += '==';
      m4.value.car.name += '==';
    };

    return {
      m1, m2, m3, m4, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Readonly 与 ShallowReadonly

Readonly:

  • 深度只读数据
  • 获取一个对象(响应式或纯对象) 或 ref 并返回原始代理的只读代理。
  • 只读代理是深层的:访问的任何嵌套 property 也是只读的。

ShallowReadonly

  • 浅只读数据
  • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换

应用场景

  • 在某些特定情况下, 我们可能不希望对数据进行更新的操作,那就可以包装生成一个只读代理对象来读取数据,而不能修改或删除
<template>
  <h2>ShallowReadonly和Readonly</h2>
  <h3>state: {{ state }}</h3>
  <hr>
  <button @click="update">更新数据</button>
</template>

<script lang="ts">
import { defineComponent, reactive, shallowReadonly } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive({
      name: '佐助',
      age: 20,
      car: {
        name: '奔驰',
        color: 'yellow',
      },
    });

    // 只读的数据,深度只读
    // const state2 = readonly(state);
    const state2 = shallowReadonly(state);

    const update = () => {
      state2.name += '===';
      state2.car.name += '===';
    };

    return {
      state, state2, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRaw 与 markRaw

toRaw

  • 返回由 reactivereadonly 方法转换成响应式代理的普通对象。
  • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。

markRaw

  • 标记一个对象,使其永远不会转换为代理。返回对象本身
  • 应用场景:
    • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
    • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
  <h2>toRaw和markRaw</h2>
  <h3>state: {{ state }}</h3>
  <hr>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
import {
  defineComponent, markRaw, reactive, toRaw,
} from 'vue';

interface UserInfo {
  name: string;
  age: number;
  likes?: string[];
}

export default defineComponent({
  name: 'App',
  setup() {
    const state = reactive<UserInfo>({
      name: '小明',
      age: 20,
    });

    const testToRaw = () => {
      // 把代理对象变成了普通对象
      const user = toRaw(state);
      user.name += '===';
    };

    const testMarkRaw = () => {
      // state.likes = ['吃', '喝'];
      // state.likes[0] += '==';
      const likes = ['吃', '喝'];
      // 标记之后无法成为代理对象
      state.likes = markRaw(likes);
      setInterval(() => {
        if (state.likes) {
          state.likes[0] += '==';
        }
      }, 2000);
    };

    return {
      state, testToRaw, testMarkRaw,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

toRef

  • 为源响应式对象上的某个属性创建一个 ref 对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  • 区别 ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  • 应用: 当要将 某个 prop 的 ref 传递给复合函数时,toRef 很有用
<template>
  <h2>toRef使用及特点</h2>
  <h3>state: {{ state }}</h3>
  <h3>age: {{ age }}</h3>
  <h3>money: {{ money }}</h3>
  <hr>
  <button @click="update">更新数据</button>
  <hr>
  <child :age="age"/>
</template>

<script lang="ts">

import {
  defineComponent, reactive, ref, toRef,
} from 'vue';
import Child from '@/components/Child.vue';

export default defineComponent({
  name: 'App',
  components: { Child },
  setup() {
    const state = reactive({
      age: 5,
      money: 100,
    });

    // 把响应式数据的state对象中的某个属性age变成了ref对象了
    const age = toRef(state, 'age');
    // 把响应式对象中的某个属性使用ref进行包装,变成了一个ref对象
    const money = ref(state.money);

    console.log(age);
    console.log(money);

    const update = () => {
      // 更新数据的
      // state.age += 2;
      age.value += 3;

      money.value += 10;
    };

    return {
      state, age, money, update,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h2>Child子级组件</h2>
  <h3>age: {{ age }}</h3>
  <h3>length: {{ length }}</h3>
</template>

<script lang="ts">

import {
  computed, defineComponent, Ref, toRef,
} from 'vue';

function useGetLength(age: Ref) {
  return computed(() => age.value.toString().length);
}

export default defineComponent({
  name: 'Child',
  props: {
    age: {
      type: Number,
      required: true,
    },
  },
  setup(props) {
    const length = useGetLength(toRef(props, 'age'));
    return {
      length,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
  • 需求: 使用 customRef 实现 debounce 的示例
<template>
  <h2>CustomRef的使用</h2>
  <input type="text" v-model="keyword">
  <p>{{ keyword }}</p>
</template>

<script lang="ts">

import { customRef, defineComponent } from 'vue';

// 自定义hook防抖的函数
// value传入的数据,将来的数据类型不确定,所以,用泛型,delay防抖的间隔事件,默认是200毫秒
function useDebounceRef<T>(value: T, delay = 200) {
  let timeoutId: number;
  return customRef((track, trigger) => ({
    // 返回数据的
    get() {
      // 告诉vue追踪数据
      track();
      return value;
    },
    // 设置数据的
    set(newValue: T) {
      // 清理定时器
      clearInterval(timeoutId);
      // 开启定时器
      setTimeout(() => {
        // eslint-disable-next-line no-param-reassign
        value = newValue;
        // 告诉vue更新界面
        trigger();
      }, delay);
    },
  }));
}

export default defineComponent({
  name: 'App',
  setup() {
    // const keyword = ref('abc');
    const keyword = useDebounceRef('abc', 500);

    return {
      keyword,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

provide 与 inject

  • provide和inject提供依赖注入,功能类似 2.x 的provide/inject
  • 实现跨层级组件(祖孙)间通信
<template>
  <h2>provide 和 inject</h2>
  <p>当前的颜色: {{ color }}</p>
  <button @click="color = 'red'">红色</button>
  <button @click="color = 'yellow'">黄色</button>
  <button @click="color = 'green'">绿色</button>
  <hr>
  <son />
</template>

<script lang="ts">

import { defineComponent, provide, ref } from 'vue';
import Son from '@/components/Son.vue';

export default defineComponent({
  name: 'App',
  components: { Son },
  setup() {
    const color = ref('red');
    provide('color', color);
    return {
      color,
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <h3>儿子组件</h3>
  <hr>
  <grand-son />
</template>

<script>
import { defineComponent } from 'vue';
import GrandSon from './GrandSon.vue';

export default defineComponent({
  name: 'Son',
  components: { GrandSon },
});
</script>

<style scoped>

</style>
<template>
  <h3 :style="{color}">孙子组件</h3>
</template>
<script>
import { defineComponent, inject } from 'vue';

export default defineComponent({
  name: 'GrandSon',
  setup() {
    const color = inject('color');

    return {
      color,
    };
  },
});
</script>

<style scoped>

</style>

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
posted @ 2021-09-03 16:19  我係死肥宅  阅读(309)  评论(0编辑  收藏  举报