Vue3的来由
使用Vue2.x的小伙伴都熟悉,Vue2.x中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法。那Vue3.x中就不可以这么玩了, 具体怎么玩我们后续再说, 先说一下Vue2.x版本这么写有什么缺陷,所以才会进行升级变更的。
回顾Vue2.x实现加减
<template>
<div>
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button @click="increase">➕1</button>
<button @click="decrease">➖1</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
computed: {
multiple() {
return 2 * this.count;
},
},
methods: {
increase() {
this.count++;
},
decrease() {
this.count--;
},
},
};
</script>
上面代码只是实现了对count的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:
甚至一个功能还会依赖其他功能,这样全搅合在一起,后期的维护成本可想而知。当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转,这其中的痛苦写过的都知道。
那开发者们就会想, 如果一个需求可以按照逻辑进行分割,将上面这张图变成下面这张图,是不是就清晰很多了呢,同时这样的代码可读性和可维护性也都将变的更高:
那么其实在vue2.x版本官方也给出了对应的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:
- 命名冲突问题
- 不清楚暴露出来的变量的作用
- 逻辑重用到其他 component 经常遇到问题
基于此,Vue3.x就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点学习Composition API。
Composition API(部分)
setup
setup是Vue3.x新增的一个选项,他是组件内使用Composition API的入口。
setup执行时机
实践出真知,如下:
export default defineComponent ({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
})
可以看出,setup执行时机是在beforeCreate之前执行,详细的可以看后面生命周期讲解。
WARNING
由于在执行setup时尚未创建组件实例,因此在 setup 选项中没有 this。
setup参数
使用setup时,它接受两个参数:
- props: 组件传入的属性
- context: Vue实例上下文对象
#Props
setup中接受的props是响应式的, 当传入新的props时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。
错误代码示例, 这段代码会让props不再支持响应式: ```css // demo.vue
export default defineComponent ({ setup(props, context) { const { secooName } = props console.log(secooName) }, })
那在开发中我们**想要使用解构,还能保持props的响应式**,有没有办法解决呢?大家可以思考一下,在后面toRefs学习的地方为大家解答。
<a name="ZaSl1"></a>
#### [#](https://fe.secoo.com/article/vue/%E5%9C%A8%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE%E4%B8%AD%E9%82%82%E9%80%85Vue3.0.html#context)Context
接下来我们来说一下setup接受的第二个参数context,我们前面说了setup中不能访问Vue2中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrs、slots和emit,分别对应Vue2.x中的$attrs属性、slots插槽 和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
```css
// demo.vue
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
reactive、ref与toRefs
在vue2.x中, 定义数据都是在data中, 但是Vue3.x 可以使用reactive和ref来进行数据定义。
那么ref和reactive他们有什么区别呢?分别什么时候使用呢?
区别:
图片引用自VueConf 2021大会上分享的一张ppt
使用场景:
ref和reactive都可以处理对js对象的双向绑定,但是vue官网建议:如果将对象分配为 ref 值,则通过 reactive 方法使该对象具有高度的响应式。如果对于处理js基础类型的双向绑定,那么只能使用ref来定义,因为reactive函数仅可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。
总结:
- js基础类型定义使用ref
- js引用类型建议使用reactive
接下来使用代码展示一下ref、reactive的使用:
<template>
<div>
<p>count: {{ count }}</p>
<p>姓名: {{ user.nickname }}</p>
<p>年龄: {{ user.age }}</p>
</div>
</template>
<script>
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
setup() {
const count = ref(0);
const user = reactive({ nickname: "hhh", age: 26, gender: "男" });
setInterval(() => {
count.value++
user.age++
}, 1000)
return {
count,
user
}
},
});
</script>
上面的代码中,我们绑定到页面是通过user.name,user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢?答案是不能直接对user进行解构, 这样会消除它的响应式, 这里就和上面我们说props不能使用ES6直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用toRefs。
toRefs会把reactive代理的对象属性全部转化成ref类型的数据,这样解构出来的数据依然具备响应式特性。具体使用方式如下:
<template>
<div>
<p>count: {{ count }}</p>
<p>姓名: {{ nickname }}</p>
<p>年龄: {{ age }}</p>
</div>
</template>
<script>
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
setup() {
const count = ref(0);
const user = reactive({ nickname: "hhh", age: 26, gender: "男" });
setInterval(() => {
count.value++
user.age++
}, 1000)
const { nickname, age } = toRefs(user)
return {
count,
nickname,
age
}
},
});
</script>
这里有一个个人建议的点是,有的同学可能会直接在返回值中去解构,像下面代码这样:
return {
count,
...toRefs(user)
}
这样解构的方式有一个缺点就是导出的值不够直观,如果该数据是在当前组件中定义的,这样解构问题不是很大。如果这个数据是从vuex或其他依赖方法中获取的话,直接导出给页面绑定,那么在后期维护的时候,你可能会疑惑页面绑定的值不知从何而来,像下面代码这样:
不推荐示例
setup() {
// 伪代码,假设vuex state里有data: { nickname: 'hhh'、age: 26 }这两条数据
const store = useStore()
return {
...store.state.data
}
},
推荐示例
setup() {
// 伪代码,假设vuex state里有data: { nickname: 'hhh'、age: 26 }这两条数据
const store = useStore()
const { nickname, age } = store.state.data
return {
nickname,
age
}
},
所以这里总结,如果模版依赖的数据是当前组件中定义的,使用…toRefs(data) 方式导出,否则使用const { data1, data2 } = toRefs(data) 方式导出,这样相对更加直观,也方便维护。
生命周期钩子
我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片是根据官网翻译后绘制的):
从图中我们可以看到Vue3.0新增了setup,这个在前面我们也详细说了, 然后是将Vue2.x中的beforeDestroy名称变更成beforeUnmount; destroyed表更为unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mount和unmount的过程。其他Vue2中的生命周期仍然保留。
上边生命周期图中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:
我们可以看到beforeCreate和created被setup替换了(但是Vue3中你仍然可以使用, 因为Vue3是向下兼容的, 也就是你实际使用的是vue2的)。其次,钩子命名都增加了on; Vue3.x还新增用于调试的钩子函数onRenderTriggered和onRenderTricked
下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x中的钩子是需要从vue中导入的:
import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from "vue";
export default defineComponent({
// beforeCreate和created是vue2的
beforeCreate() {
console.log("------beforeCreate-----");
},
created() {
console.log("------created-----");
},
setup() {
console.log("------setup-----");
// vue3.x生命周期写在setup中
onBeforeMount(() => {
console.log("------onBeforeMount-----");
});
onMounted(() => {
console.log("------onMounted-----");
});
// 调试哪些数据发生了变化
onRenderTriggered((event) => {
console.log("------onRenderTriggered-----", event);
})
}
});
computed 的用法
接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者,它也可以使用具有 get 和 set 函数的对象来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watch 与 watchEffect 的用法
watch
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, [options])
参数说明:
- source: 可以支持string, Object, Function, Array; 用于指定要侦听的响应式变量
- callback: 执行的回调函数
- options: 支持deep、immediate 和 flush 选项。
侦听ref定义的数据
import { defineComponent, ref, reactive, toRefs, watch } from "vue";
export default defineComponent({
setup() {
const year = ref(0)
setTimeout(() => {
year.value ++
},1000)
watch(year, (newVal, oldVal) =>{
console.log("新值:", newVal, "老值:", oldVal);
})
return {
year
}
},
});
侦听reactive定义的数据
const state = reactive({ nickname: "hhh", age: 26 });
setTimeout(() => {
state.age++
},1000)
// 修改age值时会触发 watch的回调
watch(
() => state.age,
(curAge, preAge) => {
console.log("新值:", curAge, "老值:", preAge);
}
);
侦听多个数据
上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch([() => state.age, year], ([curAge, newVal], [preAge, oldVal]) => {
console.log("新值:", curAge, "老值:", preAge);
console.log("新值:", newVal, "老值:", oldVal);
});
侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:
const state = reactive({
person: {
id: 100,
attrs: {
height: "175cm",
weight: "70kg"
}
}
});
watch(() => state.person, (newVal, oldVal) => {
console.log("新值:", newVal, "老值:", oldVal);
}, {
deep: true
});
如果不使用第三个参数deep:true, 是无法监听到数据变化的。
前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。
stop 停止监听
我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:
const stopWatchPerson = watch(() => state.person, (newVal, oldVal) => {
console.log("新值:", newVal, "老值:", oldVal);
}, {
deep: true
});
setTimeout(() => {
// 停止监听
stopWatchPerson()
}, 3000)
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
我对其的个人理解是,如果传入回调函数的数据是具有依赖性的,那么这个数据有变化watchEffect就会执行。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
执行结果首先打印一次count值;然后每隔100毫秒,打印count值。
从上面的代码也可以看出, 并没有像watch一样需要先传入依赖,watchEffect会自动收集依赖, 只要指定一个回调函数,在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以它与watch区别如下:
- watchEffect 不需要手动传入依赖
- watchEffect 会先执行一次用来自动收集依赖
- watchEffect 无法获取到变化前的值, 只能获取变化后的值
自定义 Hooks
开篇的时候我们使用Vue2.x写了一个实现加减的例子, 这里可以将其封装成一个hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。
useCount.js实现: ```css import { ref, computed } from ‘vue’
function useCount (initValue = 1) { const count = ref(initValue)
const multiple = computed(() => count.value * 2)
const increase = (arg) => { if (typeof arg !== ‘undefined’) { count.value += arg } else { count.value += 1 } }
const decrease = (arg) => { if (typeof arg !== ‘undefined’) { count.value -= arg } else { count.value -= 1 } }
return { count, multiple, increase, decrease } }
export default useCount
接下来在组件中使用useCount这个hook:
```css
<template>
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button @click="increase">加1</button>
<button @click="() => decrease(2)">减2</button>
</div>
</template>
<script>
import useCount from "@/hooks/useCount";
setup() {
const { count, multiple, increase, decrease } = useCount(9);
return {
count,
multiple,
increase,
decrease,
};
},
</script>
开篇Vue2.x实现,分散在data, methods, computed等, 如果刚接手项目,实在无法快速将data字段和methods关联起来,而Vue3的方式可以很明确的看出,将count相关的逻辑聚合在一起, 看起来清晰多了, 而且useCount还可以扩展更多的功能。
简单对比vue2.x与vue3.x响应式
其实在Vue3.x 还没有发布beta的时候, 很火的一个话题就是Vue3.x 将使用Proxy 取代Vue2.x 版本的 Object.defineProperty。那么为何要将Object.defineProperty换掉呢,咱们可以简单聊一下。
我相信大家在刚上手Vue2.x的时候就会经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,大家是否也一度充满疑惑。后来的学习过程中开始接触源码,才知道一切的根源都是Object.defineProperty。
简单对比
想要深入了解为什么Vue3.0使用Proxy实现数据劫持,而弃掉defineProperty的原因的小伙伴,可以自行查阅相关资料,这里我们就简单对比一下Object.defineProperty与Proxy:
- Object.defineProperty只能劫持对象的属性, 而Proxy是直接代理对象
由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是Proxy直接代理对象, 不需要遍历操作
- Object.defineProperty对新增属性需要手动进行Observe
因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再次使用Object.defineProperty进行劫持。也就是Vue2.x中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的, $set内部也是通过调用Object.defineProperty去处理的。
手写一个简单的Vue3.0源码的响应式
function trigger () {
console.log('视图更新触发');
}
function isObject(target) {
return typeof target === 'object' && target !== null
}
// 哈希表
let toProxy = new WeakMap()
let toRaw = new WeakMap()
// 响应式代理函数
function reactive(target) {
if (!isObject(target)) {
return target
}
const proxy = toProxy.get(target)
if (proxy) {
return proxy
}
if (toRaw.has(target)) {
return target
}
const handles = {
set(target, key, value, receiver) {
const hadKey = Array.isArray(target) ? key < target.length : target.hasOwnProperty(key)
if (!hadKey) {
trigger() // add
} else {
trigger() // set
}
let res = Reflect.set(target, key, value, receiver)
return res
},
get(target, key) {
const val = Reflect.get(target, key)
if (isObject(target[key])) {
return reactive(val)
}
return val
},
deleterProperty(target, key) {
delete Reflect.deleteProperty(target, key)
}
}
const observed = new Proxy(target, handles)
toProxy.set(target, observed)
toRaw.set(observed)
return observed
}
Teleport
Teleport是Vue3.x新推出的功能,没听过这个词的小伙伴可能会感到陌生;翻译过来是传送的意思,可能还是觉得不知所以,那么下边我给大家形象的描述一下。
Teleport 是什么
Teleport 就像是哆啦A梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到Teleport的特性呢,举个例子:
我们在实际开发中经常会遇到这样的情形,在子组件Header中使用到Dialog组件,此时Dialog组件就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都将变得繁琐。
正常来说,Dialog组件从用户感知的层面,应该是一个独立的组件,从dom结构应该完全剥离Vue顶层组件挂载的DOM;同时还可以使用到Vue组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog,又希望渲染的DOM结构不嵌套在组件的DOM中。
此时就需要Teleport上场了,我们可以用
接下来就用代码演示下Teleport的使用方式。
Teleport的使用
我们希望Dialog渲染的dom和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的节点:
// index.html
<body>
<div id="app"></div>
+ <div id="dialog"></div>
</body>
定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致
<template>
<teleport to="#dialog">
<div class="dialog">
<div class="dialog_wrapper">
<div class="dialog_header" v-if="title">
<slot name="header">
<span>{{ title }}</span>
</slot>
</div>
</div>
<div class="dialog_content">
<slot></slot>
</div>
<div class="dialog_footer">
<slot name="footer"></slot>
</div>
</div>
</teleport>
</template>
最后在一个子组件Header.vue中使用Dialog组件,这里主要演示Teleport的使用,不相关的代码就省略了。
// header.vue
<div class="header">
...
<navbar />
+ <Dialog v-if="dialogVisible"></Dialog>
</div>
...
Dom渲染效果如下:
可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与
Suspense
WARNING
试验性
Suspense 是一个试验性的新特性并且其 API 可能随时更改。特此声明以便社区能够为当前的实现提供反馈。
它不应该被用在生产环境。
虽然官网已经申明这是一个试验性的特性,不应该在生产环境使用,但是我们也可以提前了解下这个新增的特性的用处。我们先通过Vue2.x中的一些场景来认识它的作用。
Vue2.x中应该经常遇到这样的场景:
<template>
<div>
<div v-if="!loading">
...
</div>
<div v-if="loading">
加载中...
</div>
</div>
</template>
在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。
Vue3.x新推出的内置组件Suspense, 它提供两个template slot, 刚开始会渲染一个fallback状态下的内容,直到到达某个条件后才会渲染default状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单了
WARNING
如果使用Suspense让后代组件触发 fallback 的方式是从 setup 函数返回一个promise
Suspense 组件的使用:
<Suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</Suspense>
asyncComponent.vue异步组件:
<template>
<div>
<h4>这是一个异步加载数据</h4>
<p>用户名:{{ user.nickname }}</p>
<p>年龄:{{ user.age }}</p>
</div>
</template>
<script>
import { defineComponent } from "vue"
import axios from "axios"
export default defineComponent({
async setup(){
const rawData = await axios.get("http://xxx.sec.cn/user")
return {
user: rawData.data
}
}
})
</script>
片段(Fragment)
在 Vue2.x 中, template模板中只允许有一个根节点:
<template>
<div>
<span></span>
<span></span>
</div>
</template>
但是在 Vue3.x 中,你可以直接写多个根节点:
<template>
<span></span>
<span></span>
</template>
更好的 Tree-Shaking
Vue3.x 在考虑到 tree-shaking 的基础上重构了全局和内部API, 表现结果就是现在的全局API需要通过 ES Module的引用方式进行具名引用, 比如在Vue2.x中,我们要使用nextTick:
// vue2.x
import Vue from "vue"
Vue.nextTick(() => {
...
})
Vue.nextTick() 是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick() 只是 Vue.nextTick() 的一个简易包装,只是为了方便而把后者的回调函数的 this 绑定到了当前的实例。虽然我们可以借助 webpack 的 tree-shaking ,但是不管我们实际上是否使用Vue.nextTick(),最终都会打包进我们的生产代码, 因为 Vue实例是作为单个对象导出的, 打包器无法检测出代码中使用了对象的哪些属性。
在 Vue3.x中改写成这样:
import Vue, { nextTick, reactive } from "vue"
Vue.reactive // undefined
nextTick(() => {
...
})
常用的Vue语法变更
自定义指令
首先回顾一下 Vue2 中实现一个自定义指令:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时...
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
在 Vue2 中,自定义指令通过以下几个可选钩子创建:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
在 Vue3 中对自定义指令的 API进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下:
所以在 Vue3 中, 可以这样来自定义指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的v-focus指令, 如下:
<input v-focus />
v-model 升级
在 Vue3 beat版本的时候就了解到 v-model 发生了很大的变化, 使用过了之后才真正的get到这些变化, 我们先纵观一下发生了哪些变化, 然后再针对的说一下如何使用:
- 变更:在自定义组件上使用 v-model 时, 属性以及事件的默认名称变了
- 变更:v-bind 的 .sync 修饰符在 Vue3 中又被去掉了, 合并到了 v-model 里
- 新增:同一组件可以同时设置多个 v-model
- 新增:开发者可以自定义 v-model 修饰符
在 Vue2 中, 在组件上使用 v-model 其实就相当于传递了value属性, 并触发了input事件:
<!-- Vue2 -->
<search-input v-model="searchValue" />
<!-- 相当于 -->
<search-input :value="searchValue" @input="searchValue = $event.target.value" />
这时 v-model 只能绑定在组件的value属性上,那我们就不开心了,我们就想给自己的组件用一个别的属性,并且我们不想通过触发input来更新值,在.async出来之前,Vue2 中这样实现:
// 子组件:searchInput.vue
export default {
model: {
prop: 'search', // 为了和v-model默认的value区分,将prop改为自定义的名称
event: 'change' // 为了和v-model默认的input区分,将event改为change
}
}
修改后, searchInput 组件使用v-model就相当于这样:
<search-input v-model="searchValue" />
<!-- 相当于 -->
<search-input :search="searchValue" @change="searchValue = $event.target.value" />
但是在实际开发中,有些场景我们可能需要对一个 prop 进行“双向绑定”, 这里以最常见的modal模态框组件为例子,modal非常适合属性双向绑定,外部可以控制组件的visible显示或者隐藏,组件内部关闭可以控制visible属性隐藏,同时visible属性同步传输到外部。组件内部,当我们关闭modal时, 在子组件中以update:PropName模式触发事件:
this.$emit('update:visible', false)
然后在父组件中可以监听这个事件进行数据更新:
<modal :visible="isVisible" @update:visible="isVisible = $event.target.value" />
此时我们也可以使用v-bind.async来简化实现:
<modal :visible.async="isVisible" />
上面回顾了 Vue2 中v-model实现以及组件属性的双向绑定,那么在 Vue3 中应该怎样实现的呢?
在 Vue3 中,在自定义组件上使用v-model,相当于传递一个 modelValue 属性, 同时触发一个 update:modelValue 事件:
<modal v-model="isVisible" />
<!-- 相当于 -->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event.target.value" />
如果要绑定属性名, 只需要给v-model传递一个参数就行, 同时可以绑定多个v-model:
<modal v-model:visible="isVisible" v-model:content="content" />
<!-- 相当于 -->
<modal
:visible="isVisible"
@update:visible="isVisible = $event.target.value"
:content="content"
@update:content="content = $event.target.value"
/>
由此看出,这个写法完全没有.async什么事了, 所以,Vue3 中又抛弃了.async写法, 统一使用v-model来进行数据的双向绑定了。
最后关于v-model的就是 Vue3.x 新增了自定义修饰符,让我们来看下这个功能。
在Vue2.x中我们知道v-model提供了.trim,.number,.lazy 3个内置修饰符,但是在某些情况下,我们可能还需要添加自己定义的修饰符。比如像下面这个需求。
需求:用户输入的字符中如果有字母,则进行大写转换。
让我们创建一个自第一修饰符capitalize,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop。
需要注意的是,当组件的created生命周期钩子触发时,modelModifiers prop 会包含其值为true的capitalize修饰符。
<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `
<input type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
})
现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 元素触发 input 事件时,我们都将字符串大写。
<div id="app">
<my-component v-model.capitalize="myText"></my-component>
{{ myText }}
</div>
const app = Vue.createApp({
data() {
return {
myText: ''
}
}
})
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
template: `<input
type="text"
:value="modelValue"
@input="emitValue">`
})
app.mount('#app')
对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + “Modifiers”:
<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
<input type="text"
:value="description"
@input="$emit('update:description', $event.target.value)">
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
ok, 到这里基本就将 Vue3.0 在这次实战项目中遇到或经常遇到的一些特性知识分享完毕了。希望对大家有所帮助~
总结
本人在实战项目中用Vue3新语法开发的一个最直观体验就是,灵活,复用逻辑解耦也变的更加方便,易维护。灵活性高的同时就要求我们要有组件逻辑解耦的能力,这样才能写好一个优秀的组件。还没用过的小伙伴赶快亲自体验一下吧~