Vue面试题总结

总结了最近的一些面试题和之前的知识点


较重要(加强)

diff算法与虚拟dom

如果直接渲染到真实dom上会引起整个dom树的重绘和重排,我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
我们先根据真实DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。

  1. 在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。

patch这个函数做了以下事情
判断两节点是否值得比较,不值得比较则用Vnode替换oldVnode,(比较子节点,逐层比较)值得比较则执行patchVnode

patchVnode方法

  1. 找到对应的真实dom,称为el
  2. 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
  3. 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  4. 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  5. 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
  6. 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
  7. 状态变更时,记录新树和旧树的差异,最后把差异更新到真正的dom中
    updateChildren
    博客

虚拟DOM的优劣如何?

优点:
保证性能下限: 虚拟DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限
无需手动操作DOM: 虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率
跨平台: 虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化

虚拟DOM的优劣如何?

虚拟 DOM 真的渲染更快,性能更好吗?

Virtual DOM有什么好处?

Virtual DOM 是以对象的方式来描述真实dom对象的,那么在做一些update的时候,可以在内存中进行数据比对,减少对真实dom的操作减少浏览器重排重绘的次数,减少浏览器的压力,提高程序的性能,并且因为diff算法的差异比较,记录了差异部分,那么在开发中就会提高了开发效率
跨端
首先我们先区分一下跨平台和跨端的区别,跨平台指的是跨操作系统,而跨端是指客户端。跨平台简单的来说就是一套代码在mac和windows上都能跑,跨端就是跨客户端在web、ios、安卓等跑

这时候虚拟dom就发挥了作用,就像vue、react等使用虚拟dom来缓存修改然后整体替换来优化性能,当然也可以用虚拟dom来缓存我们js画的页面,然后通过原生方法渲染出对应的原生组件,实现渲染跨端,正好我们对应dom绑定的方法也可以通过映射原生api实现逻辑跨端。
其实Virtual DOM 的实现不仅仅是提高了开发效率和性能,并且也解决了跨终端开发的问题,例如rn, vux,uni-app 等实现跨终端开发的功能都是基于Virtual DOM的。
掘金

Virtual DOM 有什么坏处? (内存消耗)

在初始化时增加了对tpl的解析,最终需要生成Virtual DOM,那么就会产生一定的内存消耗,比初始化时直接挂载到真实dom 对象上的展示要慢一些。(纯web渲染的首页渲染慢,之前就一直被产品bb)
虚拟 DOM 真的渲染更快,性能更好吗?答案是否!虚拟dom增加了一层内存运算,那么渲染上肯定会慢上一些

掘金

vue 双向数据绑定原理?

vue.js是采用数据劫持结合发布者-订阅者模式的方式,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
Object.defineProperty(obj, prop, desc) obj 需要定义属性的当前对象,prop 当前需要定义的属性名,desc 属性描述符
proxy(target,handler)

相比于vue2.x,使用proxy的优势如下

  1. Object.defineProperty 只能劫持对象的属性,而 Proxy 是直接代理对象。
    由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
    可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)
    可以监听数组,不用再去单独的对数组做特异性操作 vue3.x可以检测到数组内部数据的变化

  2. Object.defineProperty 对新增属性需要手动进行 Observe。(监听并通知Watcher更新)
    由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

  3. 默认进行懒观察(lazy observation)。
    在 2.x 版本里,不管数据多大,都会在一开始就为其创建观察者。当数据很大时,这可能会在页面载入时造成明显的性能压力。3.x 版本,只会对「被用于渲染初始可见部分的数据」创建观察者,而且 3.x 的观察者更高效。

  4. 精准的变更通知
    比例来说:2.x 版本中,使用 Vue.set 来给对象新增一个属性时,这个对象的所有 watcher 都会重新运行;3.x 版本中,只有依赖那个属性的 watcher 才会重新运行。
    详细资料可以参考:
    《Vue.js 双向绑定的实现原理》
    Object.defineProperty和Proxy,Vue3.0为什么采用Proxy?

dep watcher

  1. Observer 将数据定义为响应式,每个 Observer 实例都有自己的 Dep 来管理依赖。实例化 Wacther 的时候进行求值会触发 getter ,进而执行 dep.depend() 将当前 Wacther 加入 Dep 维护的依赖列表,这就是依赖收集过程。

  2. 数据发生变化触发 setter 执行 dep.notify,Dep 会执行所有依赖的 update 方法并加入异步更新队列,这就是触发依赖过程。
    博客

MVVM思想(重要)(Vue并不是MVVM,而是类似于MV*)

什么是MVVM?

视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图。

MVVM的优点

1.低耦合。视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上,当View变化的时候Model可以不变化,当Model变化的时候View也可以不变;
2.可重用性。你可以把一些视图逻辑放在一个Model里面,让很多View重用这段视图逻辑。
3.独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
4.可测试。

Observer(数据监听器)

Observer的核心是通过Object.defineProprtty()来监听数据的变动,这个函数内部可以定义setter和getter,每当数据发生变化,就会触发setter。这时候Observer就要通知订阅者,订阅者就是Watcher

Watcher(订阅者)

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个update()方法
  3. 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调

Compile(指令解析器)

Compile主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图

computed 和 watch 的差异?

(1)computed 是计算一个新的属性,并将该属性挂载到 Vue 实例上,而 watch 是监听已经存在且已挂载到 Vue 实例上的数据,所以用 watch 同样可以监听 computed 计算属性的变化。
(2)computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。而 watch 则是当数据发生变化便会调用执行函数。
(3)从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据。

deep深度监听

需要监听复杂数据(对象)内部属性的变化时,设置deep属性。Vue会递归的侦听数据和属性的变化(性能消耗较大)。也就是给所有数据和属性添加handler执行函数。
性能优化 鉴于deep属性Vue性能消耗较大,对于要监听数据中某个属性的响应时,可以只给对应属性添加deep。
《做面试的不倒翁:浅谈 Vue 中 computed 实现原理》
《深入理解 Vue 的 watch 实现原理及其实现方式》
我的博客

计算属性和watch的原理

计算属性原理

这里还是按照惯例,将定义的computed属性的每一项使用Watcher类进行实例化,不过这里是按照computed-watcher的形式,来看下如何实例化的:

这里的变量watcher就是之前computed对应的computed-watcher实例,接下来会执行Watcher类专门为计算属性定义的两个方法,在执行evaluate方法进行求值的过程中又会触发computed内可以访问到的响应式数据的get,它们会将当前的computed-watcher作为依赖收集到自己的dep里,计算完毕之后将dirty置为false,表示已经计算过了。

然后执行depend让计算属性内的响应式数据订阅当前的render-watcher,所以computed内的响应式数据会收集computed-watcher和render-watcher两个watcher,当computed内的状态发生变更触发set后,首先通知computed需要进行重新计算,然后通知到视图执行渲染,再渲染中会访问到computed计算后的值,最后渲染到页面。

Ps: 计算属性内的值须是响应式数据才能触发重新计算。

watch总结:为什么计算属性有缓存功能?因为当计算属性经过计算后,内部的标志位会表明已经计算过了,再次访问时会直接读取计算后的值;为什么计算属性内的响应式数据发生变更后,计算属性会重新计算?因为内部的响应式数据会收集computed-watcher,变更后通知计算属性要进行计算,也会通知页面重新渲染,渲染时会读取到重新计算后的值
博客:

keep-alive 组件有什么作用?

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染
场景:tabs标签页 后台导航,vue性能优化

原理

Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

  1. 使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量
  2. 根据设定的include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  3. 根据组件ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  4. 获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称
  5. 获取 keep-alive 包裹着的第一个子组件对象及其组件名
    参考文章

Vue中的key到底有什么⽤?

vue 中 key 值的作用可以分为两种情况来考虑。

第一种情况是 v-if 中使用 key

由于Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当我们使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此我们可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。

第二种情况是 v-for 中使用 key

v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有唯一 id。
详细资料可以参考:
《Vue 面试中,经常会被问到的面试题 Vue 知识点整理》
《Vue2.0 v-for 中 :key 到底有什么用?》
《vue 中 key 的作用》

Vue-为什么不建议用数组的下标index作为key

key 是给每一个 vnode 的唯一id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点,高效的更新虚拟DOM

  1. 用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个生命周期中都保持稳定。

  2. 如果你的列表顺序会改变,别用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。列表顺序不变也尽量别用,可能会误导新人。

  3. 千万别用随机数作为 key,不然旧节点会被全部删掉,新节点重新创建,你的老板会被你气死。
    参考

路由实现原理是什么

a.控制地址栏改变;b.根据地址栏的改变来控制组件的切换

hash通过 window.onhashchange 监听地址栏的改变

URL的hash:URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;

优点

兼容低版本浏览器,Angular1.x和Vue默认使用的就是hash路由
只有#符号之前的内容才会包含在请求中被发送到后端,也就是说就算后端没有对路由全覆盖,但是不会返回404错误
hash值的改变,都会在浏览器的访问历史中增加一个记录,所以可以通过浏览器的回退、前进按钮控制hash的切换 会覆盖锚点定位元素的功能

缺点

不太美观,#后面传输的数据复杂的话会出现问题

history通过 window.onpopState 监听地址栏的改变

history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:
replaceState:替换原来的路径; pushState:使用新的路径;
popState:路径的回退; go:向前或向后改变路径;
forward:向前改变路径; back:向后改变路径;

优点

使用简单,比较美观
pushState()设置新的URL可以是任意与当前URL同源的URL,而hash只能改变#后面的内容,因此只能设置与当前URL同文档的URL
pushState()设置的URL与当前URL一模一样时也会被添加到历史记录栈中,而hash#后面的内容必须被修改才会被添加到新的记录栈中
pushState()可以通过stateObject参数添加任意类型的数据到记录中,而hash只能添加短字符串
pushState()可额外设置title属性供后续使用

缺点

前端的URL必须和向发送请求后端URL保持一致,否则会报404错误
由于History API的缘故,低版本浏览器有兼容性问题

博客

nextTick的实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

nextTick的实现

  1. nextTick是Vue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM;
  2. Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中1次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用;
  3. 比如,我在干什么的时候就会使用nextTick,传一个回调函数进去,在里面执行dom操作即可;
  4. 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。

vuex mutation为什么不可以包含异步回调,action 为什么可以

官网是这样说的:

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

可以看到在Mutation中使用异步和同步最终页面的总和都是正确的,也就是说在Mutation中使用异步不会对数据造成丢失和其他影响。
然而我们注意Vue Devtools显示结果,当我们去查看多次Mutation状态时,发现同步的显示Ok,异步的Count显示为0 和我们预期结果不一致,所以会造成状态改变的不可追踪,
所以官方说我们Mutation是同步的!

重点事情
造成状态改变的不可追踪
在actions中就不会出现这种状态改变不可追踪的情况
简书

Vue3

Composition API 和 script setup

Option的弊端(Option的缺陷–反复横跳,mixin和this)

说一下 Composition API和 Options API 的区别?

Composition API 也叫组合式 API,它主要就是为了解决 Vue2 中 Options API 的问题。

  1. 一是在 Vue2 中只能固定用 data、computed、methods 等选项组织代码,在组件越来越复杂的时候,一个功能相关的属性和方法就会在文件上中下到处都有,很分散,变越来越难维护

  2. 二是Vue2 中虽然可以用 minxin 来做逻辑的提取复用,但是 minxin 里的属性和方法名会和组件内部的命名冲突,还有当引入多个 minxin 的时候,我们使用的属性或方法是来于哪个 minxin 也不清楚

而 Composition API 刚才就解决了这两个问题,可以让我们自由的组织代码,同一功能相关的全部放在一起,代码有更好的可读性更便于维护,单独提取出来也不会造成命名冲突,所以也有更好的可扩展性
我们需要使用 Composition API 的逻辑来拆分代码,把一个功能相关的数据和方法都维护在一起。

使用 <script setup>可以让代码变得更加精简

这也是现在开发 Vue 3 项目必备的写法。除了我们上面介绍的功能,<script setup> 还有其它一些很好用的功能,比如能够使用顶层的 await 去请求后端的数据等等

博客

reactive, ref,toRef,toRefs用法和区别

ref,reactive

如果想给值类型(String,Number,Boolean,Symbol)添加响应式,就要用到ref
reactive 方法 根据传入的对象 ,创建返回一个深度响应式对象

toRef toRefs

toRef (针对一个响应式对象(reactive 封装)的 prop(属性)创建一个ref,且保持响应式,两者保持引用关系)

toRefs 是一种用于破坏响应式对象并将其所有属性转换为 ref 的实用方法
将响应式对象(reactive封装)转成普通对象
对象的每个属性(Prop)都是对应的ref
两者保持引用关系

使用toRefs(state)方式返回

注意reactive封装的响应式对象,不要通过解构的方式return,这是不具有响应式的。可以通过 toRefs 处理,然后再解构返回,这样才具有响应式
博客

Teleport,字面意思就是远距离传送,我们可以把它理解为传送门的意思

其实,有一个非常常见的需求就是,我们经常要通过点击一个按钮,来实现模态框的效果。而在 vue3 之前,我们基本上控制它都是点击后上下会形成一个父子组件的关系,这样子感觉独立性就没有那么强了。

通过 teleport 的方式,现在的模态框成功显示在 id 为 app 的 div 同一层下,达到了相互独立,而不再是父子层级的结果。

用Suspense发起一个异步请求(处理多个异步请求方便)

那现在呢, vue3 推出了一个新的内置组件 Suspense , Suspense 是一个特殊的组件,它会有两个 template slot ,刚开始会渲染 fallback 内容,直到达到某个条件以后,才会渲染正式的内容,也就是 default 的内容。这样呢,进行异步内容的渲染就会变得特别简单。
博客

还未被问到(加强)

vue 中 mixin 和 mixins 区别?

mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin,多个组件有相同的逻辑,抽离出来
mixin并不是完美的解决方案,会有一些问题,vue3提出的Composition API旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】
场景:PC端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合
劣势:
1.变量来源不明确,不利于阅读
2.多mixin可能会造成命名冲突
3.mixin和组件可能出现多对多的关系,使得项目复杂度变高

mixin 用于全局混入,会影响到每个组件实例。
mixins 应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并
详细资料可以参考:
《前端面试之道》
《混入》

vue和jquery的区别( 解除jquery频繁操作dom,转为直接操作数据的思想。vue支持双向绑定,vue支持组件化)

vue相对于jquery有什么优点:
一. 解除jquery频繁操作dom,转为直接操作数据的思想。
二. vue支持双向绑定
view的变化,viewmodel感知到变化view变化,通知model发生改变。反之model发生改变,view也改变。
三. vue支持组件化
提高开发效率。重复功能可单独写到组件中,供其他地方调用。

单页面应用和多页面应用区别

单页面应用

只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站
SPA的优点

1.页面切换快 通过vue-router来局部切换组件,而非刷新整个页面,来实现无刷新切换页面的技术 (路由 history hash)

页面每次切换跳转时,并不需要做html文件的请求,这样就节约了很多http发送时延,我们在切换页面的时候速度很快。

2.用户体验好

页面片段间的切换快,包括移动设备, 尤其是在网络环境差的时候, 因为组件已经预先加载好了, 并不需要发送网络请求, 所以用户体验好

SPA的缺点

1.首屏加载速度慢

首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来。相对于多页应用,首屏时间慢。

2.不易于SEO

SEO效果差,因为搜索引擎只认识html里的内容,不认识js的内容,而单页应用的内容都是靠js渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致SPA应用做出来的网页在百度和谷歌上的排名差。

多页面应用

多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等
MPA的优点

1.首屏加载速度快

当我们访问页面的时候,服务器返回一个html,页面就会展示出来,这个过程只经历了一个HTTP请求,所以页面展示的速度非常快。

2.SEO效果好

搜索引擎在做网页排名的时候,要根据网页的内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别html内容的,而我们每个页面所有的内容都放在html中,所以这种多页应用SEO排名效果好。
MPA的缺点

1.页面切换慢

因为每次跳转都需要发送一个 HTTP 请求,如果网络状态不好,在页面之间来回跳转时,就会发生明显的卡顿,影响用户体验。

2.用户体验不佳

如果网络慢,页面很容易半天加载不出来,用户体验非常不好
参考

单向数据流

单向数据流的意思是指数据的改变只能从一个方向修改

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告

如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改

举个栗子:如一个父组件有两个子组件,分别为1和2。父组件向子组件传递数据,两个组件都接收到了父组件传递过来的数据,在组件1中修改父组件传递过来的数据,子组件2和父组件的值不会发生变化。这就是单向的数据流,子组件不能直接改变父组件的状态。但是如果父组件改变相应的数据,两个子组件的数据也会发生相应的改变。

为何vue采用异步渲染

最终一次性更新DOM,避免重复操作DOM,耗费性能。

vue 常用的修饰符?

.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用

为什么v-for和v-if不建议用在一起

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
2.这种场景建议使用 computed,先对数据进行过滤

注意:3.x 版本中 v-if 总是优先于 v-for 生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
解惑传送门 ☞# v-if 与 v-for 的优先级对比非兼容

使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽(I)

vue中的插槽是一个非常好用的东西slot说白了就是一个占位的
在vue当中插槽包含三种

  1. 一种是默认插槽(匿名)
  2. 一种是具名插槽
  3. 还有一种就是作用域插槽
    匿名插槽就是没有名字的只要默认的都填到这里具名插槽指的是具有名字的

博客

Vue动态加载组件的四种方式

import /require

Vue动态加载组件的四种方式

观察者模式和发布订阅模式有什么不同?

发布订阅模式其实属于广义上的观察者模式

在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。

而在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事件,另一方面向订阅者发布事件,订阅者需要在调度中心中订阅事件。通过调度中心实现了发布者和订阅者关系的解耦。使用发布订阅者模式更利于我们代码的可维护性。
参考

defineProperty 和 Proxy 的区别

为什么要用 Proxy 代替 defineProperty ?好在哪里?

  1. Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法
  2. defineProperty 不能监听到数组下标变化和对象新增属性,Proxy 可以
  3. defineProperty 是劫持对象属性,Proxy 是代理整个对象
  4. defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能提升很大,且首次渲染更快
  5. defineProperty 会污染原对象,修改时是修改原对象,Proxy 是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
  6. defineProperty 不兼容 IE8,Proxy 不兼容 IE11

React/Vue 项目中 key 的作用

  1. key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
  2. 为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用。当 Vue.js 用v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。
    原理

Vue scoped原理

  1. 给HTML的DOM节点加一个不重复data属性(形如:data-v-2311c06a)来表示他的唯一性。
  2. 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器的哈希特征值(如[data-v-2311c06a])来私有化样式。

掘金

被问到了掌握了以及(较简单)

Vue 的生命周期是什么?

Vue 的生命周期指的是组件从创建到销毁的一系列的过程,被称为 Vue 的生命周期。通过提供的 Vue 在生命周期各个阶段的钩子函数,我们可以很好的在 Vue 的各个生命阶段实现一些操作。

Vue 的各个生命阶段是什么?

Vue 一共有8个生命阶段,分别是创建前、创建后、加载前、加载后、更新前、更新后、销毁前和销毁后,每个阶段对应了一个生命周期的钩子函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

1beforeCreate 钩子函数,在实例初始化之后,在数据监听和事件配置之前触发。因此在这个事件中我们是获取不到 data 数据的。

2)created 钩子函数,在实例创建完成后触发,此时可以访问 data、methods 等属性。但这个时候组件还没有被挂载到页面中去,所以这个时候访问不到 $el 属性。一般我们可以在这个函数中进行一些页面初始化的工作,比如通过 ajax 请求数据来对页面进行初始化。

3beforeMount 钩子函数,在组件被挂载到页面之前触发。在 beforeMount 之前,会找到对应的 template,并编译成 render 函数。

4)mounted 钩子函数,在组件挂载到页面之后触发。此时可以通过 DOM API 获取到页面中的 DOM 元素。

5beforeUpdate 钩子函数,在响应式数据更新时触发,发生在虚拟 DOM 重新渲染和打补丁之前,这个时候我们可以对可能会被移除的元素做一些操作,比如移除事件监听器。

6)updated 钩子函数,虚拟 DOM 重新渲染和打补丁之后调用。

7beforeDestroy 钩子函数,在实例销毁之前调用。一般在这一步我们可以销毁定时器、解绑全局事件等。

8)destroyed 钩子函数,在实例销毁之后调用,调用后,Vue 实例中的所有东西都会解除绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

当我们使用 keep-alive 的时候,还有两个钩子函数,分别是 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

keep-alive的生命周期
初次进入时:
created > mounted > activated,退出后触发 deactivated

再次进入:
只会触发 activated
事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中

第一次页面加载会触发哪几个钩子?

会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

父子组件的生命周期(子mounted -> 父mounted (更新和销毁也是子组件先))

组件,分别在他们的钩子函数中打印日志,观察执行顺序。得到的结果如图所示,父组件先创建,然后子组件创建;子组件先挂载,然后父组件挂载。
父beforeCreate-> 父create -> 子beforeCreate-> 子created -> 子mounted -> 父mounted
更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

注意 在父组件传递接口的数据给子组件时,一定要在子组件标签上加上v-if=”传递的接口数据”

解决方法

  1. v-if
  2. 在子组件中 watch 监听,父组件获取到值,这个值就会变化,自然是可以监听到的
    博客:

父子组件及mixin的生命周期执行顺序

mixin的生命周期钩子在组件的生命周期钩子之前执行

在父组件中引入了mixin,生命周期顺序如下:

mixin的beforeCreate > 父beforeCreate > mixin的created > 父created > mixin的beforeMount > 父beforeMount > 子beforeCreate > 子created > 子beforeMount > 子mounted > mixin的mounted >父mounted

vue组件的通信方式

props/$emit
父子组件通信父->子props,
子->父 $on、$emit 获取父子组件实例

获取实例的方式调用组件的属性或者方法parent、children Ref

父->子孙 Provide、inject 官方不推荐使用,但是写组件库时很常用

$emit/$on 自定义事件 兄弟组件通信Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue() 自定义事件
(event-bus原理:就是一个发布订阅着模式) (注册后挂载到Vue实例,实现监听和触发)
掘金
vuex 跨级组件通信Vuex、$attrs、$listeners Provide、inject

$route 和 $router 的区别?

$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。而 $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。

router传参的两种方式(params /query)

1
2
3
4

params、query是什么? params:/router1/:id ,/router1/123,/router1/789 ,这里的id叫做params
query:/router1?id=123 ,/router1?id=456 ,这里的id叫做query。

1、params是路由的一部分,必须要有。query是拼接在url后面的参数,没有也没关系。
params一旦设置在路由,params就是路由的一部分,如果这个路由有params传参,但是在跳转的时候没有传这个参数,会导致跳转失败或者页面会没有内容。
2、params、query不设置也可以传参,但是params不设置的时候,刷新页面或者返回参数会丢失,query并不会出现这种情况,这一点的在上面说过了

vue-router 中的导航钩子函数

(1)全局的钩子函数 beforeEach 和 afterEach

beforeEach 有三个参数,to 代表要进入的路由对象,from 代表离开的路由对象。next 是一个必须要执行的函数,如果不传参数,那就执行下一个钩子函数,如果传入 false,则终止跳转,如果传入一个路径,则导航到对应的路由,如果传入 error ,则导航终止,error 传入错误的监听函数。

(2)单个路由独享的钩子函数 beforeEnter,它是在路由配置上直接进行定义的。

(3)组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。它们是直接在路由组
件内部直接进行定义的。

详细资料可以参考: 《导航守卫》

vue-router的push和replace的区别

1.this.$router.push()

描述:跳转到不同的url,但这个方法会向history栈添加一个记录,点击后退会返回到上一个页面。

2.this.$router.replace()

描述:同样是跳转到指定的url,但是这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。

3.this.$router.go(n)

相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面

前端两种路由实现和使用场景–Hash模式&&History模

v-if v-show (都会引发重绘与回流)

v-show

1
2
3
4
5
该指令的作用是根据真假切换元素的显示状态
原理是修改元素的display,实现显示隐藏
指令后面的内容,最终都会解析为布尔值
值为true元素显示,值为false元素隐藏
数据改变之后,对应元素的显示状态会同步更新

v-if

1
2
3
4
该指令的作用是根据表达式的真假来切换元素的显示状态
本质是通过操作dom元素来切换显示状态
表达式的值为true,元素存在于dom中;为false,从dom树中移除
频繁的切换使用v-show,反之使用v-if,前者的切换消耗小

v-show不生效的情况(可能设置了display:block !important 层级更高的生效了)

v-if在条件切换时,会对标签进行适当的创建和销毁,而v-show则仅在初始化时加载一次,因此v-if的开销相对来说会比v-show大

display:none; 会让元素脱离文档流,不占据页面空间。会引起回流和重绘

visibility:hidden; 只是隐藏内容,并没有脱离文档流,会占据页面空间。会引起重绘。

为什么vue组件中的data是一个函数而不是一个对象?

如果 data 是一个对象,当复用组件时,因为 data 都会指向同一个引用类型地址,其中一个组件的 data 一旦发生修改,则其他重用的组件中的 data 也会被一并修改。

如果 data 是一个返回对象的函数,因为每次重用组件时返回的都是一个新对象,引用地址不同,便不会出现如上问题。

v-model (原理其实就是绑定属性和事件。)

当 v-model 使用在表单元素上时,会根据元素的不同而采用不同的处理:

  1. <input type="text">文本 和 <textarea>上使用时,会默认给元素绑定名为 value 的prop 和名为 input的事件;
  2. <input type="checkbox">复选框 和 <input type="radio">单选框 上使用时,会默认绑定名为checked 的 prop 和名为 change 的事件;
  3. <select>选择框 上使用时,则绑定名为 value 的 prop 和名为 change 的事件。
    博客
Vue3 中的 v-model

修改默认 prop 名和事件名
废除 model 选项和 .sync 修饰符
使用多个 v-model

Vuex的理解及使用场景

  • Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。
    Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,
    若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex主要包括以下几个核心模块:
  1. state:定义了应用的状态数据
  2. Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),
    就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算
  3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
  4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
  5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

vuex

博客

开发中有没有遇见过数据修改了页面不刷新的情况

最近在开发中遇到了这样一些情况,通过点击事件改变了对象里面得数据,但是页面却不刷新,后来发现,是在给对象添加属性时出现的问题。

当vue的data里边声明或者已经赋值过的对象或者数组(数组里边的值是对象)时,向对象中添加新的属性,如果更新此属性的值,是不会更新视图的。

当页面初始化时候,vue会遍历data对象所有的属性,并使用object.defineProperty把这些属性全部转化为getter/setter,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

官方定义:
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。

方法1.$set

1
2
3
4
// 上传成功回调函数
handleAvatarSuccess(res, file) {
    this.$set(this.form,'imgUrl',res.data);
},
原理

可以发现 Vue.set() 和 this.$set() 这两个 api 的实现原理基本一模一样,都是使用了set函数
vm.$set(target, key, value)中接收三个参数(targer 目标值,key 将要设置的属性,value 要设置的值)

$set的原理是在vue的原型上添加$set的方法,同时针对以下三种情况情况分别进行处理

  1. 当key已经存在于target上的时候,直接修改target中对应key的值
  2. target是数组的时候,借助vue内部拦截处理后数组的splice方法进行赋值,vue对数组的’push’,’pop’,’shift’,’unshift’,’splice’,’sort’,’reverse’方法进行拦截处理成响应式,调用这些方法,可以触发界面的更新(后续添加这部分解说),如果只是通过arr[2]=2的方式进行赋值不会触发视图更新。
  3. 对于新增的属性,通过defineReactive把数据转化成getter和setter的方式,并触发数据变化通知
    掘金

方法2.$forceUpdate

1
2
3
4
handleAvatarSuccess(res, file) {
    this.form.imgUrl = res.data;
    this.$forceUpdate();
},

官方文档


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!