Vue面试题
v-html 会有XSS风险,会覆盖子组件
computed 有缓存,data不变则不会重新计算
watch 默认不会深度监听,要deep:true开启
watch 监听引用类型,拿不到oldVal,因为指针相同,此时已经指向了新的val
style指令需要驼峰式命名方式写,例如{fontSize: '10px'}
v-if v-else 可使用变量,也可使用===表达式
-
v-if 和 v-show 的区别
v-if false不渲染,适合更新不频繁的,节省性能
v-show false也渲染
v-for 和 v-if 不能一起使用,v-for 比 v-if 优先级高,影响性能
事件的event是原生的event,没有装饰过,事件是挂载到当前元素
v-for 的 key 因为 VDOM 的 diff 算法,通过判断新vnode和旧vnode的key是否相等,从而复用与新节点对应的老节点,节约性能开销
用index做key在diff算法中不起作用,用index拼接其他值会导致节点不能复用,所以用唯一值做key,比如数据id
-
props 和 $emit
props父组件通过子组件属性给子组件传值
$emit子组件绑定父组件方法,在子组件内使用$emit触发父组件的方法
-
Vue组件如何通讯
父子组件事件props $emit
自定义事件 $emit $on
通过vuex
-
组件间通讯-自定义事件(常用兄弟组件通讯)
Vue自有实现,使用EventBus方式(bus总线机制/发布订阅者模式/观察者模式)
import Vue from 'vue' var event = new Vue() export default event
在A组件内使用
event.$emit('onHandle', params)
在B组件内的mounted中绑定自定义事件
event.$on('onHandle', this.handle)
在B组局内beforeDestroy中及时销毁监听事件,否则可能造成内存泄露
event.$off('onHandle', this.handle)
-
组件生命周期 <font color="red">* 必须背会</font>
挂载
beforeCreate->create->beforeMount->mounted
beforeCreate里没有this,实例还未初始化,在数据观测(data observer)和event/watcher事件配置前
create 初始化示例,没有渲染,取不到dom,没有$el,通常初始化某些属性值
beforeMount 挂载开始之前,相关渲染首次被调用
mounted 是渲染完了,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
先父组件create->子组件create->子组件mounted->父组件mounted
更新
beforeUpdate->updated
beforeUpdate 重新渲染和打补丁之前,在这里更改状态不会触发重新渲染
updated 由于数据更改导致的重新渲染和打补丁触发
先父组件beforeUpdate-->子组件beforeUpdate->updated->父组件->updated
销毁
beforeDestroy->Destroyed
beforeDestroy 实例销毁之前,里面可以用this
Destroyed 实例销毁之后,所以东西解绑,移除所以监听,销毁所以子实例,在服务端渲染不会被调用
先父组件beforeDestroy-->子组件beforeDestroy->Destroyed->父组件->Destroyed
-
高级特性
- <font color="red">可以不深入,但必须知道</font>
- <font color="red">熟练基本用法,了解使用场景</font>
- <font color="red">最好能和自己的项目经验结合</font>
-
自定义v-model
v-model会默认利用名为value的prop和名为input的事件,model会避免单选,复选将value特性用于不同目的的冲突
v-model等于
<input type="text" :value="val" @input="val=$event.target.value">
demo:
//父组件 <CustomVModel v-model="name"/> <script> import CustomVModel from './CustomVModel' export default { components: { CustomVModel }, data() { return { name: '' } } } </script> // 子组件内 <template> <!-- 1. input使用了 :value 而不是 v-model 2. change 和 model.even 要对应 3. text 属性要对应 model.prop 和 props.text --> <input type="text" :value="text" @input="$emit('change', $event.target.value)" /> </template> <script> export default { // 改变默认v-model的绑定属性和抛出事件 model: { prop: 'text' // 对应props text, event: 'change' }, props: { text: String, default() { return '' } } } </script>
-
$nextTick、$refs
$nextTick
Vue是异步渲染,$nextTick会在DOM渲染之后被触发
页面渲染会将 data 的修改做整合,多次 data 修改只会渲染一次
$refs 在节点内写ref="dom" 使用this.$refs.dom拿到节点的dom元素
-
slot
基本使用
父组件在子组件内插入节点内容
// 父组件 <SlotDemo> {{user.name}} </SlotDemo> // 子组件 SlotDemo <div> <slot>默认显示</slot> </div>
作用域插槽
父组件在子组件内插入节点内容,使用子组件的数据
// 父组件 <ScopedSlotDemo> <template v-slot="slotProps"> {{slotProps.slotData.name}} </template> </ScopedSlotDemo> // 子组件 ScopedSlotDemo <div> <slot :slotData="user"></slot> </div>
具名插槽
用于定义多个插槽
// 父组件 <NameSlot> <template v-slot:header> <h1>插入 header slot中</h1> </template> <p>插入 main slot中,即未命名的 slot</p> <template v-slot:footer> <p>插入 footer slot中</p> </template> </NameSlot> // 子组件 NameSlot <div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
-
动态、异步组件
动态组件
用法 :is='component-name'
需要根据数据,动态渲染的场景,即组局类型不确定,例如 信息流
<div v-for="item in components" v-key="id"> <component :is="item.name" /> </div> <script> import TextInfoComponent from './TextInfoComponent' import ImageInfoComponent from './TextInfoComponent' export default { components: { TextInfoComponent, ImageInfoComponent }, data() { return { components: [{ id: 1, name: 'TextInfoComponent', },{ id: 2, name: 'ImageInfoComponent', }] } } } </script>
异步组件
import() 函数
按需加载, 异步加载大组件
export default { conponents: { component: () => import('./component') } }
-
keep-alive
缓存组件,频繁切换,不需要重复渲染 例如 tab切换
<keep-alive> <KeepAliveStage></KeepAliveStage> </keep-alive> <script> import KeepAliveStage from './KeepAliveStage' export default { conponents: { KeepAliveStage } } </script>
-
mixin
多组件有相同的逻辑,抽离出来,和主组件混合合并代码
会出现问题
- 变量来源不明确,不利于阅读
- 多mixin会造成命名冲突
- mixin和组件可能出现多对多的关系,复杂度较高
Vue 3 提出的Composition API 旨在解决这些问题
// 定义 ./myMixin.js export default { data() { return {} }, mounted() {}, methots: {} } // 使用 import myMixin from './myMixin' export default { mixins: [myMixin] // 可以多个,自动合并 }
-
Vuex 使用
可能会考察 state 的数据结构设计
+ 基本概念
1. state // 单一状态树
2. getters // 获取属性,计算属性啥的
3. actions // 类似控制器,commit 多个 mutations,这里可以异步操作,其他的不行
4. mutations // 更改状态,必须是同步函数
5. module // 模块化Store
6. plugins //插件,相当mutation的拦截器,提供store.subscribe((mutation, state) => {})
--------------
+ Vue组件内
1. dispatch // 用来触发 actions
2. commit // 用来触发 mutations
3. mapState // 映射 state 的函数,computed 中 ...mapState()
4. mapGetters // 映射 getters 的函数,computed 中 ...mapState()
5. mapActions // 映射 actions 的函数,methods 中 ...mapActions()
6. mapMutations // 映射 mutations 的函数,methods 中 ...mapMutations()
-
Vue-router 使用
路由模式(hash,H5 history),后者需要server支持
mode:‘history’
路由配置(动态路由,懒加载)
:id那种,在$route.params.id取到
-
Vue 原理
-
如何理解MVVM
Model-View-ViewModel 数据模型-视图-视图数据
数据驱动视图
View和Model没有直接关联,ViewMode是给他们提供双向数据绑定的联系,优点是让开发者只需关注业务,不需要手动操作DOM,也不需要关注数据状态同步的问题
-
监听data变化的核心API是什么,怎么深度监听-递归
Vue 2 是通过 Object.defineProperty 的 getter 和 setter,并结合观察者模式实现数据绑定的
能监听属性的get set方法
缺点
用于深度监听要递归到底,一次性计算量大
无法监听新增属性/删除属性
无法原生监听数组,需要特殊处理
Vue 3 是通过 es6的 proxy
优点是之前操作的是对象的属性,现在是操作对象,对js引擎比较友好
缺点是兼容不好,不能用polyfill,不支持低版本浏览器
监听数组-重新定义数组原型链
const oldArrayProperty = Array.prototype // 创建新对象,原型指向oldArrayProperty,在扩展新的方法不会影响原型 const arrProto = Object.create(oldArrayProperty) ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => { arrProto[methodName] = function() { updateView() // 更新视图 oldArrayProperty[methodName].call(this, ...arguments) } })
-
虚拟DOM
vdom (Virtual DOM) 是实现vue的重要基石
用JS模拟DOM结构,计算出最小的变更,操作DOM
数据驱动视图,控制DOM操作
-
diff算法
diff算法是vdom中的核心,最关键的部分
vue2 参考snabbdom,双端比较
核心思路
patch新旧vnode的children
新的没有不管旧的有没有,就移除children
新旧都有,就去对比新旧vnode的children
新有旧没有,就添加children
对比新旧vnode的children,先用四个指针指向新旧nodeChildren的头尾
然后循环指针到中间,循环内进行对比key和sel是否相等
- 旧children的开始 和 新children的开始 对比
- 旧children的结束 和 新children的结束 对比
- 旧children的开始 和 新children的结束 对比
- 旧children的结束 和 新children的开始 对比
- 以上有相同的,就执行处理新旧节点的children,并对应移动指针
- 以上都没找到相同的,则去用新的key看看旧的里面有没有,如果没有则插入,然后移动指针,如果有再判断sel是否相同,不相同就插入,然后移动指针,相同就执行处理新旧节点的children,去掉旧节点,插入新节点,然后移动指针
// children 和 text 是不会共存的,要么是节点要么是文本 // elm 对应的dom元素 // 所有元素都可以有key Vnode = {sel, data, children, text, elm, key} //先创建Vnode h() => Vnode patch(oldVnode, newVnode) => { if 第一个参数不是Vnode 创建一个空的Vnode,关联到这个elm元素 if 判断Vnode是否相同,则去对比(两个Vnode的key 和 sel 都相等) Vnode对比: patchVnode() else 不同的Vnode直接删掉重建 } patchVnode(oldVnode, newVnode) => { 执行 prepatch hook 设置 vnode.elm,把新的 elm 赋值成旧的 elm, 让新的知道更新到了哪个 elm 得到 oldChildren 和 newChildren if 判断newVnode 有没有 children, newVnode.text == undefined (vnode.children 一般有值) if 新旧都有 children 更新children: updateChildren() else if 新 children 有,旧 children 无 (旧 text 有) if 如果就的 text 有值,则清空 text 添加 children else if 新 children 无,旧 children 有 移除 children else 旧 text 有 清空 text 清空 text else vnode.children 没有值,则删除旧的 children 并设置清空 text } // 核心方法 updateChildren(elm, oldChildren, newChildren) => { // 用于循环从四周到中间 定义指针 oldStartIndex, oldEndIndex, newStartIndex, oldStartIndex while(指针到中间停止) { if 判断元素是否是空,操作指针移动 else if oldStart == newStart 开始和开始,判断是否相同(key 和 sel 都相等) patchVnode(), 操作指针移动 else if oldEnd == newEnd 结束和结束,判断是否相同(key 和 sel 都相等) patchVnode(), 操作指针移动 else if oldStart == newEnd 开始和结束,判断是否相同(key 和 sel 都相等) patchVnode(), 操作指针移动 else if oldEnd == newStart 结束和开始,判断是否相同(key 和 sel 都相等) patchVnode(), 操作指针移动 else 以上四个都未命中 // 用新节点 key,判断能否对应 oldChildren 中某个节点的 key 在oldChildren中,用新节点的key去拿节点 if 没拿到没对应上,则 插入新节点, 操作指针移动 else 对应上 if sel 是否相等 插入新节点, 操作指针移动 else key 和 sel 都相等 patchVnode(),去掉旧节点,插入新节点, 操作指针移动 } }
vue3 参考inferno,最长递增子序列
两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列
从头对比找到相同的节点patch,发现不同则跳出
如果1没有patch完,则从后往前找相同的节点patch,发现不同则跳出
如果新节点大于与旧节点,对剩余的都创建新的vnode
如果旧节点大于新节点,对于超出的旧节点全部卸载
-
如果3,4都没有,则是对于不确定的元素(没有patch到相同的vnode)
把没有比较过的新vnode保存在map里
记录已经patch节点的熟练patched,没有经过patch的新节点数量tuBePatched
建立一个数组newIndexToOldIndexMap,每个元素都记录旧节点的索引,这个数组的索引就是新节点的索引
-
遍历旧节点
- 如果toBePatched为0,那么统一卸载旧节点
- 如果旧节点key存在,通过key找到对应新节点的index
- 如果旧节点key不存在,遍历剩下的所有新节点,试图找到新节点对应的index
- 如果没有找到对应的新节点就卸载旧节点
- 如果找到对应的新节点,把旧节点的索引记录在新节点的数组中,如果节点发生移动就记录已经移动了,最后patch新节点
如果发生移动,根据newIndexToOldIndexMap找到最长稳定序列,
如果证明不存在旧节点就创建新节点,对于发生移动的节点进行移动处理
-
-
模版编译
模版编译为 render 函数,执行 render 函数返回 vnode
基于 vnode 再执行 patch 和 diff
使用webpack vue-loader
-
JS的 with 语法
改变 {} 内自由变量的查找规则,当做 obj 属性来查找
如果找不到匹配的 obj 属性,就会报错
with 要慎用,它打破了作用域规则,易读性变差
// with 能改变 {} 内自由变量的查找方式,将 {} 内自由变量,当做 obj 的属性来查找 const obj = {a:100, b:200} console.log(obj.a) console.log(obj.b) console.log(obj.c) // undefined with(obj) { console.log(a) console.log(b) console.log(c) // 会报错 }
-
组件 渲染/更新 过程
> 初次渲染的过程
>
> > 解析模版为 render 函数
> >
> > 触发响应式,监听 data 属性的 getter setter
> >
> > 执行 render 函数,生成 vnode, 执行 patch(elm, vnode)
>
> 更新过程
>
> > 修改 data,触发 setter
> >
> > 重新执行 render 函数,生成 newVnode
> >
> > 执行 patch(vnode, newVnode)
>
> 异步渲染 $nextTick
>
> > 汇总 data 的修改,一次性更新视图
> >
> > 减少 DOM 操作次数,提升性能
-
路由原理
稍微复制的SPA,都需要路由
hash-window.onhashchange
H5 history-history.pushState 和 window.onpopstate
H5 history 需要后端支持
两者选择
toB 推荐hash,简单易用,对 url 规范不敏感
toC 可以考虑选择H5 history,但需要服务端支持
能选择简单的就别用复杂的,要考虑成本和收益
hash 特点
hash 变化会触发网页跳转,即浏览器的前期、后退
hash 变化不会刷新页面,SPA必需的特点
hash 永远不会提交到server端(前端自生自灭)
H5 history
用 url 规范的路由,但跳转时不刷新页面
history.pushState
window.onpopstate
-
面试题
v-show 和 v-if 的区别
v-show 通过 css display 控制显示和隐藏
v-if 是组件真正的渲染和销毁,而不是显示和隐藏
频繁切换显示状态用 v-show,否则用 v-if
为什么 v-for 中用 key
必须用 key,而且不能是 index 和 random
diff 算法中通过 tag 和 key 来判断,是否是 sameNode
减少渲染次数,提升渲染性能
描述 vue 组件的生命周期
单组件生命周期图
父子组件生命周期关系比如 mount 和 update
Vue 组件如何通讯
父子组件 props 和 this.$emit
自定义事件 event.off event.$emit
vuex
描述组件渲染和更新的过程
渲染图
双向数据绑定 v-model 的实现原理
input 元素的 value = this.name
绑定 input 事件 this.name = $event.target.value
data 更新触发 re-render
对 MVVM 的理解
computed 有何特点
缓存,data 不变不会重新计算
提高性能
为何组件 data 必须是一个函数
js 特性导致的,Object 是引用数据类型,会导致同一个属性改一个都变了,是函数的时候才能实例独立不相互影响
ajax 应该放在哪个生命周期
mounted
js 是单线程,ajax 是异步获取数据
放在 mounted 之前没有用,只会让逻辑更加混乱,除非有特殊需求
如何将组件所有的 props 传递给子组件
$props
<Uer v-bind="$props" />
如何自己实现 v-model
多个组件有相同的逻辑,如何抽离
mixin
mixin 的缺点
何时要使用异步组件
加载大组件
路由异步加载
何时需要使用 keep-alive
缓存组件,不需要重复渲染
多个静态 tab 页的切换
优化性能
何时需要使用beforeDestory
解绑自定义事件 event.$off
清楚定时器
解绑自定义的 DOM 事件,如 window scroll 等
什么是作用域插槽
把子组件的数据传给父组件显示
vuex 中 action 和 mutation 有何区别
action 中处理异步,mutation 不可以
mutation 做原子操作
action 可以整合多个 mutation
vue-router 常用的路由模式
hash 默认
H5 history
vue-router 两种模式是怎么实现的
hash window.onhashchange
H5 history window.history.pushState window.history.onpopstate
vue-router 两种模式的区别
hash有#号,能控制浏览器前后跳转,不会提交到server端
H5 history 是规范 url,需要 server 端配合
如何配置 vue-router 异步加载
通过 () => import()
用 vnode 描述一个 DOM 结构
监听 data 变化的核心 API 是什么
Object.defineProperty
有何缺点
深度监听需要递归、监听数组需要特殊处理,无法监听新增和删除的属性,使用vue.set vue.delete
Vue3 Proxy 兼容问题,而且不能 polyfill
Vue 如何监听数组变化
Object.defineProperty 不能监听数组变化
重新定义原型,重新push pop等方法,实现监听
Proxy 可以原生支持监听数组变化
描述响应式原理
监听 data 变化
组件渲染和更新的流程
diff 算法的时间复杂度
O(n) ,在O(n^3)基础上做的调整,只比较同一层级,比较tag不相同直接销毁重建,通过tag和key判断是不是同一组件,是就不重复对比
简述 diff 算法过程
patch(elem, vnode) 和 patch(vnode, newVnode)
patchVnode 和 addVnodes 和 removeVnodes
updateChildren,通过key判断是不是同一节点
vue为何是异步渲染,$nextTick 何用
异步渲染,合并 data 修改,提高性能
$nextTick 在 DOM 更新之后,触发回调
vue 常见性能优化
合理使用 v-show v-if
合理使用 computed
v-for 时加 key,以及避免和 v-if 同时使用
自定义事件,DOM事件及时销毁
合理使用异步组件
合理使用 keep-alive
data 层级不要太深
使用 vue-loader 在开发环境做模版编译(预编译)
webpack 层面的优化
前端通用的性能优化,如图片懒加载
使用ssr
-
Vue 3
Vue3 比 Vue2 有什么优势
性能更好
体积更小
更好的 ts 支持
更好的代码组织
更好的逻辑抽离
更多新功能
Vue3 声明周期
Options API: beforeDestory 改为 beforeUnmount destoryed 改为 unmouted
升级内容
全部用 ts 重写
性能提升,代码量减少
调整部分API
持续更新中...