本篇教程励志用简单的例子来教会你理解并使用常用vue3的新特性,适合于从vue2转vue3的码友,本文请按顺序阅读,否则可能会出现需要查找解释的情况。
在vue3.x中如果想让数据在改变时,视图层也相应发生变化就需要使用到响应性api。
想创建一个响应式数据,归根结底就两种方法 ref
、reactive
ref
接受一个内部值并返回一个响应式且可变的 ref 对象,该对象仅有一个value属性指向内部值, 可以用来读取和更改。
const num = ref(0);
console.log(num.value) // 0
num.value++; // 1
这样就可以创建一个具有响应性的数据,当然ref
也可以接收数组、对象、其他基础类型的值,但我们通常不建议在ref
中传递对象,因为响应式转换是深层的,它会影响所有它嵌套的属性,而我们应该尽量避免依赖原始对象。
reactive
返回一个对象的响应式副本。
const introduce = reactive({
name: '喵十一',
age: 19,
hobby: ['编程', '游戏', '运动']
})
console.log(introduce.name) // 喵十一
introduce.name = "十一"
console.log(introduce.name) // 十一
这样会返回一个具有相应式的对象副本,但我们不会在reactive中传递基础类型数据,原因请见下方。
ref 与 reactive的对比
可传递类型
ref
可传入基本类型和引用数据类型, 但不推荐传入对象
reactive
可传入对象/数组/json, 但不可以传入基本类型数据, 如果传入则不会返回proxy对象。
由于 reactive是基于proxy实现的响应式,所以如果返回的不是一个proxy对象则该数据不会具有响应式。
即修改数据也不会使得视图层发生变化。
修改方式
.value的方式进行修改
reactive
直接修改
reactive注意事项
如果reactive传入的对象中某一属性是用ref创建的响应式数据,那它将被自动解包。
本条极其重要。
const introduce = reactive({
name: '喵十一',
age: ref(19),
hobby: ['编程', '游戏', '运动']
})
console.log(introduce.age) // 19
console.log(introduce.age.value) // undefined
console.log(introduce.age.value = 1) // error
个人对解包的理解是变为了一个普通的属性,然后由于reactive的缘故又使整个对象具有了响应性
ref 和 reative的总结
通常情况下ref用于为基本类型创建响应性,reactive为引用类型创建响应性。
在解释这个名词之前,让我们先来了解一下vue2的选项式api的缺点
组合式api,顾名思义,就是将不同的业务逻辑拆分成一块块的像搭积木般构建起我们的业务。
这样就近乎完美的避开了vue2中选项式api的缺点
例子
我们使用一个极其简单的数量框的例子来介绍组合式api
<template>
<div class="combinationApi">
<div class="num">
<button @click="changeNum('-')">-</button>
<button>{{num}}</button>
<button @click="changeNum('+')">+</button>
</div>
<button @click="resetNum">重置</button>
</div>
</template>
<script>
import changeButton from "../composables/changeButton";
import resetButton from "../composables/resetButton";
import {ref} from "vue";
export default {
name: "combinationApi",
props: {
name: {
default: "123",
type: String
}
},
setup(props) {
// 创建一个响应式的数据
let num = ref(0);
// 使用组合式api
let { changeNum } = changeButton(num);
let { resetNum } = resetButton(num);
// 返回需要的内容
return {
num,
changeNum,
resetNum
}
}
}
</script>
可以看到我们在script中引入了两个js文件,changeButton
、resetButton
并在setup中使用了它们。
changeButton
的功能是将数量更改。
resetButton
的功能是将数量重置。
我们先来看一下这两个文件
changeButton
import { ref, reactive } from "vue";
export default function changeButton(num) {
// 改变数量事件
let changeNum = (type)=>{
type == "+" ? num.value++ : type == "-" && num.value > 0 ? num.value-- : '';
}
return {
changeNum
}
}
resetButton
export default function changeButton(num) {
// 重置按钮数量
let resetNum = ()=>{
num.value = 0;
}
return {
resetNum
}
}
功能非常的简单,他们都接收了一个参数 num,都返回了一个对 num 进行操作的方法。
这就是一个简易的组合式api,整个页面的逻辑就像搭积木一样一块一块的构建了起来,方便以后修改逻辑,找起来也不会麻烦。
setup函数会接受两个参数,props
、context
。
props
props是具有响应性的,当有新的prop传入时,它将会被更新。
正因为它具有响应性所以请不要使用结构的方式使用它,这样会失去响应性。
context
context是一个普通的js对象,这里面接收了剩余我们可能在setup中使用的值,因为是一个普通的js对象,咱们是可以放心使用结构的方式使用它
setup中可访问property
props
attrs
slots
emit
expose
setup中可返回的类型
对象
<template>
<div class="setup">
{{num}}
</div>
</template>
<script>
export default {
props: {
name: {
default: "123",
type: String
}
},
setup(props, context) {
let num = ref(0); // 该数据将在暴漏出去时自动解包
// 暴漏给template
return {
num
}
}
}
</script>
该返回的对象中的property 和 props中的property,都可在模板中直接使用,
其中的refs将会被自动解包, 所以在模板中访问时不需要在使用.value了
渲染函数
<script>
// 解构出创建渲染函数的h方法
import {h} from "vue";
export default {
props: {
},
setup(props, context) {
// 一个小姐姐的图片链接
let img = "https://img1.baidu.com/it/u=3149018235,1913956841&fm=253&fmt=auto&app=138&f=JPEG?w=231&h=500"
// 暴漏给template 一个能创建img的渲染函数
return ()=>h('img', {src: img});
}
}
</script>
本照片为百度网络资源,仅作展示:
这里返回的渲染函数将自动渲染在页面上。
查看更多渲染函数相关知识请点击这里
setup中的this
由于setup会在其他组件选项之前解析,这使得setup中的this和其他组件选择中的this完全不同,
因此在setup中使用this,因为不仅访问不到其他选项式api,还会造成和它们之间的混肴。
想使用setup语法糖非常简单,只需要在script标签上加上setup
<script setup>
....
</script>
接下来我们来讲一下setup的语法糖的好处
顶层的绑定会被暴露给模板
不需要再将数据抛出, 在此语法糖中创建的变量可以直接在模板中访问, ref创建的数据依然会被解包。
<template>
<div>{{name}}</div>
</template>
<script setup>
import {ref} from vue;
let name = ref('喵十一')
</script>
使用组件
引入的组件可以直接使用,不需要再次注册, 且引入的名称可以直接当自定义标签来使用。
<template>
<button></button>
</template>
<script setup>
import button from '@/component/button';
</script>
当然从单文件件中引入多组件
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
<script setup>
import * as Form from './form-components'
</script>
自定义指令
全局自定义指令可以直接使用,但如果在本地自定义指令则需要使用vNameDirective
的方式来命名。
例如:
<template>
<div v-msy-directive>This is a Heading</div>
</template>
<script setup>
const vMsyDirective = {
// 代码
}
</script>
defineProps 和 defineEmits
如果在setup语法糖中想使用像普通的 script 标签中的 props 和 emits, 则需要使用 defineProps 和 defineEmits来声明
<template>
</template>
<script setup>
let props = defineProps({
name: {
default: "喵十一",
type: String
}
});
let emits = defineEmits(['change']);
</script>
defineProps
和 defineEmits
仅在 setup
语法中使用且不需导入
defineExpose
用来在 setup 语法糖中确认要暴漏出去的属性,和前两个方法一样不需要导入。
useSlots 和 useAttrs
在 使用
slots
和 attrs
的情况应该是很罕见的,因为可以在模板中通过 $slots
和 $attrs
来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots
和 useAttrs
两个辅助函数:
useSlots
和 useAttrs
是真实的运行时函数,它会返回与 setupContext.slots
和 setupContext.attrs
等价的值,同样也能在普通的组合式 API 中使用。
需要和Suspense
组合使用,但Suspense
还在测试阶段,因为本文不做更多介绍,想要了解更多的可以去查询一下
props 和 emits 都可以使用传递字面量类型的纯类型语法做为参数给 defineProps
和 defineEmits
来声明:
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
defineProps
或 defineEmits
只能是要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错。
使用类型声明的时候,静态分析会自动生成等效的运行时声明,以消除双重声明的需要并仍然确保正确的运行时行为。
foo: string
类型中推断出 foo: String
。如果类型是对导入类型的引用,这里的推断结果会是 foo: null
(与 any
类型相等),因为编译器没有外部文件的信息。['foo', 'bar']
)。截至目前,类型声明参数必须是以下内容之一,以确保正确的静态分析:
现在还不支持复杂的类型和从其它文件进行类型导入。理论上来说,将来是可能实现类型导入的。
仅限类型的 defineProps
声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults
编译器宏:
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps(), {
msg: 'hello',
labels: () => ['one', 'two']
})
上面代码会被编译为等价的运行时 props 的 default
选项。此外,withDefaults
辅助函数提供了对默认值的类型检查,并确保返回的 props
的类型删除了已声明默认值的属性的可选标志。
由于 依赖于单文件上下文,所以当将其移动到
.js
或 .ts
将会失去作用。
一种将模板内容移动到 DOM Vue App 之外的方法。
可以用它来指定将内容渲染到 DOM
的哪个父元素下。
它可以做很多事情,且可以很好的解决我们的问题,比如 常见的全屏弹框组件
,如果我们想全凭 css
去挂在到全屏下通常时较困难的,因为我们可能会被引入组件的页面所影响,但我们如果使用 teleport
将组件渲染至 body
下则会全然不受印象。
<template>
<teleport to='body'>
<div class="mask">
<div class="container" :class="`${type}`">
<slot></slot>
</div>
</div>
</teleport>
</template>
<script setup>
let props = defineProps({
type: {
default: "top"
}
})
</script>
<style lang="less" scoped>
.mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
.container {
position: absolute;
left: 50%;
transform: translateX(-50%);
&.top {
top: 0;
}
&.bottom {
bottom: 0;
}
&.center {
top: 50%;
transform: translate(-50%, -50%);
}
}
}
</style>
这里我们可以看到指定了组件渲染的位置 body
用来指定渲染位置,类型 string
,一定要是一个有效的查询选择器或 HTMLElement
,请务必记住teleport
是将内容渲染到 Vue App
外,所以在 Vue App
内的查询器 或 HTMLElement
将是无效的
v-model的变化总结起来如下:
变更:
prop 和 默认事件名已更改
value
-> modelValue
;
input
-> updata:modelValue
;
新增:
移除:
v-bind
的 .sync
修饰符和组件的 model
选项已移除,可在 v-model
上加一个参数代来代替在 vue3.x
中我们如果想要改变 mode
的名称,可以为 v-model
传入一个参数
<EsyInput v-model:content="content"></EsyInput>
这样就为 v-model
传递了一个参数,也是下方的简写
<EsyInput
v-model:content="content"
@updata:content="content = $event"
></EsyInput>
v-model
<EsyInput v-model:content="content" v-model:text="text"></EsyInput>
v-model
自定义修饰符一般情况下我们使用 vue
内置的修饰符,如.trim
、.number
和 .lazy
。就可以满足需求了,但在某些情况下我门需要自定义的修饰符。
在组建的 props
中声明一个 modelModifiers
即可
export default {
props: {
// model 默认名
modelValue:{
type: String,
default: ''
},
// 自定义修饰符
modelModifiers: {
default: () => ({})
}
}
}
只需要在传入组件更新 v-model
数据时判断一下即可
export default {
props: {
// model 默认名
modelValue:{
type: String,
default: ''
},
// 自定义修饰符
modelModifiers: {
default: () => ({})
}
},
methods: {
emitValue = (e)=>{
let value = e.target.value;
// 如果存在自定义修饰符
if(this.props.modelModifiers.capitalize) {
console.log(`转换前:` + value)
// 以空格分开的每个单词或短句首字母大写逻辑
let arr = value.split(" ");
arr.forEach((item, index) => {
item ? arr[index] = item[0].toUpperCase() + item.slice(1) : '';
});
value = arr.join(" ");
console.log(`转换后:` + value)
}
this.$emit('update:modelValue', value)
}
}
}
}
// 使用自定义修饰符
<input-components v-model.capitalize="text"></input-components>
由于在 props
中声明了 modelModifiers
, 所以如果使用自定义修饰符的话,那么 modelModifiers.capitalize
一定为 true
, 就可以根据结果判断是否使用了自定义修饰符。
v-model
自定义修饰符对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + Modifiers
定义
export default {
props: {
content:{
type: String,
default: ''
},
// 自定义修饰符
contentModifiers: { // 重点
default: () => ({})
}
}
}
使用
// 使用自定义修饰符
<input-components v-model:content.capitalize="content"></input-components>
v-bind的绑定顺序会影响渲染的结果
在原来的vue2.x中如果在同一元素同时使用了 v-bind
和一个独立的 attribute
,那么这个独立的 attribute
总是会覆盖 v-bind
中的 attribute
。
<div class="ied" :v-bind="{class: 'dei'}"></div>
以上的渲染结果在vue2.x中被渲染成如下
<div class="ied"></div>
但在 vue3.x 中 是以后者覆盖前者的方式进行渲染,因此绑定的顺序决定了渲染的是谁
例如:
<div class="ied" :v-bind="{class: 'dei'}"></div>
在 vue3.x 中会被渲染成
<div class="dei"></div>
当然将独立的 attribute
放在后方也会渲染独立的 attribute
,这样的方式使得现在开发者能够对自己所希望的合并行为做更好的控制。
经过尤大和他们团队的努力,我们终于可以在 style
中使用 v-bind
来动态绑定一个属性了
<template>
<div class="vBind"></div>
</template>
<script>
export default {
setup(props, context) {
let backgroundColor = `#40faff`;
return {
backgroundColor
}
}
}
</script>
<style scoped>
.vBind {
width: 100px;
height: 100px;
background-color: v-bind(backgroundColor);
}
</style>
这个语法也同样适用于 语法糖,且支持
javascript
表达式,但需要使用引号包裹起来
<template>
<div class="vBind"></div>
</template>
<script setup>
let style = {
backgroundColor: "red"
}
let backgroundColor = "blue"
</script>
<style scoped>
.vBind {
width: 100px;
height: 100px;
background-color: v-bind(backgroundColor); /* setup 普通用法 */
background-color: v-bind('style.backgroundColor'); /* javascript表达式用法 需要带引号 */
}
</style>
这个语法的实现原理:
实际的值会被编译成 hash 的 CSS 自定义 property,CSS 本身仍然是静态的。自定义 property 会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式更新。
演示图
下方是我个人对整个过程的理解图,如有不对之处请大佬们指正。
在 vue2.x
中 同一元素上 v-for
的优先级总是高于 v-if
。
但 vue3.x
中 同一元素上 v-if
的优先级总是高于 v-for
。
当在 vue3.x
想使用自定义事件一定要在 emits
选项来定义组件可触发的事件。
这个是必须的因为 vue3.x
中移除了 .native
修饰符这会导致,任何未在 emits
中声明的事件监听器都会被算入组件的 $attrs
,并将默认绑定到组件的根节点上。
因此下方事件会执行两次,一次原生事件,一次自定义事件,所以一定要声明可出发的事件
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: [] // 不声明事件
}
</script>
因此正确的做法应该是下方这样
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: ["click"] // 声明事件
}
</script>
当使用 emits
的语法是对象不是数组时,则可以进行验证。
<script>
export default {
emits: {
click: (num)=>{
// 进行基本验证
if(num > 0) {
return true
} else {
console.warn('num must be greater than 1');
return false
}
}
}
}
</script>
当想要对一个事件进行验证时需要给他分配一个函数,该函数接收传递给 $emit
调用的参数,并返回一个布尔值以指示事件是否有效。
keyCode
作为 v-on
修饰符的支持$children
实例 propertypropsData
选项$destroy
实例方法。用户不应再手动管理单个 Vue 组件的生命周期。set
和 delete
以及实例方法 $set
和 $delete
。基于代理的变化检测已经不再需要它们了天才无非是长久的忍耐,努力吧!