7. Vue3 为什么用 Proxy 替代 defineProperty ?
思路:
- 属性拦截的几种方式
- defineProperty的问题
- Proxy的优点
- 其他考量
回答:
JS
中做属性拦截常见的方式有三种:defineProperty
、getter/setters
和Proxy
Vue2
中使用defineProperty
的原因是, 2013 年只能使用这种方式,由于该API
存在一些局限性,比如对于数组的拦截有问题,为此Vue
需要专门为数组响应式做一套实现。另外不能拦截那些新增、删除属性;最后defineProperty
方案在初始化时需要深度递归遍历处理对象才能对它进行完全拦截,明显增加了初始化的时间。- 以上两点在
Proxy
出现后迎刃而解,不仅可以对数组实现拦截,还能对Map
、Set
实现拦截;另外Proxy
的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用改善了。 Proxy
有兼容性问题,完全不支持IE
8. 怎么实现路由懒加载?
思路:
- 必要性
- 何时用
- 怎么用
- 使用细节
回答:
- 当打包构建时,Javascript 抱回变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应最贱,这样更加高效,是一种优化手段。
- 一般来说,对于所有的路由都使用动态导入是个好主意
- 给
component
选项配置一个返回 Promise组件的函数就可以定义懒加载路由.例如:
{
path: '/login',
component: () => import('../views/login/Login.vue')
},
复制代码
- 结合注释
{
path: '/login',
component: () => import(/* webpackChunkName: "login" */'../views/login/Login.vue')
},
复制代码
vite中结合rollupOptions定义分块 5. 路由中不能使用异步组件
9. history模式 和 hash 模式有何区别?
- Vue-Router 有三个模式,其中 history 和 hash 更为常用。两者差别主要在显示形式和部署上,
- hash模式在地址栏现实的时候有一个
#
,这种方式使用和部署都较简单;history模式url看起来更优雅没关,但是应用在部署时需要做特殊配置,web服务器需要做回退处理,否则会出现刷新页面404的问题。 - 在实现上
hash
模式是监听hashchange
事件触发路由跳转,history
模式是监听popstate
事件触发路由跳转。
10. 说说 nextTick 的使用和原理?
-
在
Vue
中nextTick
是等待下一次DOM
更新刷新的工具方法。 -
Vue
有一个异步更新策略,意思是如果数据变化,Vue
不会立刻更新DOM
,而是开启一个队列,把组件更新函数保存在队列中,在同一时间循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM
上,此时如果想要获取更新后的DOM
状态,就需要使用nextTick
nextTick
接受一个函数,我们可以在这个函数内部访问最新的DOM
状态 在开发时,有两个场景我们会用到nextTick
:created
中想要获取DOM
;- 响应式数据变化后获取
DOM
更新后的状态;
-
nextTick
的原理:在Vue
内部,nextTick
之所以能够让我们看到DOM
更新后的结果,是因为我们传入的callback
会被添加到队列刷新函数的后面,这样等队列内部的更新函数都执行完毕,所有DOM
操作也就结束了,callback
自然能够获取最新的DOM
值。
11. v-for 和 v-if 优先级
先回答答案: 在 vue2
中, v-for
的优先级更高 但是在 vue3
中, v-if
的优先级更高
拓展: 无论什么时候,我们都不应该把 v-for
和 v-if
放在一起, 怎么解决呢?一是可以定义一个计算属性,让 v-for
遍历计算属性。二是可以把 if
移到内部容器里(ul
ol
)或者把v-for
移植外部容器(template
)中
12. 如何监听 Vuex 状态变化?
- watch
- store.subscribe()
watch
方式,可以以字符串形式监听 $store.state.xx
; subscribe
方法参数是一个回调函数,回调函数接受mutation
对象和 state
对象,可以通过 mutation.type
判断监听的目标。 wtach 方法更简单好用, subscribe
会略繁琐,一般用 vuex
插件中(可以提一下vuex的持久化插件vuex-persist
、vuex-persistedstate
)
13. 你觉得 Vuex 有什么缺点?
- 不支持持久化,页面刷新状态就会丢失
- 使用模块比较繁琐
- 不支持
ts
(或者说很不友好)
vue3 + pinia 会是更好的组合。
14. ref 和 reactive 异同点?
- 两者都能返回响应式对象,
ref
返回的是一个响应式Ref
对象,reactive
返回的是响应式代理对象。 ref
通常是处理单值得响应式,reactive
用于处理对象类型的数据响应式ref
需要通过.value
访问, 在视图中会自动脱ref
,不需要.value
,ref
可以接收对象或数组但内部依然是reactive
实现的;reactive
如果接收Ref
对象会自动脱ref
;使用展开运算符展开reactive
返回的响应式对象会使其失去响应性,可以结合toRefs()
将值转换为Ref
对象后再展开。reactive
内部使用Prxoy
代理拦截对象各种操作,而ref
内部封装一个RefImpl
类,设置get value/set value
,拦截用户对值得访问。
16. Vue 中如何扩展一个组件?
- 逻辑扩展:
mixins
、extends
、composition api
: - 内容扩展:slots
mixins
很灵活,但是会冲突很混乱。extends
是一个不太常用的选项,更 mixins
的不同是它只能扩展单个对象,优先级比 mixins
高。
混入的数据和方法 不能明确判断来源 而且可能和当前组件内变量 产生命名冲突,composition api 可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式数据局,增强代码的可读性和维护性。
扩展:Vue.mixin(全局混入) Vue.extend(有点像是 类/组件的继承 创建一个子类)
17. vue-loader 是什么?
vue-loader
是用于处理单文件组件(SFC)的webpack loader- 因为有了
vue-loader
,我们才能用.vue
文件形式编写代码,将代码分割为template
script
style
webpack
在打包的时候,会以loader
的方式调用vue-loader
vue-loader
被执行时,它会对SFC
中的每个语言块用单独的loader
链处理,最后将这些单独的块装配成最终的组件模块
18. 子组件能否修改父组件数据
不能直接改。
组件化开发中有一个单向数据流原则,不在子组件修改父组件数据是个常识
如果你确实需要改,请通过emit向父组件发送一个事件,在父组件中修改
19. 怎么定义动态路由,怎么获取传过来的动态参数?
我么可以在路径中使用一个动态字段来实现,例如/users/:id
其中 :id
就是路径参数。 可以通过 this.$route.parmas
获取,参数还可以有多个, $route
对象还公开了其他有用的信息如 query
hash
等。
20. 说说对 Vue 数据响应式的理解
思路:
- 什么是响应式?
- 为什么vue需要响应式?
- 有什么好处?
- vue的响应式怎么实现的,有哪些优缺点?
- vue3中的响应式的新变化
回答:
- 数据响应式就是 能够监测到数据变化并且做出响应的一种机制
- 在
vue
中要解决的一个核心问题就是连接数据层和视图层,通过数据变化驱动视图更新,要做到这点就需要对数据做响应式处理。 - 通过数据响应式加上虚拟
DOM
和patch
算法,我们只需要操作数据,关心业务,完全不需要接触繁琐的DOM
操作,打打提升了开发效率,降低开发难度。 vue2
中实现数据响应式的核心就是通过Object.defineProperty()
方法对数据进行拦截,当get
数据时做依赖收集set
数据时做更新通知。这种机制很好的及绝了数据响应式的问题,但是实际使用也存在缺点,比如在初始化时的递归遍历会造成性能损失;无法监听新增或删除属性,在vue
中要通过像Vue.set/delete
这种特定的API
才能实现对对象数组属性的添加和删除,而且也不支持Ma
、Set
这些数据结构,- 为了解决这些问题,
Vue3
重写了这部分实现,利用的是ES6
中的Proxy
代理要响应化的数据。它有很多好处,初始化性能和内存都大幅改善,也不需要特殊的API
,但是不支持IE
浏览器。
21. 从 template 到 render 做了什么
问 template
到 render
的过程其实是问的 vue 编译器
工作原理。
思路:
- 引入编译器概念
- 说明编译器的必要性
- 阐述编译器工作流程
回答:
Vue
中有个独特的编译模块,称为compiler
,它的主要作用是将template
编译为js
可执行的render
函数- 之所以需要这个编译过程是为了便于我们高校的编写试图模版。相比而言,我们还是更愿意用
HTML
来编写视图,直观且高效。手写render
函数不仅效率低下,而且失去了被编译器的优化能力。 Vue
编译器 首先会对template进行解析
(Parse
),结束后会得到一个抽象语法树AST
,然后对AST
进行深加工转换(transform
),最后将得到的AST
生成为js
代码,也就是render
函数
22. 如何缓存和更新组件
- 缓存组件可以使用
keep-alive
组件,include 和 exclude 可以指定包含不包含哪些组件。 Vue3
结合vue-router
使用变化非常大,之前是keep-alive
包含router-view
,现在是router-view
包含keep-alive
- 缓存后如果想要获取数据可以使用
actived
钩子 或者beforeRouteEnter
(vue-router
的一个守卫) keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果改组件在map中存在就直接返回它。由于component
的is
属性是一个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行。
23. 虚拟DOM
- 虚拟
DOM
是什么? 虚拟DOM
的本质就是一个Javascript
对象。 - 为什么要引入虚拟
DOM
?(好处) 它能有效减少操作DOM
的次数,方便实现跨平台 - 虚拟DOM如何生成?
compiler
编译器会把template
模版编译成渲染函数,接下来在mount
挂载的过程会调用这个渲染函数,返回的对象就是虚拟DOM
。挂载结束后,会进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render
,此时会生成新的虚拟DOM
,和上次渲染结果做diff
操作,最小量的操作dom
,从而高效更新视图。
24. 什么是异步组件
- 异步组件就是不会立即加载而是会在需要的时候加载的组件。在大型应用中,我们需要分割代码为更小的块试就可以用异步组件。
- 不仅可以在路由切换时懒加载组件,还可以在组件中使用异步组件,从而更细的分割代码。
- 使用异步组件最简单的方式是直接给
defineAsyncComponet
指定一个loader
函数,结合 ES 模块 动态导入函数import
可以快速实现。Vue3
还可以结合Suspense
组件使用异步组件。 - 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是
Vue
框架,处理路由组件加载的是vue-router
。但是可以在懒加载的路由组件中使用异步组件。
25. 说说Vue长列表优化思路
- 避免大数据量:可以采用分页的方式获取
- 避免渲染大量数据:vue-virtual-scroller等虚拟滚动方案,只渲染视口范围内的数据
- 避免更新:可以使用
v-once
方式只渲染一次 - 优化更新:通过v-memo缓存组数,有条件更新,提高服用,避免不必要更新
- 按需加载数据:可以采用
懒加载
方式,在用户需要的时候在加载数据。
26. computed & watch
computed
是计算属性,watch
是侦听器。computed
通常用于处理模版中复杂的逻辑,而watch
通常用于需要监听一个响应式对象的变化而做一些操作的时候watch
可以进行异步操作,computed
不行。- 计算属性传递一个对象 有
set
和get
两个选项,是它称为即可读又可写的计算属性,如果传递的是函数的话默认就是get
选项,watch
可以传递一个对象,设置deep、immediate等选项 vue3
中watch
发生了一些变化,例如不能再侦测一个点操符之外的字符串表达式,reactivity API
中新出的watch
、watchEffect
可以完全替代watch
选项,而且功能更加强大
27. SPA 和 SSR的区别是什么?
SPA
(Single Page Application)是单页面应用。一般也称为客户端渲染,简称CSR
。SSR(Server Side Render) 即服务端渲染。一般也称为多页面应用(Mulpile Page Application),简称 MPA。SPA
只会首次请求html
文件,后续只需要请求JSON
数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO
不友好。为了解决以上缺点,就有了SSR
方案,由于HTML
内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR
方案也会有性能,开发受限等问题。- 选择上,如果有首屏加载优化需求,SEO需求时,就可以考虑SSR。
- 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染的方案。另外
nuxt.js/next.js
中给我们提供了SSG静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果。
28. diff 算法
回答思路:
- diff算法是干什么的?
- 必要性
- 何时执行
- 具体执行方式
- 拔高:说一下vue3中的优化
回答:
-
Vue
中的diff
算法称为patching
算法,虚拟DOM要想转化为真实DOM就需要通过patch
方法转换。 -
最初
Vue1.x
视图中农每个依赖均有更新函数对应,可以做到精确更新,因此不需要虚拟DOM
和patching
算法支持,但是这样粒度过细导致Vue1.x
无法承载较大应用;Vue2.x
中为了降低Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就需要引入patching
算法才能精确找到发生变化的地方并高效更新。 -
vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数
获得最新的虚拟DOM
,然后执行patch函数
,对比新旧虚拟DOM,将其转化为对应的DOM
操作。 -
patch
过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3
的patch
为例:- 首先判断两个节点是否为相同同类节点,不同则删除重新创建
- 如果双方都是文本则更新文本内容
- 如果双方都是元素节点则递归更新子元素,同时更新元素属性
- 更新子节点时又分了几种情况:
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
-
vue3
中引入的更新策略:编译期优化patchFlags
、block
等
29. 如何从0到1架构一个Vue项目,说说有哪些步骤,插件,目录结构怎么组织
- 从 0 创建项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
- 目前vue3项目我会用vite或者create-vue创建项目
- 接下来引入必要插件:vue-router、vuex/pinia、element-plus、antd-vue、axios等等
- 其他常用的库有 像lodash、dayjs、nprogress等等…
- 下面是代码规范: editorconfig、prettier、eslint
- 最后是提交规范,可以使用husky、Commitizen
- 目录结构我喜欢按照下面的结构来
+ |- /src
+ |- /assets 存放资源
+ |- /img
+ |- /css
+ |- /font
+ |- /data
+ |- base-ui 存放多个项目中都会用到的公共组件
+ |- components 存放这个项目用到的公共组件
+ |- hooks 存放自定义hook
+ |- views 视图
+ |- store 状态管理
+ |- router 路由
+ |- service 网络请求
+ |- utils 工具
+ |- global 全局注册、全局常量..
复制代码
30. 你如何实现一个Vue-Router
一个 SPA
应用的路由需要解决的问题时页面跳转内容改变同时不刷新,同时路由还需要已插件形式存在,所以:
- 首先我会定义一个
createRouter
函数,返回路由器实例,实例内部做几件事;- 保存用户传入的配置项
- 监听
hash
或者popstate
事件 - 回调里根据
path
匹配对应路由
- 将
router
定义成一个Vue
插件,即实现install
方法,内部做两件事:- 实现两个全局组件:
router-link
和router-view
,分别实现页面跳转和内容显示 - 定义两个全局变量:
$router
和$route
,组件内可以访问当前路由和路由器实例
- 实现两个全局组件:
31. 什么情况需要使用Vuex模块?
- 在项目规模变大的之后,单独一个store对象会过于庞大臃肿,此时通过模块方式可以拆分来便于维护
- 可以按之前规则单独编写资规模代码,然后在主文件中通过
modules
选项组织起来:createStore({modules: {...}})
- 使用时需要注意访问子模块状态时需要加上注册模块名。但同时
getters
、mutations
和actions
又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace
选项,此时再访问它们就要加上命名空间前缀。 - 模块的方式可以拆分代码,但是缺点也很明显,使用起来比较繁琐,容易出错,而且类型系统支持很差,不能给我们带来帮助。pinia 显然在这方面有了很大改进,是时候切换过去了。
32. vue 组件为什么只能有1个根节点
vue2
中组件确实只能有一个跟,但vue3
中组件已经可以多根组件了。- 之所以需要这样是因为
vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
,自然应该满足这个要求。 vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件时多根的,就创建一个Fragment
节点,把多个根节点作为它的children
。将来pathch
的时候,如果发现是一个Fragment
节点,则直接遍历children
创建或更新。
33. v-once 使用场景有哪些?
v-once
是vue
的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。- 如果我们有一些元素或者组件再初始化渲染之后不再需要变化,这种情况下适合使用
v-once
,这样哪怕这些数据变化,vue
也会跳过更新,是一种代码优化手段。 - 我们只需要作用的组件或元素上加上
v-once
即可。
补充:
vue3.2
之后,又增加了v-memo
,这个指令可以有条件的缓存模板并控制他们的更新。v-once
的原理:编译器发现有v-once
时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
34. 什么场景使用嵌套路由
- 在平时开发中,应用的有些界面是由多层嵌套的组件组合而来的,这种情况下,
url
各部分通常对应某个嵌套的组件,vue-router
中可以使用嵌套路由表示这种关系。 - 表现形式是在两个路由间切换时,他们有公用的视图内容。此时通常提取一个父组件,内部放上
view-router
,从而形成物理上的嵌套,和逻辑上的嵌套对应起来。定义嵌套路由时使用children
属性组织嵌套关系 - 原理上是在
router-view
组件内部判断其所处嵌套的深度,将这个深度作为匹配组件数组matched
的索引,获取对应渲染组件并渲染之。
如果你说不出来,可以直接举例子。当我开发一个页面时,如果需要显示一个顶部导航栏,通过导航栏跳转到不同的页面,而顶部的导航栏又必须要在每个页面显示时,就可以使用嵌套路由;还可以举例,当我需要查看某个列表的详情页面时,往往需要嵌套路由 (detail/:id
)
35. 如何监听 Vuex 状态变化?
- watch
- store.subscribe()
watch
方式,可以以字符串形式监听 $store.state.xx
; subscribe
方法参数是一个回调函数,回调函数接受mutation
对象和 state
对象,可以通过 mutation.type
判断监听的目标。 wtach 方法更简单好用, subscribe
会略繁琐,一般
36. Vue 实例挂载过程发生了什么?
- 挂载实例的过程就是 app.mount()的过程,整体上就做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态、创建各种响应式数据
- 简历更新机制这一步会立即执行一次组件更新函数,这会首次执行渲染函数并执行
patch
将前面获得vnode
转换为dom
;同时会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
37. key 的作用
key
的作用主要是为了更高效的更新虚拟DOM
。key
是vue
在patch
过程中判断两个节点是否是相同节点的关键条件(另一个是元素类型),如果不设置key
,它的值就是undefined
,vue
则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom
更新操作,明显是不可取的。- 实际使用的过程中必须设置
key
,而且应该尽量避免使用数组索引,这可能导致一些隐藏bug
。
38. watch 和 watchEffect
watchEffect
立即运行函数,被动地追踪它的依赖,传入的函数即是依赖收集的数据源,也是回调函数;watch
侦测一个或多个响应式数据源,在数据源变化时调用一个回调函数,通过immediate
选项也可以设置立即执行一次。watchEffect
是一种特殊的watch
。如果不关心响应式数据前后的值,可以使用watchEffect
。其他情况都可以用watch
。
39. 父子组件创建、挂载顺序
parent created -> child created -> child mounted -> parent mounted
原因:Vue
创建是一个递归的过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加 Mounted
钩子到队列,等到 patch
结束再执行它们,可见子组件的 mounted
钩子是选进入到队列中的,因此等到 patch
结束执行这些钩子时也先执行。
40. 说说你对 Vuex 的理解
- vuex是一个专门为vue应用开发的状态管理模式库,
- 当你遇到多个组件共享状态时或者项目中的组件难以管理的时候就可以使用vuex,它以一个全局单例模式管理全局的状态。
- 基本核心概念有 state、mutation、action、getters、module等
- 说些使用过程的感受 ts不友好 模块使用繁琐 页面刷新数据也会消失
41. 什么是递归组件?使用场景有哪些?
- 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
- 类似
Tree
、Menu
这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
42. 你写过自定义指令吗?
使用自定义指令分为定义、注册、和使用
- 定义有两种方式,对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在
mounted
和updated
时执行 - 注册:可以使用
app.directive
全局注册 也可以通过选项局部注册 - 使用时在注册名称前加上 v-即可。
v-copy
复制粘贴v-lazy
图片懒加载v-debounce
防抖v-permission
按钮权限v-longpress
长按
43. Vue3新特性
❤️ 谢谢支持
喜欢的话别忘了 关注、点赞哦~。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
定义指令吗?
使用自定义指令分为定义、注册、和使用
- 定义有两种方式,对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在
mounted
和updated
时执行 - 注册:可以使用
app.directive
全局注册 也可以通过选项局部注册 - 使用时在注册名称前加上 v-即可。
v-copy
复制粘贴v-lazy
图片懒加载v-debounce
防抖v-permission
按钮权限v-longpress
长按
43. Vue3新特性
❤️ 谢谢支持
喜欢的话别忘了 关注、点赞哦~。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-FgheQxcH-1714157189197)]