vue3和vue2的不同
1、生命周期的变化
整体来看,变化不大,只是名字大部分需要 + on
,功能上类似。使用上 Vue3 组合式 API 需要先引入;Vue2 选项 API 则可直接调用,如下所示。
// vue3
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
...
})
// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不被覆盖
onMounted(() => {
...
})
</script>
// vue2
<script>
export default {
mounted() {
...
},
}
</script>
常用生命周期表格如下所示。
Vue2.x | Vue3 |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
Tips: setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地去定义。
setup
setup 是 Vue3 新增的一个选项, 他是组件内使用 Composition API
的入口。
setup执行时机
export default defineComponent ({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
})
setup 执行时机是在beforeCreate之前执行。
::: warning 由于在执行setup 时尚未创建组件实例,因此在 setup 选项中没有this。:::
setup 参数
使用setup
时,它接受两个参数:
- props: 组件传入的属性
- context
setup中接受的props
是响应式的, 当传入新的props 时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。
错误代码示例, 这段代码会让props不再支持响应式:
// demo.vue
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
setup
接受的第二个参数context
,我们前面说了setup
中不能访问Vue2中最常用的this
对象,所以context
中就提供了this
中最常用的三个属性:attrs
、slot
和emit
,分别对应Vue2.x中的 $attr
属性、slot
插槽 和$emit
发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
2、多根节点
Vue3 支持了多根节点组件,也就是fragment
(碎片)。
Vue2中,编写页面的时候,我们需要去将组件包裹在<div>
中,否则报错警告。
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
3、异步组件
Vue3 提供 Suspense
组件,允许程序在等待异步组件时渲染兜底的内容,如 loading ,使用户体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default
和fallback
。Suspense
确保加载完异步内容时显示默认插槽,并将fallback
插槽用作加载状态。
<tempalte>
<suspense>
<template #default>
<todo-list />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
真实的项目中踩过坑,若想在 setup 中调用异步请求,需在 setup 前加async
关键字。这时,会受到警告async setup() is used without a suspense boundary
。
解决方案:在父页面调用当前组件外包裹一层Suspense
组件。
4、Teleport (传送门组件)
Vue3 提供Teleport
组件可将部分DOM移动到 Vue app之外的位置。比如项目中常见的Dialog
组件。
<button @click=dialogVisible = true>点击</button>
<teleport to=body>
<div class=dialog v-if=dialogVisible>
</div>
</teleport>
5、组合式API
Vue2 是 选项式API(Option API)
,一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期函数等),导致代码的可读性变差,需要上下来回跳转文件位置。Vue3 组合式API(Composition API)
则很好地解决了这个问题,可将同一逻辑的内容写到一起。
除了增强了代码的可读性、内聚性,组合式API 还提供了较为完美的逻辑复用性方案,举个🌰,如下所示公用鼠标坐标案例。
// main.vue
<template>
<span>mouse position {{x}} {{y}}</span>
</template>
<script setup>
import { ref } from 'vue'
import useMousePosition from './useMousePosition'
const {x, y} = useMousePosition()
}
</script>
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
let x = ref(0)
let y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
</script>
解决了 Vue2 Mixin
的存在的命名冲突隐患,依赖关系不明确,不同组件间配置化使用不够灵活。
Composition API
① reactive
reactive()接收一个普通对象
然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()
<template>
<div id="app">{ state.count }</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
// state 现在是一个响应式的状态
const state = reactive({
count: 0,
})
}
}
</script>
② ref
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value
。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
如果传入 ref 的是一个对象,将调用 reactive
方法进行深层响应转换。
- 模板中访问:自动解套,无需在模板内额外书写
.value
属性
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
return {
count: ref(0),
}
},
}
</script>
- 作为响应式对象的属性访问
const count = ref(1);
const state = reactive({
count
})
console.log(state.count) // 1
state.count = 222;
console.log(count.value) // 222
如果分配一个新的 ref 给现有的 ref,则替换旧的 ref
const otherCount = ref(10);
state.count = otherCount;
console.log(state.count) // 10
console.log(count.value) // 1
注意当嵌套在 reactive Object
中时,ref 才会解套。从 Array
或者 Map
等原生集合类中访问 ref 时,不会自动解套:
const arr = reactive([ref(0)])
// 这里需要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 这里需要 .value
console.log(map.get('foo').value)
③ toRefs
toRefs用于将一个reactive对象转化为属性全部为ref对象的普通对象。具体使用方式如下:
<template>
<div class="homePage">
<p>第 {{ year }} 年</p>
<p>姓名: {{ nickname }}</p>
<p>年龄: {{ age }}</p>
</div>
</template>
<script>
import { defineComponent, reactive, ref ,toRefs} from "vue";
export default defineComponent({
setup() {
const year = ref(0);
const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
setInterval(() =>{
year.value ++
user.age ++
}, 1000)
return {
year,
// 使用toRefs
...toRefs(user)
}
},
});
</script>
④ computed
使用响应式 computed
API 有两种方式:
-
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1) const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // 错误!
-
传入一个拥有
get
和set
函数的对象,创建一个可手动修改的计算状态。const count = ref(1) const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 }, }) plusOne.value = 1 console.log(count.value) // 0
⑤ watch
接受 3 个参数:
- 一个想要侦听的响应式引用或 getter 函数
- 一个回调
- 可选的配置选项
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
},{immediate:true})
// ---------等同于---------------
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
},
immediate:true
}
情况一 :监视 ref 定义的单个数据
const name = ref('张三')
watch(name, (newVal, oldVal) => {
console.log('name的值变化了',newVal, oldVal)
},{immediate:true})
情况二:监视 ref 定义的多个数据
const name = ref('张三')
//参数一改为数组形式
watch([name,age], (newVal, oldVal) => {
console.log(newVal, oldVal) // // 监听出来结果发现新的值和旧的值都是数组
},{immediate:true})
情况三:监听reactive所定义的一个响应式数据的全部属性
vue3当前存在问题:
1.reactive和ref响应式使用proxy代理无法正确监听并获取对象oldVal
2.强制开启深度监听,关闭无效(deep配置无效)
watch( obj,(newVal, oldValue) => {
// newVal=> {name: '老王', age: 52} oldValue=>{name: '老王', age: 52}
console.log(newVal, oldValue);
},{ deep: false }
);
情况四:监听reactive所定义的一个响应式数据的某一个属性,要将箭头函数写成函数return的方式才有效,一定不要踩坑哦!
const p = reactive({
name: 'zs',
age: 18
})
//()=>p.name 为要监听的函数
watch(() => p.name, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
情况五:监视reactive所定义的一个响应式数据的某些属性
watch([()=>data.count,()=>data.num],(newVal,oldVal)=>{
console.log("data中的count或num数据发生变化",newVal,oldVal);
})
⑥ watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
- 停止侦听
当 watchEffect
在组件的 setup()
函数或生命周期钩子被调用时, 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// 之后
stop()
- 清除副作用:侦听副作用传入的函数可以接收一个
onInvalidate
函数作入参
6、响应式原理
Vue2 响应式原理基础是Object.defineProperty
;Vue3 响应式原理基础是Proxy
。
1. Object.defineProperty
基本用法:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Tips: writable
和 value
与 getter
和 setter
不共存。
实现响应式:通过defineProperty
两个属性,get
及set
get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
set:属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
let obj = {}
let name = '瑾行'
Object.defineProperty(obj, 'name', {
enumerable: true, // 可枚举(是否可通过for...in 或 Object.keys()进行访问)
configurable: true, // 可配置(是否可使用delete删除,是否可再次设置属性)
// value: '', // 任意类型的值,默认undefined
// writable: true, // 可重写
get: function() {
return name
},
set: function(value) {
name = value
}
})
小结:
-
无法监听对象或数组新增、删除的元素;
-
需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。
Vue2 方案:针对常用数组原型方法
push
、pop
、shift
、unshift
、splice
、sort
、reverse
进行了hack处理;提供Vue.set
监听对象/数组新增属性。对象的新增/删除响应,还可以new
个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
2. Proxy
Proxy
是ES6新特性,通过第2个参数handler
拦截目标对象的行为。Proxy
的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。相较于Object.defineProperty
提供语言全范围的响应能力,消除了局限性。但在兼容性上放弃了(IE11以下)
基本用法:创建对象的代理,从而实现基本操作的拦截和自定义操作。
同时Proxy
并不能监听到内部深层次的对象变化,而 Vue3
的处理方式是在getter
中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归。
Proxy
可以直接监听数组的变化(push
、shift
、splice
)
总结:
Object.defineProperty
只能遍历对象属性进行劫持
proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
7、打包优化
tree-shaking:模块打包
webpack
、rollup
等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于import
和export
语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
以nextTick
为例子,在 Vue2 中,全局 API 暴露在 Vue 实例上,即使未使用,也无法通过tree-shaking
进行消除。
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和DOM有关的东西
})
复制代码
Vue3 中针对全局 和内部的API进行了重构,并考虑到tree-shaking
的支持。因此,全局 API 现在只能作为ES模块构建的命名导出进行访问。
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
复制代码
通过这一更改,只要模块绑定器支持tree-shaking
,则 Vue 应用程序中未使用的api将从最终的捆绑包中消除,获得最佳文件大小。
8、自定义渲染API
Vue3 提供的createApp
默认是将 template 映射成 html。但若想生成canvas
时,就需要使用custom renderer api
自定义render生成函数。
// 自定义runtime-render函数
import { createApp } from './runtime-render'
import App from './src/App'
createApp(App).mount('#app')
ue.nextTick(() => {
// 一些和DOM有关的东西
})
复制代码
Vue3 中针对全局 和内部的API进行了重构,并考虑到`tree-shaking`的支持。因此,全局 API 现在只能作为ES模块构建的命名导出进行访问。
```js
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
复制代码
通过这一更改,只要模块绑定器支持tree-shaking
,则 Vue 应用程序中未使用的api将从最终的捆绑包中消除,获得最佳文件大小。
8、自定义渲染API
Vue3 提供的createApp
默认是将 template 映射成 html。但若想生成canvas
时,就需要使用custom renderer api
自定义render生成函数。
// 自定义runtime-render函数
import { createApp } from './runtime-render'
import App from './src/App'
createApp(App).mount('#app')