前言
vue3正式版已经发布好几个月了。相信有不少人早已跃跃欲试,这里根据这几天的项目经验罗列几点在项目中可能用到的知识点跟大家分享总结,在展开功能点介绍之前,先从一个简单的demo帮助大家可以快速入手新项目🌉
案例🌰
在正式介绍之前,大家可以先跑一下这个 demo 快速熟悉用法
<template>
<div>
<el-button type="primary" @click="handleClick">{{
`${vTitle}${state.nums}-${staticData}`
}}</el-button>
<ul>
<li v-for="(item, index) in state.list" :key="index"> {{ item }} </li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, watch, onMounted, computed, nextTick } from 'vue';
interface State {
nums: number;
list: string[];
}
export default {
setup() {
const staticData = 'static';
let title = ref('Create');
const state = reactive<State>({
nums: 0,
list: [],
});
watch(
() => state.list.length,
(v = 0) => {
state.nums = v;
},
{ immediate: true }
);
const vTitle = computed(() => '-' + title.value + '-');
function handleClick() {
if (title.value === 'Create') {
title.value = 'Reset';
state.list.push('小黑');
} else {
title.value = 'Create';
state.list.length = 2;
}
}
const getList = () => {
setTimeout(() => {
state.list = ['小黄', '小红'];
}, 2000);
nextTick(() => {
console.log('nextTick');
});
};
onMounted(() => {
getList();
});
return {
staticData,
state,
handleClick,
title,
vTitle,
};
},
};
</script>
效果如下👇
vue3生命周期
vue3的生命周期函数只能用在setup()
里使用,变化如下👇
vue2 | vue3 |
---|---|
beforeCreate | setup |
created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
activated | onActivated |
deactivated | onDeactivated |
扩展
- 可以看出来vue2的
beforeCreate
和created
变成了setup
- 绝大部分生命周期都是在原本vue2的生命周期上带上了
on
前缀
使用
在setup中使用生命周期:
import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
// 在挂载后请求数据
getList();
})
}
};
vue3常用api
上述案例中使用了一些常用的api,下面带大家一一认识下我们的新朋友
setup()
setup
函数是一个新的组件选项。作为在组件内使用Composition API
的入口点。从生命周期钩子的视角来看,它会在beforeCreate
钩子之前被调用,所有变量、方法都在setup
函数中定义,之后return
出去供外部使用
该函数有2个参数:
- props
- context
其中context是一个上下文对象,具有属性(attrs
,slots
,emit
,parent
,root
),其对应于vue2中的this.$attrs
,this.$slots
,this.$emit
,this.$parent
,this.$root
。
setup也用作在tsx中返回渲染函数:
setup(props, { attrs, slots }) {
return () => {
const propsData = { ...attrs, ...props } as any;
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
};
},
*注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。
扩展
为什么props没有被包含在上下文中?
- 组件使用props的场景更多,有时甚至只需要使用props
- 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。
reactive, ref
reactive
和ref
都是vue3中用来创建响应式数据的api,作用等同于在vue2中的data
,不同的是他们使用了ES6
的Porxy API
解决了vue2defineProperty
无法监听数组和对象新增属性的痛点
用法
<template>
<div class="contain">
<el-button type="primary" @click="numadd">add</el-button>
<span>{{ `${state.str}-${num}` }}</span>
</div>
</template>
<script lang="ts">
import { reactive, ref } from 'vue';
interface State {
str: string;
list: string[];
}
export default {
setup() {
const state = reactive<State>({
str: 'test',
list: [],
});
//ref需要加上value才能获取
const num = ref(1);
const numadd = () => {
num.value++;
};
return { state, numadd, num };
},
};
</script>
效果如下👇
区别
- 使用时在
setup
函数中需要通过内部属性.value
来访问ref
数据,return
出去的ref
可直接访问,因为在返回时已经自动解套;reactive
可以直接通过创建对象访问 ref
接受一个参数,返回响应式ref
对象,一般是基本类型值(String
、Nmuber
、Boolean
等)或单值对象。如果传入的参数是一个对象,将会调用reactive
方法进行深层响应转换(此时访问ref
中的对象会返回Proxy
对象,说明是通过reactive
创建的);引用类型值(Object
、Array
)使用reactive
toRefs
将传入的对象里所有的属性的值都转化为响应式数据对象(ref
)
使用reactive
return 出去的值每个都需要通过reactive
对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props
中的响应式数据做此处理)
将前面的例子作如下👇修改使用起来更加方便:
<template>
<div class="contain">
<el-button type="primary" @click="numadd">add</el-button>
- <span>{{ `${state.str}-${num}` }}</span>
+ <span>{{ `${str}-${num}` }}</span>
</div>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from 'vue';
interface State {
str: string;
list: string[];
}
export default {
setup() {
const state = reactive<State>({
str: 'test',
list: [],
});
//ref需要加上value才能获取
const num = ref(1);
const numadd = () => {
num.value++;
};
- return { state, numadd, num };
+ return { ...toRefs(state), numadd, num };
},
};
</script>
toRef
toRef
用来将引用数据类型
或者reavtive数据类型
中的某个值转化为响应式数据
用法
- reactive数据类型
/* reactive数据类型 */
let obj = reactive({ name: '小黄', sex: '1' });
let state = toRef(obj, 'name');
state.value = '小红';
console.log(obj.name); // 小红
console.log(state.value); // 小红
obj.name = '小黑';
console.log(obj.name); // 小黑
console.log(state.value); // 小黑
- 引用数据类型
<template>
<span>ref----------{{ state1 }}</span>
<el-button type="primary" @click="handleClick1">change</el-button>
<!-- 点击后变成小红 -->
<span>toRef----------{{ state2 }}</span>
<el-button type="primary" @click="handleClick2">change</el-button>
<!-- 点击后还是小黄 -->
</template>
<script>
import { ref, toRef, reactive } from 'vue';
export default {
setup() {
let obj = { name: '小黄' };
const state1 = ref(obj.name); // 通过ref转换
const state2 = toRef(obj, 'name'); // 通过toRef转换
const handleClick1 = () => {
state1.value = '小红';
console.log('obj:', obj); // obj:小黄
console.log('ref', state1); // ref:小红
};
const handleClick2 = () => {
state2.value = '小红';
console.log('obj:', obj); // obj:小红
console.log('toRef', state2); // toRef:小红
};
return { state1, state2, handleClick1, handleClick2 };
},
};
</script>
小结
ref
是对原数据的拷贝,响应式数据对象值改变后会同步更新视图,不会影响到原始值。toRef
是对原数据的引用,响应式数据对象值改变后不会改变视图,会影响到原始值。
isRef
判断是否是ref
对象,内部是判断数据对象上是否包含__v_isRef
属性且值为true。
setup() {
const one = ref(0);
const two = 0;
const third = reactive({
data: '',
});
let four = toRef(third, 'data');
const { data } = toRefs(third);
console.log(isRef(one)); // true
console.log(isRef(data)); // true
console.log(isRef(four)); // true
console.log(isRef(two)); // false
console.log(isRef(third)); // false
}
unref
如果参数为ref
,则返回内部原始值,否则返回参数本身。内部是val = isRef(val) ? val.value : val
的语法糖。
setup() {
const hello = ref('hello');
console.log(hello); // { __v_isRef: true,value: "hello"... }
const newHello = unref(hello);
console.log(newHello); // hello
}
watch, watchEffect
watch
watch侦听器,监听数据变化
用法和vue2有些区别
语法为:watch(source, callback, options)
source
:用于指定监听的依赖对象,可以是表达式,getter函数或者包含上述两种类型的数组(如果要监听多个值)callback
:依赖对象变化后执行的回调函数,带有2个参数:newVal
,oldVal
。如果要监听多个数据每个参数可以是数组[newVal1, newVal2, ... newValN]
,[oldVal1, oldVal2, ... oldValN]
options
:可选参数,用于配置watch
的类型,可以配置的属性有immediate
(立即触发回调函数)、deep
(深度监听)
let title = ref('Create');
let num = ref(0);
const state = reactive<State>({
nums: 0,
list: [],
});
// 监听ref
watch(title, (newValue, oldValue) => {
/* ... */
});
// 监听reactive
watch(
// getter
() => state.list.length,
// callback
(v = 0) => {
state.nums = v;
},
// watch Options
{ immediate: true }
);
// 监听多个ref
watch([title, num], ([newTitle, newNum], [oldTitle, oldNum]) => {
/* ... */
});
// 监听reactive多个值
watch([() => state.list, () => state.nums], ([newList, newNums], [oldList, oldvNums]) => {
/* ... */
});
我们可以向上面一样将多个值的监听拆成多个对单个值监听的watch。这有助于我们组织代码并创建具有不同选项的观察者;watch方法会返回一个stop()
方法,若想要停止监听,便可直接执行该stop函数
watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更是重新运行该函数.
<template>
<div class="contain">
<el-button type="primary" @click="numadd">add</el-button>
<span>{{ num }}</span>
</div>
</template>
<script lang="ts">
import { ref, watchEffect } from 'vue';
export default {
setup() {
const num = ref(1);
const numadd = () => {
num.value++;
};
watchEffect(() => {
console.log(num.value); // 1,2,3...
});
return { numadd, num };
},
};
</script>
可以看到在组件初始化的时候该回调函数立即执行了一次,同时开始自动检测回调函数里头依赖的值,并在依赖关系发生改变时自动触发这个回调函数,这样我们就不必手动传入依赖特意去监听某个值了
computed
传入一个getter函数,返回一个默认不可手动修改的ref对象.
setup() {
let title = ref('Create');
const vTitle = computed(() => '-' + title.value + '-');
function handleClick() {
if (title.value === 'Create') {
title.value = 'Reset';
} else {
title.value = 'Create';
}
}
}
反转字符串:
setup() {
const state = reactive({
value: '',
rvalue: computed(() =>
state.value
.split('')
.reverse()
.join('')
)
})
return toRefs(state)
}
provide, inject
provide()
和inject()
用来实现多级嵌套组件之间的数据传递,父组件或祖先组件使用provide()
向下传递数据,子组件或子孙组件使用inject()
来接收数据
// 父组件
<script>
import {provide} from 'vue'
export default {
setup() {
const obj = ref('johnYu')
// 向子孙组件传递数据provide(名称,数据)
provide('name', obj)
}
}
</script>
// 孙组件
<script>
import {inject} from 'vue'
export default {
setup() {
// 接收父组件传递过来的数据inject(名称)
const name = inject('name') // johnYu
return {name}
}
}
</script>
getCurrentInstance
getCurrentInstance
方法用于获取当前组件实例,仅在setup
和生命周期中起作用
import { getCurrentInstance, onBeforeUnmount } from 'vue';
const instance = getCurrentInstance();
// 判断当前组件实例是否存在
if (instance) {
onBeforeUnmount(() => {
/* ... */
});
}
通过instance中的ctx
属性可以获得当前上下文,通过这个属性可以使用组件实例中的各种全局变量和属性
$Refs
为了获得对模板中元素或组件实例的引用,我们可以同样使用ref
并从setup()
返回它
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 获取渲染上下文的引用
const root = ref(null)
onMounted(() => {
// 仅在初次渲染后才能获得目标元素
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
.sync
在vue2.0中使用.sync
实现prop
的双向数据绑定,在vue3中将它合并到了v-model
里
vue2.0
<el-pagination
:current-page.sync="currentPage1"
>
</el-pagination>
vue3.0
<el-pagination
v-model:current-page="currentPage1"
>
</el-pagination>
v-slot
Child.vue
<template>
<div class="child">
<h3>具名插槽</h3>
<slot name="one" />
<h3>作用域插槽</h3>
<slot :data="list" />
<h3>具名作用域插槽</h3>
<slot name="two" :data="list" />
</div>
</template>
<script>
export default {
data: function() {
return {
list: ['zhangsan', 'lisi']
}
}
}
</script>
vue2用法
<template>
<div>
<child>
<div slot="one">
<span>菜单</span>
</div>
<div slot-scope="user">
<ul>
<li v-for="(item, index) in user.data" :key="index">{{ item }}</li>
</ul>
</div>
<div slot="two" slot-scope="user">
<div>{{ user.data }}</div>
</div>
</child>
</div>
</template>
vue3用法
新指令v-slot
统一slot
和slot-scope
单一指令语法。速记v-slot
可以潜在地统一作用域和普通插槽的用法。
<template>
<div>
<child>
<template v-slot:one>
<div><span>菜单</span></div>
</template>
<template v-slot="user">
<ul>
<li v-for="(item, index) in user.data" :key="index">{{ item }}</li>
</ul>
</template>
<template v-slot:two="user">
<div>{{ user.data }}</div>
</template>
<!-- 简写 -->
<template #two="user">
<div>{{ user.data }}</div>
</template>
</child>
</div>
</template>
Composition API 结合vuex4, Vue Router 4
createStore,useStore,useRouter,useRoute
在vuex4
中通过createStore
创建Vuex
实例,useStore
可以获取实例,作用等同于vue2.0中的this.$store
;
Vue Router 4
中useRouter
可以获取路由器,用来进行路由的跳转,作用等同于vue2.0的this.$router
,useRoute
就是钩子函数相当于vue2.0的this.$route
store/index.ts
import {createStore} from 'vuex';
const store = createStore({
state: {
user: null,
},
mutations: {
setUser(state, user) {
state.user = user;
}
},
actions: {},
modules: {}
});
router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { scrollBehavior } from './scrollBehaviour.ts';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('/@/views/home.vue') // vite.config.vue中配置alias
}
];
const router = createRouter({
history: createWebHashHistory(),
routes,
strict: true,
scrollBehavior: scrollBehavior,
});
export default router;
main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import { getTime } from '/@/utils'
const app = createApp(App);
app.config.globalProperties.$getTime = getTime // vue3配置全局变量,取代vue2的Vue.prototype
app.use(store).use(router)
app.mount('#app');
App.vue
import { reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { ElMessage } from 'element-plus';
export default {
name: "App",
setup() {
const store = useStore();
const router = useRouter();
// 用户名和密码
const Form = reactive({
username: "johnYu",
password: "123456",
});
// 登录
function handelLogin() {
store.commit("setUser", {
username: Form.username,
password: Form.password,
});
ElMessage({
type: 'success',
message: '登陆成功',
duration: 1500,
});
// 跳转到首页
router.push({
name: 'Home',
params: {
username: Form.username
},
});
}
return {
Form,
handelLogin
};
}
home.vue
import { useRouter, useRoute } from 'vue-router';
import Breadcrumb from '/@/components/Breadcrumb.vue';
export default defineComponent({
name: 'Home',
components: {
Breadcrumb,
},
setup() {
const route = useRoute();
// 接收参数
const username = route.params.username;
return {username}
}
})
导航守卫
由于使用 Composition API 的原因,setup
函数里面分别使用onBeforeRouteLeave
和onBeforeRouteUpdate
两个新增的 API 代替vue2.0中的beforeRouteLeave
和beforeRouteUpdate
。
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
setup() {
onBeforeRouteUpdate((to) => {
if (to.name === 'Home'){
/* ... */
}
});
}
useLink
useLink
它提供与router-link
的v-slot
API 相同的访问权限,将RouterLink
的内部行为公开为Composition API函数,用于暴露底层的定制能力
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { computed } from 'vue';
import { RouterLink, useLink } from 'vue-router';
export default {
name: 'AppLink',
props: {
...RouterLink.props,
inactiveClass: String,
},
setup(props) {
const { route, href, isActive, isExactActive, navigate } = useLink(props);
const isExternalLink = computed(
() => typeof props.to === 'string' && props.to.startsWith('http')
);
return { isExternalLink, href, navigate, isActive };
},
};
</script>
插槽 prop 的对象包含下面几个属性:
href
:解析后的 URL。将会作为一个 a 元素的 href attribute。route
:解析后的规范化的地址。navigate
:触发导航的函数。会在必要时自动阻止事件,和 router-link 同理。isActive
:如果需要应用激活的 class 则为 true。允许应用一个任意的 class。isExactActive
:如果需要应用精确激活的 class 则为 true。允许应用一个任意的 class。
扩展
样式 scoped
vue2
/* 深度选择器 */
/*方式一:*/
>>> .foo{ }
/*方式二:*/
/deep/ .foo{ }
/*方式三*/
::v-deep .foo{ }
vue3
/* 深度选择器 */
::v-deep(.foo) {}
.env环境扩展
vite中的.env
文件变量名一定要以VITE_
前缀
.env文件
VITE_USE_MOCK = true
使用:
import.meta.env.VITE_APP_CONTEXT
使用Composition API替换mixin
众所周知使用mixin
的时候当我们一个组件混入大量不同的mixin
的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。
- 每个
mixin
都可以定义自己的props
、data
,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。 - 另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。
以这个经典的Vue 2组件为例,它定义了一个"计数器"功能:
//counter.js
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
}
用法如下:
<template>
<div>
{{ count }}
<el-button @click="increment()">add</el-button>
</div>
</template>
<script>
import counter from './mixins/counter'
import getTime from './mixins/getTime'
export default {
mixins: [counter,getTime]
}
</script>
假设这边我们引用了counter和getTime两个mixin
,则无法确认count和increment()方法来源,并且两个mixin
中可能会出现重复命名的概率
下面是使用Composition API定义的完全相同的组件:
// counter.ts
import { ref } from 'vue';
export default function () {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
}
<template>
<div>
{{ count }}
<el-button @click="increment()">add</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import counter from '/@/composables/counter';
export default defineComponent({
setup() {
const { count, increment } = counter();
return {
count,
increment,
};
},
});
</script>
总结
使用Composition API可以清晰的看到数据来源,即使去编写更多的hook函数,也不会出现命名冲突的问题。🚄
Composition API 除了在逻辑复用方面有优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。vue3的Composition API会将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去
参考文章 📜
扩展 🏆
如果你觉得本文对你有帮助,可以查看我的其他文章❤️:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。