简介
有没有小伙伴跟笔者一样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) // 3
shallowReactive
浅响应式,创建一个响应式代理,它跟踪其自身 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)); // false
readonly和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) // 123
shallowReadonly
浅只读,创建一个 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>1
markRaw
标记一个对象,使其永远不会转换为 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>2
unref
如果参数是一个 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>3
toRef
可以用来为源响应式对象上的某个 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>7
toRefs
只会为源对象中包含的 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>8
triggerRef
手动执行与 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>3
context
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>4
attrs
和 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>9
import 导入的内容也会以同样的方式暴露。意味着可以在模板表达式中直接使用导入的 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: '汨罗'}}};6
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: '汨罗'}}};7
defineProps
和 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: '汨罗'}}};8
defineProps
和 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>0
useSlots
和 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>3
computed和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>5
watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
怎么理解这句话呢?就是它会自动收集依赖,不需要手动传入依赖。当里面用到的数据发生变化时就会自动触发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) // 33
onTrack
和 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) // 39
watch
除了支持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)); // false1
teleport
还支持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)); // false2
Suspense
我们在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)); // false3
Suspense
就是用来更优雅的展示内容。需要搭配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
那种选项式写法还是支持的。
beforeCreate
beforeCreate
无
created
created
无
beforeMount
beforeMount
onBeforeMount
mounted
mounted
onMounted
beforeUpdate
beforeUpdate
onBeforeUpdate
updated
updated
onUpdated
beforeDestroy
beforeUnmount
onBeforeUnmount
destroyed
unmounted
onUnmounted
errorCaptured
errorCaptured
onErrorCaptured
无
renderTracked
onRenderTracked
无
renderTriggered
onRenderTriggered
activated
activated
onActivated
deactivated
deactivated
onDeactivated
总结
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)); // true5
nextTick
在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) // 1232
v-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) // 1234
key支持在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) // 1237
v-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 !!!";};5
itemRefs
不必是数组:它也可以是一个对象,其 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>00
emits
组件里面新增了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>06
is
在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>09
slot变更
在 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>18
module
<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>41
data
在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>42
vue3 中,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>43
mixin 合并行为变更
我们先来复习下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>47
transition
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>48
transition-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>52
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>53
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>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>56
config.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>59
vue3
对任何要用作修饰符的键使用 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>73
store的使用
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