1. 区别v-if与v-show
- 隐藏: v-if干掉标签, v-show通过样式来隐藏
- 重新显示: v-if需要重新创建标签对象, v-show只需要修改样式显示出来就可以
- v-show重新显示更快, 但隐藏时还占用着内存空间: 以空间换时间
- v-show更适合切换频繁/需要隐藏的DOM结构比较大
- 使用v-if解决模板中初始解析undefined的bug 比如: {{a.b.c}} a初始为一个空对象
2. 为什么v-for与v-if不适合一起使用
- 对遍历的item数据进行限制判断
- 问题: 如果使用v-if, 每个数组元素都会解析指令来判断 ==> 效率低
- 解决: 不使用v-if, 使用计算属性, 过滤产生一个子数组 ==> 效率高
- 根据外部的数据判断
- 问题: 如果在当前标签上用v-if, 执行n次 ==> 效率低
- 解决: 添加一个/在父标签, 使用v-if, 执行1次 ==> 效率高
3. computed与watcher以及method的区别
- computed与watch的区别
- 计算属性必须同步返回计算结果, 而watch中可以在异步操作后更新数据显示
- watch可以深度监视, 计算属性只是监视了使用到的数据
- 选择:
- 如果是根据现在的数据同步计算就可以确定要显示的另一个数据 ==> computed
- 如果涉及到异步操作/深度监视 ==> watch
- 一旦一个数据变化, 我们需要做一系列操作 ===> watch
- computed与method的区别
- 计算属性有缓存, 多次读取显示只计算一次
- method, 多处显示计算多次
4. 说说Vue的常用指令
- v-text
- v-html
- v-show
- v-if / v-else / v-else-if
- v-for
- v-on
- v-bind
- v-model
- v-slot: 插槽
- v-once: 只初始渲染一次, 用于优化更新性能
5. 单个Vue组件的生命周期
- 初始化:
- beforeCreate: 不能通过this读取data数据和调用methods中的方法
- 执行了一些初始化的准备工作
- created: 可以通过this读取data数据和调用methods中的方法
- 编译模板
- beforeMount: 不能通过ref读取到页面中内容
- 挂载编译好的模板, 显示页面
- mounted: 能通过ref读取到页面中内容
- 更新: this.msg += ‘–’
- beforeUpdate (在数据更新后, 界面更新前调用): 读取的数据是最新的, 但页面是老的
- 更新页面
- updated: 读取的数据和页面都是新的
- 死亡: $destroy()
- beforeDestroy: 做一些收尾的工作, 比如: 清除定时器/解绑监听/…
- destroyed
6. Vue父子组件的生命周期顺序
- 初始化:
- beforeCreate
- created
- beforeMount
- –child beforeCreate
- –child created
- –child beforeMount
- –child mounted
- mounted
- 更新:
- beforeUpdate
- –child beforeUpdate
- –child updated
- updated
- 死亡:
- beforeDestroy
- – child beforeDestroy
- – child destroyed
- destroyed
7. 区别组件的钩子函数 actived与mounted
- mounted:初始化执行一次
- activated: 初始化mounted之后 / 每次再回到当前路由
8. 说说对动态组件、缓存组件与异步组件的理解
- 动态组件
- 通过的is属性动态加载一个组件
- is属性初始为A组件名, 加载A组件, 切换为B组件名, 加载B组件
- 缓存组件
- 默认路由组件离开或动态组件被切换, 组件都会立即死亡
- 能让原本要死亡的组件不死亡, 在背后缓存起来, 后面需要时, 直接使用缓存的
- 可以通过include与exclude属性来控制哪些组件要缓存或不缓存
- 异步组件
- 在引入组件时使用import动态引入: const Home = () => import(‘./Home.vue’)
- 组件会被单独打包, 且只有在第一次访问时才会请求加载对应的打包文件 ==> 减小首屏打包文件打小
- 除了组件, 其它模块也可以异步懒加载
9. 说说对递归组件的理解
-
递归组件: 组件内部有自己的子组件标签
-
应用场景: 用于显示树状态结构的界面
-
注意: 递归组件必须有name
-
编码: 实现一个简单的可开关的树状结构界面的 Tree 组件
<template> <ul> <li v-for="(item, index) in data" :key="index" @click.stop="handleClick(item)"> {{item.text}} <Tree2 v-if="item.expend" :data="item.children"/> </li> </ul> </template> <script> export default { // name: 'Tree2', // 作为内部的标签名 props: ['data'], methods: { handleClick (item) { if (item.hasOwnProperty('expend')) { item.expend = !item.expend } else { this.$set(item, 'expend', true) } } } } </script>
10. Vue组件间有哪些通信方式
-
根据组件间关系分类
- 父向子
props(非函数)
v-model
$refs, $children
插槽 - 子向父
props(函数)
vue自定义事件
v-model
.sync
$parent
作用域插槽 - 祖孙间
a t t r s 与 attrs与 attrs与listeners 与v-bind/v-on配合使用
provide与inject - 兄弟或其它/任意
全局事件总线
Vuex
- 父向子
-
另一种分类方式
- 属性相关
props
v-model
a t t r s 与 attrs与 attrs与listeners
作用域插槽(子向父传递)
- 自定义事件相关
自定义事件
全局事件总线
v-model
.sync
- 其它
$refs, $children, $parent
provide与inject
插槽
vuex
11. 子向父通信有哪些方式
- props(函数)
- vue自定义事件
- v-model
- .sync
- $parent
- 作用域插槽
12. 说说Vue的自定义事件与全局事件总线
-
vue自定义事件
-
实现子组件向父组件通信
-
相关语法:
-
父组件中绑定自定义事件监听:
<Child @eventName=“callback($event)”>
child.$on(‘eventName’, callback)
-
子组件中分发事件
this.$emit(‘eventName’, 2)
-
-
应用:
elment-ui的组件的事件监听语法都用的是自定义事件 <el-button @click=“test”>
我们项目中的组件也用了不少自定义事件
-
-
全局事件总线
- 实现任意组件间通信
- 编码
将入口js中的vm作为全局事件总线对象:
beforeCreate() {
Vue.prototype.KaTeX parse error: Expected 'EOF', got '}' at position 20: …= this }̲ 传递数据的组件分发事…bus. e m i t ( ′ e v e n t N a m e ′ , d a t a ) 接收数据的组件处理监听 : t h i s . emit('eventName', data) 接收数据的组件处理监听: this. emit(′eventName′,data)接收数据的组件处理监听:this.bus.$on(‘eventName’, (data) => {}) - 应用:
前台项目中使用全局事件总线 - 理解: 为什么将vm放到Vue的原型对象上, 所有组件都可见呢?
- VueComponent.prototype = Object.create(Vue.prototype);
- VC的原型对象的原型对象就是Vue的原型对象
13. Vue响应式数据原理
-
简洁表达
-
对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布
-
数组: 重写更新数组元素的一系列方法 + 订阅-发布
- 调用原生的对应对数组元素进行相应的操作
- 更新界面去
-
-
详细表达(主要说对象的)
-
初始化
- 实现数据代理
- 通过defineproperty给vm定义与data中属性对应的带getter/setter的属性
- 在getter中, 读取data中对应的属性值返回 => 读取this.msg => 读取的是data中msg属性值
- 在setter中, 将最新值保存到data对应的属性上 => this.msg = ‘abc’ => 'abc’会保存到data的msg上
- 创建observer
- 目标: 对data中所有层次的属性进行监视/劫持
- 通过defineproperty给data中所有层次属性, 都重新定义, 加上getter与setter
- getter: 用来建立dep与watcher的关系
- setter: 用来当data数据发生改变去更新界面
- 为data中所有层次的属性创建一个对应的dep ==> 用来将来更新界面的
- 创建compile
- 目标1: 实现界面的初始化显示 / 目标2: 为将更新做准备
- 为模板中每个包含表达式(事件表达式除外)的节点创建一个对应的watcher
- 给watcher绑定用于更新对应节点的回调函数
- 将watcher添加到n个对应的dep中
- 目标1: 实现界面的初始化显示 / 目标2: 为将更新做准备
- 实现数据代理
-
更新
- this.msg = ‘abc’
- 由于有数据代理 ==> data的msg更新为了’abc’
- 由于有数据劫持 ==> data中msg的setter调用了
- 在setter中, 通过对应的dep去通知所对应的watcher去更新对应的节点 ==> 使用了订阅发布模式
-
-
Vue数据响应式原理结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vhB1MyRF-1676984728734)(images/image-20220225180944565.png)]
14. Vue双向数据绑定原理
- 通过v-model来实现双向数据绑定
- v-model的本质
- 将动态的data数据通过value属性传给input显示 ==> data到view的绑定
- 给input标签绑定input监听, 一旦输入改变读取最新的值保存到data对应的属性上 ==> view到data的绑定
- 双向数据绑定是在单向数据绑定(data–>view)的基础, 加入input事件监听(view ==> data)
15. s e t 和 set和 set和nextTick的使用场景
- 项目功能: 列表项点击动态显示输入框, 并自动获取焦点
- 编码实现:
- 动态给列表项数据对象添加edit属性为true ==> 标识显示输入框
- 获取当前input对象, 调用focus() ==> 获取焦点
- 问题
- 向响应式对象上直接点添加属性不是响应式 ==> 输入框不会显示 ==> 使用$set添加edit属性
- 得不到input, 调用focus会报错 ==> 因为界面还没有更新, 还没有input ==> 使用$nextTick指定在DOM更新后才去执行回调, 在回调中获取input
16. 区别MVVM与MVC
- MVVM: 前台的技术
- M: Model模型, 也就是包含数据的js对象 ==> data对象
- V: View视图,动态显示模型对象中的数据的页面(前台渲染) ==> 模板页面
- VM: ViewModel视图模型, 通过vm读取model中的数据显示到view上, 同时view输入数据改变, vm也可以将输入数据保存到model中 ==> Vue/组件的实例
- MVC: 后台的技术
- M: Model(模型)包含从数据库中查询得到的数据的对象
- V: View(视图)动态显示模型对象中的数据的页面(后台渲染)
- C: Controller(控制器)接收用户提交的请求参数, 操作数据库生成动态数据并产生模型对象
17. Vue.use()做了什么
- 对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
- 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)
18. 说说Vue的mixin技术
- 用来复用多个组件中相同的js代码的技术
- 将多个组件相同的js代码提取出来, 定义在一个mixin中配置对象
- 在多个组件中通过mixins配置引入mixin中的代码, 会自动合并到当前组件的配置中
19. Vue的组件data为什么必须是一个函数
- 同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同)
- 如果是data是对象, 组件的多个实例共用一个data对象
- 如果是函数, 组件对象通过调用函数得到的一个新的data对象
20. vuex的5大属性
- state
- mutations
- actions
- getters
- modules
- namespaced: true
21. vuex的数据流结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SY3eoPoZ-1676984728736)(images/vuex.png)]
22. vuex中的mutation可以执行异步操作吗?
- 功能可以 ==> 异步更新数据后界面确实会自动更新
- 问题 ==> vuex的调试工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对)
- 扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在mutation中同步更新state, 才能被记录到
23. vuex中的状态数据的响应式的原理?
-
创建了一个vm对象
-
state中的数据都是vm的data数据(是响应式的)
-
组件中读取的state数据本质读取的就是data中的数据
-
一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新
new Vue({ data: { home: { categoryList: [], xxx: {} }, user: { userInfo: {} } } })
24. vuex数据刷新丢失的问题
- 绑定事件监听: 在卸载前保存当前数据
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
sessionStorage.setItem('CART_LIST_KEY',
JSON.stringify(this.$store.state.shopCart.cartList))
})
window.removeEventListener('beforeunload')
- 在初始时读取保存数据作为状态的初始值
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],
25. 路由组件间通信方式
- query参数
- params参数
- props(需要配置, 而不是标签属性)
- true ==> 将路由的params参数映射成props ==> 只能传递param参数
- 对象 ==> 将对象中的属性映射成props ==> 只能传递自定义的参数
- route => {} ==> 将函数返回的对象中的属性映射成props ==> 能会传递params和query参数和自定义的
- meta
- vuex
26. 跳转携带的参数, 刷新就丢失了
如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失
因为url中没有携带的参数数据路径
27. 编程式路由跳转到当前路由, 参数不变, 会报出错误?
-
前一个项目没这个问题, 后一个项目有问题
-
3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题
- 3.1.0之前: 返回值为undefined
- push(location)
- push(location, () => {}, () => {})
- 3.1.0之后: 如果没有指定回调函数返回promise对象
- push(location).then(() => {}).catch(() => {})
- vue-router在3.1.0版本(2019.8)引入了push()的promise的语法, 如果没有通过参数指定回调函数就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径和参数都没有变化, 会抛出一个失败的promise
- 说明文档: https://github.com/vuejs/vue-router/releases?after=v3.3.1
- 3.1.0之前: 返回值为undefined
-
解决:
-
办法1: 在每次push时指定回调函数或catch错误
push('/xxx', () => {}) ===> push('/xxx').catch(() => {})
-
办法2: 重写VueRouter原型上的push方法 (比较好)
- 1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的promise
- 2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以
const originPush = VueRouter.prototype.push VueRouter.prototype.push = function (location, onComplete, onAbort) { console.log('push()', onComplete, onAbort) // 判断如果没有指定回调函数, 通过call调用源函数并使用catch来处理错误 if (onComplete===undefined && onAbort===undefined) { // 使用的新语法 return originPush.call(this, location).catch(() => {}) } else { // 如果有指定任意回调函数, 通过call调用源push函数处理 return originPush.call(this, location, onComplete, onAbort) } }
-
-
扩展问题
声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数
28. history与hash路由的区别和原理
-
区别:
-
history: 路由路径不#, 刷新会携带路由路径, 默认会出404问题, 需要配置返回首页
-
404:
- history有: 刷新请求时会携带前台路由路径, 没有对应的资源返回
- hash没有: 刷新请求时不会携带#路由路径
-
解决:
-
开发环境: 如果是脚手架项目本身就配置好
==> webpack ==> devServer: {
historyApiFallback
: true}当使用 HTML5 History API 时, 所有的
404
请求都会响应index.html
的内容
-
-
生产环境打包运行:
-
配置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 = () => {}
29. 如何实现登陆后, 自动跳转到前面要访问的路由界面
-
在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的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 || '/')
30. 路由导航守卫的理解和使用
导航守卫是什么?
- 导航守卫是vue-router提供的下面2个方面的功能
- 监视路由跳转 -->回调函数
- 控制路由跳转 --> 放行/不放行/强制跳转到指定位置 next(path)
- 应用
- 在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限)
- 在跳转到登陆界面前, 判断用户没有登陆才显示
导航守卫分类
-
全局守卫: 针对任意路由跳转
-
全局前置守卫
router.beforeEach((to, from, next) => { // ... })
-
全局后置守卫
router.afterEach((to, from) => {})
-
-
路由独享守卫
-
前置守卫
{ path: '/foo', component: Foo, beforeEnter: (to, from, next) => {} }, { path: '/ff', component: Foo, },
-
-
组件守卫: 只针对当前组件的路由跳转
-
进入
beforeRouteEnter (to, from, next) { // 不能使用this, this是undefined next((comp) => { // 延迟到组件对象创建后才执行 // comp就是组件对象 }) },
-
-
更新:
beforeRouteUpdate (to, from, next) {}
-
离开
beforeRouteLeave (to, from, next) {}
31. 请说明key的作用和原理
1. 虚拟DOM的key的作用?
1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用
2). 详细的说: 当列表数组中的数据发生变化生成新的虚拟DOM后, 进行新旧虚拟DOM的diff比较
a. 有一个对应的key
item数据没变, 直接使用原来的真实DOM
item数据变了, 对原来的真实DOM进行数据更新
b. 没有一个对应的key
根据item数据创建新的真实DOM显示
2. key为index的问题
1). 添加/删除/排序 => 产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
2). 如果item界面还有输入框 => 产生错误的真实DOM复用 ==> 不仅效率低, 界面效果也有问题
注意: 如果不存在添加/删除/排序操作, 用index没有问题
3. 解决:
使用item数据的标识数据作为key, 比如id属性值
32. 理解Vue的虚拟DOM
一个较轻的普通JS对象 ==> 真实DOM是一个较重的对象 => 轻重看对象内部属性多少
虚拟DOM对象有时也称为虚拟节点对象(vNode), 它包含了用于生成一个真实DOM/Node的必要信息, 比如:
标签结构:
<ul id="list" name="xxx">
<li key="1">abc1</li>
<li key="2">abc2</li>
</ul>
虚拟DOM/节点
{
tag: 'ul',
props: {
id: 'list',
name: 'xxx'
}
children: [
{tag: 'li', props: {}, key: 1, children: ['abc1']},
{tag: 'li', props: {}, key: 2, children: ['abc2']}
],
key: undefined
}
33. 说说Vue的 Diff算法
目标:
比较的结果是要确定: 哪些原来真实DOM可以复用(但内部内容可能要更新), 要创建哪些真实DOM
处理流程
只做同层比较 => 虚拟DOM也是一个倒立树状态结构, 只进行同层比较, 这样比较次数少,效率高
确定要比较的新旧虚拟节点
有相同的key, 确定为对应的虚拟节点
没有相同的key, 直接创建一个新的真实DOM
key如果没有指定, 值为undefined, 按顺序对应比较
先比较标签名
如果不同, 直接创建新的真实DOM
如果相同, 复用原来对应的真实DOM => 如果数据内容有变化, 更新真实DOM内部内容
简单流程: 同层比较 => 比较key => 比较标签名 => 复用
34. nextTick的作用和原理
是什么?
官方描述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
在数据更新后, 调用nextTick(callback),在callback中才可以读取到更新后的DOM
使用场景?
更新数据后, 想操作更新后的DOM
项目功能1: 使用swiper实现动态轮播
项目功能2: 动态显示input输入框后, 需要自动获取焦点
原理?
更新数据后, DOM不会立即更新, 而是在所有数据都变化完后, 将DOM更新作为一个异步任务统一执行
理解Tick: 取出队列中的一个回调任务到调用栈中执行就是一个tick, 有时也指定队列中的一个回调任务
nextTick(callback): 将callback指定为下一个异步回调任务执行
为什么要先更新数据再调用nextTick()呢?
第一个数据更新才触发将DOM更新放入任务队列
这样nextTick指定的callback就是放在DOM更新任务之后
这样callback中才能读取到更新后的DOM
nextTick中用的是哪个异步技术呢?
简单说: 优先使用微任务, 如果不支持才选择宏任务
详细说: promise => MutationObserver => setImmediate => setTimeout