Vue3与Vue2的区别

一、合成型API

Vue2使用的是选项型API(optionsAPI),将代码分割成不同的属性,如:data、methods、computed

Vue3使用的是合成型API(compasitionAPI),它将使用一个新的方法setup(),通过refs,reactive和toRefs定义响应式数据

二、proxy实现双向数据绑定

在Vue2中主要使用Object.defineProperty来劫持数据,然后通过发布订阅者模式来实现数据更新

而在Vue3中则使用了ES6的Proxy API来实现数据的代理。它的优势有:

  • defineProperty只能监听某个属性,不能对整个对象进行监听。而Proxy可以
  • defineProperty为了实现对对象进行监听,会用到for in和闭包,Proxy则省去了这些性能损耗
  • 可以监听数组的内部变化

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取:${key}:${res}`)
return res
},
set(target, key, receiver) {
const res = Reflect.set(target, key, receiver)
console.log(`设置:${key}:${res}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key, receiver)
console.log(`删除:${key}:${res}`)
return res
}
})

return observed
}

三、优化diff算法

在Vue3中,不再比对所有的DOM,而采用的block tree的做法。
具体就是增加一个属性PatchFlag,用来标记当前节点是什么类型的节点,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const enum PatchFlags {
TEXT = 1, // 动态的文本节点
CLASS = 1 << 1, // 2 动态的class
STYLE = 1 << 2, // 4 动态的style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态key,当key变化时需要完整的diff算法作比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有key属性的Fragment
NEET_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态slot
HOISTED = -1, // 特殊标志是负整数,代表永远不会用作diff
BAIL = -2, // 一个特殊标志,指代差异算法
}

此外重新渲染的算法也做了优化,使用闭包进行缓存。这使得Vue3的速度比Vue2快6倍。
具体做法就是使用静态提升,比如下面这段HTML代码:

1
2
<span>你好</span>
<div>{{ message }}</div>

没做静态提升之前:

1
2
3
4
5
6
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode('span', null, '你好'),
_createVNode('div', null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

做了静态提升之后:

1
2
3
4
5
6
7
8
const _hoisted_1 = /*#__PURE__*/_createVNode('span', null, '你好', -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode('div', null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

这里其实利用了闭包的机制,将_hoisted_1存到内存中去,下次渲染只要取_hoisted_1即可。
另外Vue3还给_hoisted_1添加了PatchFlag为-1的标志,代表该节点将不在参与diff算法。
静态提升默认是关闭的,需要通过设置app.config.compilerOptions.hoistStatic来开启

四、生命周期钩子命名方式改变

Vue3生命周期函数:

  • setup 开始创建组件
  • onBeforeMount 组件挂载到页面之前加载
  • onMounted 组件挂载到页面之后加载
  • onBeforeUpdate 组件更新之前
  • onUpdated 组件更新之后
  • onBeforeUnmount 组件销毁前
  • onUnmounted 组件销毁后

需要注意的是,Vue3的所有生命周期函数都需要先引入再使用。
另外除了以上这些函数,Vue3还增加了onRenderTracked和onRenderTriggered两个函数

setup函数的特性:

  • 接收两个参数:props和context(包含attrs、slots、emit)
  • 生命周期处于beforeCreate之前
  • 执行setup,组件实例尚未被创建(因此setup中的this是undefined)
  • 与模板一起使用,需要返回一个对象
  • setup函数中的props参数不能使用ES6结构,这会让它失去响应性。如需结构props,可以通过toRefs来完成该操作。
  • setup内使用响应式数据,需要通过.value获取
  • 从setup函数中返回对象上的property可以在模板中被访问,它将自动展开内部值,不需要在模板中追加.value
  • setup只能同步不能异步

五、支持碎片

Vue3支持碎片(fragment),也就是说可以有多个根节点

六、父子传参方式不同

Vue3的setup第二个参数content对象中就有emit,取出来就可以直接用了

七、指令与插槽使用方法不同

在Vue2中,

  • 插槽可以简写成slot
  • 另外v-for和v-if一起使用时,v-for的优先级更高,不建议在一起使用。

在Vue3中,

  • 插槽必须用v-slot定义
  • v-for和v-if一起使用时,v-if会被当作v-for的判断语句,两者不会冲突。
  • 移除keyCode作为v-on的修饰符,当然也不支持config.keyCode
  • 移除v-on.native修饰符
  • 移除过滤器filter

八、main.js不一样

Vue2中可以使用prototype的形式操作,引入的是构造函数

Vue3中需要使用结构的形式进行操作,引入的是工厂函数。Vue3没有根标签

九、代码上的区别

以下是Vue3的一些定义示例

  • 定义组件

在Vue2中,定义组件可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
name: 'reactive',
data() {
return {
arr: [1, 2, 3]
}
},
mounted() {

}
}
</script>

在Vue3中则是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import { ref, onMounted } from 'vue'
setup(() => {
var arr = ref([1, 2, 3])
onMounted(() => {

})

return {
arr
}
})
</script>

其中setup也可以使用语法糖直接写成以下形式:

1
<script setup></script>
  • 定义数据

在Vue2中定义数据是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
props: {
title: String
},
data() {
return {
username: '',
password: ''
}
}
}
</script>

而Vue3中则要这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
import { reactive } from 'vue'

export default {
props: {
title: String
},
setup() {
let state = reactive({
username: '',
password: ''
})

return { state }
}
}
</script>

其中reactive用来建立响应式数据,它首先需要通过vue引入,最后在setup通过return返回

当然也可以使用ref来定义基本类型的数据,不过用ref定义的数据需要通过value属性来获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref, reactive }

let msg = ref('hello world')
let obj = reactive({
name: 'juejin',
age:3
})
const changeData = () => {
msg.value = 'hello juejin'
obj.name = 'hello world'
}
</script>

接着我们可以在HTML代码中通过这个state获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="form-element">
<h2>{{ state.title }}</h2>
<input type="text" v-model="state.username" placeholder="username" />
<input type="password" v-modle="state.password" placeholder="password" />
<button @click="login">
Submit
</button>
<p>
Values: {{ state.username + ' ' + state.password }}
</p>
</div>
</template>
  • 定义函数

Vue3中声明函数跟定义变量类似,都可以在setup中定义,并且返回给外部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
props: {
title: ''
},
setup() {
const state = reactive({
username: '',
password: ''
})

const login = () => {

}

return {
login,
state
}
}
}
  • 使用生命周期函数

跟Vue2不同的是,Vue3的生命周期需要引入。

1
2
3
4
5
6
7
8
9
10
11
12
import { onMounted } from 'vue'

export default {
props: {
title: ''
},
setup() {
onMounted(() => {
console.log('组件已挂载')
})
}
}
  • 计算属性

在Vue3中是引入computed来包裹一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { computed, reactive } from 'vue'

export default {
props: {
title: String
},
setup() {
const state = reactive({
username: '',
password: '',
lowerCaseUsername: computed(() => state.username.toLowercase())
})

}
}
  • prop属性

跟Vue2一样需要在props属性中接收,不同的是在setup中需要通过参数来访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 父组件
<template>
<div>
<Child :msg="parentMsg">
</div>
</template>
<script>
import { ref, defineComponent } from 'vue'
import Child from './Child.vue'

export default defineComponent({
components: {
Child
},
setup() {
const parentMsg = ref('父组件信息')
return {
parentMsg
}
}
})
</script>

// 子组件
<template>
<div>
{{ parentMsg }}
</div>
</template>
<script>
import { defineComponent, toRef } from 'vue'
export default defineComponent ({
props: ['msg'], // 通过props属性接收
setup(props) {
console.log(props.msg)
let parentMsg = toRef(props, 'msg')
return {
parentMsg
}
}
})
</script>

如果是使用setup语法糖,可以通过defineProp接收,由于defineProps是编译宏,所以无需引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//父组件
<template>
<div>
<Child :msg="parentMsg">
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMsg = ref('父组件信息')
</script>

//子组件
<template>
<div>
{{ parentMsg }}
</div>
</template>
<script setup>
import { toRef, defineProps } from 'vue'
const props = defineProps(['msg'])
console.log(props.msg)
let parentMsg = toRefs(props, 'msg')
</script>
  • emit事件

在Vue3中可以通过解构setup的第二个参数来获得emit函数:

1
2
3
4
5
6
7
8
9
10
export default {
setup(props, { emit }) {
const login = () => {
emit('login', {
username: props.username,
password: props.password
})
}
}
}

如果使用语法糖,那就用defineEmits来发射数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父组件
<template>
<div>
<Child @sendMsg='getFromChild'>
</div>
</template>
<script setup>
import Child from './Child.vue'
const getFromChild = val => {
console.log(val) // 我是子组件数据
}
</script>

// 子组件
<template>
<div>
<button @click="sendFun">send</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
const emits = defineEmits(['sendMsg'])
const sendFun = () => {
emits('sendMsg', '要传输的信息')
}
</script>
  • provide/inject

provide/inject也可以用于父子组件间的通讯
provide是一个对象,或者包含返回对象的函数。这个对象里面包含要传给子组件的属性
inject是一个数组,或者一个对象,用于获取父组件的provide,任何深层嵌套的子组件都可以获取到父组件的provide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 父组件
<script>
import Child from './Child'
import { defineComponent, provide } from 'vue'

export default defineComponent({
components: {
Child
},
setup() {
const msg1 = '子组件信息1'
const msg2 = '子组件信息2'
provide('msg1', msg1)
provide('msg2', msg2)

return {}
}
})
</script>

// 子组件
<script>
import { inject, defineComponent } from 'vue'
export default defineComponent({
setup() {
console.log(inject('msg1').value)
console.log(inject('msg2').value)
}
})
</script>

在setup语法糖中可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 父组件
<script setup>
import Child from './Child'
import { ref, provide } from 'vue'
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
provide('msg1', msg1)
provide('msg2', msg2)
</script>

// 子组件
<script setup>
import { inject } from 'vue'
console.log(inject('msg1').value)
console.log(inject('msg2').value)
</script>
  • watch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>{{ addSum }}</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const a = ref(1)
const b = ref(2)

const addSum = computed(() => {
return a.value + b.value
})

watch(a, (newValue, oldValue) => {
console.log(`a从${oldValue}变成了${newValue}`)
})
</script>

在Vue2中,如果希望watch在组件初始化的时候运行,需要将属性immediate设为true。
而在Vue3中,则可以使用watchEffect,在watchEffect中不需要传入依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>{{ watchTarget }}</div>
</template>
<script setup>
import { watchEffect, ref } from 'vue'
let watchTarget = ref(0)
watchEffect(() => {
console.log(watchTarget.value)
})
setInterval(() => {
watchTarget.value ++
}, 1000)
</script>

watchEffect会在刚进入页面的时候执行一次,然后随着延时器的触发,执行一次。

  • 路由

因为Vue3取消了this,因此路由需要通过useRouter和useRoute来引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>
<button @click="toPage">路由跳转</button>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'

export default defineComponent({
beforeRouteEnter(to, from, next) {
next()
},
beforeRouteLeave(to, from, next) {
next()
},
beforeRouteUpdate(to, from, next) {
next()
},
setup() {
const route = useRoute()
const router = useRouter()
const toPage = () => {
router.push(xxx)
}
route.params // 获取路由参数
route.query // 获取query

return {
toPage
}
}
})
</script>

由于setup触发的时候,示例已经创建了。
而beforeRouteEnter是在实例创建之前触发的,因此不能用在setup语法糖中,需要用原来的写法。
而另外两个钩子函数可以通过引入onBeforeRouteUpdate和onBeforeRouteLeave来实现。

十、Suspense

允许程序在等待异步组件时渲染兜底内容,比如加载一个loading条。它有两个命名插槽:default和fallback,其中fallback用于展示加载状态。

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<suspense>
<template #default>
<todolist />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>

如果想在setup中调用异步请求,则需要在setup之前加上async。但这时会收到警告
解决办法是在调用这个组件的父页面使用一层suspense包裹住。

十一、Teleport

Teleport可以将部分DOM移动到Vue app之外的位置,比如dialog一般是写在组件最下面。那如果跟这组件一起添加到节点中,就会出现dialog无法覆盖整个页面的问题。这个问题在Vue2中是通过设置position:fixed来解决的。

有了Teleport,便可以将dialog写在组件中,然后通过to属性指定最终挂载的位置,比如我想要将dialog挂载到body上,就可以这么写:

1
2
3
<teleport to="body">
<dialog-comp></dialog-comp>
</teleport>

下面用Teleport实现一个确认对话框插件。一个Modal组件主要分为以下四个部分:

  • 遮罩层
  • 标题内容
  • 主题内容
  • 确定和取消按钮

首先是插件目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- plugins
|---- modals
|------ Content.tsx // 维护Modal的内容,用于h函数和jsx语法
|------ Modal.vue // 基础组件
|------ config.ts // 全局默认配置
|------ index.ts // 入口
|------ locale // 本地化相关
|-------- index.ts
|-------- lang
|---------- en-US.ts
|---------- zh-CN.ts
|---------- zh-TW.ts
|------- modal.type.ts // ts类型声明

这个插件最终会被app.use(Modal)使用。

接着就开始写核心组件Modal.vue的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<Teleport to="body" :disabled="!isTeleport">
<div v-if="modalValue" class="modal">
<div
class="mask"
:style="style"
@click="maskClose && !loading && handleCancel()"
></div>
<div class="modal__main">
<div class="modal_title line line--b">
<span>{{ title || t('r.title') }}</span>
<span
v-if="close"
:title="t('r.close')"
class="close"
@click="!loading && handleCancel()"
>x</span>
</div>
<div class="modal__content">
<Content v-if="typeof content === 'function'" :render="content" />
<slot v-else>
{{ content }}
</slot>
</div>
<div class="modal__btns line line--t">
<button :disabled="loading" @click="handleConfirm">
<span class="loading" v-if="loading"></span>{{ t("r.confirm") }}
</button>
<button @click="!loading && handleCancel()">
{{ t("r.cancel") }}
</button>
</div>
</div>
</div>
</Teleport>

其中modal__content部分,可以根据传入content类型的不同,对应显示不同的内容
比如可以通过字符串和默认插槽的形式:

1
2
3
4
5
6
7
8
9
10
// 字符串
<Modal v-model="show"
title="演示 content"
content="hello world"></Modal>

// 插槽
<Modal v-model="show"
title="演示slot">
<div>hello world</div>
</Modal>

另外通过API形式调用Modal组件,可以使用下面两种方式创建content:

  • h函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $modal.show({
    title: '演示h函数',
    content(h) {
    return h(
    'div',
    {
    style: 'color:red;',
    onClick: ($event: Event) => console.log('clicked', $event.target)
    },
    'hello world ~'
    )
    }
    })
  • JSX语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $modal.show({
    title: '演示jsx语法',
    content() {
    return (
    <div
    onClick={($event: Event) => console.log('clicked', $event.target)}
    >
    hello world ~
    </div>
    )
    }
    })

那么如何通过API形式来调用Modal组件呢?

在Vue2中可以通过Vue.extend获得组件实例,然后挂载到某个body上面:

1
2
3
4
import Modal from './Modal.vue'
const ComponentClass = Vue.extend(Modal)
const instance = new ComponentClass({ el: document.createElement('div') })
document.body.appendChild(instance.$el)

虽然Vue3中移除了Vue.extend,但可以通过createVNode实现

1
2
3
4
5
import Modal from './Modal.vue'
const container = document.createElement('div')
const vnode = createVNode(Modal)
render(vnode, container)
document.body.appendChild(container)

在Vue2中,如果想挂载到全局,可以借助this

1
2
3
4
5
export default {
install(vue) {
vue.prototype.$modal = modal
}
}

而Vue3中已经没有this这个概念,因此需要挂载到app.config.globalProperties上面:

十二、引入tree-shaking

Vue3引入tree-shaking,将无效模块剪辑,仅打包需要的,让体积更小。

在Vue2中,所有的属性都挂在this下面,这样会导致某些属性没有用到,依旧会被打包进去。

而Vue3则是引入了import语法,只有被引入的并且有实际用到的依赖,才会被打进包内。

十三、事件监听缓存

Vue3在CompilerOptions中新增了一个属性叫cacheHandlers,这个属性用于控制事件监听是否进行缓存,默认值为false

在false的情况下绑定事件节点会被视为动态节点,因此会每次都去追踪它的变化,但同一个事件监听函数一般是不变,这就造成了性能浪费。
比如下面这段代码:

1
2
3
<div>
<button @click="onClick">点我</button>
</div>

编译后的结果可以通过Vue3 Template Explorer查看

在没开启事件监听的情况下,我们会得到以下代码:

1
2
3
4
5
6
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})

开启事件监听之后,我们则会得到以下代码:

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}

可以看到,开启缓存之后,onClick事件会被缓存到_cache[1]之中。并且没有了PatchFlag,这样下次diff的时候就不会参与进去。

具体的配置方式就是在Vue3中createApp创建实例之后,通过app.config.compilerOptions.cacheHandlers来设置。

参考
vue2和vue3区别
简述:Vue2和Vue3开发区别
想知道Vue3与Vue2的区别?五千字教程助你快速上手Vue3!
web前端面试 - 面试官系列
一文完全吃透 vue3 teleport
Vue3源码