vue3出来也有一段时间了,笔者目前在公司已经正式用 vue3 开发了两个项目,并且都已顺利上线了一段时间,现在就结合我这段时间的使用感受以及我学习vue3的一些理解,对vue3做个简单的总结。
Vue3
更快
按照官方的说法,vue3 比 vue2 性能方面提升了 1.2 ~ 2倍,性能方面的提升包括了以下优化
diff算法
vue3 对 diff 算法做了优化,加入了 PatchFlags 来动态的标记需要更新的部分,PatchFlags可以具体标记元素哪些部分可能会有更新,具体如下:
export const enum PatchFlags {
TEXT = 1, // 动态文字内容
CLASS = 1 << 1, OB10 // 动态 class
STYLE = 1 << 2, OB100 // 动态样式
PROPS = 1 << 3, OB1000 // 动态 props
FULL_PROPS = 1 << 4, OB10000 // 有动态的key,也就是说props对象的key不是确定的
HYDRATE_EVENTS = 1 << 5, OB100000 // 合并事件
STABLE_FRAGMENT = 1 << 6, OB1000000 // children 顺序确定的 fragment
KEYED_FRAGMENT = 1 << 7, OB10000000 // children中有带有key的节点的fragment
UNKEYED_FRAGMENT = 1 << 8, OB100000000 // 没有key的children的fragment
NEED_PATCH = 1 << 9, OB1000000000 // 只有非props需要patch的,比如`ref`
DYNAMIC_SLOTS = 1 << 10, OB10000000000 // 动态的插槽
// 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag
// 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2,
}
复制代码
之前 vue2 在做 diff 时会对比全部的节点,然后再对有变化的节点进行更新
而 vue3 会先对元素做标记,标记需要更新的部分(比如一个元素只有class是动态的,其他都是写死静态的,那么就标记 class 的 PatchFlags),diff 时只对比需要更新的部分,不会对比全部,从而优化了性能。
事件缓存
vue3 还做了事件监听缓存(cacheHandlers),也就是说如果元素绑定的事件是不会改变的,事件会被缓存下来,下次创建渲染元素时,直接使用缓存的事件
以下是在 vue3 的编译网站( vue-next-template-explorer.netlify.app/#/ )尝试的代码
<button @click="clickFun">按钮</button>
复制代码
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickFun && _ctx.clickFun(...args)))
}, "按钮"))
}
复制代码
静态提升
vue3 会判断出哪些元素是静态的,不要更新的,比如下面的 .title
元素,这些元素会被 vue 提升,只创建一次,后续更新不会再重新创建,只是直接复用,节省性能。而 vue2 中无论元素需不需要重新创建,每次渲染时都会重新创建一遍。
<template>
<div>
<div class='title'>静态数据</div>
<div>
{{ data }}
</div>
</div>
</template>
复制代码
当然 vue3 的性能优化不止这些,这里只是简单说了几个有代表性的,而且 vue3 还在继续升级,优化
更小
vue3 采用按需编译的方式,全局和内部 API 已经被重构为支持 tree-shaking。
过去 vue2 使用的 Api 很多都是挂载在单个单个 Vue 对象的全局上,需要使用时通过 Vue.
或者this.
直接使用,但是有些时候,比如一些小的项目,我们可能用不到 vue 中的很多其他api,那么这些没使用的 api 照理是不应该打包进我们最终的项目中去的,当时 vue2 是会一起打包进去的。而到了 vue3 ,我们可以通过导入的方式,表明我们具体需要使用哪些功能,比如
import { nextTick } from 'vue'
复制代码
通过这种方式,其他未被使用的 api 会在最终打包中被 tree-shaking 清除,以达到更小的打包体积。
typescript的支持
在 vue2 中我们想使用 typescript 需要借助额外的包 vue-class-component 装饰器,而 vue3 是由typescript编写的,天然支持 typescript ,如果要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent
全局方法定义组件。
import { defineComponent } from 'vue'
const Component = defineComponent({
// 已启用类型推断
})
复制代码
生命周期
vue2 和 vue3 的生命周期图示
通过比对上面的图片可以发现两者存在以下区别
我们从上往下看
创建实例的区别
vue2和vue3在创建实例的方面有了很大的变化,我们先看下面创建实例的代码
-
vue2创建实例
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' new Vue({ router, store, render: (h) => h(App), }).$mount('#app') 复制代码
-
vue3创建实例
import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; createApp(App).use(ElementPlus).use(store).use(router).mount("#app"); 复制代码
通过代码可以发现:vue2是通过 new 来创建Vue实例,Vue是一个构造函数,通过Vue构造的对象会同享所有的全局配置
import GlobalButton from './components/GlobalButton.vue'
Vue.compoent('global-button', GlobalButton)
const appA = new Vue().$mount('#app-A')
const appB = new Vue().$mount('#app-B')
复制代码
如以上代码,global-button 组件已经全局注册在Vue上,appA和appB都可以使用这个组件,而我们无法做到例如只在appA中全局注册global-button 组件,所以从某种意义上讲,vue2并没有所谓应用
的这种概念。
而在vue3中
import { createApp } from "vue";
import appA from "./appA.vue";
import appB from "./appB.vue";
import GlobalButton from './components/GlobalButton.vue'
const appA = createApp(appA)
appA.component('global-button', GlobalButton)
appA.mount("#app-A");
const appB = createApp(appB).mount("#app-B");
复制代码
createApp这个工厂函数会返回提供应用上下文的应用实例,允许链式,注意:mount要放在最后,因为mount不会返回应用本身,其返回的是根组件实例。
该实例是独立的,与其他实例互不干扰,不受其他实例影响。
Has ’el‘ option?的区别
从图中可以发现,vue2 的图 有一个 Has ’el‘ option? 的棱形,vue2在这些完beforeCreate 和 created 后判断有没有挂载在 el 元素上(也就是$mount(el)),有则继续执行,没有则等到有了后才继续执行,而在 vue3 中,可以看到 el 在一开始的方块里(app.mount(el))就有了,可以理解为 vue3 是在一切准备好再开始的。
el处理的区别
走到 “ Has ‘template’ option? ” 在 NO 的分支时:
vue2 是 Compile el's outerHTML as template
。而vue3 是 Compile el's innerHTML as template
。
这里可以发现一个是 outerHTML ,一个是 innerHTML,二者区别可以看以下例子,可以发现,outerHTML包括了元素本身的标签
<div id="app">
<span>文本</span>
</div>
<script>
let app = document.querySelector('#app')
console.log(app.outerHTML) // <div id="app"><span>文本</span></div>
console.log(app.innerHTML) // <span>文本</span>
</script>
复制代码
所以在生命周期的图中继续往下我们可以发现
vue2 是Create vm.$el and replace "el" with it
的方式也就是替换的方式
vue3 是Create app.$el and append it to el
的方式也就是插入的方式
对比vuecli创建的vue2和vue3的项目中,我们可以发现 App.vue 文件vue2中有 id为app的div元素,而vue3则没有
// vue2 App.vue 文件
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
}
</script>
// vue3 App.vue 文件
<template>
<router-view />
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
});
</script>
复制代码
组件销毁生命周期的区别
从图中我们可以发现 beforeDestroy 被改名为了 beforeUnmount;destroyed 被改名为 unmounted
组合式API
vue3的重大更新之一,与vue2的以data、methods、watch等构成的选项式 api 不同,组合式API可以以更自由,更接近原生的书写方式提高代码的灵活性、重用性,在大型项目中,这一点显得尤为重要。
看以下vue2 和 vue3 的简单例子
// vue2
<template>
<div>
<div class="">
{{ fruit }}
</div>
<el-button @click="changeFruit">修改fruit值</el-button>
<div class="">
{{ animal }}
</div>
<el-button @click="changeAnimal">修改animal值</el-button>
</div>
</template>
<script>
export default {
data() {
return {
fruit: 'apple', // 逻辑1
animal: 'monkey', // 逻辑2
}
},
methods: {
changeFruit() {
this.fruit = 'banner'
}, // 逻辑1
changeAnimal() {
this.animal = 'tiger'
}, // 逻辑2
},
}
</script>
复制代码
// vue3
<template>
<div>
<div class="">
{{ fruit }}
</div>
<el-button @click="changeFruit">修改fruit值</el-button>
<div class="">
{{ animal }}
</div>
<el-button @click="changeAnimal">修改animal值</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
let fruit = ref<string>("apple"); // 逻辑1
function changeFruit() {
fruit.value = "banner";
} // 逻辑1
// ------------------------------------------
let animal = ref<string>("monkey"); // 逻辑2
function changeAnimal() {
animal.value = "tiger";
} // 逻辑2
return {
fruit,
changeFruit,
animal,
changeAnimal,
};
},
});
</script>
复制代码
// 也可以这样写
<script lang="ts">
import { defineComponent, ref } from "vue";
function fruitSetup(){
let fruit = ref<string>("apple"); // 逻辑1
function changeFruit() {
fruit.value = "banner";
} // 逻辑1
return {
fruit,
changeFruit
}
}
function animalSetup(){
let animal = ref<string>("monkey"); // 逻辑2
function changeAnimal() {
animal.value = "tiger";
} // 逻辑2
return {
animal,
changeAnimal
}
}
export default defineComponent({
setup() {
return {
...fruitSetup(),
...animalSetup(),
};
},
});
</script>
复制代码
可以发现,组合式 API 可以摆脱data,methods等选项的拘束,将相同逻辑代码写在一块,便于后续维护和迭代。上面的例子只是非常简单的示例,在日常开发中,特别是大型项目中,一个大的组件可能有非常多的逻辑关注点,这时,选项式API会加大代码的阅读难度,使得维护变得困难,逻辑点的分离也隐藏了潜在的逻辑问题,增加了隐藏风险。组合式 API 的诞生解决了以上的问题。
参考下图,按颜色划分的逻辑关注点
setup
setup是组件的选项,是组合式 API 的入口函数,在beforeCreate钩子之前(组件被创建之前),props被解析之后执行。
在setup函数中访问 this 为 undefined。也就是说不需要再像 vue2 一样写很多的 this 。
那么为什么 vue2 需要 this,因为受到作用域的限制,而组合式API 可以摆脱这种限制,让我们可以以更加灵活,自由的方式将组件需要的各种变量,函数等等组合到需要的组件中去,也进一步提高了代码的复用性。
<template>
<div @click="changeDataText">
{{ dataText }}
</div>
<div @click="changeDataInfo">
{{ dataInfo }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { commonSetup, commonSetupSecond } from '@/common/commonSetup.ts'
export default defineComponent({
setup(){
const { dataText, changeDataText } = commonSetup()
const { dataInfo, changeDataInfo } = commonSetupSecond()
return {
dataText,
changeDataText,
dataInfo,
changeDataInfo
}
},
})
</script>
复制代码
说到vue代码的复用性,我们自然而然就会想到 Mixin ,这个让我们又爱又恨的东西,首先 Mixin 确实很方便,在vue2 中我们可以用它将很多相同功能的代码写在一起供多个组件去使用,但是真实的情况是一个组件往往不会用到其使用的 Mixin 中的全部功能,但是为了图方便,有些时候大家可能会选择直接导入 Mixin 简单粗暴,但这也为我们组件引入了很多不需要的功能,甚至一个组件引入多个 Mixin,造成 Mixin 滥用,再者,Mixin的引用往往是隐式导入,比如在组件中看到一个函数的调用,但却不知道这个函数是从哪里来的,等等 Mixin 的问题造成后续接手项目的人一头雾水,增加了项目的维护成本。
而组合式API 完全可以取代 mixin 的作用,我们可以将公用部分抽离在一个函数,也就是 hooks,在各个组件中引入我们需要的变量,函数等等,如上面的代码所示,当然vue3中也支持Mixin,但官方文档已经表示不推荐使用,vue3的项目,Mixin应该能不用就坚决不用,用组合式 API 不香吗。
setup函数的参数
setup函数有两个参数
-
props
与使用选项式 API 时的
this.$props
类似,props
对象仅包含显性声明的 prop,并且,所有声明了的 prop,不管父组件是否向其传递了,都将出现在props
对象中。其中未被传入的可选的 prop 的值会是undefined
。props是响应式的,但是不能解构使用,否则会失去响应式。 -
context :
context是一个上下文对象,其包含了attrs、slots、emit、expose四个属性,
-
attrs 等同于 我们熟悉的 $attrs
-
slots 等同于 $slots
-
emit 等同于 $emit
-
expose 是 Vue 3.2 版本 中新增的一个函数,expose 只能被调用一次,可以通过该函数指定组件向外暴漏的 property,让组件外通过
ref
、$parent
或$root
等获取的该组件实例可以使用指定的property<template> <div></div> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'ExposeComponent', setup(props, { expose }){ function exposeExample(){ console.log('外部方法') } function internalExample(){ console.log('内部方法') } // 暴漏exposeExample给外部访问 expose({ exposeExample }) // 组件内部可以访问的 return { internalExample } }, }) </script> 复制代码
这样组件外部就访问不了 internalExample 方法了
-
组合式API生命周期钩子
生命周期钩子大概有以下几种
-
onBeforeMount
-
onMounted
-
onBeforeUpdate
-
onUpdated
-
onBeforeUnmount
-
onUnmounted
-
onActivated
-
onDeactivated
.....
组合式API的生命周期钩子和选项式 API 的生命周期选项类似,只是在前面加了”on“单词,对应的触发逻辑是一样的
组合式API的生命周期钩子不需要 beforeCreate 和 created ,也就是说没有 onBeforeCreate 和 onCreated ,因为setup 执行在这两者之前,需要在beforeCreate 和 created 中执行的情况直接写在 setup() 中执行即可。
import { onMounted, onUnmounted } from 'vue'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted')
})
onUnmounted(() => {
console.log('unmounted')
})
}
}
复制代码
响应式API
vue的特点之一就是其双向绑定的特性,方便。
vue2 中是使用 Object.defineProperty 来对数据进行劫持,实现双向绑定
let definePropertyObj = {};
Object.defineProperty(definePropertyObj, 'prop', {
configurable: false,
set: (v) => {
definePropertyObj._v = v
},
get: () => {
return definePropertyObj._v
}
});
复制代码
可以发现,使用 Object.defineProperty 来对数据进行劫持,由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此一开始 vue 就需要拿到数据的 property ,以便 Vue 对其进行转换并使其具有响应式
<template>
<div>
<div id='name'>{{ obj.name }}</div>
<div class="footer">
<button @click="changeName">修改name</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
obj: {},
}
},
methods: {
changeName() {
this.obj.name = 'name值'
},
},
}
</script>
复制代码
如上面的例子,点击按钮之后,“#name”的元素内容并不会发生改变,除非一开始data中的 obj 有 name 属性
所以,vue2 无法检测到 property 的添加或删除,vue2也提供了 vue.$set
和vue.$delete
的 Api 来解决以上的问题
而vue3 基于ES2015 Proxy
对引用类型实现了"深层"的响应式转换
let proxyObj = {};
let proxy = new Proxy(proxyObj, {
set: (obj, prop, value) => {
proxyObj[prop] = value;
},
get: (obj, prop) => {
return obj[prop]
},
deleteProperty: () => {}
});
复制代码
以此,vue3 可以实现监听对象属性的新增和删除,那么 vue3 是否放弃了Object.defineProperty 全部改用 proxy 呢,当然不是,往下看
vue3 中提供了各种响应式 API 来转换我们的变量。
reactive
import { reactive } from 'vue'
const state = reactive({value: 'vue'})
console.log(state)
复制代码
通过打印我们可以发现
返回一个 Proxy 对象
结合上面讲到的proxy的例子,我们可以发现 proxy 返回原始对象的响应式对象,reactive 基于proxy 返回对象的响应式副本,于是,上面中的 obj 的增删改都是响应式的,reactive的参数只能是引用类型。
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。
const value = ref('vue')
console.log(value)
复制代码
通过打印我们可以发现
返回一个 RefImpl 对象,我们就简称它为 ref 对象吧
ref 对象暴漏出有且仅有一个 value
属性 ,指向该内部值。
import { ref } from 'vue'
const str = ref('vue2')
console.log(str.value) // vue2
str.value = 'vue3'
console.log(str.value) // vue3
复制代码
当 ref 对象在setup中返回并在模板中访问时,它会自动浅层次解包内部值,也就是不需要 .value 访问, 只有访问嵌套的 ref 时需要在模板中添加 .value
,看下官网的例子
<template>
<div>
<span>{{ count }}</span>
<button @click="count ++">Increment count</button>
<button @click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
复制代码
如果 ref 的参数是对象,则 vue 内部会自动转为使用 reactive 转化
const state = ref({
data: 'vue'
})
console.log(state)
console.log(state.value)
复制代码
通过依次打印我们可以发现 state.value 的值是一个 Proxy 对象
ref 对比 reactive
从上面我们知道 reactive 是返回一个 Proxy 对象,使用 Proxy 进行数据劫持
而 ref 包裹的值如果是基本类型时(复杂类型自动转为 reactive),使用 Object.defineProperty 进行数据劫持
那么 Proxy 和 Object.defineProperty 性能的比较怎么样呢
通过看大佬的案例 thecodebarbarian.com/thoughts-on…
const Benchmark = require('benchmark');
let suite = new Benchmark.Suite
let obj = {};
let definePropertyObj = {};
let proxyObj = {};
Object.defineProperty(definePropertyObj, 'prop', {
configurable: false,
set: v => definePropertyObj._v = v
});
let proxy = new Proxy(proxyObj, {
set: (obj, prop, value) => { proxyObj[prop] = value; }
});
suite.add('normal', function() {
obj.prop = 'gd';
})
.add('definePropertyObj', function() {
definePropertyObj.prop = 'gd'
})
.add('proxyObj', function() {
proxy.prop = 'gd'
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ 'async': true });
复制代码
/*
normal x 839,613,391 ops/sec ±1.93% (90 runs sampled)
definePropertyObj x 846,333,142 ops/sec ±0.78% (91 runs sampled)
proxyObj x 926,137 ops/sec ±23.25% (75 runs sampled)
Fastest is definePropertyObj
*/
复制代码
可以发现,Object.defineProperty 的性能是远高于 proxy 的,Vue3 用 proxy 也是为了监听对象属性的新增和删除。所以在实际使用中,建议优先使用ref,在vue3.2 中,官方对 ref 做了优化,读取速度提高约 260%,写入速度提高约 50%。
插一句,在对象的属性还是对象的情况,vue2 在对对象做响应式转换时,是通过递归对象使用Object.defineProperty将子对象变成也响应式。而 vue3 则是做了惰性处理,只有在该对象属性被访问时才执行操作将其转为响应式。这也是 vue3 做的一个性能优化处理。
isRef
检查值是否为一个 ref 对象。
import { ref, isRef } from 'vue'
const str = 'vue2'
const refStr = ref('vue3')
console.log(isRef(str)) // false
console.log(isRef(refStr)) // true
复制代码
unref
如果参数是一个 ref 对象,则返回内部值(value值),否则返回参数本身。
import { ref, unref } from 'vue'
const str = 'vue2'
const refStr = ref('vue3')
console.log(unref(str)) // 'vue2'
console.log(unref(refStr)) // 'vue3'
复制代码
toRef
为响应式对象的某个属性(property)新建一个 ref 对象,ref 可以被传递,它会保持对其源 property 的响应式连接,什么意思呢,可以看下面的例子
const states = reactive({
num: 0,
});
const numRef = toRef(states, "num");
console.log(states.num); // 0
console.log(numRef.value, isRef(numRef)); // 0 true
numRef.value++;
console.log(numRef.value); // 1
console.log(states.num); // 1
states.num++;
console.log(numRef.value); // 2
console.log(states.num); // 2
复制代码
如果 toRef 对普通对象使用呢
const obj = {
str: 'vue'
}
const strRef = toRef(obj, 'str')
console.log(strRef.value) // vue
function changeStrRef(){
strRef.value = 'vue3'
console.log(obj) // { str: "vue3" }
console.log(strRef.value) // vue3
}
function changeObjStr(){
obj.str = 'vue2'
console.log(obj) // { str: "vue2" }
console.log(strRef.value) // vue2
}
复制代码
可见,普通对象也可以创建,可是页面并不会跟着响应
toRefs
顾名思义,上面的 toRef 的复数版,也就是为响应式对象的所有属性(property)新建一个 ref 对象
const states = reactive({
num: 0,
figure: 1,
});
const { num, figure } = toRefs(states); // 对 toRefs 返回的值进行结构并不会影响其响应性
console.log(states.num); // 0
console.log(states.figure); // 1
console.log(num.value); // 0
console.log(figure.value); // 1
states.num++;
console.log(num.value); // 1
figure.value++;
console.log(states.figure); // 2
复制代码
结合 reactive 和 toRefs 可以便利地在template中引用响应式数据
<template>
<div>
<div>姓名:{{ name }}</div>
<div>性别:{{ sex }}</div>
<div>年龄:{{ age }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
export default defineComponent({
name: "responsive",
setup() {
const state = reactive({
name: 'Paul',
sex: '男',
age: 18
})
return {
...toRefs(state)
}
},
});
</script>
复制代码
这里可能有人有疑问,setup里直接
return {
...state
}
复制代码
不行吗?
当然不行,上面的写法相当于
return {
name: state.name,
sex: state.sex,
age: state.age,
}
复制代码
相当于暴漏出去简单的字符串和数值,这样就没有响应式了,也就是state里的属性改变时,视图数据不会跟着改变。
readonly
readonly 可以返回一个对象的只读代理,并且是深层的只读(与 const 的区别)。
readonly 是对象,可以是响应式对象或普通对象,也可以是 ref 对象。
<script lang="ts">
import { defineComponent, readonly, onMounted } from "vue";
export default defineComponent({
name: "readonly",
setup() {
const readonlyObj = readonly({
value: 'apple'
});
onMounted(() => {
readonlyObj.value = 'banner';
console.log(readonlyObj.value); // apple
});
},
});
</script>
复制代码
以上代码会报下图的错误
isProxy、isReactive、isReadonly
-
isProxy 并不是判断是否new Proxy对象,而是检查对象是否是由 reactive 或 readonly 创建的 proxy,返回boolean值
const state = reactive({ num: 0 }) const readonlyObj = readonly({ num: 0 }); const refObj = ref({ num: 0 }) const numRef = ref(0) let proxyObj = Object.create(null); let newProxy = new Proxy(proxyObj, {}); console.log(isProxy(state)) // true console.log(isProxy(readonlyObj)) // true console.log(isProxy(numRef)) // false console.log(isProxy(refObj)) // false console.log(isProxy(refObj.value)) // true // 这里可以验证 vue3 中ref参数是对象时,自动调用reactive console.log(isProxy(newProxy)) // false 复制代码
-
isReactive
检查对象是否是由
reactive
创建的响应式代理。const state = reactive({ num: 0 }) const readonlyObj = readonly({ num: 0 }); const readonlyState = reactive(state) const refObj = ref({ num: 0 }) console.log(isReactive(state)) // true console.log(isReactive(readonlyObj)) // false console.log(isReactive(refObj)) // false console.log(isReactive(refObj.value)) // true console.log(isReactive(readonlyState)) // true 复制代码
上面代码可以发现,如果 readonly 的参数是 reactive 创建的返回对象,则也可以通过 isReactive 检查
-
isReadonly
检查对象是否是由
readonly
创建const readonlyObj = readonly({ num: 0 }); console.log(isReadonly(readonlyObj)) // true 复制代码
toRaw
返回 reactive 或 readonly 代理的原始对象。我们都知道,Proxy 是根据原始对象创建的 proxy 代理对象,reactive 和 readonly是基于Proxy实现的,toRaw 可以返回其原始对象
const obj = {}
const reactiveObj = reactive(obj)
console.log(toRaw(reactiveObj) === obj) // true
复制代码
Computed
computed 默认接收一个 getter 函数,返回一个不可修改的响应式 ref 对象。
computed的参数也可以是一个包括 get 和 set 函数的对象,返回一个可以修改的ref 对象。
注意,computed 返回的是 ref 对象,那么在setup中访问修改计算属性的值时,需要加上 .value。
<template>
<div>
<div>
{{ num }}
</div>
<div>
{{ result }}
</div>
<div>
{{ setResult }}
</div>
<el-button @click="addNum">num增加1</el-button>
<el-button @click="setNum">设置setResult</el-button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
name: "computed",
setup() {
let num = ref<number>(0);
const result = computed(()=>num.value+1)
const setResult = computed({
get: () => num.value+2,
set: (val) => num.value = num.value - val
})
function setNum (){
setResult.value = 1
}
function addNum(){
num.value++
}
return {
num,
result,
addNum,
setResult,
setNum,
};
},
});
</script>
复制代码
Watch
和之前的 this.$watch 等效
import { watch } from vue
复制代码
watch 可以接收三个参数,
第一个参数是监听源,监视源可以是一个getter函数、一个ref、一个响应对象或这些类型的数组,第二个参数是包含监听源新旧两个参数的回调函数,第三个参数是options,也就是我们熟悉的 deep 和 immediate 参数
单个数据源
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
name: "watch",
setup() {
let num = ref<number>(0);
watch(num, (newVal, oldVal) => {
console.log('监听到修改(watch: num)', newVal, oldVal)
}
)
watch(()=>num.value, (newVal, oldVal) => {
console.log('监听到修改(watch: ()=>num.value)', newVal, oldVal)
}
)
num.value++ // 监听到修改(watch: num) 1 0
// 监听到修改(watch: ()=>num.value) 1 0
return {
num,
};
},
});
</script>
复制代码
上面代码执行 num++ 后可以发现,打印的值其实是一样的
watch监听reactive
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
name: "watch",
setup() {
const state = reactive({
name: "杰克",
});
watch(
state,
(newValues, prevValues) => {
console.log(newValues, prevValues);
console.log(newValues.name, prevValues.name);
}
);
state.name = "麦克";
},
});
</script>
复制代码
按照上面的例子,如果直接监听 state ,我们打印发现 newValues, prevValues 其实是一样的,
这时候我们可以将上面代码修改一下
watch(
() => state.name,
(newValues, prevValues) => {
console.log(newValues, prevValues);
}
);
复制代码
这时候可以发现,我们要的效果就出来了。
多数据源
watch也可以通过传递数组来实现监听多个数据
let surnames = ref("");
let name = ref("");
watch([surnames, name], (newValues, prevValues) => {
console.log(newValues, prevValues);
});
surnames.value = "Michael"; // logs: ["Michael", ""] ["", ""]
name.value = "Jackson"; // logs: ["Michael", "Jackson"] ["Michael", ""]
复制代码
watchEffect
vue3 新增的函数,官方对它的解释是:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
先看个示例
<template>
<div>
<el-button @click="changeAppleAmount">苹果加一</el-button>
<el-button @click="changeBannerAmount">香蕉加一</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
name: "watchEffect",
setup() {
let appleAmount = ref(0);
let bannerAmount = ref(0);
watchEffect(() =>
console.log(
`苹果一共${appleAmount.value}个,香蕉一共${bannerAmount.value}个`
)
);
function changeAppleAmount() {
appleAmount.value++;
}
function changeBannerAmount() {
bannerAmount.value++;
}
return {
changeAppleAmount,
changeBannerAmount,
};
},
});
</script>
复制代码
可以发现 watchEffect 里的函数一开始就会执行一遍,然后,watchEffect 里的函数涉及的值发生变化时,watchEffect就会再次执行。
那么这个函数有点像是 夹在 Computed 和 Watch 中间的产物,它既不像 Computed 一样需要 return 某个值,也不像 Watch 一样需要 显式的写出监听源,同时他也无法拿到变化的值的前后旧值和新值。
watch 和 watchEffect 停止监听
watch 和 watchEffect 会在所在组件卸载时停止监听,但是我们可以通过调用其返回值来手动停止监听
<template>
<el-card>
<el-button @click="quantity++">数量加一</el-button>
<el-button @click="stopWatchEffect">停止watchEffect</el-button>
<el-button @click="stopWatch">停止watch</el-button>
</el-card>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, watch } from "vue";
export default defineComponent({
name: "watchEffect",
setup() {
const quantity = ref(0);
const watchEffectStop = watchEffect(() =>
console.log(`watchEffect: ${quantity.value}`)
);
const watchStop = watch(quantity, (newValue, oldValue) =>
console.log(`watch: ${newValue}`)
);
function stopWatchEffect() {
watchEffectStop(); // 停止watchEffect的监听
}
function stopWatch() {
watchStop(); // 停止watch的监听
}
return {
quantity,
stopWatchEffect,
stopWatch,
};
},
});
</script>
复制代码
模板引用(ref)
在 选项式API 中我们可以通过 ref 来获取指定的元素
<template>
<div class="test" ref="test"></div>
</template>
<script>
export default {
mounted() {
let test = this.$refs.test
console.log(test)
},
}
</script>
复制代码
在组合式 API 中使用 ref,可以看一个小例子
<template>
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="120px"
class="demo-ruleForm"
>
<el-form-item label="名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="submitForm()">Create</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { ElForm } from "element-plus";
export default defineComponent({
setup() {
const ruleFormRef = ref<InstanceType<typeof ElForm>>();
const rules = {
name: [{ required: true }],
};
const ruleForm = ref({
name: "",
});
function submitForm() {
ruleFormRef.value?.validate((valid) => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
return false;
}
});
}
return {
ruleForm,
ruleFormRef,
rules,
submitForm,
};
},
});
</script>
复制代码
这里我们在setup中返回的ruleFormRef暴漏出去,并将其应用在div的 ref 属性值中,注意,这里的 ref 中前面不能加冒号,因为如果这里加了冒号,ref 的值就是空 ,不加就是字符串的 “ruleFormRef”。在 vue3 的虚拟DOM补丁算法中,如果 VNode 的 ref
键对应于渲染上下文中的 ref,则vue3会将该 VNode 的相应元素或组件实例分配给该 ref 的值,这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
route router
在组合式 api 中使用route router时,可以使用 useRoute, useRouter
下面代码 useRoute() 和 useRouter() 的 route 和 router 使用上基本和之前的 router 一致
<script lang="ts">
import { defineComponent, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
setup() {
const route = useRoute()
const router = useRouter()
watch(
() => route.path,
() => {
...
},
{ immediate: true }
)
function goPath() {
router.push(path)
}
}
})
</script>
复制代码
vuex
在组合式 api 中使用 store 时,也可以使用 useStore,useStore() 返回的 store 使用上和vue2 的 $store 一致
<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from 'vuex'
export default defineComponent({
setup() {
const store = useStore()
const companyInfo = computed(() => store.state.user.companyInfo)
return {
companyInfo,
}
},
})
</script>
复制代码
指令
自定义指令
vue2的自定义指令钩子函数
- bind:只调用一次,指令第一次绑定到元素时调用。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件 VNode 更新时调用。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
vue3 更新了自定义指令的钩子函数
- created:在绑定元素的 attribute 或事件监听器被应用之前调用。
- beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。(同vue2的bind)
- mounted:在绑定元素的父组件被挂载后调用。(同vue2的inserted)
- beforeUpdate:在更新包含组件的 VNode 之前调用。
- updated:在包含组件的 VNode 及其子组件的 VNode更新后调用。(同vue2的componentUpdated)
- beforeUnmount:在卸载绑定元素的父组件之前调用。
- unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。(同vue2的unbind)
可以发现, vue3 在 vue2 的基础上扩展了更多的钩子,丰富了指令的应用场景,在命名上也更加让人容易理解。
vue3 的钩子函数的参数还是和以前一样(即 el
、binding
、vnode
和 prevVnode
)
其他更新
多根节点的组件的支持
vue2中组件的 template 中一定需要一个根节点组件,而到了 vue3 组件可以包含多个根节点,但是我们需要定义 attribute 应该分布在哪个根节点组件,不然比如我们引用该组件,在组件标签上定义 class 就不知道这个class 是定义在哪个根节点上了
<template>
<p class='header'>...</p>
<div v-bind="$attrs">...</div>
<div>...</div>
</template>
复制代码
注意:但是vue指令和 attribute 不同,不会通过 $attrs 被传入元素中,当指令被用与多根节点的组件时,指令会被忽略,且vue会抛出警告
Teleport
类似 React 的 Portal
,是一个标签,可以自由控制元素渲染在哪个父节点下
<template>
<div>
<div>组件内</div>
<teleport to='body'>
<div class="teleport">嵌入body</div>
</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
})
</script>
复制代码
定义自定义事件 emits
vue的自定义事件我们已经很熟悉了,在vue3中自定义事件需要在emits选项中定义,如果不定义,虽然也可以执行,不过vue会发出警告,vue希望我们定义所有发出的事件,以便更好地记录组件应该如何工作。
而且如果emits定义了原生事件,将使用组件中的事件替代原生事件。
在 vue2 中,我们可以通过 .native 修饰符将原生事件绑定到子组件的根元素上,而 vue3 已经移除了v-on.native修饰符,我们可以在 emits 选项上不定义对应的原生事件,则原生事件默认就绑定到组件根元素中
// 父组件
<template>
<children @focus="parentsEvent" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import children from "./components/children.vue";
export default defineComponent({
components: { children },
setup() {
function parentsEvent() {
console.log("子元素聚焦");
}
return {
parentsEvent,
};
},
});
</script>
// 子组件
<template>
<input type="text" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "children",
emits: [], // 这里不定义 emits: ["focus"],那么子组件的 input 元素聚焦时就会触发父组件的 parentsEvent 事件
});
</script>
复制代码
移除过滤器 filter
其实 filter 完全可以用 methods 或是 computed 代替
移除 .sync
正常情况下,我们不能修改接收到的prop值,在 vue2 中,我们可以使用 .sync 修饰符来对一个prop进行”双向绑定“
// 父组件
<template>
<div>
<myDialog :dialogVisible.sync="visible" />
<el-button @click="showDialog">打开弹窗</el-button>
</div>
</template>
<script>
import myDialog from './components/myDialog.vue'
export default {
components: { myDialog },
data() {
return {
visible: false,
}
},
methods: {
showDialog() {
this.visible = true
},
},
}
</script>
// 子组件
<template>
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
:before-close="close"
>
<el-button type="primary" @click="close">关闭</el-button>
</el-dialog>
</template>
<script>
export default {
props: {
dialogVisible: {
type: Boolean,
default: false,
},
},
methods: {
close() {
this.$emit('update:dialogVisible', false)
},
},
}
</script>
复制代码
而在 vue3 中 .sync 已被移除,因为vue3 更改了组件上 v-model 的用法,已经不再需要 .sync 了
<MyComponent v-model="componentVisible" />
// 相当于
<MyComponent :modelValue="componentVisible" @update:modelValue="componentVisible = $event"/>
复制代码
v-model 也可以加参数
<MyComponent v-model:visible="componentVisible" />
// 相当于
<MyComponent :componentVisible="componentVisible" @update:componentVisible="componentVisible = $event"/>
复制代码
示例:
// 父组件
<template>
<div>
<MyDialog v-model:dialogVisible="visible" />
<el-button @click="open">打开弹窗</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import MyDialog from "./components/myDialog.vue";
export default defineComponent({
components: { MyDialog },
setup() {
const visible = ref(false);
function open() {
visible.value = true;
}
return {
visible,
open,
};
},
});
</script>
// 子弹窗组件
<template>
<el-dialog
:model-value="dialogVisible"
title="Tips"
width="30%"
:before-close="close"
>
<span>This is a message</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "MyDialog",
props: {
dialogVisible: {
type: Boolean,
default: false,
},
},
emits: ["update:dialogVisible"],
setup(props, { emit }) {
function close() {
emit("update:dialogVisible", false);
}
return {
close,
};
},
});
</script>
复制代码
同时,一个组件上也可以有多个 v-model
<MyComponent v-model:visible="componentVisible" v-model:userId="dataUserId" />
复制代码
移除了 $listeners
$listeners已经在 Vue 3中被移除,事件监听器已经和 $attrs 合并
// 父组件
<template>
<children @parentsEvent="parentsEvent" :count="count" />
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import children from "./components/children.vue";
export default defineComponent({
name: "listenersAttrs",
components: { children },
setup() {
const count = ref(0);
function parentsEvent(value: string) {
console.log(value);
}
return {
count,
parentsEvent,
};
},
});
</script>
// 子组件
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
export default defineComponent({
name: "children",
setup(props, { attrs }) {
onMounted(() => {
console.log(attrs);
});
},
});
</script>
复制代码
打印结果如下
移除了 $on、$off 和 $once
vue2 中我们可以 new Vue() 一个实例,再通过在这个实例上进行 $on、$off 、 $once 的操作来实现跨组件通信,也就是所谓的 vue bus,但是目前 new Vue() 已经没有了,$on、$off 和 $once也被移除了,毕竟,bus 太多的话,项目看上去就太乱了,还是用 vuex 或其他代替吧。
vue3.2
<script setup>
<script setup> 中我们可以不必声明export default 和 setup 方法, 这种写法可以将所有<script setup> 声明的顶层的绑定,包括变量,函数,以及import导入的内容暴漏出去,在模板中直接使用,看下示例
<template>
<div>
{{ data }}
<MyComponents/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import MyComponents from './components/MyComponents.vue';
const data = 'vue3.2'
</script>
复制代码
defineProps 、 defineEmits
在<script setup>中由于没有了 setup函数,也没有props,emits选项,那么在这种写法中使用props和emits进行组件通信就需要借助defineProps 和 defineEmits。
defineProps 和 defineEmits只有在<script setup>中才能使用,且不需要从“vue”或者其他地方导入,直接就可以用。
defineProps 接收与 props选项相同的值,defineEmits 也接收 emits选项相同的值。
// 父组件
<template>
<div>
<MyComponents :propsCount='propsCount' @changePropsCount='changePropsCount'/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import MyComponents from './components/MyComponents.vue';
const propsCount = ref(1)
function changePropsCount(){
propsCount.value++
}
</script>
// 子组件
<template>
<div>
<p>{{ propsCount }}</p>
<el-button @click="emit('changePropsCount')">增加</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const props = defineProps({
propsCount: Number,
});
const emit = defineEmits(['changePropsCount'])
</script>
复制代码
defineExpose
与常规的使用 setup 的写法一样,如果组件需要暴漏出去属性给外部使用,setup函数的第二个参数有个 expose 的方法属性
而在<script setup>中可以使用 defineExpose,使用方法与 expose 一致
<script setup>
import { ref } from 'vue'
const outData = ref(1)
defineExpose({
outData
})
</script>
复制代码
useSlots useAttrs
同样,常规的 setup函数写法,setup函数的第二个参数有 attrs 和 slots 属性
而在<script setup>中可以使用 useSlots useAttrs
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
// slots 和 attrs的使用与之前一致
复制代码
与普通<script>搭配使用
也许你会发现<script setup>的写法好像没法定义组件名name
我们可以将其与普通<script>一起使用
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ComponentName',
})
</script>
<script setup>
...
</script>
复制代码
v-bind
一个让人眼前一新的功能,单文件的 style 标签中可以通过 v-bind 使用当前组件的变量,如下
<template>
<div class="box"></div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "css-v-bind",
setup() {
let width = ref("100px");
let height = ref("100px");
let background = ref("#00cd96");
return {
width,
height,
background,
};
},
});
</script>
<style scoped>
.box {
width: v-bind(width);
height: v-bind(height);
background: v-bind(background);
}
</style>
复制代码
v-memo
v-memo指令,一个可以避免无效的重复渲染的指令,该指令接收一个数组作为依赖值(数组里可以有多个依赖值),根据数组里的值判断是否需要更新渲染。
<template>
<div>
<div v-memo="[divMemoValue]">
{{ innerData }}
</div>
<button @click="changeDivData">修改div数据</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const divMemoValue = ref(1);
const innerData = ref("盒子数据1");
function changeDivData() {
innerData.value = "盒子数据2";
}
return {
divMemoValue,
innerData,
changeDivData,
};
},
});
</script>
复制代码
上面的例子可以发现,点击按钮修改 innerData 数据,视图并不会发生改变,因为 v-memo里的 divMemoValue 没有发生修改,所以不会触发重新渲染dom。
根据这个原理,其实我们可以结合我们实际开发的项目情况,来合适的减少重新渲染,提高性能。
如果 v-memo的值是一个空数组,则对应的元素或组件将只渲染一次,不会更新,和 v-once 功能一样
这一点,特别是体现在和 v-for 的结合使用中
和 v-for 结合使用
看下面的例子
<template>
<div>
<div
v-for="item in list"
:key="item.order"
v-memo="[item.order === activeOrder]"
>
{{ item.order === activeOrder ? "选中了" : "没选中" }}
<button @click="selectItem(item.order)">选择</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const list = ref([
{
order: 1,
label: "数据1",
},
{
order: 2,
label: "数据2",
},
{
order: 3,
label: "数据3",
},
{
order: 4,
label: "数据4",
},
]);
const activeOrder = ref(1);
function selectItem(order: number) {
activeOrder.value = order;
}
return {
activeOrder,
list,
selectItem,
};
},
});
</script>
复制代码
上面例子中,当我们更新了选中的列表数据时,只有原先选中的和新选中的两条数据的“item.order === activeOrder”条件会变化,其他两条数据的“item.order === activeOrder”还是一样是false,所以跳过了其他两条数据的更新,优化了性能。
当然,这只是个简单的例子,实际开发中,可能数据有成百上千条,每条数据渲染的dom可能更复杂,实际优化的性能会特别可观。