本文是vue3+ts项目系列第3篇《vue3组合式api及重要属性变更》,会…
一、前言
在 react 和 vue 社区中也越来越多人开始使用TypeScript,使用 TS 可以增加代码的可读性和可维护性。从发布的 Vue3 正式版本来看, Vue3 的源码是用 TS 编写的,更好的 TypeScript 支持也是这次升级的一大亮点。当然,在实际开发中如何正确拥抱 TS 以及如何迁移到 Vue3 也是项目中我们不得不面对的问题,这里针对 Vue3 和 TS单独做了一个系列和大家做一下交流,本篇是 vue3+ts项目系列3篇《vue3组合式api及重要属性变更》。
本系列其他内容如下:
- vue3+ts项目系列第1篇《vue3项目从0到1搭建》
- vue3+ts项目系列第2篇《TypeScript 语法汇总》
- 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
- All runtime features included: 22.5kb. More features but still lighter than Vue 2。大多数可选功能(如 v-model、
- 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
最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~
本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!