前言
距离 vue3 2020年09月18日正式发布已经过去快两年半了, 以前还能说是对新技术、新特性的观望,新技术用到生产会产生什么什么样不可预知的问题,但是经过两年半的沉淀该有的都有了,刚好也是有个新起的项目技术栈使用的是vue3,经过一个多月vue3的体验,对这个学习过程记录下来做个总结,也便于日后的梳理。
版本
既然要开始使用vue3,那就直接从最新的版本开始"vue": "^3.2.0"
。3.2之后只需在script
中添加setup
,然后就可以在标签中直接使用vue3的组合式API
,也不用写 setup 函数,响应式的属性和方法也不用 return 出来才能使用,像 ref
, reactive
这些方法通过 unplugin-auto-import
这个插件也不用每次都 import
导入进来,总之比最开始的时候在书写方面简洁了许多,要不怎么说是经过了两年半的沉淀呢。
准备工作
- 初始化
npm init vue@latest
- 安装 vscode 插件 Volar,注意 vue2项目中使用 Vetur,vue3中使用Volar
- 使用
unplugin-auto-import
插件给每个.vue
文件导入vue组合式API
- 将浏览器的启用自定义格式设置工具勾选上
可以直观的看到响应式变量的值,调试的时候不用多点一次。
接下来让我们愉快的体验下vue3 setup,先用一张图感受一下组合式API的直观感受
认识 ref 系列
ref
ref: 接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
,所以访问的时候要通过 .value
的形式访问,在 <tamplate>
会自动解析,不需要 .value
const state = ref(1)
console.log('state---', state)
console.log('state.value---', state.value)
shallowRef
shallowRef: ref()
的浅层作用形式,只有对 .value
的访问是响应式的,和 ref()
不同的是不会被深层递归地转为响应式。
<template>
<div>state:{{ state }}</div>
</template>
<script setup>
const state = shallowRef({ count: 1 })
// 不会触发视图更改
setTimeout(() => {
state.value.count = 2
}, 2000)
// 会触发视图更改
setTimeout(() => {
state.value = { count: 2 }
}, 2000)
</script>
ref()
和shallowRef()
不能同时使用,ref会调用triggerRefValue会更新视图,会导致shallowRef的数据一并被更新
triggerRef
triggerRef: 强制触发依赖于一个浅层ref的副作用,这通常在对浅引用的内部值进行深度变更后使用,比如更新 shallowRef()
深层的视图更新。
const state1 = shallowRef({ count: 1 })
setTimeout(() => {
// 不会触发更改
state.value.count = 2
// 调用triggerRef会触发视图更改
triggerRef(state)
}, 2000)
customRef
customRef: 创建一个自定义的 ref,提供的track, trigger这两个依赖的收集和触发方法,也是方便我们在任何地方调用,让 ref 根据自己的需求更加灵活,比如在 set 的时候加个防抖,一般 track 收集在 get 中调用,trigger 触发在 set 中调用,除非你有自己的想法。
<template>
<div>name:{{ name }}</div>
<button @click="change">修改 customRef</button>
</template>
<script setup>
const myRef = value => {
let timer
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newVal) {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('触发了set')
value = newVal
trigger()
}, 500)
},
}
})
}
const name = myRef('测试')
const change = () => {
name.value = '测试变了'
}
</script>
认识Reactive系列
reactive
reactive: 返回一个对象的响应式代理,用来绑定复杂的数据类型,不可以绑定普通的数据类型,相比 ref
,不用通过 .value
的方式来访问
let obj = reactive({ count: 0 })
obj.count = 2
关于数组异步赋值问题及解决方案,在开发中经常碰到异步赋值的问题,在vue3中不能通过像vue2那样直接用=
赋值的操作,这样会破坏数据原有的响应式,只能通过数据原有的方法,或者在它的外层再包裹一层结构来解决
let arr = reactive([])
setTimeout(() => {
arr = [1, 2, 3] // 这样arr赋值会脱离响应式
console.log(arr);
},1000)
// 解决办法:使用数组的push方法
setTimeout(() => {
arr.push(...[1, 2, 3])
console.log(arr)
}, 1000)
// 解决办法:再外层包裹一个对象
let arr = reactive({
list: [],
})
setTimeout(() => {
arr.list = [1, 2, 3]
console.log(arr)
}, 1000)
shallowReactive
shallowReactive: reactive()
的浅层作用形式。和 reactive()
不同的是这里没有深层级的转换,一个浅层响应式对象里只有根级别的属性是响应式的,如果是深层的数据只会改变值,不会改变视图
<template>
<div>shallowReactive:state:{{ state }}</div>
<button @click="change1">test1</button>
<button @click="change2">test2</button>
</template>
<script setup>
let obj = {
a: 1,
first: {
b: 2,
second: {
c: 3,
},
},
}
const state = shallowReactive(obj)
const change1 = () => {
state.a = 7
}
const change2 = () => {
state.first.b = 8
state.first.second.c = 9
console.log(state)
}
</script>
如上图所示,点击
test2
的时候虽然数据变了,但是视图并未更新,也不能像shallowRef
那样通过triggerRef
来解决视图更新,而且也不能和reactive
一起使用,一般只会在数据层级深且数据量大只使用顶级属性的时候才会用shallowReactive
解决性能问题
认识to系列
toRef: 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。重点是基于响应式对象,用它包裹普通对象没有什么意义,虽然值会改变,但是是非响应式的。
const obj = reactive({
foo: 2,
bar: 1,
})
const barRef = toRef(obj, 'bar')
// obj响应式对象的bar属性 转化为响应式对象
const change = () => {
barRef.value++
console.log('obj', obj)
console.log('barRef', barRef)
}
const change2 = () => {
obj.bar++
console.log('obj', obj)
console.log('barRef', barRef)
}
toRefs
toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref,每个单独的 ref 都是使用 toRef()
创建的,可以帮我们批量创建ref来方便我们解构使用。
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state)
const { foo, bar } = toRefs(state)
foo.value = 999
console.log('state:', state)
console.log('stateAsRefs:', stateAsRefs)
console.log('foo:', foo)
console.log('bar:', bar)
toRaw
toRaw: 根据一个 Vue 创建的代理返回其原始对象,将响应式对象转化为普通对象,这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,需谨慎使用。
const foo = {}
const reactiveFoo = reactive(foo)
reactiveFoo.a = 1
const toRawFoo = toRaw(reactiveFoo)
console.log('foo:', foo)
console.log('reactiveFoo:', reactiveFoo)
console.log('toRawFoo:', toRawFoo)
console.log('toRawFoo === foo:', toRawFoo === foo)
readonly和shallowReadonly
readonly
readonly: 接受一个对象 (不论是响应式还是普通的) 或是一个ref,返回一个原值的只读代理,强制更改会有警告。
const state = reactive({ count: 1 })
const copy = readonly(state)
copy.count++
shallowReadonly
shallowReadonly: readonly()
的浅层作用形式,只有根层级的属性变为了只读,深层次的属性修改不会有警告。
computed计算属性
computed: 就是当依赖的属性的值发生变化的时候,才会触发更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。它接受一个getter函数,返回一个只读的响应式ref对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
computed
总的来说vue2和vue3的差别不是很大,就是写法有些区别。
let price = ref(1)
// 只读
let cPrice = computed(() => {
return '¥' + price.value
})
console.log('cPrice:', cPrice.value)
// 可写
let cWritePrice = computed({
get() {
return '¥' + price.value
},
set(value) {
price.value = 'set' + value
},
})
cWritePrice.value = 2
console.log('price:', price.value)
console.log('cWritePrice:', cWritePrice.value)
watch侦听器
watch: 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
- 第一个参数是侦听器的源。这个来源可以是以下几种:
-
一个函数,返回一个值
const state = reactive({ count: 0 }) watch( () => state.count, (newVal, oldVal) => { console.log(newVal, oldVal) } )
-
一个 ref
const count = ref(0) watch(count, (newVal, oldVal) => { console.log(newVal, oldVal) })
-
一个响应式对象,侦听器会自动启用深层模式,但是无法正确获得oldValue!!,会自动启用深层模式
const state = reactive({ foo: { a: 1, }, bar: { b: 2, }, }) watch(state, (newVal, oldVal) => { console.log(newVal, oldVal) // 下图可以看出oldVal和newVal是一样的 }, { // deep: true // 自动启用深层模式 })
-
或是由以上类型的值组成的数组
watch( [() => state.foo, () => state.bar], (newValue, oldValue) => { console.log(newValue, oldValue) }, { deep: true, } )
-
- 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
- 第三个可选的参数是一个对象,支持以下这些选项:
-
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。 -
deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调,使用reactive监听深层对象开启和不开启deep 效果一样 -
flush
:调整回调函数的刷新时机。pre sync post 更新时机 组件更新前执行 强制效果始终同步触发 组件更新后执行
-
watchEffect高级侦听器
watchEffect: 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(() => {
const bar = state.bar
const foo = state.foo
console.log('watchEffect配置的回调执行了')
})
watchEffect,watch,computed的区别
-
watch
:既要指明监听的属性,也要指明监听的回调。 -
watchEffect
:不用指明监听哪个属性,监听的回调中用到哪个属性,那就监听哪个属性。 -
watchEffect
有点像computed
,但computed
注重的计算出来的值(回调函数的返回值),必须要写返回值,而watchEffect
更注重的是过程(回调函数的函数体),不用写返回值。
自定义hook
函数
什么是hook
hook
本质是一个函数,把setup
函数中使用的组合式API进行了封装。
和vue2.x中的mixin
比较
- mixin的缺点:变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护;多个
mixin
的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突,所以在 vue3 中已弃用。 - 自定义
hook
的优势:复用代码, 很清楚复用功能代码的来源。
实现一个记录当前鼠标点击位置的hook
创建一个hooks文件夹,里面创建文件usePoint.js
import { reactive, onMounted, onBeforeUnmount, toRefs } from 'vue'
export default function () {
let point = reactive({
x: 0,
y: 0,
})
const savePoint = event => {
point.x = event.pageX
point.y = event.pageY
console.log(event.pageX, event.pageY)
}
onMounted(() => {
window.addEventListener('click', savePoint)
})
onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})
return toRefs(point)
}
在组件中使用
<template>
<h2>当前点击时鼠标的坐标为:x:{{pointX}},y:{{pointY}},point:{{ point }}</h2>
</template>
<script setup>
import usePoint from '@/hooks/usePoint'
const point = usePoint()
const { x: pointX, y: pointY } = usePoint()
</script>
CSS相关
新增的选择器
- 深度选择器
:deep()
:处于scoped
样式中的选择器如果想要做更“深度”的选择,可以使用:deep()
这个伪类,可以不用在使用::v-deep
- 插槽选择器
:slotted
:默认情况下,作用域样式不会影响到<slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用:slotted
伪类以明确地将插槽内容作为选择器的目标 - 全局选择器
:global
:如果想让其中一个样式规则应用到全局,可以使用:global
伪类来实现,不用再另外创建一个<style>
CSS Modules
CSS Modules: <style module>
标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style
对象暴露给组件,也可以通过 module <style module="classes">
自定义注入名称
<template>
css modules
<!-- 默认注入名称 -->
<p :class="$style.red">red</p>
<!-- 自定义注入名称 -->
<p :class="classes.green">green</p>
</template>
/* 默认注入的名称 $style */
<style module>
.red {
color: red;
}
</style>
/* 自定义注入的名称 classes */
<style module="classes">
.green {
color: green;
}
</style>
在css中使用js变量
CSS 中的 v-bind()
:单文件组件的 <style>
标签支持使用 v-bind
CSS 函数将 CSS 的值链接到动态的组件状态,且支持 JavaScript 表达式 (需要用引号包裹起来)
<template>
<p>hello</p>
</template>
<script setup>
const theme = {
color: 'red'
}
</script>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
新增的内置组件
Fragment
- 在Vue2中: 组件
<template>
必须有一个根标签 - 在Vue3中: 组件
<template>
可以没有根标签, 内部会将多个标签包含在一个Fragment
虚拟元素中 - 好处: 减少标签层级, 减小内存占用
Teleport
-
Teleport 可以将其插槽内容渲染到指定 DOM 节点,不受父级
style
、v-show
等属性影响,但data
、prop
数据依旧能够共用的技术; -
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。
-
使用: 通过to 属性插入到指定元素位置,如 body,html,自定义className等等,不能是组件中的某些位置否则会报错。
<teleport to="移动位置">我是一个弹窗<</teleport>
Suspense
<Suspense>
接受两个插槽:#default
和#fallback
。它将在内存中渲染默认插槽的同时展示后备插槽内容。- 如果在渲染时遇到异步依赖项 (异步组件和具有
async setup()
的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。 <script setup>
中可以使用顶层await
。结果代码会被编译成async setup()
- 使用:实际开发中没有使用,个人觉得列表数据加载中配合骨架屏使用应该不错
示例
异步组件
<template>
<div>异步组件</div>
</template>
<script setup>
const res = async () => {
return await new Promise(resolve => {
setTimeout(() => {
resolve()
}, 2000)
})
}
await res()
</script>
异步引入组件
<template>
<Suspense>
<AsyncView />
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
</template>
<script setup>
const AsyncView = defineAsyncComponent(() => import('./AsyncView.vue'))
</script>
v-model
v-model
其实是一个语法糖 通过props和emit组合而成的,在Vue3中v-model 是破坏性更新的
vue2和vue3中两者的区别
- 默认值的改变
- prop:value -> modelValue
- 事件:input -> update:modelValue
- 新增 支持多个v-model
- 新增 支持自定义 修饰符 Modifiers
- v-bind 的 .sync 修饰符和组件的 model 选项已移除
示例
<!-- 父组件 -->
<UserName v-model:first-name="first" v-model:last-name="last" />
<!-- 子组件 -->
<template>
<input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
<input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>
<script setup>
defineProps({
firstName: String,
lastName: String,
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
也可以看看我之前写的vue2和vue3组件封装父子组件之间的通信之v-model
指令directive
指令钩子函数
相比vue2中的指令,vue3中的生命周期更好理解,它的生命周期和vue2的生命周期触发时机是一样的,参数和vue2基本一样。
const vMyDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
// 简写 对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为
// 除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示
const vMyDirective = (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
/* ... */
}
钩子参数
-
el
:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
-
vnode
:代表绑定元素的底层 VNode。 -
prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
指令注册
全局注册
// 全局注册
const app = createApp({})
app.directive('myDirective', {
/* ... */
})
局部注册:在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。
const vMyDirective = () => {
/* ... */
}
实现一个简单的指令v-color
<template>
<div style="width: 200px; height: 200px" v-color="'red'"></div>
</template>
<script setup>
const vColor = (el, binding) => {
el.style.background = binding.value
}
</script>
最后
文中有问题或者有异议也欢迎大家指出。