【面试题】Vue2
01. 谈谈对MVVM的理解
什么是MVC?
- MVC是Model-View-Controller 的缩写,即 模型—视图—控制器
- Model:从数据库中查询到的数据对象
- View:动态显示模型对象数据的页面,后台渲染(jsp)
- Controller:接受用户提交的请求参数,操作数据库生成动态数据并产生模型对象
- MVC是 单向通信 。即View和Model,必须通过Controller来承上启下。
什么是MVVM?
- MVVM由Model、View、ViewModel(同步View和Model的对象) 三部分构成
- MVVM模式:不需要用户手动的操作dom的,主要是实现数据双向绑定
- 利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新
02. 谈谈对Vue的理解*
- Vue.js是一个用于创建用户界面的渐进式JavaScript框架
- 特点
- MVVM模式的数据驱动(谈谈MVVM模式)
- 组件化开发(灵活,低耦合,提高维护性)
- 声明式编码(简化DOM操作,数据驱动视图)
- 虚拟DOM+Diff算法(虚拟DOM+Diff算法)
- ie8版本一下不支持(Object.definePrototype在ie8下不支持)
03. 区别v-if与v-show
v-if | v-show |
---|---|
控制dom添加与删除 | 控制元素的样式 |
会到导致重排与重绘 | 会引起重绘,不会引起重排 |
首次加载快,不频繁切换的场景 | 如果是频繁切换场景需要使用 |
04. 为什么v-for与v-if不能一起用
- v-for的优先级比v-if的优先级高
- 重复计算的问题
- 解决:
- v-if放在外层
- 使用computed进行计算后,再v-for
05. computed与watcher以及method的区别*
- method
- 没有缓存,多次计算显示多次
- computed
- 有缓存,多次读取
- 只能写同步代码
- 只能监视使用到的数据
- 场景:根据现有数据同步计算新的数据
- watch
- 可以写异步代码
- 可以进行深度监视
- 场景:深度监视,或者监视某个数据变化的操作
优先级:props==>methods===>data===>computed===>watch
可以从以下角度说明:
- 缓存角度
- 同步异步
- 数据监视
- 优先级
06. 说说vue的常用指令
- v-text
- v-html
- v-show
- v-if / v-else / v-else-if
- v-for
- v-on
- v-once
- v-bind
- v-model
- v-slot
- v-pre
vue自定义指令:
07. Vue单个组件的生命周期(对比Vue3)*
vue2生命周期:
另外加上:
- activated:被keep-alive缓存的组件激活时调用
- deactivated:keep-alive缓存的组件失活时调用
- errorCaptured:在捕获一个来自后代组件的错误时被调用。
vue2与vue3生命周期的区别:
- beforeDestroy改为了beforeUnmount
- new Vue() 改为 createApp()
- 判断el配置项和vm.$mount()在vue3中没有了
组合式API形式的生命周期钩子:
- beforeCreate===>setup()
- created=======>setup()
- beforeMount ===>onBeforeMount
- mounted=======>onMounted
- beforeUpdate===>onBeforeUpdate
- updated =======>onUpdated
- beforeUnmount ==>onBeforeUnmount
- unmounted =====>onUnmounted
08. Vue父子组件的生命周期
- 初始化:
- beforeCreate
- created
- beforeMount
- --child beforeCreate
- --child created
- --child beforeMount
- --child mounted
- mounted
- 更新:
- beforeUpdate
- --child beforeUpdate
- --child updated
- updated
- beforeUpdate
- 死亡:
- beforeDestroy
- -- child beforeDestroy
- -- child destroyed
- destroyed
- beforeDestroy
09. 区别组件的钩子函数actived与mounted*
- mounted:组件实例被挂在到dom上时被调用
- actived:该钩子仅在keep-alive组件内部使用,组件被激活时使用
10. 说说动态组件、缓存组件与异步组件*
- 动态组件:
- 通过的is属性动态加载一个组件
- 缓存组件:
- 默认路由组件离开或动态组件被切换, 组件都会立即死亡
- keepAlive来缓存组件,其中使用include和exclude来控制缓存哪些组件
- 异步组件:
- 在引入组件时使用import动态引入: const Home = () => import('./Home.vue')
- 组件会被单独打包, 且只有在第一次访问时才会请求加载对应的打包文件 ==> 减小首屏打包文件打小
11. 说说递归组件的理解
递归组件: 组件内部有自己的子组件标签 应用场景: 用于显示树状态结构的界面
12. Vue组件间的通信方式*
- props
- 自定义事件
- 全局事件总线
- v-model
- .sync
- $refs
- $children与$parent
- $attrs与$listeners
- provide与inject
- vuex
- pinia
- v-slot
v-model原理:
<CustomInput v-model='name'/>
// 等价于
<CustomInput :value='name' @input='name = $event'/>
<input type='text' :value='value' @input='$emit('input',$event.target.value)'/>
props: ['value']
// el-ui中表单项相关组件都用到了v-model:Input/Select/checkoutbox
.sync原理:
<child :money.sync='total'/>
// 等价于
<child :money='total' @update:money='total = $event'/>
<button @click='$emit('updata:money',money - 100)'>花钱<button/>
// element-ui在有显示隐藏的组件上: Dialog / Drawer
13. Vuex的五大属性
- state: state 为单一状态树,在 state 中需要定义我们所需要管理的数组、对象、字符串等等
- getters: 类似 Vue.js 的计算属性, 当我们需要从 store 的 state中派生出一些状态,那么我们就需要使用 getter,getter 会接收 state 作为第一个参数,而且 getter 的返回值会根据它的依赖被缓存起来,只有 getter 中的依赖值(state 中的某个需要派生状态的值)发生改变的时候才会被重新计算
- mutations: 更改 store 中 state 状态的唯一方法就是提交 mutation,就很类似事件。每个 mutation 都有一个字符串类型的事件类型和一个回调函数,我们需要改变 state 的值就要在回调函数中改变。我们要执行这个回调函数,那么我们需要执行一个相应的调用方法:store.commit。
- actions: action 可以提交 mutation,在 action 中可以执行 store.commit,而且 action 中可以有任何的异步操作。在页面中如果我们要嗲用这个 action,则需要执行 store.dispatch。
- Module: module 其实只是解决了当 state 中很复杂臃肿的时候,module 可以将 store 分割成模块,每个模块中拥有自己的 state、mutation、action和 getter。
import {reqSearchInfo} from "@/api";
const actions = {
async getSearchInfo(miniStore, searchParams) {
let result = await reqSearchInfo(searchParams);
if (result.code === 200) {
miniStore.commit('SAVE_SEARCH_INFO', result.data);
} else {
alert(result.message);
}
}
};
const mutations = {
SAVE_SEARCH_INFO(state, info) {
state.searchInfo = info;
}
};
const state = {
searchInfo:{}
};
const getters = {
goodsList(state){
return state.searchInfo.goodsList
},
attrsList(state){
return state.searchInfo.attrsList
},
trademarkList(state){
return state.searchInfo.trademarkList
},
total(){
return state.searchInfo.total;
}
};
export default {
actions,
mutations,
state,
getters
};
this.$store.dispatch('getSearchInfo', this.searchParams);
14. Vuex的数据结构图*
15. Vuex的mutation可以执行异步操作吗
不能执行异步任务,异步会导致内部状态难以追踪,devtool难以追踪state状态
16. Vuex状态数据的响应式原理
vuex本质上是将state值绑定到了一个vue对象上(然后说说vue响应式原理watcher)
17. Vue的对象响应式与数组响应式
数组响应式:Vue 将被侦听的数组的变更方法进行了包裹(在Array.prototype的7个方法做了拦截重写),所以它们也将会触发视图更新。方法:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
对象响应式:Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 对于已经创建的实例,可以使用 Vue.set() 方法向嵌套对象添加响应式 property。(说说响应式)
18. new Vue的内部流程
- 初始化(_init方法)
- props、data、生命周期,事件机制的初始化
- 例如data初始化就是创建observer对象,该对象与data绑定,通过Object.defineProperty...
- 模板解析
- 数据变化触发页面重新渲染
- vue会将HTML解析成一个AST描述对象(该对象是通过children和parent链接而成的树形结构,完整地描述了HTML标签的所有信息)
- vue根据AST对象生产render函数
- 先虚后实
- 得到render函数后,vue不会直接渲染DOM树,而是通过render函数得到vNode
- Vue在渲染之前对比vNode(减少Dom操作)
- 根据diff之后的结果,执行真正的dom节点插入更新删除操作,同时触发vue实例的生命周期钩子
- 之后就是观察数据变化,决定是否需要重新渲染页面
19. 虚拟DOM理解及diff算法*
为什么要有Virtual DOM?
- 前端优化性能的秘诀就是减少dom操作,因为变动dom会造成浏览器的回流和重绘
- 对更新的内容进行更新,对于没有改变的内容不做任何处理
- 更好的跨平台,nodejs就没有dom,如果想实现SSR(服务端渲染),那么就需要借助vDom,因为vDom本质就是js对象
new Vue()的的render参数里面:h函数主要用来产生vNode
diff算法?
- 用js对象结构表示dom数的结构,然后用这个树构建真正的dom树,插入到文档中
- 当状态变化时,重新构造一个vNode树,然后把新vNode树和旧的vNode树进行比较,记录两棵树的差异
- 把差异应用到真正的dom树上,视图就更新了
diff算法是通过同层级树节点进行比较而不是对树进行逐层搜索遍历,时间复杂度只有后O(n),很高效。
diff算法是如何判断是同一个vNode呢?
源码是比较了key相同,tag(标签)相同等数据,所以不能用下标作为列表渲染的key
- 只做同层比较,确定要比较的新旧虚拟节点
- 没有key: 依次比较,多出的虚拟DOM, 直接创建新的真实DOM
- 有key: 找同名的key比较,没有找到, 直接创建新的真实DOM
- 比较标签名,如果不同, 直接创建新的真实DOM
- 复用原来对应的真实DOM => 如果数据内容有变化, 更新真实DOM内部内容
- 简单流程: 同层比较 => 比较key => 比较标签名 => 复用
20. 说说数据代理与数据劫持*
数据代理:让程序员更加方便的读取到_data中属性
- new Vue(options)时传入的data,被vue放在了vm上名为_data
- 使用时,需要通过_data.xxx来读取和修改,很麻烦
- 通过数据代理,将_data上的数据循环添加到vm上,页面直接读取
let data = {
a: 1,
b: 2
}
let vm = new Vue({
el: 'app',
data
})
console.log(vm._data === data) // true
// 底层实现
Object.definePrototype(vm, 'a', {
get(){
return _data.a
},
set(value){
_data.a = value
}
})
数据劫持:为了捕获到数据改变,进而重新解析模板
修改vm上的属性,会发生下面的过程:
vm.name='hello'
==> name(proxySetter)
==> vm._data.name='hello'
==> 页面变化
最后一步_vm上数据变化,为什么页面也会同步变化呢?
- 原理:_data上数据Object.definePrototype
- _data上每个数据都有对于的:reactiveSetter、reactiveGetter
let _data = {}
Object.definePrototype(_data, 'name', {
get(){
return 'xxx'
},
set() {
console.log('修改数据,然后重新去解析页面')
}
})
21. 说说Vue的响应式原理(发布订阅模式)*
22. 说说nextTick异步的原理*
23. 说说$set原理*
24. 说说keep-alive原理*
25. history与hash路由的区别及原理
区别:
- history
- 路径无#,刷新会携带路由路径,默认会出现404问题,需要配置返回首页
- 解决:
- 开发环境:脚手架项目本身就配置好
- webpack ==> devServer: { historyApiFallback : true}
- 生产环境:配置nginx
- location / { try_files $uri $uri/ /index.html; # 所有404的请求都返回index页面 }
- 开发环境:脚手架项目本身就配置好
- hash
- 路径有#,刷新不会携带路由路径,请求的总是根路径,返回首页没有404
原理:
- history
- 内部利用的是history对象的pushState()和replaceState() (H5新语法)
- hash
- 内部利用的是location对象的hash语法
- 写hash路径 location.hash = '#/xxx'
- 读hash路径: location.hash
- 监视hash路径的变化: window.onhashchange = () => {}
- 内部利用的是location对象的hash语法
26. 如何实现登录自动跳转要访问路由
- 在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数
if (userInfo.name) {
next()
} else {
// 如果还没有登陆, 强制跳转到login
next('/login?redirect='+to.path) // 携带目标路径的参数数据
}
- 在登陆成功后, 跳转到redirect参数的路由路径上
await this.$store.dispatch('login', {mobile, password})
// 成功了, 跳转到redirect路由 或 首页
const redirect = this.$route.query.redirect
this.$router.replace(redirect || '/')
27. 路由导航守卫的理解与使用*
- 作用:
- 监视路由跳转
- 控制路由跳转
- 应用:
- 跳转页面前,进行用户权限检查限制(如是否已经登录/是否有访问路由权限)
- 分类:
- 全局守卫:针对任意路由跳转
- 全局前置守卫
router.beforeEach((to, from, next) => { })
- 全局后置守卫
router.afterEach((to, from) => {})
- 全局前置守卫
- 路由独享守卫
- 前置守卫
{ path: '/foo', component: Foo, beforeEnter: (to, from, next) => {} },
- 前置守卫
- 组件守卫:只针对当前组件的路由跳转
- 进入:
beforeRouteEnter (to, from, next) { next((comp) => { }) },
- 更新:
beforeRouteUpdate (to, from, next) {}
- 离开:
beforeRouteLeave (to, from, next) {}
- 进入:
- 全局守卫:针对任意路由跳转
28. 比较Pinia与Vuex*
- vuex有五大核心概念,pinia只有三个
- state
- getters
- actions
- mutations(vuex独有)
- modules(vuex独有)
- pinia更好的支持ts
- pinia不再需要注入,导入函数,自动补全,使用方便
- pinia是扁平架构,不具有命名空间,对于每一个store就是一个命名空间
pinia的基本使用
import { defineStore } from "pinia";
export const useUserStore = defineStore("userInfo", {
/**
* 类似于组件data,用来保存全局状态
*/
state: () => {
return {
count: 100,
foo: "bar"
};
},
/**
* 类似于组件的computed,用来封装计算属性,有缓存功能
*/
getters: {
compCount(state) {
console.log('计算属性,有缓存');
return state.count * 10;
}
},
/**
* 类似组件的methods,封装业务逻辑,修改state
*/
actions: {
changeUser(num: number) {
this.count += num;
this.foo = "hello";
}
}
});
import { useUserStore } from "../store";
import { storeToRefs } from "pinia";
const userStore = useUserStore();
const { count, foo, compCount } = storeToRefs(userStore);
const handlerClick = () => {
// 1. 直接修改
// userStore.count++
// userStore.foo = 'hello'
// 2. 通过$patch传入对象修改
// userStore.$patch({
// count: userStore.count + 1
// foo: 'hello'
// });
// 3. 通过$patch传入函数修改
// userStore.$patch((state) => {
// state.count++;
// state.foo = 'hello'
// });
// 4. 调用action修改
userStore.changeUser(10);
};