Skip to content
本页目录

Vue3

vue3作为vue2的升级版本 作为新项目开发是或不可少的框架选择。

本章节我们会主要介绍vue3的面试题以及对比vue2的优势,当然也会涉及到vue3的原理内容

章节主要内容:

  • Vue3比vue2有什么优势?
  • 描述vue3生命周期
  • 如何看待Composition API 和 Options API
  • 如何理解ref toRef toRefs?
  • vue3升级了哪些重要功能?
  • Composition API如何实现逻辑复用?
  • Vue3如何实现响应式?
  • watch与watchEffect的区别是什么?
  • setup中如何获取组件实例?
  • vue3为何比vue2快?
  • Vite是什么?
  • Composition API 和 React Hooks对比

1. Vue3比Vue2有什么优势?

  • 性能更好 【重要内容
  • 体积更小 【对比vue2】
  • 更好的TS支持【源码使用TS开发
  • 更好的代码组织 【大项目代码拆分更好了】
  • 更好的逻辑抽离【公共代码更好拆分了】 - Composition API
  • 更多新功能【对比vue2加了更多API】 Fragment Teleport Suspense

虽然有很多更具体的数据,但不用可以去记录这些数据,跟我们日常开发也没有太多关系,了解即可。

image-20230424172620814

2. Vue3生命周期

生命周期需要单独来看

Options API 生命周期

  • beforeDestroy 改为 beforeUnMount
  • destroyed 改为 unMounted
  • 其他沿用Vue2生命周期

具体代码看演示🌮

Composition API

新的 Composition API 有了新的生命周期。

具体代码看演示🌮

3. 如何看待Composition API 和 Options API

问题回答方向:

  1. Composition API带来了什么
  2. Composition API和 Options API 如何选择
  3. 别误解Composition API

Composition API带来了什么?

更好的代码组织,更好的逻辑复用,更好的类型推导

相关文章: https://cn.vuejs.org/guide/extras/composition-api-faq.html

vue2 https://github.com/vuejs/vue-cli/blob/a09407dd5b9f18ace7501ddb603b95e31d6d93c0/packages/@vue/cli-ui/src/components/folder/FolderExplorer.vue#L198-L404

vue3 https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e

image-20230424215013419

关于类型推导:

image-20230424215453561
  • vue2 中的 this 的方式是比较微妙的。(比如 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)
  • vue2 设计时,是没有考虑 ts 的类型推导的

如何选择

  1. 不建议公用,会引起混乱
  2. 小型项目,业务逻辑简单,用Options API
  3. 中大型项目、逻辑复杂,用Composition API

别误解Composition API

  • Composition API属性高阶技巧,不是基本必会
  • Composition API是为了解决复杂业务逻辑场景设计的
  • Composition API就像Hooks在React中地位

4. 如何理解ref toRef toRefs?

思路:

  1. 是什么
  2. 最佳使用方式
  3. 进阶,深入理解

网络中对于ref的讨论: https://v3.cn.vuejs.org/api/refs-api.html

是什么

ref

创建响应式对象一般用 reactive

第一,响应式

  • 生成值类型的响应式数据
  • 可直接用于模板和 reactive
  • 可通过 .value 修改值

第二,模板引用 【拿DOM元素、组件实例】

具体看代码演示

toRef

可以用来为源响应式对象上的 property 新创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

  • 针对一个响应式对象的 property
  • 创建一个 ref 具有响应式
  • 保持引用关系

【注意】必须是响应式对象,普通对象不具有响应式。

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref 。

  • 响应式对象转换为普通对象 obj
  • obj 的每个属性都是对应的 ref
  • 保持引用关系

【注意】必须是响应式对象,普通对象不具有响应式。

使用场景1:

当从合成函数返回响应式对象时,toRefs 非常有用。

这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

js
function useFeatureX() {
  const state = reactive({
    x: 1,
    y: 2
  })

  // 逻辑运行状态,省略 N 行

  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    const { x, y } = useFeatureX()

    return {
      x,
      y
    }
  }
}

最佳使用方式

  • reactive做对象的响应式,用ref做值类型的响应式
  • setup中返回 toRefs(state), 或者toRef(state,'xxx') 保持数据的响应式
  • 合成函数中返回响应式对象中用 toRefs

进阶,深入理解

  • 为何需要ref
  • 为何需要.value
  • 为何需要 toRef, toRefs

为何需要ref

  • 返回值类型,会丢失响应式
  • 如在:setup, computed, 合成函数,都要可能返回值类型
  • Vue如不定义ref,用户将自造ref,反而混乱

简单来讲:不设置ref去处理值类型的响应式反而会带来更多的问题

为什么需要.value?

  • 因为要保证响应式,需要把值类型封装成一个对象形式,所以用 .value 表示这个值
  • 模板,reactive 使用时,不需要 .value
  • 其他情况都需要 .value

结合 computed 的内部实现,可如下解释:

js
// 伪代码:不具有响应式,即改变值不会触发渲染
function computed(getter) {
  let value
  watchEffect(() => {
    value = getter() // value 是值类型,值拷贝,value 的变化不会传递到下游
  })
  return value
}

// 伪代码:具有响应式
function computed2(getter) {
  const ref = {
    value: null,
  }
  watchEffect(() => {
    ref.value = getter() // ref 是引用类型,指针拷贝,ref 的变化会传递到下游
  })
  return ref
}

// 使用场景
const a = computed(() => 100)

const b = computed2(() => 100)

// 最终a为undefined  b为{value: 100}
// 原因值类型不具备响应式   只能用引用类型

上述代码,可以把 watchEffect 改为 setTimeout 模拟一下

为何需要 toRef, toRefs

设计初衷: 不丢失数据响应式的同时,把对象数据 分解/扩散

必须存在:因为直接 property 或者 ...state ,会直接拿到属性的值。如果属性是值类型,则会丢失响应式 !!!(响应式是 Proxy 实现的)

注意事项:不创造响应式,而是延续响应式 (所以针对普通对象不行,必须是响应式对象)

提示

toRef toRefs 必须针对一个响应式对象,普通对象是无法变成响应式的。 它俩的设计初衷,本来也是针对响应式对象的 https://vue-composition-api-rfc.netlify.app/zh/api.html#toref 即,想要实现响应式,只有两个方法:ref reactive

简单来讲:

  • ref reactive创造响应式
  • toReftoRefs 延续响应式

总结

理解和应用要分开总结,这很重要。即便你听不懂原理,也要知道如何使用。

  • ref 值类型响应式 reactive引用类型响应式
  • toReftoRefs用于延续响应式【解构后用】
  • .value是为了设计响应式而必须存在的

5. Vue3升级了哪些重要的功能

我花了业余时间,详细对比了 v2 和 v3 的文档目录,找出了以下比较重要的改动。 只说重点改动,足够回复面试题了。要看详细的,需要去查阅文档。

另外,只说使用上的,不说原理上的。

::: waring 注意

(切记,不要像背诵课文似的)

:::

可使用 vite 安装

(后面会讲vite)

createApp

创建方法

js
// vue2.x
const app = new Vue({ /* 选项 */ })

// vue3
const app = Vue.createApp({ /* 选项 */ })

全局 API

js
// vue2.x
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

// vue3
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

emits

组件中 emits: ['fnName'] ,否则有警告 Extraneous non-emits event listeners

image-20230425113341658

生命周期

已讲过

多事件处理

vue
<!-- 在 methods 里定义 one two 两个函数 -->
<button @click="one($event), two($event)">
  Submit
</button>

Fragment

取消了 v2.x 的“一个组件必须有一个根元素”

html
<!-- vue2.x 组件模板 -->
<template>
  <div class="blog-post">
    <h3>{{ title }}</h3>
    <div v-html="content"></div>
  </div>
</template>

<!-- vue3 组件模板 -->
<template>
  <h3>{{ title }}</h3>
  <div v-html="content"></div>
</template>

去掉了 .sync

vue
<!-- vue 2.x -->
<MyComponent v-bind:title.sync="title" />

<!-- vue 3.x -->
<MyComponent v-model:title="title" />

文档

demo 代码参考 VModel/index.vue

异步组件

新增 defineAsyncComponent 方法。

js
import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.vue')
    )
  }
})

vue2.x 写法如下

js
new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component.vue')
  }
})

vue3 移除了过滤器 filter

html
<!-- 以下 filter 在 vue3 中不可用了!!! -->

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

Teleport

html
<!-- data 中设置 modalOpen: false -->

<button @click="modalOpen = true">
    Open full screen modal! (With teleport!)
</button>

<teleport to="body">
    <div v-if="modalOpen" class="modal">
        <div>
            telePort 弹窗 (父元素是 body)
            <button @click="modalOpen = false">Close</button>
        </div>
    </div>
</teleport>

Suspense

【注意】Suspense 是一个实验性的 API ,后面可能会改变 —— v3.0.2

当内容组件未加载完成时,先显示 loading 的内容。

html
<Suspense>
    <template>
        <Test1/> <!-- 是一个异步组件 -->
    </template>

    <!-- #fallback 就是一个具名插槽。即:
      Suspense 组件内部,有两个 slot ,
      其中一个具名为 fallback -->
    <template #fallback>
        Loading...
    </template>
</Suspense>

demo 代码参考 SuspenseTest/index.vue

Composition API

主要的功能有:

  • reactive
  • ref 相关
  • readonly
  • computed
  • watch
  • watchEffect
  • setup
  • 各生命周期钩子函数

上述仅仅是比较重要改动,足够应对面试题了。 全面的改动,可以查看 v2 升级 v3 的指南 https://v3-migration.vuejs.org/zh/

6. Composition API 如何实现逻辑复用

抽离代码 复用逻辑 在vue3中又被叫做封装Hook函数(复用函数)

  1. 抽离逻辑代码到一个函数
  2. 函数命名约定为 useXxx 格式(React Hook也是)
  3. 在setup中引用useXxx 函数

具体效果演示

7. Vue3如何实现响应式?

核心: proxyAPI做对象代理

下面我们一起来看看如何使用Proxy

回顾Vue2的Object.defineProperty

原理: 利用Object.defineProperty对 对象的每个属性进行getter与setter操作,深层内容进行递归监听即可。

缺点: 1. 深度监听需要一次性递归【性能开销大】 2. 无法监听新增属性/删除属性【Vue.set Vue.delete】

  1. 无法监听数组操作【需要重写数组方法

Proxy用法

基本使用

image-20230427195429520

本质上还是对一个对象实现get、set、delete监听操作,每次做操作时实现自己的逻辑即可

具体用法代码演示

Reflect作用

  • proxy能力完美切合,一一对应
  • 规范化、标准化、函数式
  • 替代掉Object上的工具函数

如何理解规范化、标准化?

js
// 正常写法
const obj = { a:1, b:200 }
// 1. 判断属性是否属于一个对象
'a' in obj  => true

// 2. 删除对象的属性
delete a

// 3. 获取对象所有属性
Object.getOwnPropertyNames(obj) => ['a', 'b']

js
// Reflect写法
const obj = { a:1, b:200 }
// 1. 判断属性是否属于一个对象
Reflect.has(obj, 'a')

// 2. 删除对象的属性
Reflect.deleteProperty(obj, 'a')

// 3. 获取对象值
Reflect.ownKeys(obj)  => ['a', 'b']

对比Object.defineProprty

  • 深度监听,性能更好 【使用过的属性才会进行深度监听】
  • 可监听 新增/删除 属性 【proxy本身支持】
  • 可监听数组的变化 【proxy本身支持】

Proxy无法兼容所有浏览器,无法polyfill ie11

8. watch和watchEffect的区别

  • 两者都可监听 data属性的变化
  • watch需要明确监听哪个属性
  • watchEffect会根据其中的属性,自动监听其变化

具体看代码演示

9. setup中如何获取组件实例

在setup中没有this指向 无法和Options API中获取Vue实例

解决方案: getCurrentInstance获取实例

注意点

若使用Options API 可照常使用this

image-20230427202135363

10. Vue3为何比Vue2快

说到vue3大部分人的印象就是Vue3比Vue2快!!! 但是却很少人知道为什么Vue3比Vue2快,快在哪里? 哪些地方做了优化?

我们可以以下几个点来看待:

  1. Proxy响应 2. PatchFlag 3. hoistStatic 4. cacheHandler 5. SSR优化 6. tree-shaking

PS:通过 vue3 template explorer 来体验下面的内容。

Proxy 代理

Proxy已做讲解不重复讲解

提升点:对深层结构会选择性监听代理 一开始并不会把一个对象所有深层属性代理

所以初始化会更快

PatchFlag 静态标记

【案例 1】生成 PatchFlag 是 1 /* TEXT */

html
<div>
  <p>静态文字</p>
  <p>{{ msg }}</p>
</div>
js
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "静态文字"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

【案例 2 】生成 PatchFlag 是 9 /* TEXT, PROPS */

html
<div>
  <span>静态文字</span>
  <span :id="hello" class="bar">{{ msg }}</span>
</div>
js
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", null, "静态文字"),
    _createVNode("span", {
      id: _ctx.hello,
      class: "bar"
    }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

即,所有的静态内容都没有 PatchFlag 标记,动态内容都会有。

这个 PatchFlag 到底有什么作用呢?

有了 PatchFlag 就可以区分出来动态节点和静态节点,只对比动态节点即可。

image-20230427203238764

hoistStatic 静态提升

编译如下模板

html
<div>
  <span>静态文字1</span>
  <span>静态文字2</span>
  <span>静态文字3</span>
  <span>{{ msg }}</span>
</div>

开启 HoistStatic 选项,可以看到静态节点全部提升到父作用域去定义,并缓存起来。

典型的“拿空间换时间”的优化方法,缓存就不用重复生成。

还有,如果多个静态节点挨着在一起,超过一定的数量,会被合并起来。 例如编译如下模板

html
<div>
  <span>静态文字1</span>
  <span>静态文字2</span>
  <span>静态文字3</span>
  <span>静态文字4</span>
  <span>静态文字5</span>
  <span>静态文字6</span>
  <span>静态文字7</span>
  <span>静态文字8</span>
  <span>静态文字9</span>
  <span>静态文字10</span>
  <span>{{ msg }}</span>
</div>

结果:

js
import { createVNode as _createVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>静态文字1</span><span>静态文字2</span><span>静态文字3</span><span>静态文字4</span><span>静态文字5</span><span>静态文字6</span><span>静态文字7</span><span>静态文字8</span><span>静态文字9</span><span>静态文字10</span>", 10)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

cacheHandler 缓存事件

渲染如下模板

html
<div>
  <span @click="onClick">
    静态文本
  </span>
</div>

开启 cacheHandlers 开关,可以看到 onClick 事件会被缓存到 _cache[1] 中。

SSR 时的优化

编译如下模板,输出 SSR

html
<div>
  <span>静态文字1</span>
  <span>静态文字2</span>
  <span>静态文字3</span>
  <span>{{ msg }}</span>
</div>

静态文件直接输出,绕过了 vdom 到 dom 的过程。 这跟 PatchFlag 有点类似。

tree-shaking

编译以下几段模板,可以看到 import 的内容不一样。

html
<!-- 模板1 -->
<div>
  <span>静态文字</span>
</div>

<!-- 模板2 -->
<div>
  <span>静态文字</span>
  <span>{{ msg }}</span>
</div>

<!-- 模板3 -->
<div>
  <span>静态文字</span>
  <input v-model="msg">
</div>

即,针对不同情况,vue 会自动引入所需要的功能,而不会全部引入。 这样就让使用者可以压缩他们项目打包的体积,即 tree-shaking

总结内容

  • Proxy 实现响应式,初始化时更快。之前已经讲过
  • 模板编译优化:静态内容直接输出、数据缓存 —— 所有的算法优化,都是拿空间换时间
  • diff 算法优化,让组件渲染更快。diff 算法和模板是相关的。PS:其实 diff 算法本身的对比逻辑,跟之前时一样的。可优化的是一些细节和条件分支。
  • tree-shaking

11. Vite 是什么?

https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-application

  • 一个打包工具,Vue 作者发起的

  • 借助 Vue 的影响力,和 webpack 竞争

  • 优势:开发环境启动快,无需打包

  • 开发环境基于 ES6 module https://www.caniuse.com/?search=module

  • 生产环境打包时使用 rollup

核心: 开发环境使用type='module' 提升速度

html
<script type="module">
    import add from './src/add.js'

    const res = add(1, 2)
    console.log('add res', res)
</script>

vite对比webpack的开发环境运行

image-20230427204255864image-20230427204302005

12. 与React Hooks 对比

PS:这个题目需要你了解 React Hooks ,不了解的,可以先去看看

  • 组件生命周期中 setup 只调用一次,而 React Hooks 在组件 render 和每次 update 时都会被调用
  • 无需使用 useMemo useCallback
  • “不需要顾虑调用顺序,也可以用在条件语句中” (React Hooks 的 useXXX 则有严格规定,这一点是 React Hooks 不太简单的地方)
  • (后两个,都是依赖于第一个特性的)

另外,reactive ref 这俩概念,和 react Hooks 的 useState 相比,还是后者好理解

13. script setup语法

vscode 插件 禁用 Vetur 下载 Volar

准备环境

vue-cli 创建项目之后,升级到 3.2 版本,重新安装即可 yarn add vue@next

基本使用

  • 顶级变量,可以直接用于模板中
  • 引入的组件,可以直接用于模板中
  • 使用 ref reactive 等 vue3 功能

和其他 script 使用

  • 没有 return 和 exports ,需要适应
  • script 和 template 位置调换,这样更易阅读

属性和事件

  • defineProps
  • defineEmits

defineExpose

向父组件暴露数据


setup script 中不能使用 jsx

14. vue3 JSX

【注意】如果对 JSX 语法不熟悉,可以先去学习 React 部分。

开始

  • vue3 中 JSX 的基本使用
  • JSX 和 template 的区别
  • JSX 和 slot
  • JSX 和 作用域 slot

vue3 中 JSX 的基本使用

  • 基本使用
  • 使用 .jsx 格式的文件,使用 defineComponent
    • 可以传入一个配置
    • 也可以传入一个 setup 函数
  • 引入子组件,传递属性

JSX 和 template 的区别

语法的区别:

  • JSX 就是 js 代码,它可以使用任何 js 的能力。
  • template 是模板语法,目前只能插入一些简单的 js 表达式。逻辑则需要指令,如 v-for v-if 等。
  • JSX 已经成为了 ES 规范语法,babel 支持。而 template 只是 vue 自家的语法规范。

但本质是相同的:他们都会被编译成 render 函数,用于渲染 vnode
在 vue React 原理都讲过,这里不再重复

插值

  • template 用
  • JSX 用 {xxx}

自定义组件

  • template 可用 <custom-component></custom-component> 或者 <CustomComponent></CustomComponent>
  • JSX 必须使用 <CustomComponent></CustomComponent>

传递属性和事件

  • template 中使用 a="xx":a="xx"@xx="xx"
  • JSX 中全部使用 a={xx}

条件,循环

  • template 使用指令 v-for v-if
  • JSX 中使用 js 表达式

JSX 和 slot

因为 template 只能内嵌简单的 js 表达式,无法内嵌组件,所以 vue 只能自造一个 <slot> 语法。

vue3 setup 中可以使用 context.slots.default() 获取子组件

使用 tabs 组件演示 JSX 如何操作 slot

JSX 和作用域 slot

回顾作用域 slot ,就是:父组件想要获取子组件的信息,并渲染到 slot 中

作用域 slot 很难理解,一直是 vue 初学者的噩梦。但用 JSX 将会变的很好理解,因为它就是 js 代码逻辑。

代码的方式,在 react 中叫做“renderProps”