一、合成型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 ) ], 64 )) }
|
做了静态提升之后:
1 2 3 4 5 6 7 8
| const _hoisted_1 = _createVNode('span', null, '你好', -1 )
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _createVNode('div', null, _toDisplayString(_ctx.message), 1 ) ], 64 )) }
|
这里其实利用了闭包的机制,将_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也可以使用语法糖直接写成以下形式:
在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()) })
} }
|
跟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'], 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>
|
在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,任何深层嵌套的子组件都可以获取到父组件的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>
|
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
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源码