Vue.js相关知识汇总
前言
记录有关 Vue.js 的相关知识问题,方便复习。
Vue 基础
使用Object.defineProperty()
来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,都不能触发组件的重新渲染,因为Object.defineProperty
不能拦截到这些操作。
更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,则是通过使用Proxy
对对象进行处理,从而实现数据劫持。使用Proxy
的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为Proxy
是 ES6 的语法
Computed
和Methods
的区别
可以将同一个函数定义为一个method
或者一个计算属性。对于最终的结果,两种方式是相同的。
不同点:
computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
method 调用总会执行该函数。
v-if
和v-show
的区别
手段:
v-if
是动态的向 DOM 树内添加或者删除 DOM 元素;v-show
是通过设置 DOM 元素的display
样式属性控制显示隐藏;编译过程:
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于 css 切换;编译条件:
v-if
是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译;v-show
是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,并且 DOM 元素保留;性能消耗:
v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗。使用场景:
v-if
适合运营条件不大可能改变;v-show
适合频繁切换。
data
为什么是一个函数而不是对象
JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生改变。
而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
对 SPA 单页面的理解,它的优缺点分别是什么?
SPA(single-page application)尽在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
基于上面一点,SPA 相对对服务器压力小;
前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
v-for 和 v-if
能说一说双向绑定使用和原理吗?
双向绑定是vue
的特色之一
- 双向绑定的定义
双向绑定是一个指令v-model
,可以绑定一个响应式数据到视图,同时视图中变化能改变改值. - 双向绑定带来的好处
使用v-model
可以减少大量繁琐的事件处理代码,提高开发效率。 - 在哪使用双向绑定
通常在表单项上使用v-model
,还可以在自定义组件(比如自己做的用户交互输入)上使用,表示某个值的输入和输出控制 - 使用方式、使用细节、vue3 变化
原生的表单可以直接使用v-model
将值绑定到 value 上,并且还可以结合.lazy
,.number
,.trim
对 v-model 的行为做进一步限定;但是v-model
用在自定义组件上会有很大差别,vue3 中它类似于sync
修饰符,最终展开的结果是modelValue
属性和update:modelValue
事件;vue3 中甚至可以用参数形式指定多个不同的绑定,例如v-model:foo
和v-model:bar
,非常强大. - 原理实现描述
我做过测试,输出包含v-model
模版的组件渲染函数,发现它会被转换为value
属性的绑定以及一个事件监听,事件回调函数中会做相应变量更新操作,说明v-model
实际上是 vue 的编译器完成的。
子组件可以直接修改父组件的数据么,说明原因。
实际上能改父组件的数据,但在组件化开发过程中有个单项数据流原则,不在子组件修改父组件是个常识问题。
- 讲讲单项数据流原则,表明为何不能这么做
- 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。如果子改父,Vue 就会在浏览器抛出警告。
- 举几个常见场景的例子说说解决方案。
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
1 | const props = defineProps(["initialCounter"]); |
- 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
1 | const props = defineProps(["size"]); // prop变化,计算属性自动更新 |
- 结合实践讲讲如果需要修改父组件状态应该如何做
实践中如果确实想要改变父组件属性应该 emit 一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的 prop,但是我们还是能够直接改内嵌的对象或属性。
怎么定义动态路由?怎么获取传过来的动态参数?
- 什么是动态路由
很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。 - 什么时候使用动态路由,怎么定义动态路由
例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User }
,其中*:id* 就是路径参数 - 参数如何获取
路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以this.$route.params
的形式暴露出来。 - 细节、注意事项
参数还可以有多个,例如/users/:username/posts/:postId
;除了$route.params
之外,_$route_ 对象还公开了其他有用的信息,如$route.query
、$route.hash
等。
key 的作用?
考察对虚拟 DOM 和 patch 细节的掌握程度,能够反映面试者理解层次。
- 给出结论,key 的作用是用于优化 patch 性能。
key 的作用主要是为了更高效的更新虚拟 DOM。 - key 的必要性。
vue 在 patch 过程中判断两个节点是否是相同节点是 key 是一个必要条件,渲染一组列表时,key 往往是唯一标识,所以如果不定义 key 的话,vue 只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个 patch 过程比较低效,影响性能。 - 实际使用方式。
实际使用中在渲染一组列表时 key 必须设置,而且必须是唯一标识,应该避免使用数组索引作为 key,这可能导致一些隐蔽的 bug;vue 中在使用相同标签元素过渡切换时,也会使用 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。 - 总结:可以从源码层面描述一下 vue 如何判断两个节点是否相同。
从源码中可以知道,vue 判断两个节点是否相同时主要判断两者的 key 和元素类型等,因此如果不设置 key,它的值就是 undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的 dom 更新操作,明显是不可取的。
说一下 Vue 子组件和父组件创建和挂载顺序。
- 给结论
创建过程自上而下,挂载过程自下而上;即:
- parent created
- child created
- child mounted
- parent mounted
- 阐述理由
之所以会这样是因为 Vue 创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加 mounted 钩子到队列,等到 patch 结束再执行它们,可见子组件的 mounted 钩子是先进入到队列中的,因此等到 patch 结束执行这些钩子时也先执行。
你知道哪些 Vue 的最佳实践?
我从编码风格、性能、安全方面说几条:
- 编码风格方面。
- 命名组件时使用“多词”风格避免和 HTML 元素冲突
- 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或 jsx 中使用“肉串命名”
- 使用 v-for 时务必加上 key,且不要跟 v-if 写在一起
- 性能方面。
- 路由懒加载减少应用尺寸
- 利用 SSR 减少首屏加载时间
- 利用 v-once 渲染那些不需要更新的内容
- 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用 shallowRef 或 shallowReactive 降低开销
- 避免不必要的组件抽象
- 安全。
- 不使用不可信模板,例如使用用户输入拼接模板:template: + userProvidedString +
- 小心使用 v-html,:url,:style 等,避免 html、url、样式等注入攻击
Vue3.0 设计目标是什么,做了哪些优化?
还是问新特性,陈述典型新特性,分析给我们带来的变化。
- Vue3 的最大设计目标是替代 Vue2(皮一下),为了实现这一点,Vue3 在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等
- 易用性方面主要是 API 简化,比如 v-model 在 Vue3 中变成了 Vue2 中 v-model 和 sync 修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成 VNode 的 h(type, props, children),其中 props 不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。
- 开发体验方面,新组件 Teleport 传送门、Fragments 、Suspense 等都会简化特定场景的代码编写,SFC Composition API 语法糖更是极大提升我们开发体验。
- 扩展性方面提升如独立的 reactivity 模块,custom renderer API 等
- 可维护性方面主要是 Composition API,更容易编写高复用性的业务逻辑。还有对 TypeScript 支持的提升。
- 性能方面的改进也很显著,例如编译器优化、改造成了基于 proxy 的响应式
Vue 组件为什么只能有一个根元素?
- vue2 中组件确实只能有一个根,但 vue3 中组件已经可以多根节点了。
- 之所以需要这样是因为 vdom 是一颗单根树形结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个 vdom,自然应该满足这个要求。
- vue3 中之所以可以写多个根节点,是因为引入了 Fragment 的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个 Fragment 节点,把多个根节点作为它的 children。将来 patch 的时候,如果发现是一个 Fragment 节点,则直接遍历 children 创建或更新。
怎么实现路由懒加载呢?
- 必要性
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。 - 何时用
一般来说,对所有的路由都使用动态导入是个好主意。 - 怎么用
给 component 选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
1 | { path: '/users/:id', component: () => import('./views/UserDetails') } |
- 使用细节
结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
可以做 webpack 代码分块
SPA 和 SSR 的区别是什么?
- 两者分析
SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。 - 两者优缺点分析
SPA 应用只会首次请求 html 文件,后续只需要请求 JSON 数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且 SEO 不友好。为了解决以上缺点,就有了 SSR 方案,由于 HTML 内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时 SSR 方案也会有性能,开发受限等问题。 - 使用场景差异
在选择上,如果我们的应用存在首屏加载优化需求,SEO 需求时,就可以考虑 SSR。 - 其他选择
但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR 反而浪费资源,我们可以考虑预渲染(prerender)方案,通过插件提前渲染部分 html 内容。另外 nuxt.js/next.js 中给我们提供了 SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些 CI 手段,可以起到很好的优化效果,且能节约服务器资源。
你有写过自定义指令吗?自定义指令的应用场景有哪些?
- 定义
Vue 有一组默认指令,比如 v-model 或 v-for,同时 Vue 也允许用户注册自定义指令来扩展 Vue 能力 - 何时用
自定义指令主要完成一些可复用低层级 DOM 操作 - 如何用
- 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在 mounted 和 updated 时执行
- 注册自定义指令类似组件,可以使用
app.directive()
全局注册,使用{directives:{xxx}}
局部注册 - 使用时在注册名称前加上 v-即可,比如 v-focus
常用指令
还没使用过,如果我使用的话。会把防抖、图片懒加载这种做成一个自定义指令vue3 变化
vue3 中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在 v3.2 之后,可以在 setup 中以一个小写 v 开头方便的定义自定义指令,更简单了!
v-once 的使用场景有哪些?
v-once
是什么v-once
是 vue 的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。什么时候使用
如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once
,这样哪怕这些数据变化,vue 也会跳过更新,是一种代码优化手段。如何使用
我们只需要作用的组件或元素上加上v-once
即可。扩展
v-memo
vue3.2 之后,又增加了``指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。探索原理
编译器发现元素上面有 v-once 时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
什么是递归组件?
递归组件用得比较少,但是在Tree
、Menu
这类组件中会被用到
- 定义
如果某个组件通过组件名称引用它自己,这种情况就是递归组件。 - 使用场景
实际开发中类似 Tree、Menu 这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。 - 使用细节
使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件 name 属性,用来查找组件定义,如果使用 SFC,则可以通过 SFC 文件名推断。组件内部通常也要有递归结束条件,比如 model.children 这样的判断。 - 原理阐述
查看生成渲染函数可知,递归组件查找时会传递一个布尔值给 resolveComponent,这样实际获取的组件就是当前组件本身。
说说你对虚拟 DOM 的理解?
- vdom 是什么?
虚拟 dom 顾名思义就是虚拟的 dom 对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构。 - 引入 vdom 的好处
将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
- 操作 dom 是比较昂贵的操作,频繁的 dom 操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作 dom 的次数,从而减少页面重绘和回流。
方便实现跨平台 - 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
- Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
vdom 如何生成,又如何成为 dom
在 vue 中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler 编译为渲染函数,在接下来的挂载(mount)过程中会调用 render 函数,返回的对象就是虚拟 dom。但它们还不是真正的 dom,所以会在后续的 patch 过程中进一步转化为 dom。在后续的 diff 中的作用
挂载过程结束后,vue 程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新 render,此时就会生成新的 vdom,和上一次的渲染结果 diff 就能得到变化的地方,从而转换为最小量的 dom 操作,高效更新视图。
怎么缓存当前的组件? 缓存后怎么更新?
缓存用 keep-alive,它的作用与用法
开发中缓存组件使用 keep-alive 组件,keep-alive 是 vue 内置组件,keep-alive 包裹动态组件 component 时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。1
2
3<keep-alive>
<component :is="view"></component>
</keep-alive>使用细节,例如缓存指定/排除、结合 router 和 transion
结合属性 include 和 exclude 可以明确指定缓存哪些组件或排除缓存指定组件。vue3 中结合 vue-router 时变化较大,之前是 keep-alive 包裹 router-view,现在需要反过来用 router-view 包裹 keep-alive:1
2
3
4
5<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>组件缓存后更新可以利用 activated 或者 beforeRouteEnter
- beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行 beforeRouteEnter
1 | beforeRouteEnter(to, from, next){ |
- actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
1 | activated(){ |
- 原理阐述
keep-alive 是一个通用组件,它内部定义了一个 map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的 component 组件对应组件的 vnode,如果该组件在 map 中存在就直接返回它。由于 component 的 is 属性是个响应式数据,因此只要它变化,keep-alive 的 render 函数就会重新执行。