前端面试八股文 Vue
讲一下Vue框架的原理?
我们使用Vue开发应用,实际上是编写若干Vue组件,实现模板、data、生命周期钩子等,然后执行new Vue(),将根组件挂载到指定的DOM节点上面,当我们编写的组件中生命周期钩子里面的或者在模板的元素事件中改变数据时候,视图会响应地更新。这样就实现了应用。
new Vue()之后,Vue会从根组件开始,遍历整个组件树,对每个组件进行处理。
对于一个Vue组件,Vue首先会进行模板编译,将模板编译为render函数,render函数返回虚拟DOM,如果遇到子组件,也对子组件做同样操作,最终形成一个虚拟DOM树。(compile)
Vue会把虚拟DOM映射到真实DOM并渲染到指定节点上,这样就实现了视图的渲染。
Vue在组件初始化时候还会设置数据为响应式,并将依赖于数据的渲染方法、computed、watch收集起来。
当数据改变后,Vue会根据初始化时候收集的依赖,更新视图,这时候我们就看到最新的界面了。
讲讲Vue双向绑定原理
Vue的双向绑定是基于Object.defineProperty()(Vue3换成了Proxy)和发布-订阅模式实现的。
Vue双向绑定的原理大致思路如下:
- Vue会使用document.fragment劫持根元素里包含的所有节点,这些节点不仅包括标签元素,还包括文本,甚至换行的回车。
- Vue会把data中所有的数据,用defindProperty()变成Vue的访问器属性,这样每次修改这些数据的时候,就会触发相应属性的get,set方法。
- 接下来编译处理劫持到的dom节点,遍历所有节点,根据nodeType来判断节点类型,根据节点本身的属性(是否有v-model等属性)或者文本节点的内容(是否符合{{文本插值}}的格式)来判断节点是否需要编译。
- 对v-model,绑定事件当输入的时候,改变Vue中的数据。
- 对文本节点,将他作为一个观察者watcher放入观察者列表,当Vue数据改变的时候,会有一个主题对象,对列表中的观察者们发布改变的消息,观察者们再更新自己,改变节点中的显示,从而达到双向绑定的目的。
Vue采用了哪种软件架构设计模式?MVVM和MVC、MVP的区别
Vue采用了MVVM。
- MVVM是一种软件架构设计模式,它抽离了视图、数据和逻辑,并限定了Model和View只能通过VM进行通信,VM订阅Model并在数据更新时候自动同步到视图。
- MVC将应用抽象为数据层(Model)、视图层(View)、逻辑层(controller),降低了项目耦合。但MVC并未限制数据流,Model和View之间可以通信。
- MVP则限制了Model和View的交互都要通过Presenter,这样对Model和View解耦,提升项目维护性和模块复用性。
而MVVM是对MVP的P的改造,用VM替换P,将很多手动操作的数据=>视图的自动化同步,降低了代码复杂度,提升可维护性。
Vue常用指令有哪些?
- v-show
- v-if、v-else-if、v-else
- v-for
- v-bind 绑定属性、class和style
- v-model 用于实现双向绑定,本质上是一个语法糖,例如对于input组件,v-model等价于v-on:input + v-bind:xxx
- v-on 绑定事件
v-for和v-if同时使用有问题吗?
在Vue2中v-for的优先级高于v-if,会在每次渲染时候都要遍历列表并判断是否需要渲染,这个遍历操作其实是有一部分冗余或者完全不必要的。
而在Vue3中,v-if的优先级高于v-for,如果此时v-if中的条件使用了v-for的循环参数,会报错。
应该用以下方式替换v-if和v-for同时使用的方案:
- 如果是为了过滤一个列表中的项目,可以将列表作为计算属性,在computed中过滤出需要渲染的列表,再进行渲染。这样避免了每次渲染都计算(只在computed依赖的属性变化时候才计算),同时渲染列表是过滤了的,那么循环的次数也可能减少。
- 如果是为了控制整个列表的展示和隐藏,可以将判断条件放到父元素上或者外层增加一层template标签。这样展示和隐藏的判断只需要执行一次(在列表最开始)。
Vue3和Vue2的区别
- Vue3新增组合式API,更好地聚合功能代码。
- 全局Vue API更改为使用应用程序实例,createApp创建一个应用实例。例如Vue.use()改为createApp().use()。
- 同一元素上的v-if和v-for的优先级发生改变,v-if无法访问v-for的变量。
- Vue3基于Proxy实现响应式,Vue2则是Object.defineProperty();
Vue父子组件挂载顺序
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created ->子 beforeMount -> 子 mounted -> 父 mounted
更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
Vue组件间通信方式有哪些?
- props(组件属性):父组件可以通过Props将数据传递给子组件,子组件通过接收这些属性来访问和使用父组件传递的数据。
- v-on + $emit:子组件通过emit触发自定义事件,并将数据作为参数传递给父组件。父组件通过监听并处理这些自定义事件来接收传递的数据。
- $attrs:通过attrs可以实现祖孙组件的通信,祖先组件通过v-bind传递多个属性,中间的组件可以直接通过
v-bind="$attrs"
将所有接收到的属性继续传递。 - provide和inject:父组件可以通过provide将数据或函数提供给子孙组件,子组件通过inject来接收和使用这些数据或函数。
- parent、children、ref:通过parent和children可以获取到父组件和子组件的引用,通过ref可以获取到Vue实例或DOM节点的引用。
- eventBus:事件总线,适用于较轻量的应用。
- Vuex:通过Vuex可以实现全局状态管理,使得所有组件都可以访问和修改全局状态。
Vue computed和watch的区别
应用场景不同
computed用在根据data属性或者其他computed计算得到一个新值的情况,computed的值一般被用在渲染中。
watch用在监听数据变化,然后做一些其他的操作的场景。
执行过程不同
对于computed,在依赖的data属性变化后,computed并不会重新计算新的值,而是等到访问的时候再判断,如果依赖的data有改动则重新计算并返回结果,如果依赖的data没有改动,就不计算,直接返回当前结果。
对于watch,依赖的数据变化后就会执行watch的回调。
Vue组件中data为什么是函数
如果data不是一个函数,或者函数中不是返回一个对象字面量,那么实例化多个组件的话,不同组件间会共享同一个data,data的改变也会影响到每个实例,这是不符合预期的。
Vue组件data可以使用箭头函数么?
可以使用箭头函数,但是需要注意this指向。
如果使用箭头函数,data函数中的this不会指向vue实例,如果需要访问vue实例,可以通过data函数的参数来实现。
data: vm => ({ a: vm.myProp })
讲讲Vuex的作用和使用
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态。集中式状态管理。
它主要有以下API
-
state
用于保存状态
-
getters
getters和state的关系类似于Vue组件data属性和computed属性的关系,getters根据state或者其他getters计算出另一个变量的值,当其依赖的数据变化时候,它也会实时更新。
-
mutations
用于直接操作改变state
-
actions
可以异步更新状态,通过调用mutations中的方法修改state
-
module
用来组织整个应用的状态管理代码,使状态划分模块,更易于管理
-
辅助函数
mapState、mapGetters、mapMutations、mapActions
-
createStore
createStore则用来创建状态管理对象
Vuex的数据流是组件中触发actions,actions提交mutations,mutations修改state。 组件根据 states或getters来渲染页面。
Vue-router原理以及两种模式区别
前端路由有两种模式,HTML5 history模式和hash模式,这两种模式本质是不同的底层浏览器技术,但是上层Vue Router做了统一化的封装,因此在我们开发组件和配置路由时候使用这两种模式的区别并不大。默认是hash模式。
这两种模式有几个主要区别
- HTML5 history模式的路由没有"#"字符,而是在域名后直接写路径,更加优雅
- 由于hash模式"#"后面的字符不会发给服务器,因此可以减轻对服务器的访问压力,但是同时也会造成SEO(搜索引擎优化)比较差
- HTML5 history需要服务器在访问不同的路径时候都能fallback到index.html,所以需要服务器配合增加相关的配置,因此相对麻烦
前端路由的原理关键有2点
- 可以修改url,但不会引起刷新,从而在不刷新的页面的情况下跳转路由。
- 监听url改变,根据url渲染对应组件。
hash模式和history模式的原理都是基于这两点。hash是通过浏览器提供的location API修改url,通过onhashchange方法监听hash改变;history通过浏览器提供的history.pushState或者history.replacestate修改url,通过popState事件监听url改变。
讲讲Vue的虚拟DOM,原理,好处是什么?相对于手动操作DOM,性能更好吗?
Vue的虚拟DOM是一种对真实DOM的抽象,它是以JavaScript对象为基础的,而DOM与平台强相关。虚拟DOM通过对比状态变化前后的差异,并将这些差异更新到真实DOM中,从而提高渲染性能。
相对于手动操作DOM,虚拟DOM有以下几个优点:
- 首先,它能够保证性能的下限,通过diff算法找出最小差异,然后批量patch,这样虽然比不上手动优化,但相较于粗暴的DOM操作性能要好很多。
- 其次,虚拟DOM无需手动操作DOM,只需要写好View-Model的代码逻辑,框架会根据虚拟DOM和数据双向绑定,帮我们以可预期的方式更新视图,这极大地提高了开发效率。
- 此外,虚拟DOM具备跨平台的优势,由于它不依赖真实平台环境,因此使它可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等。
然而,虚拟DOM也存在一些无法进行极致优化的缺点。在某些性能要求极高的应用中,虚拟DOM无法进行针对性的极致优化。
因此,虽然虚拟DOM相较于手动操作DOM在性能上有所提升,但并不能完全替代手动优化。在实际开发中,需要根据具体应用的需求来选择适合的优化策略。
说说Vue的keep-alive使用及原理。
keep-alive是Vue的内置组件,它的作用是将不活动的组件保存在内存中,而不是销毁。这可以提高组件的启动速度和响应速度,同时避免了不必要的性能浪费。
keep-alive的工作原理如下:
- 首次加载组件时,keep-alive会将组件实例缓存起来,同时将组件的vm.$el(组件实例的根DOM元素)从DOM树中移除。
- 当切换到其他组件后,原始组件的vm.$el会被放入一个名为_inactive的数组中保存起来。
- 如果再次切换回原始组件,原始组件的vm.$el会从_inactive数组中取出,并重新插入到DOM树中。
需要注意的是,由于组件被缓存起来,所以组件的生命周期钩子函数(如created、mounted等)只在首次加载时触发一次,后续切换时不会再触发。
keep-alive的使用方法也很简单,只需要将需要缓存的组件放在<keep-alive>
标签内即可,同时还可以使用include
和exclude
属性来指定需要缓存和不需要缓存的组件。
Vue的nextTick方法的原理及使用场景?
在 Vue中,当改变一个数据对象时,视图并不会立即更新。Vue会将这些改变放入一个队列中,然后在下一个事件循环中处理它们。这种机制使得 Vue 能够进行批量更新,并避免不必要的计算和 DOM 操作。
nextTick
方法允许在队列的末尾添加一个回调函数,以便在所有的数据改变都被处理并且 DOM 更新后执行该回调函数。
Vue使用MutationObserver/Promise/setTimeout实现nextTick。Vue判断浏览器兼容性,按照MutationObserver -> Promise -> setTimeout的优先级实现nextTick。
nextTick的使用场景是在修改数据之后,想要访问修改后的DOM时候,可以用Vue.nextTick,在下个循环中访问,这样才能访问到修改后的结果。
讲讲Vue的diff算法
Vue diff算法的目标是更新DOM,它对比新旧虚拟DOM,根据对比的结果将旧的DOM更新为新的DOM,从snabbdom发展而来。
diff算法的大体过程是,从组件根节点开始对比两棵虚拟DOM树,以下新旧节点比较的过程:
- 如果新旧节点的引用一致,可以认为没有变化。
- 比较新旧节点文本,需要修改的话,则更新文本。
- 如果新节点没有子节点,老节点有子节点,直接删除老的子节点。
- 如果新节点有子节点,老节点没有子节点,则创建新的子节点。
- 新旧节点都有子节点,而且新旧节点的子节点引用不一样,会调用updateChildren函数比较子节点。
updateChildren比对新旧节点的子节点使用的是首位指针法,按照如下顺序比对:
- 新前旧前:如果命中,新前指针和旧前指针下移一位
- 新后旧后:如果命中,新后指针和旧后指针上移一位
- 新前旧后:如果命中,将旧后指针指向的节点移动到旧前指针之前的位置
- 新后旧前:如果命中,将旧前指针指向的节点移动到旧后指针之后的位置
如果四种情况都没有命中,则查找新前指针指向的节点是否在旧节点的子节点中,如果在,就将相应的节点移动到旧前指针之前,如果不在,就将新节点添加到旧前指针之前。
这样比对,直到新前指针>新后指针or旧前指针>旧后指针,如果此时新节点的子节点有剩余,添加到旧节点的子节点尾部,如果旧的有剩余,则直接删除
前端面试八股文 Vue