点击下方“前端开发爱好者”,选择“设为星标”
第一时间关注技术干货!
哈喽,大家好 我是
xy
👨🏻💻。可能是好久没有面试了,突然被面试官问:Vue3 有哪些新特性?和 Vue2 有哪些不同点?
竟然不知道怎么回答了!
前几天收到一份面试邀请
,面试官看了我的简历说:既然你对 Vue2 和 Vue3 都这么精通了,那你来说说这两者有什么不同之处吧?还有 Vue3 有哪些新的特性?越详细越好!
啊,大脑惊现短暂的空白!
对于工作了有 8
年的前端程序员来说,面试官问出这个问题好像是在质疑
我的实力?
可能是好久没面试了,对于这些八股文
的东西我都忘的差不多了,虽然也能讲出不少,比如:
Vue3 中使用 Composition API
替代 Vue2 中的 Options API
数据双向绑定 Vue3 使用 ES6 的 Proxy API
,而 Vue2 使用 ES5 的 Object.defineProperty()
不过,我深知,即使工作再久,对于技术的细节也不能马虎。
Vue2
和 Vue3
之间确实存在诸多差异
,而 Vue3 更是带来了许多令人振奋的新特性
。
为了以后能更好地回答这个问题,我特地查阅
了相关资料,整理出了一份详尽的对比与解析,希望能帮助到同样对 Vue 感兴趣的你。
Composition API
是 Vue3
中的新特性,它提供了一种更加灵活的方式来组织和复用逻辑。与传统的 Options API
相比,Composition API
可以更好地处理复杂的组件和逻辑复用问题。
<template>
<div>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
Vue3 从一开始就设计为与 TypeScript
兼容,这意味着你可以享受到 TypeScript
带来的类型检查和自动补全等好处。
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const message = ref<string>('Hello, Vue 3!');
return { message };
}
});
</script>
Vue2 使用 Object.defineProperty
为每个属性创建 getter
和 setter
,通过 getter 和 setter 来捕获操作以实现响应式更新; 很多情况下,属性的新增和删除拦截不到(比如数组的长度变化)
Vue3 通过 Proxy
实现响应式更新,proxy 是 ES6
新特性,提供了更多的拦截操作;能监听到属性的删除和新增
// Vue3 响应式原理源码
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
if (!isObject(target)) {
if (__DEV__) {
warn(
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
target,
)}`,
)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
在 Vue 3 中,你可以创建包含多个根节点
的组件,这在之前的版本中是不允许的。
<template>
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</template>
Teleport 是一个新的组件,它允许你将子组件移动
到 DOM
的不同位置。这对于处理模态框
、提示框
等组件非常有用。
<template>
<teleport to="#modal-container">
<div v-if="isVisible" class="modal">Modal content</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue';
const isVisible = ref(false);
</script>
Suspense
是一个内置组件,用来在组件树中协调对异步依赖的处理。
它让我们可以在组件树上层等待下层
的多个嵌套异步依赖项解析完成,并可以在等待时
渲染一个加载状态。
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
Vue3
引入了静态标记(patchFlags
),能够识别静态节点,即不会动态改变的节点,在后续渲染中可以直接复用,减少不必要的 diff
计算。
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false,
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance,
} as VNode
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
export const PatchFlagNames: Record<PatchFlags, string> = {
// 动态文本内容
[PatchFlags.TEXT]: `TEXT`,
// 动态类名
[PatchFlags.CLASS]: `CLASS`,
// 动态样式
[PatchFlags.STYLE]: `STYLE`,
// 动态属性,不包含类名和样式
[PatchFlags.PROPS]: `PROPS`,
// 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
[PatchFlags.FULL_PROPS]: `FULL_PROPS`,
// 带有监听事件的节点
[PatchFlags.NEED_HYDRATION]: `NEED_HYDRATION`,
// 不会改变子节点顺序的 fragment
[PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
// 带有 key 属性的 fragment 或部分子节点
[PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
// 子节点没有 key 的fragment
[PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,
// 只会进行非 props 的比较
[PatchFlags.NEED_PATCH]: `NEED_PATCH`,
// 动态的插槽
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
// 仅在开发环境中使用的标志
[PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`,
// 静态节点,diff阶段忽略其子节点
[PatchFlags.HOISTED]: `HOISTED`,
// 代表 diff 应该结束
[PatchFlags.BAIL]: `BAIL`,
}
Vue3
改进了 diff
算法,使其更具针对性,例如在对比过程中,对于没有动态内容的节点可以更快地跳过
,而对于有动态内容的部分则精确查找
差异。
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized = false,
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
}
}
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
}
Tree-shaking
:模块打包 webpack
、rollup
等中的概念。
移除 JavaScript
上下文中未引用的代码。
主要依赖于 import
和 export
语句,用来检测代码模块是否被导出
、导入
,且被 JavaScript 文件使用。
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
。
Tree shaking 是基于 ES6
模板语法(import 与 exports),主要是借助 ES6 模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking
无非就是做了两件事
:
ES6 Module
判断哪些模块已经加载模块
和变量
未被使用或者引用,进而删除
对应代码Vue2
是选项 API(Options API
),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
<!-- MyComponent.vue -->
<template>
<div>
<h1>{{ message }}</h1>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue 2!',
count: 0,
};
},
methods: {
increment() {
this.count++;
this.message = `Count: ${this.count}`;
},
},
};
</script>
Vue3
组合式 API(Composition API
)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性
、内聚性
,其还提供了较为完美的逻辑复用
性方案。
<!-- MyComponentV3.vue -->
<template>
<div>
<h1>{{ message }}</h1>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
const count = ref(0);
function increment() {
count.value++;
message.value = `Count: ${count.value}`;
}
</script>
对于生命周期
来说,整体上变化不大,只是大部分生命周期钩子名称上 + “on
”,功能上是类似的。
不过有一点需要注意,Vue3 在组合式 API(Composition API
)中使用生命周期钩子时需要先引入,而 Vue2 在选项 API(Options API
)中可以直接调用生命周期钩子
Vue2 | Vue3 |
---|---|
beforeCreate() | setup() |
created() | setup() |
beforeMount() | onBeforeAMount() |
mounted() | onMounted() |
beforeUpdate() | onBeforeUpdate() |
undated() | onUpdated() |
beforeDestroy() | onBeforeunmount() |
destroyed() | onUnmounted() |
Vue2 生命周期使用:
export default {
data() {
return {};
},
// mounted()
mounted(){
// ...
}
};
Vue3 生命周期使用:
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
// ...
});
</script>
Vue2
中,Mixins
是一种全局特性,可以在多个组件之间共享代码。
你可以创建一个 Mixin
对象,然后在组件中通过 Mixins 选项引入这个对象,从而将 Mixin 中的属性和方法合并到组件中。
如果多个 Mixins 中有相同的属性或方法
,可能会导致命名冲突。另外,由于 Mixins 是全局的,它们会增加组件的耦合度
,使得代码难以维护
Vue3 的 Hooks
允许你将相关的逻辑组合到一起,形成一个逻辑单元
,组件内部使用的,而不是全局的,这减少了命名冲突和耦合度。
import { ref } from 'vue'
export default function() {
const count = ref(0);
const add = () => {
count.value++;
}
const decrement = () => {
count.value--;
}
return {
count,
add,
decrement
}
}
// 在用到的文件中引入此 hook.js 文件
<script setup>
//引入 useCount hooks 文件
import useCount from "../hooks/useCount"
// 解构出 count, add, decrement
const { count, add, decrement } = useCount()
</script>
Vuex
和 Pinia
都是 Vue.js
的状态管理库,它们的主要目的是在组件之间共享
和管理
状态。尽管它们的目标相同,但它们在实现方式和使用范围上有一些关键区别。
Vuex:
Pinia:
Vuex 是 Vue.js 的官方状态管理库,成熟稳定,适用于各种规模的项目。
而 Pinia
是一个相对较新的库,提供了更轻量级、更灵活的状态管理方案,特别适合对 TypeScript
有需求或者希望在大型应用中更好地组织状态的开发者。
Vite
和 Webpack
都是 JavaScript 模块打包工具,它们可以将许多模块按照依赖关系打包成一个或多个文件
Vite 的主要优点:
Webpack 的主要优点:
Vite 和 Webpack 对比:
性能:Vite 在开发过程中具有更高的性能,因为它采用了现代浏览器原生支持 ES 模块的特性,无需对整个应用进行打包。而 Webpack 在开发过程中需要对整个应用进行打包,可能导致较慢的启动速度。
配置:Vite 的配置相对简单,易于上手。Webpack 的配置较为复杂,但提供了更多的功能和灵活性。
生态系统:Webpack 拥有庞大的社区和丰富的插件生态系统,支持各种模块系统和资源类型。Vite 是一个相对较新的项目,生态系统相对较小。
总之,Vite
和 Webpack
都是优秀的模块打包工具,具有各自的优势。在选择构建工具时,可以根据项目需求、团队经验和个人喜好来决定使用哪个工具。
对于现代前端项目,特别是使用 Vue.js 的项目,Vite 可能是一个值得尝试的选择。
公众号
:前端开发爱好者
专注分享web
前端相关技术文章
、视频教程
资源、热点资讯等,如果喜欢我的分享,给 🐟🐟 点一个赞
👍 或者 ➕关注
都是对我最大的支持。
欢迎长按图片加好友
,我会第一时间和你分享前端行业趋势
,面试资源
,学习途径
等等。
添加好友备注【进阶学习】拉你进技术交流群
关注公众号后,在首页: