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
虽然有很多更具体的数据,但不用可以去记录这些数据,跟我们日常开发也没有太多关系,了解即可。
2. Vue3生命周期
生命周期需要单独来看
Options API 生命周期
beforeDestroy
改为beforeUnMount
destroyed
改为unMounted
- 其他沿用Vue2生命周期
具体代码看演示🌮
Composition API
新的 Composition API 有了新的生命周期。
具体代码看演示🌮
3. 如何看待Composition API 和 Options API
问题回答方向:
- Composition API带来了什么
- Composition API和 Options API 如何选择
- 别误解Composition API
Composition API带来了什么?
更好的代码组织,更好的逻辑复用,更好的类型推导
相关文章: https://cn.vuejs.org/guide/extras/composition-api-faq.html
vue3 https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e
关于类型推导:
- vue2 中的 this 的方式是比较微妙的。(比如 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)
- vue2 设计时,是没有考虑 ts 的类型推导的
如何选择
- 不建议公用,会引起混乱
- 小型项目,业务逻辑简单,用Options API
- 中大型项目、逻辑复杂,用Composition API
别误解Composition API
- Composition API属性高阶技巧,不是基本必会
- Composition API是为了解决复杂业务逻辑场景设计的
- Composition API就像Hooks在React中地位
4. 如何理解ref toRef toRefs?
思路:
- 是什么
- 最佳使用方式
- 进阶,深入理解
网络中对于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 非常有用。
这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:
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 的内部实现,可如下解释:
// 伪代码:不具有响应式,即改变值不会触发渲染
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
创造响应式toRef
和toRefs
延续响应式
总结
理解和应用要分开总结,这很重要。即便你听不懂原理,也要知道如何使用。
- ref 值类型响应式 reactive引用类型响应式
toRef
,toRefs
用于延续响应式【解构后用】.value
是为了设计响应式而必须存在的
5. Vue3升级了哪些重要的功能
我花了业余时间,详细对比了 v2 和 v3 的文档目录,找出了以下比较重要的改动。 只说重点改动,足够回复面试题了。要看详细的,需要去查阅文档。
另外,只说使用上的,不说原理上的。
::: waring 注意
(切记,不要像背诵课文似的)
:::
可使用 vite 安装
(后面会讲vite)
createApp
创建方法
// vue2.x
const app = new Vue({ /* 选项 */ })
// vue3
const app = Vue.createApp({ /* 选项 */ })
全局 API
// 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
生命周期
已讲过
多事件处理
<!-- 在 methods 里定义 one two 两个函数 -->
<button @click="one($event), two($event)">
Submit
</button>
Fragment
取消了 v2.x 的“一个组件必须有一个根元素”
<!-- 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 2.x -->
<MyComponent v-bind:title.sync="title" />
<!-- vue 3.x -->
<MyComponent v-model:title="title" />
文档
- vue2.x https://v2.cn.vuejs.org/v2/guide/components-custom-events.html#sync-修饰符
- vue3 https://cn.vuejs.org/guide/components/v-model.html#v-model-arguments
demo 代码参考 VModel/index.vue
异步组件
新增 defineAsyncComponent
方法。
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
vue2.x 写法如下
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component.vue')
}
})
vue3 移除了过滤器 filter
<!-- 以下 filter 在 vue3 中不可用了!!! -->
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
Teleport
<!-- 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 的内容。
<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函数(复用函数)
- 抽离逻辑代码到一个函数
- 函数命名约定为 useXxx 格式(React Hook也是)
- 在setup中引用useXxx 函数
具体效果演示
7. Vue3如何实现响应式?
核心: proxy
API做对象代理
下面我们一起来看看如何使用Proxy
回顾Vue2的Object.defineProperty
原理: 利用Object.defineProperty对 对象的每个属性进行getter与setter操作,深层内容进行递归监听即可。
缺点: 1. 深度监听需要一次性递归【性能开销大】 2. 无法监听新增属性/删除属性【Vue.set Vue.delete】
- 无法监听数组操作【需要重写数组方法】
Proxy用法
基本使用
本质上还是对一个对象实现get、set、delete监听操作,每次做操作时实现自己的逻辑即可
具体用法代码演示
Reflect作用
- 和
proxy
能力完美切合,一一对应 - 规范化、标准化、函数式
- 替代掉Object上的工具函数
如何理解规范化、标准化?
// 正常写法
const obj = { a:1, b:200 }
// 1. 判断属性是否属于一个对象
'a' in obj => true
// 2. 删除对象的属性
delete a
// 3. 获取对象所有属性
Object.getOwnPropertyNames(obj) => ['a', 'b']
// 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
10. Vue3为何比Vue2快
说到vue3大部分人的印象就是Vue3比Vue2快!!! 但是却很少人知道为什么Vue3比Vue2快,快在哪里? 哪些地方做了优化?
我们可以以下几个点来看待:
- Proxy响应 2. PatchFlag 3. hoistStatic 4. cacheHandler 5. SSR优化 6. tree-shaking
PS:通过 vue3 template explorer 来体验下面的内容。
Proxy 代理
Proxy已做讲解不重复讲解
提升点:对深层结构会选择性监听代理 一开始并不会把一个对象所有深层属性代理
所以初始化会更快
PatchFlag 静态标记
【案例 1】生成 PatchFlag 是 1 /* TEXT */
<div>
<p>静态文字</p>
<p>{{ msg }}</p>
</div>
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 */
<div>
<span>静态文字</span>
<span :id="hello" class="bar">{{ msg }}</span>
</div>
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 就可以区分出来动态节点和静态节点,只对比动态节点即可。
hoistStatic 静态提升
编译如下模板
<div>
<span>静态文字1</span>
<span>静态文字2</span>
<span>静态文字3</span>
<span>{{ msg }}</span>
</div>
开启 HoistStatic
选项,可以看到静态节点全部提升到父作用域去定义,并缓存起来。
典型的“拿空间换时间”的优化方法,缓存就不用重复生成。
还有,如果多个静态节点挨着在一起,超过一定的数量,会被合并起来。 例如编译如下模板
<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>
结果:
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 缓存事件
渲染如下模板
<div>
<span @click="onClick">
静态文本
</span>
</div>
开启 cacheHandlers
开关,可以看到 onClick 事件会被缓存到 _cache[1]
中。
SSR 时的优化
编译如下模板,输出 SSR
<div>
<span>静态文字1</span>
<span>静态文字2</span>
<span>静态文字3</span>
<span>{{ msg }}</span>
</div>
静态文件直接输出,绕过了 vdom 到 dom 的过程。 这跟 PatchFlag 有点类似。
tree-shaking
编译以下几段模板,可以看到 import
的内容不一样。
<!-- 模板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' 提升速度
<script type="module">
import add from './src/add.js'
const res = add(1, 2)
console.log('add res', res)
</script>
vite对比webpack的开发环境运行
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”