大脑一片空白:难倒 90% 前端的 Vue 面试题!

点击下方“前端开发爱好者”,选择“设为星标

第一时间关注技术干货!

哈喽,大家好 我是 xy👨🏻‍💻。可能是好久没有面试了,突然被面试官问:Vue3 有哪些新特性?和 Vue2 有哪些不同点?竟然不知道怎么回答了!

前言

前几天收到一份面试邀请,面试官看了我的简历说:既然你对 Vue2 和 Vue3 都这么精通了,那你来说说这两者有什么不同之处吧?还有 Vue3 有哪些新的特性?越详细越好!

啊,大脑惊现短暂的空白!

对于工作了有 8 年的前端程序员来说,面试官问出这个问题好像是在质疑我的实力?

可能是好久没面试了,对于这些八股文的东西我都忘的差不多了,虽然也能讲出不少,比如:

  • Vue3 中使用 Composition API 替代 Vue2 中的 Options API

  • 数据双向绑定 Vue3 使用 ES6 的 Proxy API,而 Vue2 使用 ES5 的 Object.defineProperty()

不过,我深知,即使工作再久,对于技术的细节也不能马虎。

Vue2Vue3 之间确实存在诸多差异,而 Vue3 更是带来了许多令人振奋的新特性

为了以后能更好地回答这个问题,我特地查阅了相关资料,整理出了一份详尽的对比与解析,希望能帮助到同样对 Vue 感兴趣的你。

Vue3 新特新一览

Composition API

Composition APIVue3 中的新特性,它提供了一种更加灵活的方式来组织和复用逻辑。与传统的 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>

TypeScript 的支持

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 为每个属性创建 gettersetter,通过 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
}

vue3 支持 Fragments(多个根节点)

在 Vue 3 中,你可以创建包含多个根节点的组件,这在之前的版本中是不允许的。

<template>
 <header>Header</header>
  <main>Main content</main>
 <footer>Footer</footer>
</template>

Teleport

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 是一个内置组件,用来在组件树中协调对异步依赖的处理。

它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

  <Suspense>
  <!-- 具有深层异步依赖的组件 -->
  <Dashboard />

  <!-- 在 #fallback 插槽中显示 “正在加载中” -->
  <template #fallback>
  Loading...
  </template>
</Suspense>

虚拟 DOM 优化

Vue3 引入了静态标记(patchFlags),能够识别静态节点,即不会动态改变的节点,在后续渲染中可以直接复用,减少不必要的 diff 计算。

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps
) | null = null,
  childrenunknown = null,
  patchFlag = 0,
  dynamicPropsstring[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false,
{
  const vnode = {
    __v_isVNodetrue,
    __v_skiptrue,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIdsnull,
    children,
    componentnull,
    suspensenull,
    ssContentnull,
    ssFallbacknull,
    dirsnull,
    transitionnull,
    elnull,
    anchornull,
    targetnull,
    targetStartnull,
    targetAnchornull,
    staticCount0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildrennull,
    appContextnull,
    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`,
}

Diff 算法优化

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

Tree-shaking:模块打包 webpackrollup 等中的概念。

移除 JavaScript 上下文中未引用的代码。

主要依赖于 importexport 语句,用来检测代码模块是否被导出导入,且被 JavaScript 文件使用。

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

Tree shaking 是基于 ES6 模板语法(import 与 exports),主要是借助 ES6 模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking 无非就是做了两件事

  • 编译阶段利用 ES6 Module 判断哪些模块已经加载
  • 判断哪些模块变量未被使用或者引用,进而删除对应代码

Vue2、Vue3 使用差异对比

Composition API 替代 Options API

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!',
      count0,
    };
  },
  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>

hooks 替代 mixins

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

VuexPinia 都是 Vue.js 的状态管理库,它们的主要目的是在组件之间共享管理状态。尽管它们的目标相同,但它们在实现方式和使用范围上有一些关键区别。

Vuex

  • 由 Vue.js 官方团队开发和维护。
  • 是 Vue.js 生态系统中最流行和广泛使用的状态管理库。
  • 提供了一个集中式的状态存储,允许开发者在组件之间共享状态。
  • 使用单一状态树(single state tree)来存储整个应用的状态。
  • 提供了严格的规则(如定义 Actions, Mutations 和 Getters)来确保状态的变更可追踪且易于调试。
  • 支持中间件(middlewares)以在状态变更过程中添加额外的逻辑。
  • 有大量的插件和扩展,方便开发者与其他库集成。

Pinia

  • 由 Vue.js 核心开发者之一(Eduardo San Martin Morote,也被称为 posva)创建和维护。
  • 相对于 Vuex,Pinia 更轻量级,更灵活,易于集成。
  • 提供了一个基于插件的架构,允许开发者根据需要扩展功能。
  • 支持 TypeScript 类型推断,使得在 TypeScript 项目中使用更加方便。
  • 不强制使用 Actions, Mutations 和 Getters,允许开发者根据需要自由组织代码。
  • 支持创建多个 store,使得在大型应用中更好地组织和管理状态。
  • 提供了一个可选的、内置的持久化插件,方便开发者将状态保存到本地存储。 总结:

Vuex 是 Vue.js 的官方状态管理库,成熟稳定,适用于各种规模的项目。

Pinia 是一个相对较新的库,提供了更轻量级、更灵活的状态管理方案,特别适合对 TypeScript 有需求或者希望在大型应用中更好地组织状态的开发者。

Vite 和 Webpack

ViteWebpack 都是 JavaScript 模块打包工具,它们可以将许多模块按照依赖关系打包成一个或多个文件

Vite 的主要优点

  • 快速的冷启动速度
  • 高效的模块热更新(HMR)
  • 灵活的插件系统
  • 良好的 TypeScript 支持

Webpack 的主要优点

  • 广泛的社区支持和生态系统
  • 丰富的插件系统
  • 支持各种模块系统和资源类型
  • 可以与其他构建工具(如 Babel)无缝集成

Vite 和 Webpack 对比

  • 性能:Vite 在开发过程中具有更高的性能,因为它采用了现代浏览器原生支持 ES 模块的特性,无需对整个应用进行打包。而 Webpack 在开发过程中需要对整个应用进行打包,可能导致较慢的启动速度。

  • 配置:Vite 的配置相对简单,易于上手。Webpack 的配置较为复杂,但提供了更多的功能和灵活性。

  • 生态系统:Webpack 拥有庞大的社区和丰富的插件生态系统,支持各种模块系统和资源类型。Vite 是一个相对较新的项目,生态系统相对较小。

总之,ViteWebpack 都是优秀的模块打包工具,具有各自的优势。在选择构建工具时,可以根据项目需求、团队经验和个人喜好来决定使用哪个工具。

对于现代前端项目,特别是使用 Vue.js 的项目,Vite 可能是一个值得尝试的选择。

写在最后

公众号前端开发爱好者 专注分享 web 前端相关技术文章视频教程资源、热点资讯等,如果喜欢我的分享,给 🐟🐟 点一个 👍 或者 ➕关注 都是对我最大的支持。

欢迎长按图片加好友,我会第一时间和你分享前端行业趋势面试资源学习途径等等。

添加好友备注【进阶学习】拉你进技术交流群

关注公众号后,在首页:

  • 回复 面试题,获取最新大厂面试资料。
  • 回复 简历,获取 3200 套 简历模板。
  • 回复 React 实战,获取 React 最新实战教程。
  • 回复 Vue 实战,获取 Vue 最新实战教程。
  • 回复 ts,获取 TypeScript 精讲课程。
  • 回复 vite,获取 Vite 精讲课程。
  • 回复 uniapp,获取 uniapp 精讲课程。
  • 回复 js 书籍,获取 js 进阶 必看书籍。
  • 回复 Node,获取 Nodejs+koa2 实战教程。
  • 回复 数据结构算法,获取数据结构算法教程。
  • 回复 架构师,获取 架构师学习资源教程。
  • 更多教程资源应有尽有,欢迎 关注获取。

相关推荐

  • 万万没想到,用浏览器打开终端竟这么容易实现
  • 萝卜快写、萝卜快画来了?自动写小说、自动画漫画,两个最新的开源项目
  • 高手必知的Linux三剑客 (grep、sed、awk)
  • 列表是怎么实现的?解密列表的数据结构
  • 一文读懂数据血缘分析原理与建设方法
  • 橙单,一个免费的代码生成神器
  • Git版本管理工具,每个工程师都应该知道的基础操作!
  • Obsidian插件:Make.md为你量身打造一个完美的个人系统。
  • 从零预训练LLAMA3的完整指南:一个文件,探索Scaling Law
  • 开源仅 1 天就斩获近万星!超越 RAG、让大模型拥有超强记忆力的 Mem0 火了!
  • 拿下NeurIPS 2024金牌。
  • 博士申请 | 香港理工大学石杰明老师招收大数据/机器学习方向全奖博士/博后
  • 文末送书 | 连续25年美国统计类教材排名第一,这本统计学神书中文版来啦!
  • ICML 2024 | 图上的泛化挑战:从不变性到因果性
  • 转行跳槽做量化一定要注意的几个大坑。
  • 超强图解 Pandas 18 招!
  • 不是付费订阅用不起,而是“开源平替”更有性价比
  • [开源]自主研发基于SpringBoot + Activiti 开发的轻量级工作流框架
  • 终于有人讲明白了,数据资产、标签体系、指标体系、数据体系与用户画像(附案例+资料下载)
  • 看完这篇文章还不懂K-means聚类算法,就来找我