总结一下面试碰到的Vue相关问题,完整版参阅前端小白的面试问题集
1.虚拟Dom是什么,为什么会有这项技术?
Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化, 虚拟DOM就是为了解决浏览器性能问题而被设计出来的。
如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。
所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
但是当某些特殊需求(比如需要删除table(1000个元素)中的所有数据,然后增加),这样普通的dom只需要全部删除之后增加,虚拟dom需要大量比较,但是我们无法预见数据是如何改变的,在多数非极端情况下,Vdom的效率还是比较高的。
2.说一说Diff算法
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。
Diff算法有三大策略:
- Tree Diff
- Component Diff
- Element Diff
三种策略的执行顺序也是顺序依次执行。 Tree Diff 是对树每一层进行遍历,找出不同。
Component Diff 是数据层面的差异比较
- 如果都是同一类型的组件(即:两节点是同一个组件类的两个不同实例,比如:
<div id="before"></div>
与<div id="after"></div>
),按照原策略继续比较Virtual DOM树即可 - 如果出现不是同一类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点
Element Diff真实DOM渲染,结构差异的比较
当节点处于同一层级时,Diff提供三种DOM操作:删除、移动、插入。
将OldVnode 和 NewVnode的首尾位置分别标记为oldS、oldE、newS、newE,优化比较过程。
3.Vuex有哪些重要概念
Vuex的核心概念:state、getter、mutation、action、module
- state:状态,mapstate可以在页面里简化调用
- Getter:相当于vue中的computed计算属性,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,这里我们可以通过定义vuex的Getter来获取,Getters 可以用于监听、state中的值的变化,返回计算后的结果,
- muation:修改state中的值
- action:调用mutation最大的好处是可以异步,分发请求
- module:分割大型模块
4.Vuex的底层原理
基本的Vue状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
实现原理:vuex是利用vue的mixin混入机制,在beforeCreate钩子前混入vuexInit方法,vuexInit方法实现了store注入vue组件实例,并注册了vuex store的引用属性$store。store注入过程如下图所示:
5.Vue的双向绑定原理
MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。
对于视图更新数据
- 可以采用事件监听的方法,比如
v-on(用于绑定HTML事件)
;v-bind(用于设置HTML属性)
;v-on(用于绑定HTML事件)
;v-model:在表单控件元素上创建双向数据绑定:
- 原理:原生监听事件-oninput,onclick,hover... addeventlistener
对于数据更新视图
- 数据的每次读和写能够被我们看的见,即我们能够知道数据什么时候被读取了或数据什么时候被改写了,我们将其称为数据变的‘可观测’;
-- 1.Vue2.0:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
-- 2.Vue3.0:proxy()
- 完成了数据的'可观测',我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
6.如何实现redo和undo操作
HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持) 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的 功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。 Vue-Router 利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。
7.父子组件的数据传递
父组件向子组件传值:
- 子组件在props中创建一个属性,用以接收父组件传过来的值
- 父组件中注册子组件
- 在子组件标签中添加子组件props中创建的属性
- 把需要传给子组件的值赋给该属性
// 父组件 food.vue
<template>
<apple :type="type"></apple>
</template>
<script>
data (){
return {
type: 0
};
}
</script>
// 子组件 apple.vue
<template>
<span>{{childType}}</span>
</template>
<script>
props: ['type'],
created () {
this.childType = this.formatterType(type);
},
method () {
formatterType (type) {
if (type === 0) {
return "0 水果";
}
if (type === 1) {
return "1 蔬菜";
}
return '';
}
}
</script>
子组件向父组件传值:
- 子组件中需要以某种方式例如点击事件的方法来触发一个自定义事件
- 将需要传的值作为$emit的第二个参数,该值将作为实参传给响应自定义事件的方法
- 在父组件中注册子组件并在子组件标签上绑定对自定义事件的监听 如何理解组件化
<!-- 父组件 -->
<template>
<div class="test">
<test-com @childFn="parentFn"></test-com>
<br/>
子组件传来的值 : {{message}}
</div>
</template>
<script>
export default {
// ...
data: {
message: ''
},
methods: {
parentFn(payload) {
this.message = payload;
}
}
}
</script>
<!-- 子组件 -->
<template>
<div class="testCom">
<input type="text" v-model="message" />
<button @click="click">Send</button>
</div>
</template>
<script>
export default {
// ...
data() {
return {
// 默认
message: '我是来自子组件的消息'
}
},
methods: {
click() {
this.$emit('childFn', this.message);
}
}
}
</script>
8.如果要你写一个组件,需要注意什么?
- 提高开发效率
- 方便重复使用
- 简化调试步骤
- 提升整个项目的可维护性
- 便于协同开发
9.Vue的生命周期
Vue 一共有8个生命阶段,分别是创建前、创建后、加载前、加载后、更新前、更新后、销毁前和销毁后,每个阶段对应了一个生命周期的钩子函数。
(1)beforeCreate 钩子函数,在实例初始化之后,在数据监听和事件配置之前触发。因此在这个事件中我们是获取不到 data 数据的。
(2)created 钩子函数,在实例创建完成后触发,此时可以访问 data、methods 等属性。但这个时候组件还没有被挂载到页面中去,所以这个时候访问不到 $el 属性。一般我们可以在这个函数中进行一些页面初始化的工作,比如通过 ajax 请求数据来对页面进行初始化。
(3)beforeMount 钩子函数,在组件被挂载到页面之前触发。在 beforeMount 之前,会找到对应的 template,并编译成 render 函数。
(4)mounted 钩子函数,在组件挂载到页面之后触发。此时可以通过 DOM API 获取到页面中的 DOM 元素。
(5)beforeUpdate 钩子函数,在响应式数据更新时触发,发生在虚拟 DOM 重新渲染和打补丁之前,这个时候我们可以对可能会被移除的元素做一些操作,比如移除事件监听器。
(6)updated 钩子函数,虚拟 DOM 重新渲染和打补丁之后调用。
(7)beforeDestroy 钩子函数,在实例销毁之前调用。一般在这一步我们可以销毁定时器、解绑全局事件等。
(8)destroyed 钩子函数,在实例销毁之后调用,调用后,Vue 实例中的所有东西都会解除绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
当我们使用 keep-alive 的时候,还有两个钩子函数,分别是 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
10.MVC/MVP/MVVM的区别?
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率。
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Co ntroller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中我们使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此我们可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
MVVM 模式中的 VM,指的是 ViewModel,它和 MVP 的思想其实是相同的,不过它通过双向的数据绑定,将 View 和 Model 的同步更新给自动化了。当 Model 发生变化的时候,ViewModel 就会自动更新;ViewModel 变化了,View 也会更新。这样就将 Presenter 中的工作给自动化了。我了解过一点双向数据绑定的原理,比如 vue 是通过使用数据劫持和发布订阅者模式来实现的这一功 能。
11.看过Vue源码吗?
12.computed 原理
1.每个 computed 属性都会生成对应的观察者(Watcher 实例),观察者存在 values 属性和 get 方法。computed 属性的 getter 函数会在 get 方法中调用,并将返回值赋值给 value。初始设置 dirty 和 lazy 的值为 true,lazy 为 true 不会立即 get 方法(懒执行),而是会在读取 computed 值时执行。🐷划重点,这里是Vue避免重复渲染的方法
2.将 computed 属性添加到组件实例上,并通过 get、set 获取或者设置属性值,并且重定义 getter 函数。
3.页面初始渲染时,读取 computed 属性值,触发重定义后的 getter 函数。由于观察者的 dirty 值为 true,将会调用 get 方法,执行原始 getter 函数。getter 函数中会读取 data(响应式)数据,读取数据时会触发 data 的 getter 方法,会将 computed 属性对应的观察者添加到 data 的依赖收集器中(用于 data 变更时通知更新)。观察者的 get 方法执行完成后,更新观察者的 value 值,并将 dirty 设置为 false,表示 value 值已更新,之后在执行观察者的 depend 方法,将上层观察者(该观察者包含页面更新的方法,方法中读取了 computed 属性值)也添加到 getter 函数中 data 的依赖收集器中(getter 中的 data 的依赖器收集器包含 computed 对应的观察者,以及包含页面更新方法(调用了 computed 属性)的观察者),最后返回 computed 观察者的 value 值。
4.当更改了 computed 属性 getter 函数依赖的 data 值时,将会根据之前依赖收集的观察者,依次调用观察者的 update 方法,先调用 computed 观察者的 update 方法,由于 lazy 为 true,将会设置观察者的 dirty 为 true,表示 computed 属性 getter 函数依赖的 data 值发生变化,但不调用观察者的 get 方法更新 value 值。再调用包含页面更新方法的观察者的 update 方法,在更新页面时会读取 computed 属性值,触发重定义的 getter 函数,此时由于 computed 属性的观察者 dirty 为 true,调用该观察者的 get 方法,更新 value 值,并返回,完成页面的渲染。
5.dirty 值初始为 true,即首次读取 computed 属性值时,根据 setter 计算属性值,并保存在观察者 value 上,然后设置 dirty 值为 false。之后读取 computed 属性值时,dirty 值为 false,不调用 setter 重新计算值,而是直接返回观察者的 value,也就是上一次计算值。只有当 computed 属性 setter 函数依赖的 data 发生变化时,才设置 dirty 为 true,即下一次读取 computed 属性值时调用 setter 重新计算。也就是说,computed 属性依赖的 data 不发生变化时,不会调用 setter 函数重新计算值,而是读取上一次计算值。
13.vuex 中muation为什么不能有异步操作,如果要异步要怎么做?
每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
Action:可以异步,但不能直接操作State。
14.Vue设置响应式数据除了set还有什么方法?
对象添加属性还可以使用Object.assign({},obj1,obj2)返回获取的新对象替换原有对象
使用JavaScript的数组操作函数,这些方法都会返回一个新数组,也是数组替换原理(也有一种说法是内部重写了相关的方法,让变化能够被检测)
forceUpatde
15.vue select标签优化
16.vue 如何获取真实dom
<div class='box' ref='myBox'>你好</div>
this.$refs.myBox.style.color = 'red' // 让你好的颜色显示为红色
17.vue3 新特性
17.1 检测机制
vue2中基于Object.defineProperty
的observer
实现,vue3中则基于Proxy
的observer
实现
- 对属性的添加、删除动作的监测
- 对数组基于下标的修改,对于
length
修改的监测 - 对Map、Set的支持
默认为惰性监测
- 1、 在2.x版本中,响应式数据都会在启动的时候进行监测,如果数据量较大,会有严重的性能消耗
- 2、 在3.x版本中,只有应用初始可见部分所用到的数据会被监测到
更精准的变动通知:
- 1、在2.x版本中,通过
Vue.set
强制添加一个新的属性,所有依赖这个对象的watch
函数都被执行一次 - 2、在3.x中,只有依赖这个具体属性的
watch
函数被通知
更好的调试:
- 1、通过使用新增的
renderTracked
和renderTriggered
钩子,可以精确的追踪到一个组件发生重新渲染的触发时机及原因 暴露出observable
的api
来创建响应式对象,可以替代掉event bus
,做一些跨组件的通信
17.2 性能优化
组件渲染:
- 1、在2.x版本中,父组件重新渲染时,其子组件也必须重新渲染(前提是修改的数据是子组件的props,才会触发子组件的重新渲染)
- 2、在3.x版本中,可以单独重新渲染子组件或者父组件
静态树提升
- 1、 在3.x版本中,编译器可以检测到静态组件,将其提升,降低渲染成本
静态属性提升
17.3 合成API(Composition API)
18. Vue Loader是什么?
Vue Loader 是一个 webpack
的 loader
,它允许你以一种名为单文件组件 (SFCs)
的格式撰写 Vue
组件:
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
Vue Loader 还提供了很多酷炫的特性:
- 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 的部分使用 Sass 和在 的部分使用 Pug;
- 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
- 使用 webpack loader 将 和 中引用的资源当作模块依赖来处理;
- 为每个组件模拟出 scoped CSS;
- 在开发过程中使用热重载来保持状态。
19. Vue created 和 mounted 的区别?
- **created:**在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
- **mounted:**在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
20. keep-alive
我们在平时开发中,总有部分组件没必要多次 Init,我们需要将组件进行持久化,使组件状态维持不变,在下一次展示时,也不会重新init keepalive 音译过来就是保持活跃,所以在vue中我们可以使用keepalive来进行组件缓存 基本使用
//keepalive包含的组件会被进行缓存
<keep-alive>
<component />
</keep-alive>
其他操作及hook
//只缓存组件name为a或者b的组件
<keep-alive include="a,b">
<component :is="currentView"/>
</keep-alive>
//组件名为c的组件不缓存
<keep-alive exclude="c">
<component :is="currentView"/>
</keep-alive>
// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子也就是只缓存a组件
<keep-alive include="a,b" exclude="b">
<component :is="currentView"/>
</keep-alive>
// 如果缓存的组件超过了max设定的值5,那么将删除第一个缓存的组件
<keep-alive exclude="c" max="5">
<component :is="currentView"/>
</keep-alive>
21.父子组件的生命周期
- 加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
- 子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新过程
父beforeUpdate->父updated
- 销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
20.vue中的nexttick原理
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 Watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
nexttick实际上就是将你的callback放到下一个事件循环tick
中更新,也就是当前的DOM刷新完成之后。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环 “tick” 中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
21.vue-router
在html中使用
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
在应用入口配置
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router)
app.mount('#app')
// 现在,应用已经启动了!