文章目录
- 相对于 Vue2.0,Vue3.0 带来了很多新的特性和优势,主要包括:
- 更快的渲染速度和更小的代码体积;
- 更好的 TypeScript 支持;
- 更好的组件复用机制和逻辑封装;
- 更方便的响应式数据管理和更好的性能;
- 更好的开发者工具和更丰富的生态环境。
- 本文主要从vue3.0的响应机制,基础使用(computed,watch,props,emit,provide,eject等),两种写法,组件通信,生命周期,内置组件,路由(router),store(pinia) 等方面进行分析。写作不易,请关注收藏。
(注:进入正文前,向大家推荐一个好用的网站inscode,里面也有我对vue3.0写的一些demo,可以查看)
安装及创建
安装
// npm
npm init vue@latest
// CDN
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
// ES5模块
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
一.选项式与组合式
在学习之前,得先了解vue3.0的两种API风格,选项式 API (Options API) 和 组合式 API (Composition API),区分并使用,这对下面查看demo有很大的帮助。
可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。其实就是对VUE2.0的兼容,写法也和2.0的基本一样。这里就不多解释,直接贴个官网的代码demo看一下:
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与< script setup>搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,< script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与 < script setup> 改造后和上面的模板完全一样的组件:
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
实际上,选项式 API 是在组合式 API 的基础上实现的!
区别:
-
当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。
-
当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。
下面的内容,将采用组合式API进行功能演示
二、响应机制(ref、reactive)
- 先理解一个概念:ref用于将基本数据类型转换为响应式对象,而reactive则用于将对象转换为响应式对象。
这里的基本数据类型就是number,string,boolean,null,undefine,对象就是我们平时使用的Array,Object。
1.reactive:
- 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
- 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
let state = reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
- 同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性:
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)
2.ref:
- 可以用于创建简单的响应式变量,例如数字、字符串、布尔值或对象等。使用ref创建的变量需要通过.value来访问或修改值。
- ref底层本质其实还是reactive
- 系统会自动根据我们传入的值将它转化成ref(xx) —> reactive({value:xx})
import { ref, reactive } from 'vue'
const count = ref(0)
console.log(count.value) // output: 0
count.value++ // count.value = count.value + 1
console.log(count.value) // output: 1
- 在上面的代码中,我们使用ref函数将0转换为一个响应式对象count,通过count.value来访问和修改这个值。
- ref注意点:
在vue中使用ref的值不通过value获取
在js中使用ref的值必须通过value获取
3.isRef 与 isReactive
判断是ref,还是reactive
import {isRef,isReactive} from 'vue'
let age = ref(18);
console.log(isRef(age)); // 判断是否是ref数据类型
console.log(isReactive); // 判断是否是reactive数据类型
4.toRef 与 toRefs
刚才说到 reactive 创建的对象,直接赋值给另一个对象,或者解构属性时,会失去响应性。其实vue也提供了相关的解决方法,那就是toRef 与 toRefs,
- toRefs用于将一个响应式对象转换为一个普通对象,但是保留每个属性的响应式。例如:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
const stateRefs = toRefs(state)
console.log(stateRefs.count.value) // 0
console.log(stateRefs.message.value) // 'Hello'
state.count++ // 触发更新
stateRefs.count.value++
console.log(stateRefs.count.value) // 2
console.log(state.count) // 2
- 在上面的例子中,我们先使用reactive创建一个响应式对象state,然后使用toRefs将其转换为普通对象stateRefs。此时,stateRefs中的每个属性都是一个ref对象,我们可以通过属性名加上.value来获取属性的值。当我们修改state中的count属性时,会自动触发更新。
- toRef用于将一个响应式对象中的某个属性转换为一个普通值,这个普通值不具有响应式。例如:
import { reactive, toRef } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
const countRef = toRef(state, 'count')
console.log(countRef.value) // 0
state.count++ // 触发更新
console.log(countRef.value) // 1
countRef.value++
console.log(countRef.value) // 2
console.log(state.count) // 2
- 在上面的例子中,我们使用toRef将state中的count属性转换为一个普通值countRef。我们可以直接通过.value获取其值。当我们修改state中的count属性时,会自动触发更新。
三、组件通信
- 第一个组件(品类组件)
defineProps 用于接收父组件传递过来的自定义属性。
defineEmits 用于声明父组件传递过来的自定义事件。
usePageStore,配合 computed 实现访问 pinia中的状态数据。
# 文件名 CnCate.vue
<template>
<div class='cates'>
<span
v-for='item in cates'
v-text='item.label'
:class='{"on": tab===item.tab}'
@click='change(item.tab)'
>
</span>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed } from 'vue'
import { usePageStore } from '../store'
// 接收自定义属性
const props = defineProps({
tab: { type: String, default: '' }
})
const emit = defineEmits(['update:tab'])
// 从vuex中访问cates数据
const pageStore = usePageStore()
const cates = computed(()=>pageStore.cnode.cates)
const change = (tab) => {
emit('update:tab', tab) // 向父组件回传数据
}
</script>
<style lang="scss" scoped>
.cates {
padding: 5px 20px;
background-color: rgb(246, 246, 246);
}
.cates span {
display: inline-block; height: 24px;
line-height: 24px; margin-right: 25px;
color: rgb(128, 189, 1); font-size: 14px;
padding: 0 10px; cursor: pointer;
}
.cates span.on {
background-color: rgb(128, 189, 1);
color: white; border-radius: 3px;
}
</style>
- 第二个组件(分页组件)
使用 toRefs 把 props 变成响应式的。在Vue3中,默认情况下 props是不具备响应式的,即父组件中的数据更新了,在子组件中却是不更新的。
使用 computed 实现动态页码结构的变化。
defineProps、defineEmits,分别用于接收父组件传递过来的自定义属性、自定义事件。
# 文件名 CnPage.vue
<template>
<div class='pages'>
<span @click='prev'><<</span>
<span v-if='page>3'>...</span>
<span
v-for='i in pages'
v-text='i'
:class='{"on":i===page}'
@click='emit("update:page", i)'
>
</span>
<span>...</span>
<span @click='emit("update:page", page+1)'>>></span>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed, toRefs } from 'vue'
let props = defineProps({
page: { type: Number, default: 1 }
})
const { page } = toRefs(props)
const emit = defineEmits(['update:page'])
const pages = computed(()=>{
// 1 1 2 3 4 5 ...
// 2 1 2 3 4 5 ...
// 3 1 2 3 4 5 ...
// 4 ... 2 3 4 5 6 ...
// n ... n-2 n-1 n n+1 n+2 ...
const v = page.value
return v<=3 ? [1,2,3,4,5] : [v-2,v-1,v,v+1,v+2]
})
const prev = () => {
if (page.value===1) alert('已经是第一页了')
else emit('update:page', page.value-1)
}
</script>
- 在父级组件中使用 自定义组件
v-model:tab=‘tab’ 是 :tab 和 @update:tab 的语法糖简写;
v-model:page=‘page’ 是 :page 和 @update:page 的语法糖简写;
使用 watch 监听品类和页面的变化,然后触发调接口获取新数据。
# 文件名 Cnode.vue
<template>
<div class='app'>
<!-- <CnCate :tab='tab' @update:tab='tab=$event' /> -->
<CnCate v-model:tab='tab' />
<!-- <CnPage :page='page' @update:page='page=$event' /> -->
<CnPage v-model:page='page' />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import CnCate from './components/CnCate.vue'
import CnPage from './components/CnPage.vue'
const tab = ref('')
const page = ref(1)
const stop = watch([tab, page], ()=>{
console.log('当品类或页码变化时,调接口')
})
</script>
四、路由vue-router
3.0的路由也是使用vue-loader,通过npm或yarn安装依赖,引入:
// 以前vue2是
// import Router from 'vue-router'
// 引入 createRouter 替换new Vue
import { createRouter,createWebHashHistory } from "vue-router"
const routes = [
{
path: '/',
name: 'home',
component: import('../view/home.vue'),
},
{
path: '/testRouter',
name: 'testRouter',
component: defineAsyncComponent(() => import('../view/testRouter.vue')), // 异步组件
},
]
export default createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
上面通过createRouter创建了路由实例,然后再创建vue实例时,使用use引入。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
在我们组件中,使用useRouter获取路由实例,当然也可以通过getCurrentInstance().appContext.config.globalProperties获取,两者是一样的。
hooks:
// 把使用全局数据的功能封装成Hooks
import { getCurrentInstance } from 'vue'
const useGlobal = (key) => {
return getCurrentInstance().appContext.config.globalProperties[key]
}
export {
useGlobal
}
组件:
import { useGlobal } from '../hooks'
import { useRouter } from 'vue-router'
const router1 = useGlobal('$router') // 这里必须先保存起来,不然执行完setup之后,currentInstance就会被重置为null了。
const print1 = () => {
console.log(router1, 'router1(通过当前实例获取)')
}
const router2 = useRouter()
const print2 = () => {
console.log(router2, 'router2(通过useRouter获取)')
}
function gotoTodoList() {
router1.push('/todo-list')
}
- 注意,使用时,必须先保存起来,不然执行完setup之后,currentInstance就会被重置为null了。useRouter() 没有先接收,方法调用时获取到的也是一个undefined!!
关于defineAsyncComponent ,上面可以在路由中引入,也可以在组件中引入。
// vue2.0是通过() => import()实现异步
// import这种一进入页面就会加载,defineAsyncComponent ,以异步组件定义的,会在页面调用时加载,可以通过控制台network看出,
// defineAsyncComponent可以全局定义,
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
// 也可以在父组件直接定义
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
五、状态管理store(pinia)
vue3.0 store使用的是pinia,连官网都是推荐的pinia,并不是说vuex不能使用了,Pinia 现在是 Vue 团队官方推荐的状态管理解决方案。Vuex 现在处于维护模式。它仍然有效,但将不再接收新功能。建议将 Pinia 用于新应用。
安装
yarn add pinia
# 或者使用 npm
npm install pinia
创建
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
定义及使用:
- 在 Options API 中有一个响应式state的等价函数data
- getters,相当于computedOptions API 中的computed
- actions,相当于methodsOptions API 中的methods
- 没有mutations,因为对 now 的任何更改state都会注册一个隐式突变,无论它在哪里执行
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
pinia也支持组合式写法
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
- ref() 就是 state 属性
- computed() 就是 getters
- function() 就是 actions
在组件里使用:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>
六、生命周期
- 没有了beforeCreated,created,被setup替代了。setup表示组件被创建之前、props被解析之后执行,它是组合式 API 的入口
- 选项式的 beforeDestroy、destroyed 被更名为 beforeUnmount、unmounted。
- 新增了两个选项式的生命周期 renderTracked、renderTriggered,它们只在开发环境有用,常用于调试。
- 在使用 setup组合时,不建议使用选项式的生命周期,建议使用 on* 系列 hooks生命周期。
import {
onBeforeMount, // 在组件被挂载之前被调用
onMounted, // 在组件挂载完成后执行
onBeforeUpdate, // 在组件即将因为响应式状态变更而更新其 DOM 树之前调用
onUpdated, // 在组件因为响应式状态变更而更新其 DOM 树之后调用
onBeforeUnmount, // 在组件实例被卸载之前调用
onUnmounted, // 在组件实例被卸载之后调用
onRenderTracked, // 当组件渲染过程中追踪到响应式依赖时调用
onRenderTriggered, // 当响应式依赖的变更触发了组件渲染时调用,仅在开发模式下可用
onActivated, // 组件实例是 <KeepAlive> 缓存树的一部分,当组件被插入到 DOM 中时调用。
onDeactivated, // 组件实例是 <KeepAlive> 缓存树的一部分,当组件从 DOM 中被移除时调用。
// 以上的在服务端渲染时候都不会被调用。
onErrorCaptured, // 在捕获了后代组件传递的错误时调用
onServerPrefetch // 异步函数,在组件实例在服务器上被渲染之前调用, 仅会在服务端渲染中执行
} from 'vue'
七、内置组件
- 新增了两个组件Teleport、Suspense
Teleport
它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue“把以下模板片段传送到 body 标签下”。
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
其实就相当于我们很多UI组件的模态框或dialog,有个append-to-body属性,就是这种效果。
Suspense
是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
组合式 API 中组件的 setup() 钩子可以是异步的:
export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}
如果使用 < script setup>,那么顶层 await 表达式会自动让该组件成为一个异步依赖:
<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>
<template>
{{ posts }}
</template>
异步组件默认就是“suspensible”的。这意味着如果组件关系链上有一个 < Suspense>,那么这个异步组件就会被当作这个 < Suspense> 的一个异步依赖。在这种情况下,加载状态是由 < Suspense> 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。
八、Hooks
在Vue2中,在同一个.vue组件中,当 data、methods、computed、watch 的体量较大时,代码将变得臃肿。为了解决代码臃肿问题,我们除了拆分组件外,引入mixins之外别无它法。
在Vue3中,同样存在这样的问题:当我们的组件开始变得更大时,逻辑关注点将越来越多,这会导致组件难以阅读和理解。但是,在Vue3中,我们除了可以拆分组件,还可以使用 Hooks封装来解决这一问题。
所谓 Hooks封装,就是把不同的逻辑关注点抽离出来,以达到业务逻辑的独立性。这一思路,也是Vue3 对比Vue2的最大亮点之一。
在 setup 组合的开发模式下,把具体某个业务功能所用到的 ref、reactive、watch、computed、watchEffect 等提取到一个以 use* 开头的自定义函数中去。
(1) 导入 defineComponent 和需要的 hooks,例如 ref 和 watch。
import { defineComponent, ref, watch } from 'vue';
(2)编写组件逻辑,使用 hooks 定义数据和逻辑。
export default defineComponent({
setup() {
// 定义响应式数据
const count = ref(0);
// 定义响应式组件引用
const myComponent = ref(null);
// 定义方法
const incrementCount = () => {
count.value++;
};
// 监听 count 的变化
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为了 ${newValue}`);
});
return {
count,
myComponent,
incrementCount,
};
},
});
在上面的代码中,我们使用 ref 定义了一个响应式数据 count,并且定义了一个方法 incrementCount 来修改这个数据。我们还使用 watch 监听 count 的变化,并在变化时输出变化前后的值。最后,我们将这些数据和方法通过 return 暴露出来,以供组件使用。
(3)在组件中使用暴露出来的数据和方法。
<template>
<div>
<span>计数器:</span>
<button @click="incrementCount">{{ count }}</button>
<my-component ref="myComponent" />
</div>
</template>
<script>
import { defineComponent, ref, watch } from 'vue';
import MyComponent from './MyComponent.vue';
export default defineComponent({
components: {
MyComponent,
},
setup() {
const count = ref(0);
const myComponent = ref(null);
const incrementCount = () => {
count.value++;
};
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为了 ${newValue}`);
});
return {
count,
myComponent,
incrementCount,
};
},
mounted() {
console.log(this.$refs.myComponent);
},
});
</script>
在上面的代码中,我们在组件中使用了暴露出来的数据和方法,例如使用 {{ count }} 来显示计数器的值。我们还将一个自定义组件 MyComponent 引入到了这个组件中,并在 mounted 钩子中打印了这个组件的引用。
封装成 use* 开头的Hooks函数,不仅可以享受到封装带来的便利性,还有利于代码逻辑的复用。Hooks函数的另一个特点是,被复用时可以保持作用域的独立性,即,同一个Hooks函数被多次复用,彼此是不干扰的。
我总结了两种场景:一种是功能类Hooks,即为了逻辑复用的封装;另一种是业务类Hooks,即为了逻辑解耦的封装。
九.写在最后
上面的内容还不完整,后续会慢慢补充完整。
如果内容有什么不对或者缺失,还请指教,谢谢。