简介
有没有小伙伴跟笔者一样vue3项目做了好几个了,但是一直没有去总结vue3的新特性呢?今天笔者通过对比vue2来总结vue3新特性,希望可以让你们在回顾vue2知识点的时候还能学习vue3新的知识。相信你认真看完一定会有收获。
新插件
正所谓工欲善其事,必先利其器。在讲解vue3新特性之前,笔者先来介绍几个插件。这样会大大提高我们的开发效率和体验。
Volar
使用vscode开发vue2项目的小伙伴肯定都认识Vetur这个神级插件。但是在vue3中这个插件就显得捉襟见肘了,比如vue3的多片段这个插件就会报错。
这个时候就需要使用Volar,Volar可以理解为Vue3版本的Vetur,代码高亮,语法提示,基本上Vetur有的它都有。
Vue 3 Snippets
在vue2中我们一直使用Vue 2 Snippets,在vue3我们推荐使用Vue 3 Snippets,因为它支持vue3的同时完全向前兼容vue2,所以小伙伴们赶快去升级吧。
Vue.js devtools beta
vue2版本的chrome devtools不再支持vue3,vue3我们需要单独下载Vue.js devtools beta。(下载devtools是需要梯子的哦,如果没有可以联系笔者)。
在下载Vue.js devtools beta之前,我们需要先卸载vue2版本的Vue.js devtools,不然会有警告。
兼容性
vue3固然好用但是我们还是不能盲目追求新东西,在使用vue3开发之前我们需清楚的知道它的兼容性。
vue2 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的浏览器。
vue3 不支持 IE11 及以下版本。
响应性 API
在vue2中,我们只要定义在data()方法中的数据就是响应式数据。或者使用Vue.observable()方法来定义响应式数据。
还可以使用this.$set( target, propertyName/index, value )或Vue.set( target, propertyName/index, value )来给对象或数组添加响应式属性。使用this.$delete( target, propertyName/index)或Vue.delete( target, propertyName/index)来给对象或数组删除响应式属性。
在vue2中使用Vue.observable()方法。
const state = Vue.observable({ count: 0 })但在vue3中主要是使用ref和reactive来定义响应式数据。由于vue3使用的是proxy进行响应式监听,所以新增、删除属性也都是响应式的,也就不需要使用上面的set delete了。
ref和isRef
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
一般用来定义基本类型的响应式数据。注意这里说的是一般,并不是说ref就不能定义引用类型的响应式数据。
使用ref定义的响应式数据在setup函数中使用需要加上.value,但在模板中可以直接使用。
isRef检查值是否为一个 ref 对象。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>ref除了定义响应式数据还可以定义模板引用,类似vue2的this.$refs这个后面笔者在讲模板引用的时候会细说。
<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>shallowRef
创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。
这句话怎么理解呢?就是我们使用shallowRef创建出来的数据不是响应式的,也就是我们的修改页面并不会重新渲染。但是我们直接修改数据.value是会响应式的。
下面我们来看例子。
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};通过上面的例子我们可以发现,当响应式数据是基本数据类型的时候ref和shallowRef没有差别。但是如果数据是引用数据类型的话ref的数据是响应式的而shallowRef不是,shallowRef需要给value重新赋值才会触发响应式。
reactive和isReactive
reactive用来定义引用类型的响应式数据。注意,不能用来定义基本数据类型的响应式数据,不然会报错。
reactive定义的对象是不能直接使用es6语法解构的,不然就会失去它的响应式,如果硬要解构需要使用toRefs()方法。
isReactive用来检查对象是否是由 reactive 创建的响应式代理。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>reactive 将解包所有深层的 refs,同时维持 ref 的响应性。
怎么理解这句话呢,就是使用reactive定义响应式对象,里面的属性是ref定义的话可以直接赋值而不需要再.value,并且数据的修改是响应式的。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 3shallowReactive
浅响应式,创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。
并且与 reactive不同,任何使用 ref 的 property 都不会被代理自动解包。
简单理解就是响应式只会在第一层,不会深层响应式。类似于浅拷贝。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // falsereadonly和isReadonly
接受一个对象 (响应式或纯对象) 或 ref数据 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。
怎么理解这句话呢,就是说只要是对象不管是普通对象还是reactive定义的对象或者是ref定义的数据,定义成readonly后就不能被修改了。
这里需要特别注意,是readonly返回的对象变成只读,源对象不会受到影响,所以修改源对象还是可以的。
isReadonly用来检查对象是否是由 readonly 创建的只读代理。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true与 reactive 一样,如果任何 property 使用了 ref,当它通过代理访问时,则被自动解包。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 123shallowReadonly
浅只读,创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。
并且与 readonly不同,任何使用 ref 的 property 都不会被代理自动解包。
简单理解就是只读的限制只会在第一层,不会深层只读。类似于浅拷贝。
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};isProxy
检查对象是否是由 reactive或 readonly 创建的 proxy。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>0上面的例子有些小伙伴看了会比较懵逼,为什么readonly有些是true有些又是false呢?其实你弄懂了readonly就大概会清楚了。readonly是不能处理基本数据类型的,所以readonly不成功就会返回false。
toRaw
返回 reactive 或 readonly代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。不建议保留对原始对象的持久引用。请谨慎使用。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>1markRaw
标记一个对象,使其永远不会转换为 proxy。返回对象本身。
因为不会被proxy,也就是说不会响应式,相当于一个普通值。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>2unref
如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>3toRef
可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>4当你要将 prop 的 ref 传递给复合函数时,toRef 很有用:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>5即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用,可选 prop 并不会被 toRefs 处理。
toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>6当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>7toRefs 只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef。
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。
这个在我们自定义响应式的时候非常有用。比如我们在获取、设置值的时候做些特殊处理。
这个在vue2中是没办法直接修改响应值的实现的,但是在vue3可以。
下面是一个延迟响应式的例子。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>8triggerRef
手动执行与 shallowRef 关联的任何作用 (effect)。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>9组合式 API
为了让相关代码更紧凑vue3提出了组合式api,组合式api能将同一个逻辑关注点相关代码收集在一起。 组合式api的入口就是setup方法。
setup
用官方语言说,setup是一个组件选项,在组件被创建之前,props 被解析之后执行。它是组合式 API 的入口。
setup的写法有两种,可以跟vue2一样直接导出也可以导出defineComponent对象。若要对传递给 setup() 的参数进行类型推断,你需要使用 defineComponent。
<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>0执行时机
从生命周期的角度来看,它会在beforeCreate之前执行。也就是创建组件会依次执行setup、beforeCreate、create。
this指向
在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。
参数
setup 选项是一个接收 props 和 context 的函数。
props
setup 函数中的第一个参数是 props。props就是我们父组件给子组件传递的参数。
正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。
<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>1因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构请使用toRefs方法。
<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>2如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:
<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>3context
context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。
<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>4attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 的 property 是非响应式的。如果你打算根据 attrs 或 slots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。
这里我们重点说下expose的使用。
假如我们想在父组件中直接调用子组件的方法该怎么做呢?我们就可以在子组件中使用expose把属性或方法暴露出去。在父组件我们就可以通过子组件的ref直接调用了。
<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>5返回值
setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。所以我们在模板中需要使用到的数据都需要通过setup方法return出来。
上面的话怎么理解呢?就是我们在模板,或者vue2选项式写法的计算属性、方法、生命周期钩子等等中使用的数据都需要在setup方法中通过return返回出来。
结合模板使用
如果 setup 返回一个对象,那么该对象的 property 以及传递给 setup 的 props 参数中的 property 就都可以在模板中访问到。
<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>6这里我们通过ref、reactive创建了响应式数据,具体差别后面会再细说。
结合渲染函数使用
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>7返回一个渲染函数将阻止我们返回任何其它的东西。我们可以通过我们上面介绍的 expose 来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问。
单文件setup
要使用这个语法,需要将 setup attribute 添加到 <script> 代码块上:
<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>8里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的代码会在每次组件实例被创建的时候执行。
顶层的绑定会被暴露给模板
当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用:
<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>9import 导入的内容也会以同样的方式暴露。意味着可以在模板表达式中直接使用导入的 helper 函数,并不需要通过 methods 选项来暴露它:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};0响应式
响应式状态需要明确使用响应式 APIs来创建。和从 setup() 函数中返回值一样,ref 值在模板中使用的时候会自动解包:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};1使用组件
<script setup> 范围里的值也能被直接作为自定义组件的标签名使用:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};2将 MyComponent 看做被一个变量所引用。如果你使用过 JSX,在这里的使用它的心智模型是一样的。其 kebab-case 格式的 <my-component> 同样能在模板中使用。不过,我们强烈建议使用 PascalCase 格式以保持一致性。同时也有助于区分原生的自定义元素。
动态组件
由于组件被引用为变量而不是作为字符串键来注册的,在 <script setup> 中要使用动态组件的时候,就应该使用动态的 :is 来绑定:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};3递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。
请注意这种方式相比于 import 导入的组件优先级更低。如果有命名的 import 导入和组件的推断名冲突了,可以使用 import 别名导入:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};4命名空间组件
可以使用带点的组件标记,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};5使用自定义指令
全局注册的自定义指令将以符合预期的方式工作,且本地注册的指令可以直接在模板中使用,就像上文所提及的组件一样。
但这里有一个需要注意的限制:必须以 vNameOfDirective 的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};6const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};7defineProps 和 defineEmits
在 <script setup> 中必须使用 defineProps 和 defineEmits API 来声明 props 和 emits ,它们具备完整的类型推断并且在 <script setup> 中是直接可用的:
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};8defineProps 和 defineEmits 都是只在 <script setup> 中才能使用的编译器宏。他们不需要导入且会随着 <script setup> 处理过程一同被编译掉。
defineProps 接收与 props相同的值,defineEmits 也接收 emits相同的值。
传入到 defineProps 和 defineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。
defineExpose
使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏,他也是不需要导入且会随着 <script setup> 处理过程一同被编译掉。
const sRef1 = shallowRef(0);console.log("shallowRef:", sRef1.value); // 0// 假设点击页面按钮,触发该方法const changeShallowRef1 = () => { // 直接修改value,页面会同步修改,也就是会响应式 sRef1.value++;};const sRef2 = shallowRef({ name: "demi1" });// 假设点击页面按钮,触发该方法const changeShallowRef2 = () => { // 不直接修改value,而是修改属性值 sRef2.value.name = "randy"; sRef2.value.address = { city: "汨罗" }; // 添加新属性 // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"} // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};// 假设点击页面按钮,触发该方法const changeShallowRef3 = () => { // 但是我们直接重新赋值,页面会立马重新渲染 sRef2.value = {name: "randy", address: {city: '汨罗'}}};9当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例可以获取到a 、b属性 (ref 会和在普通实例中一样被自动解包)。跟前面说的expose是一样的。
useSlots 和 useAttrs
在 <script setup> 使用 slots 和 attrs 的情况应该是很罕见的,因为可以在模板中通过 $slots 和 $attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots 和 useAttrs 两个辅助函数:
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>0useSlots 和 useAttrs 是真实的运行时函数,它会返回与 setupContext.slots 和 setupContext.attrs 等价的值,同样也能在普通的组合式 API 中使用。
与普通的 <script> 一起使用
<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:
无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。
声明命名导出。
运行副作用或者创建只需要执行一次的对象。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>1该场景下不支持使用 render 函数。请使用一个普通的 <script> 结合 setup 选项来代替。
顶层 await
<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup():
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>2另外,await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式。
注意 async setup() 必须与 Suspense 组合使用,Suspense 目前还是处于实验阶段的特性。
不能和src混合使用
<script setup> 不能和 src attribute 一起使用。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>3computed和watch
计算属性和监听器
computed
computed是计算属性,意思就是会缓存值,只有当依赖属性发生变化的时候才会重新计算。
在vue2中计算属性很简单,是一个对象,只需要简单定义就可以使用。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>4在vue3中,是函数式的,并且需要先引入。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>5watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
怎么理解这句话呢?就是它会自动收集依赖,不需要手动传入依赖。当里面用到的数据发生变化时就会自动触发watchEffect。并且watchEffect 会先执行一次用来自动收集依赖。而且watchEffect 无法获取到变化前的值,只能获取变化后的值。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>6在上面这个例子中,首先会执行watchEffect输出27,当我们触发updateUser2Age方法改变age的时候,因为user2.age是watchEffect的依赖,所以watchEffect会再次执行,输出28。
停止侦听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>7清除副作用
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
副作用即将重新执行时
侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
清除副作用很多同学可能不太理解,下面笔者用个例子解释下。
假设我们需要在input框输入关键字进行实时搜索,又不想请求太频繁我们就可以用到这个功能了。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>8上面的例子中watchEffect依赖了text.value,所以我们只要在input输入值就会立马进入watchEffect。如果不处理的话后端服务压力可能会很大,因为我们只要输入框值改变了就会发送请求。
我们可以利用清除副作用回调函数,在用户输入完一秒后再向后端发送请求。因为第一次是不会执行onInvalidate回调方法的,只有在副作用重新执行或卸载的时候才会执行该回调函数。
所以在我们输入的时候,会一直输出"watchEffect" text对应的值,当我们停止输入一秒后会输出"input" text对应的值,然后发送请求给后端。这样就达到我们最开始的目标了。
类似的还可以应用到事件监听上。这个小伙伴们可以自己试试。
副作用刷新时机
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 前执行。也就是会在组件生命周期函数onBeforeUpdate之前执行。
<template> <div> <h3>user2</h3> <div>user2: {{ user2.name }}</div> <button @click="updateUser2Name">update user2 name</button> <h3>user3</h3> <div>user3 name: {{ name }} user3 age: {{ age }}</div> <button @click="updateUser3Name">update user3 name</button> <h3>count2</h3> <div>count2: {{ count2 }}</div> <button @click="plus2">plus2</button> <button @click="decrease2">decrease2</button> </div></template><script>import { defineComponent, reactive, toRefs } from "vue";export default defineComponent({ setup() { const _user = { name: "randy2" } const user2 = reactive(_user); const updateUser2Name = () => { // reactive定义的变量可以直接修改 user2.name += "!"; // 原始对象的修改并不会响应式,也就是页面并不会重新渲染 // _user.name += "!"; // 代理对象被改变的时候,原始对象会被修改 // console.log(_user); }; // 使用toRefs可以响应式解构出来,在模板能直接使用啦。 const user3 = reactive({ name: "randy3", age: 24 }); const updateUser3Name = () => { user3.name += "!"; }; // 使用reactive定义基本数据类型会报错 const count2 = reactive(0); const plus2 = () => { count2.value++; }; const decrease2 = () => { count2.value--; }; // 检查对象是否是由 reactive 创建的响应式代理。 console.log(isReactive(user2)); // true console.log(isReactive(count2)); // false return { user2, updateUser2Name, // ...user3, // 直接解构不会有响应式 ...toRefs(user3), updateUser3Name, count2, plus2, decrease2, }; },});</script>9上面的例子,当我们触发updateUser2Age方法修改age的时候,会先执行watchEffect然后执行onBeforeUpdate。
如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 pre)。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 30上面的例子,当我们触发updateUser2Age方法修改age的时候,会先执行onBeforeUpdate然后执行watchEffect。
flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。sync这个参数是什么意思呢?很多同学可能不理解,这里我们重点解释下。
当watchEffect只有一个依赖的时候这个参数和pre是没区别的。但是当有多个依赖的时候,flush: post和 flush: pre只会执行一次副作用,但是sync会执行多次,也就是有一个依赖改变就会执行一次。
下面我们看例子
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 31在上面的例子中,watchEffect有name和age两个依赖,当我们触发updateUser3NameAndAge方法的时候,如果flush: "sync"这个副作用会执行两次,依次输出watchEffect randy3! 27、watchEffect randy3! 28、onBeforeUpdate。
如果你想让每个依赖发生变化都执行watchEffect但又不想设置flush: "sync"你也可以使用nextTick等待侦听器在下一步改变之前运行。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 32上面的例子会依次输出watchEffect randy3! 27、onBeforeUpdate、watchEffect randy3! 28、onBeforeUpdate。
从 Vue 3.2.0 开始,我们也可以使用别名方法watchPostEffect 和 watchSyncEffect,这样可以用来让代码意图更加明显。
watchPostEffect
watchPostEffect就是watchEffect 的别名,带有 flush: 'post' 选项。
watchSyncEffect
watchSyncEffect就是watchEffect 的别名,带有 flush: 'sync' 选项。
侦听器调试
onTrack 和 onTrigger 选项可用于调试侦听器的行为。
onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
onTrigger 将在依赖项变更导致副作用被触发时被调用。
这个有点类似前面说的生命周期函数renderTracked和renderTriggered,一个最初次渲染时调用,一个在数据更新的时候调用。
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 33onTrack 和 onTrigger 只能在开发模式下工作。
watch
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
与 watchEffect 相比,watch 有如下特点
惰性地执行副作用
更具体地说明应触发侦听器重新运行的状态
可以访问被侦听状态的先前值和当前值
我们首先来看看在vue2中我们是怎么监听的。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 34或者
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 35接下来我们来看看vue3中的监听。
监听单一源
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 36监听多个源
监听多个源我们使用数组。
这里我们需要注意,监听多个源只要有一个源发生变化,回调函数都会执行。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 37监听引用数据类型
有时我们可能需要监听一个对象的改变,而不是具体某个属性。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 38上面的写法有没有问题呢?当我们触发updateUser2Age方法修改age的时候可以发现我们输出newVal, oldVal两个值是一样的。这就是引用数据类型的坑。当我们不需要知道oldVal的时候这样写没问题,但是当我们需要对比新老值的时候这种写法就不行了。
我们需要监听这个引用数据类型的拷贝。当引用数据类型简单的时候我们可以直接解构成新对象,但是当引用数据类型复杂的时候我们就需要用到深拷贝了。深拷贝前面笔者有文章介绍,可以自己写深拷贝方法或者引用lodash库。
这样输出来的值才是正确的。
vue2中好像没办法解决这个问题。
const count = ref(1)// 可以直接定义,而不是{count: count.value}const obj = reactive({ count })// 这种写法也是支持的// const obj = reactive({})// obj.count = count// ref 会被解包console.log(obj.count === count.value) // true// 它会更新 `obj.count`count.value++console.log(count.value) // 2console.log(obj.count) // 2// 它也会更新 `count` refobj.count++console.log(obj.count) // 3console.log(count.value) // 39watch除了支持vue2的深度监听deep: true和立即执行immediate: true还支持 watchEffect的停止侦听、清除副作用、副作用刷新时机、侦听器调试,用法是一样的这里笔者就不再赘述了,小伙伴们可以自行研究。
teleport
teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
什么意思呢?就是我们组件的html节点可以通过teleport挂载到任意位置。
比如我们的about组件里面就可以通过<teleport to="#app">把<div>我是通过teleport传递过来的,挂载在app下面</div>挂载到id为app的html节点下。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false0我们来看看渲染效果,发现我们写在about页面的元素被挂载到了app节点下。
teleport的to属性必须是有效的查询选择器或 HTMLElement
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false1teleport还支持disabled选项。此可选属性可用于禁用 <teleport> 的功能,这意味着其插槽内容将不会移动到任何位置,而是在你在周围父组件中指定了 <teleport> 的位置渲染。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false2Suspense
我们在vue2中肯定写过这样的代码
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false3Suspense就是用来更优雅的展示内容。需要搭配defineAsyncComponent使用。
<suspense> 组件有两个插槽。它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false4注意,Suspense 是一个试验性的新特性,其 API 可能随时会发生变动。特此声明,以便社区能够为当前的实现提供反馈。生产环境请勿使用。我们目前了解有这个东西即可。
多片段
vue3现在正式支持了多根节点的组件,也就是片段!什么意思呢?下面看个例子就明白了。
在 vue2 中,由于不支持多根节点组件,当其被开发者意外地创建时会发出警告。结果是,为了修复这个问题,许多组件被包裹在了一个 <div> 中。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false5在 vue3 中,组件可以包含多个根节点!但是,这要求开发者显式定义 attribute 应该分布在哪里。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false6这一段是什么意思呢?我们先来说说$attrs。
$attrs
在vue2中,我们知道this.$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false7上面的例子输出没在子组件接收的age和id所以会输出{age: 24, id: 'child3'}(因为class 和 style会被忽略)。
在vue3中有了更改,attrs被转移到setup的第二个参数context上,context.attrs。并且class 和 style也都不再忽略了。也就是说class 和 style也会在attrs里面。
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false8上面会输出全部未被接收的prop,输出{age: 24, class: 'child3', id: 'child3', style: {color: 'blue'}}。
在vue2中由于只有一个片段,所以未在props定义的属性会直接挂载在根片段上。但是vue3由于支持多个片段,所以如果使用了多片段并且有未在props定义的属性就会抛出警告,因为它不知道把这些未定义在props中的属性挂载到哪个片段上,所以就需要我们使用v-bind="$attrs"来显示指定了。
我们先来看看只有一个片段的时候,未定义在props中的属性都挂载在根片段上了。
但是我们使用多片段的时候它会提示警告,[Vue warn]: Extraneous non-props attributes (name2, class, id, style) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.
当我们在第1个div定义v-bind="$attrs"后我们发现未定义在props中的属性都挂载在该片段上了。
看到这小伙伴们是不是就懂了呢。虽然vue3支持多片段,但是我们需要定义v-bind="$attrs"。
既然讲到了$attrs,我们再讲讲$listeners的改动。
$listeners
我们知道在vue2中$listeners包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。
但是在vue3,$listeners被移除了,父作用域中的事件监听器也被放到了attrs里面。相当于是合并在一起了。
生命周期改动
首先我们来看看vue2和vue3的生命周期函数。
vue3虽然提倡把生命周期函数都放到setup中,但是vue2那种选项式写法还是支持的。
beforeCreatebeforeCreate无createdcreated无beforeMountbeforeMountonBeforeMountmountedmountedonMountedbeforeUpdatebeforeUpdateonBeforeUpdateupdatedupdatedonUpdatedbeforeDestroybeforeUnmountonBeforeUnmountdestroyedunmountedonUnmountederrorCapturederrorCapturedonErrorCaptured无renderTrackedonRenderTracked无renderTriggeredonRenderTriggeredactivatedactivatedonActivateddeactivateddeactivatedonDeactivated总结
vue2相较于vue3少了renderTracked、renderTriggered两个生命周期方法。
销毁生命周期方法名也发生了变化,由beforeDestroy、destroyed变为beforeUnmount、unmounted,这样是为了更好的与beforeMount、mounted 相对应。
vue3写在setup函数中生命周期方法名就是前面多加了on。
基本的生命周期函数我想不必笔者多说小伙伴们应该都很清楚了。下面重点说下 renderTracked、renderTriggered、errorCaptured 三个方法。
renderTracked
简单理解就是,首次渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性,这个方法会被触发多次。
我们来看例子
const user1 = shallowReactive({ name: "demi1", address: { city: "汨罗", count: 10 },});// 假设点击页面按钮,触发该方法const changeUser1 = () => { // 响应式,页面会发生变化 user1.name = "demi1 !!!";};// 假设点击页面按钮,触发该方法const changeUser2 = () => { // 非响应式,也就是页面不会发生变化 user1.address.city = "岳阳"; user1.address.count++; // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式 console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}};console.log(isReactive(user1)); // trueconsole.log(isReactive(user1.address)); // false9页面首次加载只会触发onRenderTracked方法。
因为模板里面用到了name和user.age所以该方法会被触发两次输出{key: 'value', target: RefImpl, type: 'get'}和{key: 'age', target: {age: 27}, type: 'get'}。因为name是ref定义的,所以key始终是value,并且只是读操作,所以type为get。user是reactive定义的,并且我们只使用了age属性所以key是age并且只是读操作,所以type为get。
renderTriggered
简单理解就是,页面更新渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性被修改,这个方法会被触发多次。
我们来看例子
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true0我们点击changeName按钮来修改name,这里只会触发onRenderTriggered方法一次。并且输出{key: 'value', target: RefImpl, type: 'set'},因为是修改所以type是set。
errorCaptured
errorCaptured在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true1生命周期全流程
有些人可能好奇,所有的生命周期函数顺序到底是怎么样的呢?
我们分单组件和父子组件来说明。
单组件
页面首次加载
setup -> beforeCreate -> created -> beforeMount -> renderTracked -> mounted
页面更新
renderTriggered -> beforeUpdate -> updated
页面卸载
beforeUnmount -> unmounted
父子组件
页面首次加载
setup -> beforeCreate -> created -> beforeMount -> renderTracked -> child setup -> child beforeCreate -> child created -> child beforeMount -> child renderTracked -> child mounted -> mounted
页面更新
纯父组件属性更新 renderTriggered -> beforeUpdate -> updated
纯子组件属性更新 renderTriggered -> beforeUpdate -> updated
父组件属性更新,该属性在子组件中有被使用 renderTriggered -> beforeUpdate -> child beforeUpdate -> child updated -> updated
子组件属性更新,该属性在父组件中有被使用 child renderTriggered -> renderTriggered -> beforeUpdate -> child beforeUpdate -> child updated -> updated
页面卸载
beforeUnmount -> child beforeUnmount -> child unmounted -> unmounted
注意上面生命周期函数调用顺序在vue2中也是一致的,只不过vue2没有 setup、 renderTracked、renderTriggered,并且销毁方法是beforeDestroy、destroyed。
hook名称修改
在 vue2 中,我们可以通过事件来监听组件生命周期中的关键阶段。这些事件名都是以 hook: 前缀开头,并跟随相应的生命周期钩子的名字。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true2在 vue3 中,这个前缀已被更改为 vnode-。额外地,这些事件现在也可用于 HTML 元素,和在组件上的用法一样。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true3或者在驼峰命名法的情况下附带前缀 vnode:
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true4全局API改动
任何全局改变 Vue 行为的 API 现在都会移动到应用实例上app上,以下是部分全局 API 及其相应实例 API 的表,如需了解很多可以查看官网。
位置变更
app)app通过createApp方法创建。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true5nextTick
在vue2中我们是这样使用的
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true6在vue3中是这样的,需要手动引入
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true7在vue2中Vue.nextTick() 这样的全局 API 是不支持 tree-shake 的,不管它们实际上是否被使用了,都会被包含在最终的打包产物中。
而vue3中的引入时写法可以tree-shaking能减少打包体积。
模板指令改动
v-model
在vue2中 v-model 指令在表单 <input>、<textarea> 及 <select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
text 和 textarea 元素使用 value property 和 input 事件;
checkbox 和 radio 使用 checked property 和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
在vue2中只对上面几个表单项做了特殊处理。如果在自定义组件上使用v-model需要在组件内通过model参数指明v-model的属性和事件。
如果不指明model它的值默认是value,事件默认是input事件。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true8有了model这样我们的自定义组件Child4也能使用v-model啦。在input输入框输入的时候会$emit出change事件,这个事件会直接修改父组件value2的值。
除了使用v-model,vue2还可以使用.sync修饰符来直接修改父元素数据。
// ref定义的数据会被限制,不能被修改let name1 = ref("readonly randy");// readOnlyName1才是只读的let readOnlyName1 = readonly(name1);const changeName1 = () => { readOnlyName1.value += "!"; // 这里直接修改源对象还是可以的 // name1.value += "!";};// 基本数据类型数据会无效,能被修改let readOnlyName2 = readonly("readonly randy");readOnlyName2 = "randy";console.log(readOnlyName2); // randy// reactive定义的对象会被限制,不能被修改const reactiveUser1 = reactive({ name: "readonly randy" });let readonlyUser1 = readonly(reactiveUser1);const changeUserName1 = () => { readonlyUser1.name += "!"; // 这里直接修改源对象还是可以的 // reactiveUser1.name += "!";};// 普通对象也会被限制,不能被修改let readonlyUser2 = readonly({ name: "readonly randy" });readonlyUser2.name = "randy";console.log(readonlyUser2.name); // readonly randyconsole.log(isReadonly(readOnlyName1)); // trueconsole.log(isReadonly(readOnlyName2)); // falseconsole.log(isReadonly(readonlyUser1)); // trueconsole.log(isReadonly(readonlyUser2)); // true9父组件传递给子组件的值如果带了.sync就可以在子组件通过update:xxx事件直接修改该值而不用再暴露事件在父组件去修改。
这样我们点击子组件按钮触发updateSyncTestValue方法,父组件的syncTest值会变成new syncTest value。
在vue3中v-model得到了加强,自定义组件也可以使用了v-model。而不用去指定model或者使用.sync参数了。
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1230当我们点击子组件按钮触发changeName方法,会直接修改父组件的name1值。
自定义参数名
我们可以通过向 v-model 传递参数来修改这些名称:
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1231当我们点击子组件按钮触发changeName1、changeName2方法,会直接修改父组件的name1、name2值。
多个参数
我们还可以通过向 v-model 传递多个参数,这在vue2中是不可以的。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1232v-model 修饰符
vue3除了支持.trim、.number 和 .lazy修饰符。还支持添加自己的自定义修饰符。
下面笔者写个.capitalize修饰符,用来转换字母为大写。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1233我们通过传递.capitalize,在子组件props中接收modelModifiers,这个属性里面存放传递的修饰符,比如我们传递了.capitalize它的值就是{capitalize: true},所以我们可以根据这个属性还自定义操作。
对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers",这里笔者就不再细说了。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1234key支持在template使用
在vue2中,key是不能定义在template节点上的。但是在vue3中支持了。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1235修改v-if和v-for优先级
在vue2中v-for的优先级是比v-if高的,在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。
下面我们根据数据show字段进行遍历展示,在vue2中是可行的。但是在vue3这样是不可行的。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1236在vue3中需要这样写。因为在vue3中v-if的优先级比v-for更高,所以在v-if中访问不到list。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1237v-bind="object" 现在排序敏感
在一个元素上动态绑定 attribute 时,同时使用 v-bind="object" 语法和独立 attribute 是常见的场景。然而,这就引出了关于合并的优先级的问题。
在 vue2 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object 中的绑定。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1238在 vue3 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。后面的会覆盖前面的。
const raw = { count: ref(123)}// 这里就类似const copy = reactive(raw)const copy = readonly(raw)console.log(raw.count.value) // 123console.log(copy.count) // 1239移除v-on.native修饰符
默认情况下,传递给带有 v-on 的组件的事件监听器只能通过 this.$emit 触发。如果要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};0这里的被.native修饰的click就是原生事件,当点击的时候才会触发。
在vue3中v-on 的 .native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。
因此,对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)。
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};1子组件
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};2在上面的例子中,因为子组件定义了emits: ['close'],也就是说组件说明了,我只会暴露出close事件,其他的事件你就当原生事件处理就可以了。所以click事件就是原生事件了。
v-for 中的 ref
在 vue2 中,在 v-for 中使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。
在 vue3 中,此类用法将不再自动创建 $ref 数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};3选项式 API:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};4组合式 API:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};5itemRefs 不必是数组:它也可以是一个对象,其 ref 可以通过迭代的 key 被设置。如有需要,itemRefs 也可以是响应式的,且可以被侦听。
组件改动
函数式组件
在vue2中我们使用functional定义函数式组件。有如下特点
作为性能优化,因为它们的初始化速度比有状态组件快得多
返回多个根节点
比如使用 <dynamic-heading> 组件,负责提供适当的标题 (即:h1、h2、h3 等等),在 vue2 中,这可以通过单文件组件编写:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};6或者,对于喜欢在单文件组件中使用 <template> 的用户:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};7但是在 vue3 中,所有的函数式组件都是用普通函数创建的。换句话说,不需要定义 { functional: true } 组件选项。也就是说 functional 已经被移除了。
它们将接收两个参数:props 和 context。context 参数是一个对象,包含组件的 attrs、slots 和 emit property。
此外,h 现在是全局导入的,而不是在 render 函数中隐式提供。
以前面提到的 <dynamic-heading> 组件为例,下面是它现在的样子。
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};8异步组件
在vue2中异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const user2 = shallowReadonly( reactive({ name: "demi2", address: { city: "汨罗2", count: 10 }, }));console.log(isReadonly(user2)); // trueconsole.log(isReadonly(user2.address)); // falseconst changeUser2 = () => { // 响应式,页面会同步修改 user2.address.city = "岳阳"; user2.address.count++; // 非响应式,也就是页面不会重新渲染该值 user2.name = "demi1 !!!";};9在vue3中异步组件通过defineAsyncComponent定义
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>00emits
组件里面新增了emits选项,可以通过 emits 选项在组件上定义发出的事件。
下面看例子
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>01在这个例子中,子组件对外暴露了submit事件,所以需要在emits里面定义。
原生事件替代
当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的事件替代原生事件侦听器。
下面笔者再出一个例子就会明白了。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>02上面的例子我们在input输入框输入内容的时候控制台会打印值。虽然我们在子组件上监听了click方法,但是并不会起作用,他被input事件替代了。
虽然vue支持这种写法但是笔者不太建议用原生方法命名,容易混淆。
暴露的事件一定要定义emits中,因为没被定义在emits中的事件会被当做原生事件处理。当你命名和原生事件一样的时候就会发现有问题了。
比如上面的例子,当你定义的方法名和原生事件名一样比如click,又没在emits里面定义,这样会导致你自定义的事件会触发你的方法而且当你点击的时候还会触发你的方法,会触发两次。
事件验证
与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以对它进行验证。
要添加验证,请为事件分配一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>03验证不通过并不会报错,而是出现控制台警告。
自定义元素
在vue2中template里面只能识别html标签或者组件,如果是其它不认识的标签会报错。如果要自定义元素怎么办呢?比如自定义一个<basic-button>
在vue2中需要通过 Vue.config.ignoredElements 将标签配置为自定义元素
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>04在vue3中需要通过 app.config.compilerOptions.isCustomElement 传递。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>05这样我们在vue模板里面就能正常使用该标签了。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>06is
在vue2中,is可以用在普通元素和<component>上。不管用在哪个上面都会渲染is指定的组件。
下面渲染的都是Child3组件
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>07在vue3中,is也可以用在普通元素和<component>上。但是只有用在<component>上才会渲染指定组件,用在普通元素上只会作为一个属性传递。如果想在普通元素上渲染组件怎么办呢?这就需要加上vue:前缀了。
下面渲染的都是Child3组件
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>08渲染函数改动
渲染函数API改变
在vue3中渲染函数h现在全局导入,而不是作为参数传递给渲染函数。并且在setup中需要返回函数,而不是渲染函数h。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>09slot变更
在 vue2 中, 具名插槽的写法:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>10在父组件中使用:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>11如果我们要在 slot 上面绑定数据,可以使用作用域插槽,实现如下:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>12<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>13在 vue2 中具名插槽和作用域插槽分别使用slot和slot-scope来实现, 在 vue3 中将slot和slot-scope进行了合并同意使用,使用v-slot代替。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>14样式改动
样式穿透
在vue2中我们使用/deep/来做样式穿透,在vue3中推荐使用:deep()
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>15全局样式和局部样式
我们知道使用scoped修饰的style样式只会在当前文件生效。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>16但是我们想创建全局样式应该怎么做呢?
在vue3中有两种方法
创建一个不带 scoped 的style的标签。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>17还可以使用 :global 伪类来实现
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>18module
<style module> 标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件:
<style module> 实现了和 scope CSS 一样将 CSS 仅作用于当前组件的效果。所以我们不需要再加scoped属性了。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>19我们还可以通过给 module attribute 一个值来自定义注入的类对象的 property 键:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>20注意使用模块化的话我们的样式不要嵌套哦,不然会获取不到。
与组合式 API 一同使用
注入的类可以通过 useCssModule API 在 setup() 和 <script setup> 中使用。对于使用了自定义注入名称的 <style module> 模块,useCssModule 接收一个对应的 module attribute 值作为第一个参数。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>21状态驱动的动态 CSS
单文件组件的 <style> 标签可以通过 v-bind 这一 CSS 函数将 CSS 的值关联到动态的组件状态上:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>22这里的样式是响应式的,js中值改变样式也会更新。
插槽选择器
默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以确切地将插槽内容作为选择器的目标:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>23其实我们把样式写在父元素也是可以实现该功能的。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>24其他改动
Provide / Inject
使用过vue2的同学肯定知道Provide / Inject使用来组件间传递值的。我们来看看vue2和vue3中使用差别。
在vue2中,我们能直接使用。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>25在vue3中,我们需要引入provide和inject,并且在setup方法中返回才能使用。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>26为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>27现在,如果这两个 property 中有任何更改,Child 组件也将自动更新!
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>28然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>29最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly,这样就万无一失啦。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>30模板引用
模板引用在vue2中是非常简单的,通过this.$refs就能获取到。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>31在vue3中相对麻烦,需要先引入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>在JSX中
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>33在v-for中
我们知道,vue2中,如果是for循环的话this.$refs会是一个数组。但是在vue3 中 v-for内部使用时没有特殊处理。相反,使用函数引用执行自定义处理。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>34在侦听模板中
我们也可以不在生命周期钩子使用而是在监听器中使用,但与生命周期钩子的一个关键区别是,watch() 和 watchEffect() 在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>35因此,使用模板引用的侦听器应该用 flush: 'post' 选项来定义,这将在 DOM 更新后运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>36自定义指令
指令的钩子函数已经被重命名,以更好地与组件的生命周期保持一致。并且,expression 字符串不再作为 binding 对象的一部分被传入。
在 vue2 中,自定义指令通过使用下列钩子来创建,以对齐元素的生命周期,它们都是可选的:
bind - 指令绑定到元素后调用。只调用一次。
inserted - 元素插入父 DOM 后调用。
update - 当元素更新,但子元素尚未更新时,将调用此钩子。
componentUpdated - 一旦组件和子级被更新,就会调用这个钩子。
unbind - 一旦指令被移除,就会调用这个钩子。也只调用一次。
下面是一个例子:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>37<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>38此处,在这个元素的初始设置中,通过给指令传递一个值来绑定样式,该值可以在应用中任意更改。
然而,在 vue3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使钩子的目标事件十分相似。我们现在把它们统一起来了:
created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
bind → beforeMount
inserted → mounted
beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated。
componentUpdated → updated
beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
unbind -> unmounted
最终的 API 如下:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>39因此,API 可以这样使用,与前面的示例相同:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>37<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>41data
在vue2中,我们可以通过 object 或者是 function 定义 data 选项。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>42vue3 中,data 选项已标准化为只接受返回 object 的 function。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>43mixin 合并行为变更
我们先来复习下mixin。mixin对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
mixin和组件的data数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
这次mixin的调整主要是在data()。
当来自组件的 data() 及其 mixin 或 extends 基类被合并时,合并操作现在将被浅层次地执行:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>44在 vue2 中,生成的 $data 是:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>45在 vue3 中,其结果将会是:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>46因为组件已经有了user属性,所以不会再替换了。
侦听数组
在vue3中当使用 watch 选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定 deep 选项。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>47transition
class 名更改
过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。
下面笔者用两张图总结
在vue2中
在vue3中
<transition> 组件的相关 prop 名称也发生了变化:
leave-class 已经被重命名为 leave-from-class (在渲染函数或 JSX 中可以写为:leaveFromClass) enter-class 已经被重命名为 enter-from-class (在渲染函数或 JSX 中可以写为:enterFromClass)
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>48transition-group
在 vue2 中,<transition-group> 像其它自定义组件一样,需要一个根元素。默认的根元素是一个 <span>,但可以通过 tag attribute 定制。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>49在 vue3 中<transition-group> 不再默认渲染根元素,但仍然可以用 tag attribute 创建根元素。
template标签
没有特殊指令的标记 (v-if/else-if/else、v-for 或 v-slot) 的 <template> 现在被视为普通元素,并将渲染为原生的 <template> 元素,而不是渲染其内部内容。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>50上面的例子在vue2中会渲染出randy,在vue3中会渲染<template></template>并没有内容。
被挂载的应用不会替换元素
当我们的index.html是这样的
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>51根组件是这样的
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>52vue2会渲染成
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>53vue3会渲染成
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>54看出差别了吧,就是vue3不会替换被挂载的元素。
移除API
在vue3中移除了部分api。
filter
在vue2中我们这样使用filter。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>55在vue3中移除过滤器,不在支持。建议使用computed去替代。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>56config.keyCodes
在 vue2 中,keyCodes 可以作为修改 v-on 方法的一种方式。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>57此外,也可以通过全局的 config.keyCodes 选项定义自己的别名。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>58<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>59vue3对任何要用作修饰符的键使用 kebab-cased (短横线) 名称。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>60因此,这意味着 config.keyCodes 现在也已弃用,不再受支持。
$on,$off 和 $once
$on,$off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口。
在 vue2 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ($on,$off 和 $once)。这可以用于创建一个事件总线,以创建在整个应用中可用的全局事件监听器:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>61<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>62<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>63在vue3中已经从实例中完全移除了 $on、$off 和 $once 方法。$emit 仍然包含于现有的 API 中,因为它用于触发由父组件声明式添加的事件处理函数。
$children
$children 实例 property 已从 vue3中移除,不再支持。
在 vue2 中,开发者可以使用 this.$children 访问当前实例的直接子组件:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>64在 vue3 中,$children property 已被移除,且不再支持。如果你需要访问子组件实例,我们建议使用 ref。
propsData
propsData 选项之前用于在创建 Vue 实例的过程中传入 prop,现在它被移除了。如果想为 vue3 应用的根组件传入 prop,请使用 createApp 的第二个参数。
在 vue2 中,我们可以在创建 Vue 实例的时候传入 prop:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>65在 vue3 中 propsData 选项已经被移除。如果你需要在实例创建时向根组件传入 prop,你应该使用 createApp 的第二个参数:
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>66$destroy
在vue3中移除了$destroy,用户不应再手动管理单个 Vue 组件的生命周期。
set delete $set $delete
在vue3中移除了全局函数 Vue.set 和 Vue.delete 以及实例方法 this.$set 和 this.$delete。因为在vue3中响应式检测是基于proxy的,基于代理的变化检测已经不再需要它们了。
inline-template
在 vue2 中,Vue 为子组件提供了 inline-template attribute,以便将其内部内容作为模板使用,而不是作为分发内容。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>67上面会在页面渲染
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>68我们看看不加inline-template attribute
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>69上面会在页面渲染<my-component>的真实内容,如果<my-component>没内容将渲染为空。
在 vue3 中将不再支持此功能。
router的使用
vue2.x使用的是vue-router@3.x,vue3.x使用的是vue-router@4.x。
创建
不再使用new Router()创建实例,而是使用createRouter方法。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>70路由跳转
由于vue3的setup方法没有this,所以不能再使用this.$router获取路由对象啦。在vue3中需要使用useRouter方法。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>71请注意,在模板中我们仍然可以访问 $router 和 $route,所以不需要在 setup 中返回 router 或 route。
路由参数
由于vue3的setup方法没有this,所以不能再使用this.$route获取当前路由对象啦。在vue3中需要使用useRoute方法。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>72请注意,在模板中我们仍然可以访问 $router 和 $route,所以不需要在 setup 中返回 router 或 route。
路由钩子
在vue3中有改动的路由钩子是组件内钩子。在vue2中有beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave三个钩子。但是在vue3中移除了beforeRouteEnter。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>73store的使用
vue2.x使用的是vuex@3.x,vue3.x使用的是vuex@4.x。
创建
不再使用new Vuex.Store()创建实例,而是使用createStore方法。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>74获取store
除了获取store的方式有所改变,其它方法都没变。
<template> <h3>count1</h3> <div>count1: {{ count1 }}</div> <button @click="plus">plus</button> <button @click="decrease">decrease</button> <div>user1: {{ user1.name }}</div> <button @click="updateUser1Name">update user1 name</button></template><script>import { defineComponent, ref } from "vue";export default defineComponent({ setup() { const count1 = ref(0); const plus = () => { count1.value++; }; const decrease = () => { count1.value--; }; const user1 = ref({ name: "randy1" }); const updateUser1Name = () => { // ref定义的变量需要使用.value修改 user1.value.name += "!"; }; console.log(isRef(count1)); // true return { count1, plus, decrease, user1, updateUser1Name }; },});</script>75参考文档
官方文档
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
原文:https://juejin.cn/post/7098575243240800286