本文是vue3+ts项目系列第3篇《vue3组合式api及重要属性变更》,会…

一、前言

在 react 和 vue 社区中也越来越多人开始使用TypeScript,使用 TS 可以增加代码的可读性和可维护性。从发布的 Vue3 正式版本来看, Vue3 的源码是用 TS 编写的,更好的 TypeScript 支持也是这次升级的一大亮点。当然,在实际开发中如何正确拥抱 TS 以及如何迁移到 Vue3 也是项目中我们不得不面对的问题,这里针对 Vue3 和 TS单独做了一个系列和大家做一下交流,本篇是 vue3+ts项目系列3篇《vue3组合式api及重要属性变更》。

本系列其他内容如下:

二、背景

Vue作为一种渐进式框架, 借鉴了 React 的组件化和虚拟 DOM、Angular 的模块化和双向数据绑定。随着 Vue 3 内核 API 与实现已趋稳定, 可以看到相对vue2.x,Vue3做了很多重要的变更,特别是Composition API的引入。

3.0 对比 2.x 的重要变更有 6 个方面:

  • Performance(性能): 优化了虚拟 DOM,有了更加优化的编译,实现了更加高效的组件初始化
    • Rewritten virtual dom implementation (重写了虚拟 DOM)
    • Compiler-informed fast paths (优化编译)、
    • More efficient component initialization (更高效的组件初始化)
    • 1.3-2x better update performance (1.3~2 倍的更新性能)
    • 2-3x faster SSR (2~3 倍的 SSR 速度)
  • Tree-shaking support (支持 Tree-shaking): 按需求引用的内置的指令和方法
    • All runtime features included: 22.5kb. More features but still lighter than Vue 2。大多数可选功能(如 v-model、)现在都是支持 Tree-shaking 的
    • Bare-bone HelloWorld size: 13.5kb. 11.75kb with only Composition API support
    • All runtime features included: 22.5kb. More features but still lighter than Vue 2
  • Composition API
    • Usable alongside existing Options API (可与现有选项 API 一起使用)
    • Flexible logic composition and reuse (灵活的逻辑组成和重用)
    • Reactivity module can be used as a standalone library (Reactivity 模块可以作为独立的库使用)
  • Fragment, Teleport, Suspense
    • Fragment: vue2时,由于组件必须只有一个根节点,很多时候会添加一些没有意义的节点用于包裹。Fragment组件就是用于解决这个问题的
    • Teleport其实就是React中的Portal,提供一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
    • 和React中的Supense是一样的。Suspense 让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容
  • Better TypeScript support (更好的 TypeScript 支持度)
  • Custom Renderer API (自定义的 Renderer API)

本文主要接下来主要涉及到的内容为 Custom Renderer API。

三、Composition API 等核心特性

核心关注点:

  • setup
  • reactive
  • ref引用 & toRefs
  • Lifecycle Hooks
  • computed
  • watch
  • 依赖注入 provide & inject

1、setup

setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口。

  • setup 函数相当于 vue2.x 中 beforeCreate 和 created, 在 beforeCreate 之后、created 之前执行
  • setup第一个形参,接收 props 数据
  • setup第二个形参是一个上下文对象, 在 setup() 函数中无法访问到 this, 可以用这个context来访问

Tips:

setup 第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。遵循vue2.x的原则

  • props 定义
  • 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错
  • 不要解构props。解构的props会失去响应性

示例:

import { inject, defineComponent } from "vue";
export default defineComponent({
  name: "ListItem",
  props: {
    data: Object
  },
  steup({data}) {
    console.log("--userData--", data);
    return {
      data
    };
  }
});

2、reactive

reactive() 函数接收一个普通对象,返回一个响应式的数据对象。

示例:

import { defineComponent, onUnmounted, reactive, ref, watchEffect } from "vue";

export default defineComponent({
  name: "About",
  components: {},
  setup() {
    const state = reactive({
      msg: '欢迎来到 "关于 vue3 和TS的语法DEMO"',
      testWatchEffectCount: 0
    });
    // watchEffect —— 1.自动收集数据源作为依赖、2.只有变更后的值、3.默认会执行一次寻找依赖,然后属性改变也会执行
    const count = ref(0);
    watchEffect(() => {
      console.log("--watchEffect-value--", count.value);
      state.testWatchEffectCount = count.value;
    });
    setInterval(() => {
      count.value++;
    }, 500);

    const stop = watchEffect(() => {
      console.log("--stop-effect--");
    });
    // 清除副作用
    watchEffect(onInvalidate => {
      console.log(count.value, "0-副作用");
      const token = setTimeout(() => {
        console.log(count.value, "1-副作用");
      }, 5000);
      onInvalidate(() => {
        // count(watchEffect函数依赖项) 改变时或停止侦听时,取消之前的异步操作
        token.cancel();
      });
    });

    onUnmounted(() => {
      stop();
    });
    return {
      state
    };
  }
});
</script>

3、ref引用 & toRefs

ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive

  • ref() 函数根据给定的值创建一个响应式的数据对象,返回值是一个对象,这个对象上只包含一个 .value 属性
  • toRefs() 函数可以将 reactive() 创建出来的响应式对象,转换为普通的对象,只不过,这个对象上的每个属性节点,都是 ref() 类型的响应式数据
  • 通过 ref() 还可以引用页面上的元素或组件,和vue2的ref概念类似
    • 元素引用
    • 组件引用

示例:父组件

import { ref, reactive, toRefs, onMounted } from 'vue'

export default {
    setup() {
        const count = ref(0) // 创建响应式数据对象 count,初始值为 0
        console.log(count.value) // 在setup内访问count值需要.value 属性才可以,但在template中可以直接访问

        const state = reactive({count: 0, name:'weedsFly'}) // 用reactive集中创建多个响应式对象
        const add = () => { // methods写在setup内
          state.count++
        }

        // 元素引用
        const h1Ref = ref(null) // 创建一个 DOM 引用
        onMounted(() => { // 在 DOM 首次加载完毕之后,才能获取到元素的引用
          h1Ref.value.style.color = 'pink' // h1Ref.value 是原生DOM对象
        })
        // 组件引用
        const compRef = ref(null) // 创建一个组件的 ref 引用
        showCompData = () => { // 展示子组件中 count 的值
          console.log(compRef.value.count) 
        }

        return {
          count,
          // ...state,  // 使用展开运算符后 用reactive创建的响应式数据 变成了 固定的值
          ...toRefs(state), // 可以用toRefs函数 将传进来的非响应式对象 转成 ref() 类型的响应式数据
          add,
          h1Ref,
          compRef,
          showCompData
        }
    },
 }

示例:子组件

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0) // 定义响应式的数据
    return {
      count
    }
  }
}

4、Lifecycle Hooks

新版的生命周期函数,可以按需导入到组件中,且只能在 setup() 函数中使用

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

Tips:

无法设置 reactive的state

示例:

import { defineComponent, onUnmounted, reactive, ref, watchEffect } from "vue";

export default defineComponent({
  name: "About",
  components: {},
  setup() {
    const state = reactive({
      msg: '欢迎来到 "关于 vue3 和TS的语法DEMO"',
      testWatchEffectCount: 0
    });
    // watchEffect —— 1.自动收集数据源作为依赖、2.只有变更后的值、3.默认会执行一次寻找依赖,然后属性改变也会执行
    const count = ref(0);
    watchEffect(() => {
      console.log("--watchEffect-value--", count.value);
      state.testWatchEffectCount = count.value;
    });
    setInterval(() => {
      count.value++;
    }, 500);

    const stop = watchEffect(() => {
      console.log("--stop-effect--");
    });
    // 清除副作用
    watchEffect(onInvalidate => {
      console.log(count.value, "0-副作用");
      const token = setTimeout(() => {
        console.log(count.value, "1-副作用");
      }, 5000);
      onInvalidate(() => {
        // count(watchEffect函数依赖项) 改变时或停止侦听时,取消之前的异步操作
        token.cancel();
      });
    });

    onUnmounted(() => {
      stop();
    });
    return {
      state
    };
  }
});

5、computed

computed() 用来创建计算属性,computed() 函数的返回值是一个 ref 的实例

  • computed创建只读的计算属性(传入一个 function 函数,可以得到一个只读的计算属性)
  • computed创建可读可写的计算属性

示例:

import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const computedCount = computed(() => count.value + 1)
    computedCount.value = 9 //  computed value is readonly.

    const count2 = ref(0)
    const computedCount2 = computed({
      get: () => count2.value + 1,
      set: val => {
        count2.value = val - 1
      }
    })
    computedCount2.value = 100 // 为计算属性赋值的操作,会触发 set 函数 
    console.log(count2.value) // 触发 set 函数后,count 的值会被更新
    
    return {
      count,
      computedCount,
      count2,
      computedCount2
    }
  },
}

6、watch

  • 监视单个数据源变动
    • 监视单个reactive创建的数据
    • 监视单个ref创建的数据源
  • 监视多个数据源
    • 监视多个reactive创建的数据源
    • 监视多个ref创建的数据源
  • 清除watch监视
  • 在 watch 中清除无效的异步任务(与节流防抖同效)

示例:

import { reactive, ref, watch } from 'vue'

export default {
  setup() {
    // 1-1 监视单个reactive创建的数据源
    const state = reactive({count: 100})
    watch(
      () => state.count,
      (newVal, oldVal) => { console.log(newVal, oldVal)},
      {lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
    )

    // 1-2 监视单个ref创建的数据源
    const count2 = ref(100)
    const stop2 = watch(
      count2,
      (newVal, oldVal) => { console.log(newVal, oldVal)},
      {lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
    )

    // 2-1 监视多个reactive创建的数据源
    const state3 = reactive({count: 100, name: 'Laiyj'})
    watch(
      [() => state3.count, () => state3.name],
      ([newCount, newName], [oldCount, oldName]) => {
        console.log(newCount, oldCount)
        console.log(newName, oldName)
      },
      {lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
    )

    // 2-2 监视多个ref创建的数据源
    const count4 count = ref(100)
    const name4 = ref('Fei')
    watch(
      [count4, name4],
      ([newCount, newName], [oldCount, oldName]) => {
        console.log(newCount, oldCount)
        console.log(newName, oldName)
      },
      {lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
    )

    setTimeout(() => {
      state.count++;
      count2++;
      state2.count++;
      state2.name = 'lian';
      count4++;
      name4 = 'lian4';
    }, 500)
  }

  // 3、清除watch监视
  const clearWatch = () => {
    stop()
  }

  // 4、watch 中清除无效的异步任务(与节流防抖同效)
  const keyword6 = ref('')
  const asyncPrint = (val) => { // 执行异步任务,并得到关闭异步任务的 timerId
    return setTimeout(() => {
      console.log(val)
    }, 1000)
  }
  watch(
    keyword6,
    (newVal, oldVal, onClean) => {
      const timeId = asyncPrint()
      onClean(() => {clearTimeout(timeId)}) // 如果 watch 监听被重复执行了,则会先清除上次未完成的异步任务
    }
  )

  return {
    state,
    count2,
    state3,
    count4,
    name4
    clearWatch,
    keyword6
  }
}

7、依赖注入 provide & inject

  • provide() 和 inject() 可以实现嵌套组件之间的数据传递
  • 只能在 setup() 函数中使用。
  • 父级组件中使用 provide() 函数向下传递数据;子级组件中使用 inject() 获取上层传递过来的数据

Tips:

steup() 初始化,在brforeCreate 前后, 所以,对于动态组件,类似 list 内加载 list-item, item是无法拿到list provide的值的

示例:父组件

import {
  defineComponent,
  reactive,
  computed,
  provide,
  readonly,
  ref,
  onMounted
} from "vue";
import ListItem from "./components/ListItem.vue";
import TestInject from "./components/TestInject.vue";
interface ListByStatusData {
  page: number;
  pageSize: number;
}
interface MovieItem {
  id: string;
  name: string;
  desc: string;
}

export default defineComponent({
  name: "List",
  components: {
    ListItem,
    TestInject
  },
  setup() {
    let list: Array<MovieItem> = [];
    /**
     * @name: reactive
     * @desc: 核心流程
     * @author: lianpf
     * @date: 2021.03.06
     * */
    let state = reactive({
      title: "List Page",
      count: 0,
      total: 0,
      list,
      provideStatus: false
    });

    const params = computed(() => ({
      count: state.count + 1
    }));
    /**
     * @name: 异步请求函数 & 泛型函数
     * @desc: 核心流程
     * @author: lianpf
     * @date: 2021.03.06
     * */
    type FnType = (x: number, y: number) => Promise<Array<MovieItem>>;
    const getInitList: FnType = (page, pageSize) => {
      console.log(`--req-params-page:${page}-pageSize:${pageSize}--`);
      return new Promise((resolve, reject) => {
        try {
          let res: Array<MovieItem> = [
            {
              id: "100",
              name: "list-item-001",
              desc: "001-001-001-001"
            },
            {
              id: "200",
              name: "list-item-002",
              desc: "002-002-002-002"
            },
            {
              id: "300",
              name: "list-item-003",
              desc: "003-003-003-003"
            }
          ];
          resolve(res);
        } catch (e) {
          reject(e);
        }
      });
    };
    // const tempList = () => getInitList()
    let reqParams: ListByStatusData = {
      page: 1,
      pageSize: 10
    };

    // 异步流控制函数
    const asyncFlow = async () => {
      let resData = await getInitList(reqParams.page, reqParams.pageSize);
      state.count = 2;
      state.list = resData;
      list = resData;
    };

    onMounted(async () => {
      await asyncFlow();
    });
    /**
     * @name: 依赖注入 —— 父组件通过 provide 函数向子级组件共享数据(不限层级)
     * @desc: 核心功能
     * @author: lianpf
     * @date: 2021.03.06
     * */
    const parentColor = ref("salmon");
    // provide('要共享的数据名称', 被共享的数据)
    provide("themeColor", readonly(parentColor));
    const updateThemeColor = () => {
      state.provideStatus = !state.provideStatus;
      parentColor.value = state.provideStatus ? "skyblue" : "salmon";
    };
    // 父组件 function update “注入”的值
    provide("updateThemeColor", updateThemeColor);
    provide("location", "North Pole");
    provide("geolocation", {
      longitude: 90,
      latitude: 135
    });

    return {
      state,
      params,
      list
    };
  }
});
</script>

示例: 子组件

import { inject, defineComponent } from "vue";

export default defineComponent({
  name: "TestInject",
  setup() {
    /**
     * @name: 依赖注入
     * @desc: 核心功能 —— 调用 inject 函数时,通过指定的数据名称,获取到父级共享的数据
     * @author: lianpf
     * @date: 2021.03.06
     *  */
    const userThemeColor = inject("themeColor");
    const updateThemeColor = inject("updateThemeColor");
    console.log("--userThemeColor--", userThemeColor);

    const userLocation = inject("location", "The Universe");
    const userGeolocation = inject("geolocation");

    return {
      userThemeColor,
      updateThemeColor,
      userLocation,
      userGeolocation
    };
  }
});
</script>

四、项目结构

├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── logo.png
│   │   └── styles
│   │       └── varible.styl
│   ├── components
│   │   ├── HeaderNav.vue
│   │   └── HelloWorld.vue
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   ├── shims-vue.d.ts
│   ├── store
│   │   └── index.ts
│   ├── types
│   │   └── movie.ts
│   └── views
│       ├── About.vue
│       ├── Detail.vue
│       ├── Home.vue
│       ├── List.vue
│       └── components
│           ├── ListItem.vue
│           └── TestInject.vue
└── tsconfig.json

五、源码

example源码地址: vue3-ts


最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~

微信公众号

本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!