19 道高频 vue 面试题解答(上)
子组件可以直接改变父组件的数据吗?
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
用 Vue3.0 写过组件吗?如果想实现一个 Modal 你会怎么设计?
一、组件设计
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式
现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同
这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可
这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug
和更少的程序体积
二、需求分析
实现一个Modal
组件,首先确定需要完成的内容:
遮罩层
标题内容
主体内容
确定和取消按钮
主体内容需要灵活,所以可以是字符串,也可以是一段 html
代码
特点是它们在当前vue
实例之外独立存在,通常挂载于body
之上
除了通过引入import
的形式,我们还可通过API
的形式进行组件的调用
还可以包括配置全局样式、国际化、与typeScript
结合
三、实现流程
首先看看大致流程:
目录结构
组件内容
实现
API
形式事件处理
其他完善
目录结构
Modal
组件相关的目录结构
因为 Modal 会被 app.use(Modal)
调用作为一个插件,所以都放在plugins
目录下
组件内容
首先实现modal.vue
的主体显示内容大致如下
最外层上通过 Vue3 Teleport
内置组件进行包裹,其相当于传送门,将里面的内容传送至body
之上
并且从DOM
结构上来看,把modal
该有的内容(遮罩层、标题、内容、底部按钮)都实现了
关于主体内容
可以看到根据传入content
的类型不同,对应显示不同得到内容
最常见的则是通过调用字符串和默认插槽的形式
通过 API 形式调用Modal
组件的时候,content
可以使用下面两种
h 函数
JSX
实现 API 形式
那么组件如何实现API
形式调用Modal
组件呢?
在Vue2
中,我们可以借助Vue
实例以及Vue.extend
的方式获得组件实例,然后挂载到body
上
虽然Vue3
移除了Vue.extend
方法,但可以通过createVNode
实现
在Vue2
中,可以通过this
的形式调用全局 API
而在 Vue3 的 setup
中已经没有 this
概念了,需要调用app.config.globalProperties
挂载到全局
事件处理
下面再看看看Modal
组件内部是如何处理「确定」「取消」事件的,既然是Vue3
,当然采用Compositon API
形式
在上面代码中,可以看得到除了使用传统emit
的形式使父组件监听,还可通过_hub
属性中添加 on-cancel
,on-confirm
方法实现在API
中进行监听
下面再来目睹下_hub
是如何实现
其他完善
关于组件实现国际化、与typsScript
结合,大家可以根据自身情况在此基础上进行更改
Proxy 与 Object.defineProperty 优劣对比
Proxy 的优势如下:
Proxy 可以直接监听对象而非属性;
Proxy 可以直接监听数组的变化;
Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
如何保存页面的当前的状态
既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
前组件会被卸载
前组件不会被卸载
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
(1)将状态存储在 LocalStorage / SessionStorage
只需要在组件即将被销毁的生命周期 componentWillUnmount
(react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点:
兼容性好,不需要额外库或工具。
简单快捷,基本可以满足大部分需求。
缺点:
状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)
如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象
(2)路由传值
通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点:
简单快捷,不会污染 LocalStorage / SessionStorage。
可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)
缺点:
如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
优点:
代码量少
不需要考虑状态传递过程中的错误
缺点:
增加 A 组件维护成本
需要传入额外的 prop 到 B 组件
无法利用路由定位页面
除此之外,在 Vue 中,还可以是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的 activated、deactivated 这两个生命周期钩子函数会被执行被包裹在 keep-alive 中的组件的状态将会被保留:
router.js
常见的事件修饰符及其作用
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
说说 Vue 的生命周期吧
什么时候被调用?
beforeCreate :实例初始化之后,数据观测之前调用
created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
.beforeMount:在挂载之前调用,相关
render
函数首次被调用mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
beforeDestroy:实例销毁前调用,实例仍然可用。
destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
mounted:实例已经挂载完成,可以进行一些 DOM 操作。
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax 放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用 beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
Vue 模版编译原理
vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。
解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。
优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
生成阶段:将最终的 AST 转化为 render 函数字符串。
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
为什么 Vue 采用异步渲染呢?
Vue
是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue
会在本轮数据更新后,在异步更新视图。核心思想 nextTick
。
dep.notify()
通知 watcher 进行更新, subs[i].update
依次调用 watcher 的 update
, queueWatcher
将 watcher 去重放入队列, nextTick( flushSchedulerQueue
)在下一 tick 中刷新 watcher 队列(异步)。
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
相关代码如下
如何从真实 DOM 到虚拟 DOM
涉及到 Vue 中的模板编译原理,主要过程:
将模板转换成
ast
树,ast
用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)优化树
将
ast
树生成代码
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
对 keep-alive 的理解,它是如何实现的,具体缓存的是什么?
如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
(1)keep-alive
keep-alive 有以下三个属性:
include 字符串或正则表达式,只有名称匹配的组件会被匹配;
exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
max 数字,最多可以缓存多少组件实例。
注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。
主要流程
判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
获取组件实例 key ,如果有获取实例的 key,否则重新生成。
key 生成规则,cid +"∶∶"+ tag ,仅靠 cid 是不够的,因为相同的构造函数可以注册为不同的本地组件。
如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。 5.最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。
(2)keep-alive 的实现
render 函数:
会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件
keep-alive 只对第一个组件有效,所以获取第一个子组件。
和 keep-alive 搭配使用的一般有:动态组件 和 router-view
keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys 数组最后,以便清除最不常用组件。
实现步骤:
获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例 vNode
需要缓存,判断他当前是否在缓存数组里面:
存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
不存在,将组件 key 放入数组,然后判断当前 key 数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
最后将这个组件的 keepAlive 设置为 true
(3)keep-alive 本身的创建过程和 patch 过程
缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
首次渲染
组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM
判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
(4)LRU (least recently used)缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
新数据插入到链表头部
每当缓存命中(即缓存数据被访问),则将数据移到链表头部
链表满的时候,将链表尾部的数据丢弃。
什么是 mixin ?
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
Vue 中的 key 到底有什么用?
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:
Vue 组件如何通信?
Vue 组件通信的方法如下:
props/$emit+v-on
: 通过 props 将数据自上而下传递,而通过 $emit 和 v-on 来向上传递信息。EventBus: 通过 EventBus 进行信息的发布与订阅
vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流
$attrs/$listeners
: Vue2.4 中加入的$attrs/$listeners
可以进行跨级的组件通信provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础
还有一些用 solt 插槽或者 ref 实例进行通信的,使用场景过于有限就不赘述了。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
评论