面试题-Vue
面试题-Vue
简述 MVVM
什么是 MVVM
MVVM:视图模型双向绑定
,是Model-View-ViewModel
的缩写,也就是把MVC
中的Controller
演变成`ViewModel
- Model
层代表数据模型
- View
代表 UI 组件
- ViewModel 是
View
和Model
层的桥梁,数据会绑定到ViewModel
层并自动将数据渲染到页面中,视图变化的时候会通知ViewModel
层更新数据
以前是操作 DOM 结构更新视图,现在是数据驱动视图
。
MVVM 的优点
低耦合
:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变化,当 Model 变化的时候 View 也可以不变可重用性
:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑独立开发
:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计可测试
Vue 底层实现原理
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()
来劫持各个属性的 setter
和 getter
,在数据变动时发布消息给订阅者,触发相应的监听回调
将 Vue 作为例子,模型(Model)只是普通的 javascript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观
Observer(数据监听器) : Observer 的核心是通过 Object.defineProprtty()
来监听数据的变动,这个函数内部可以定义 setter
和 getter
,每当数据发生变化,就会触发 setter
,这时候 Observer 就要通知订阅者,订阅者就是 Watcher
Watcher(订阅者) : Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个 update()方法
- 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调
Compile(指令解析器) : Compile 主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图
传送门:☞ 20 分钟吃透 Diff 算法核心原理
谈谈对 vue 生命周期的理解?
每个Vue
实例在创建时都会经过一系列的初始化过程,vue
的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件
create阶段
:vue 实例被创建beforeCreate
: 最初调用触发,创建前,此时 data 和 methods 中的数据都还没有初始化,data 和 events 都不能用created
: 创建完毕,data 中有值,未挂载,data 和 events 已经初始化好,data 已经具有响应式;在这里可以发送请求
mount阶段
: vue 实例被挂载到真实 DOM 节点beforeMount
:在模版编译之后,渲染之前触发,可以发起服务端请求数据,SSR 中不可用;基本用不上这个 hookmounted
: 在渲染之后触发,此时可以操作 DOM,并能访问组件中的 DOM 以及$ref
,SSR 中不可用
update阶段
:当 vue 实例里面的 data 数据变化时,触发组件的重新渲染beforeUpdate
:更新前,在数据变化后,模版改变前触发,切勿使用它监听数据变化updated
:更新后,在数据改变后,模版改变后触发,常用于重渲染后的打点,性能检测或触发 vue 组件中非 vue 组件的更新
destroy阶段
:vue 实例被销毁beforeDestroy
:实例被销毁前,组件卸载前触发,此时可以手动销毁一些方法,可以在此时清理事件、计时器或者取消订阅操作destroyed
:卸载完毕后触发,销毁后,可以做最后的打点或事件触发操作
组件生命周期
生命周期(父子组件) 父组件 beforeCreate --> 父组件 created --> 父组件 beforeMount --> 子组件 beforeCreate --> 子组件 created --> 子组件 beforeMount --> 子组件 mounted --> 父组件 mounted
加载渲染过程 父 beforeCreate --> 父 created --> 父 beforeMount --> 子 beforeCreate --> 子 created --> 子 beforeMount --> 子 mounted --> 父 mounted
挂载阶段 父 created --> 子 created --> 子 mounted --> 父 mounted
更新阶段(父子组件会互相影响) 父 beforeUpdate --> 子 beforeUpdate --> 子 updated --> 父 updated
销毁阶段 父 beforeDestroy --> 子 beforeDestroy --> 子 destroyed --> 父 destroyed
computed & watch
通俗来讲,既能用 computed
实现又可以用 watch
监听来实现的功能,推荐用 computed
computed
计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量改变时,计算属性也会跟着改变,它可以设置 getter 和 setterwatch
监听的是已经在 data 中定义的变量,当该变量变化时,会触发watch
中的方法。
watch(属性监听):是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
computed(计算属性):属性的结果会被缓存
,当 computed
中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取,除非依赖的响应式属性变化时才会重新计算;主要当做属性来使用, computed
中的函数必须用 return
返回最终的结果;data 不改变,computed 不更新
使用场景
computed
:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能watch
:当一条数据影响多条数据的时候使用,例:搜索数据
组件中的 data 为什么是一个函数?
一个组件被复用多次的话,就会创建多个实例;如果 data 是对象的话,对象属于引用类型,就会影响到所有的实例;所以为了保证组件之间 data 不冲突,data 必须是一个函数
本质上,这些实例用的都是同一个构造函数
为什么 v-for 和 v-if 不建议用在一起
- 当
v-for
和v-if
处于同一个节点时,v-for
的优先级比v-if
更高,这意味着v-if
将分别重复运行于每个v-for
循环中,如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费 - 这种场景建议使用
computed
,先对数据进行过滤
注意
3.x 版本中 v-if
总是优先于 v-for
生效;由于语法上存在歧义,建议避免在同一元素上同时使用两者
v-if 和 v-show 的区别
v-if
在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点v-show
会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
使用场景
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景v-show
适用于需要非常频繁切换条件的场景
如何实现 v-model
v-model
是 Vue 提供的一个语法糖,用于动态绑定
<input v-model="value" />
// 等价于
<input :value="value" @input="value = $event.target.value" />
Vue 内置指令
Vue 自定义指令
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
unbind:只调用一次,指令与元素解绑时调用。
Vue 修饰符有哪些
事件修饰符
- .stop:阻止事件继续传播
- .prevent:阻止标签默认行为
- .capture:使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
- .self:只当在 event.target 是当前元素自身时触发处理函数
- .once:事件将只会触发一次
- .passive:告诉浏览器你不想阻止事件的默认行为
v-model 的修饰符
- .lazy:通过这个修饰符,转变为在 change 事件再同步
- .number:自动将用户的输入值转化为数值类型
- .trim:自动过滤用户输入的首尾空格
键盘事件的修饰符
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统修饰键
- .ctrl
- .alt
- .shift
- .meta
鼠标按钮修饰符
- .left
- .right
- .middle
v-for 中 key 的作用
key 的作用是为了在 diff 算法执行时更快的找到对应的节点,
提高diff速度,更高效的更新虚拟DOM
;为了在数据变化时强制更新组件,以避免 “就地复用” 带来的副作用。
当 Vue.js 用
v-for
更新已渲染过的元素列表时,它默认用 “就地复用” 策略;如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素;重复的 key 会造成渲染错误
vue 组件的通信方式
父子组件通信
- 父 -> 子
props
,子 -> 父$emit
- 获取父子组件实例
$parent
&$children
,通过实例的方式调用组件的属性或者方法
- 父 -> 子
跨级组件通信
- Vuex
$ref
$attrs
&$listeners
$attrs
:在组件上使用v-bind="$attrs"
, 将父组件中不被认为props
特性绑定的属性传入子组件中,通常配合interitAttrs
选项一起使用$listeners
:它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素
Provide
&inject
官方不推荐使用,但是写组件库时很常用
兄弟组件通信
Event Bus
// 事件总线 class Bus { constructor() { // 存放事件的名字 this.callbacks = {}; } $on(name, fn) { //事件名字,事件函数体 this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { //事件名字,事件参数 if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } }
// main.js Vue.prototype.$bus = new Bus();
// bus1.vue this.$bus.emit("test", ""); // bus2.vue this.$bus.on("test", this.handle);
$set 方法
了解 Vue 响应式原理都知道在两种情况下修改数据 Vue 是不会触发视图更新的
- 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
- 直接更改数组下标来修改数组的值
原理
因为响应式数据 我们给对象和数组本身都增加了__ob__
属性,代表的是 Observer
实例
当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪,然后会触发对象__ob__
的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice
方法去更新数组
nextTick
作用
nextTick
是 Vue 提供的一个全局API
,是在下次DOM
更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick
,则可以在回调中获取更新后的DOM
;
实现原理
- 在下次
DOM
更新循环结束之后执行延迟回调,在修改数据之后立即使用nextTick
来获取更新后的DOM
nextTick
主要使用了宏任务和微任务;根据执行环境分别尝试采用Promise、MutationObserver、setImmediate
,如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。
插槽
slot
说白了就是一个占位的
插槽类型
匿名插槽:没有名字的默认都填到这里
具名插槽:有名字的,根据名字填充到规定的位置
作用域插槽:以让父组件使用插槽时插槽内容能够访问子组件中的数据
<!-- 父组件 --> <template slot-scope="{ user }"> <div>user.name</div> <div>user.age</div> <template> <!-- 子组件 --> <slot :user="user"></slot ></template> </template>
keep-alive
作用
keep-alive
是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载
- 常用的两个属性:
include
/exclude
,允许组件有条件的进行缓存 - 两个生命周期:
activated
/deactivated
,用来得知当前组件是否处于活跃状态
场景:tabs、标签页、后台导航、vue 性能优化
原理
Vue.js 内部将 DOM
节点抽象成了一个个的 VNode
节点,keep-alive
组件的缓存也是基于 VNode
节点的而不是直接存储 DOM
结构
它将满足条件 pruneCache&pruneCache
的组件在 cache
对象中缓存起来,在需要重新渲染的时候再将vnode
节点从 cache
对象中取出并渲染。
在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
在 vue-router 中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<router-view></router-view>
</keep-alive>
mixin
mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到 mixin,多个组件有相同的逻辑,抽离出来
mixin 并不是完美的解决方案,会有一些问题 vue3 提出的 Composition API 旨在解决这些问题
场景:PC 端新闻列表和详情页一样的右侧栏目,可以使用 mixin 进行混合
劣势:
- 变量来源不明确,不利于阅读
- 多 mixin 可能会造成命名冲突
- mixin 和组件可能出现多对多的关系,使得项目复杂度变高
Vue Router 相关
完整的导航解析流程:
- 导航被触发
- 在失活的组件里调用 beforeRouteLeave 守卫
- 调用全局的 beforeEach 守卫
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)
- 在路由配置里调用 beforeEnter
- 解析异步路由组件
- 在被激活的组件里调用 beforeRouteEnter
- 调用全局的 beforeResolve 守卫 (2.5+)
- 导航被确认
- 调用全局的 afterEach 钩子
- 触发 DOM 更新
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
路由模式
hash 模式
兼容性好但是不美观
location.hash
的值实际就是 URL 中 # 后面的东西
它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
可以为 hash 的改变添加监听事件 window.addEventListener("hashchange", funcRef, false)
每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
history 模式
虽然美观,但是刷新会出现 404 需要后端进行配置
利用了 HTML5 History Interface 中新增的 pushState()
和 replaceState()
方法
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础
路由传参
编程式的导航
通过 params 属性传值
params 必须搭配 name 使用
// 路由配置文件 { path: '/test', name: 'test', component: Test } // 跳转 this.$router.push({ name: 'test', params: { id: 1 } }) // 获取参数 this.$route.params
通过 query 属性传值
// 路由配置文件 { path: '/test', name: 'test', component: Test } // 跳转 this.$router.push({ path: 'test', query: { id: 1 } }) // 获取参数 this.$route.query
动态路由
// 路由配置文件 { path: '/test/:id', name: 'test', component: Test } // 跳转 this.$router.push('/test/' + id) // 获取参数 this.$route.params.id
声明式的导航
let path1 = {
name: 'test',
params: {
id: 1
}
}
let path2 = {
path: 'test',
query: {
id: 1
}
}
let path3 = '/test/' + id
<router-link :to="path1">target</router-link>
<router-link :to="path2">target</router-link>
<router-link :to="path3">target</router-link>
Vuex 的理解及使用场景
Vuex 是一个专为 Vue 应用程序开发的状态管理模式
Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新
改变 store 中的状态的唯一途径就是显式地提交 (commit)mutation
,这样使得我们可以方便地跟踪每一个状态的变化 Vuex 主要包括以下几个核心模块:
- State:定义了应用的状态数据
- Getter:像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
Vuex 页面刷新数据丢失怎么解决
需要做 vuex 数据持久化,一般使用本地存储的方案来保存数据,可以自己设计存储方案,也可以使用第三方插件
export default function (app) {
// 离开页面刷新前,将store中的数据存到session
window.addEventListener("beforeunload", () => {
sessionStorage.setItem("storeCache", JSON.stringify(app.store.state));
});
// 页面加载完成,将session中的store数据
window.addEventListener("load", () => {
const storeCache = sessionStorage.getItem("storeCache");
if (storeCache) {
// 将session中的store数据替换到store中
app.store.replaceState(JSON.parse(storeCache));
}
// 获取数据后,清除
sessionStorage.removeItem("storeCache");
});
}
Vue 项目的性能优化
- 对象层级不要过深,否则性能就会差
- 不需要响应式的数据不要放到 data 中(可以用
Object.freeze()
冻结数据) v-if
和v-show
区分使用场景computed
和watch
区分使用场景v-for
遍历必须加 key,key 最好是 id 值,且避免同时使用v-if
- 大数据列表和表格性能优化-虚拟列表/虚拟表格
- 防止内部泄漏,组件销毁后把全局变量和事件销毁
- 图片懒加载
- 路由懒加载
- 第三方插件的按需引入
- 适当采用
keep-alive
缓存组件 - 防抖、节流运用
- 服务端渲染 SSR or 预渲染
虚拟 DOM 是什么 有什么优缺点
由于在浏览器中操作 DOM 是很昂贵的,频繁的操作 DOM,会产生一定的性能问题,这就是虚拟 Dom 的产生原因,Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现
Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。
优点
保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点
无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。