mvvm模式的理解
MVVM 是一种设计思想,MVVM可拆解为 M(数据层) V(视图层) VM(视图数据同步层) 利用数据驱动UI改变,UI改变也会同步到数据层面,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM从而提高开发效率。
v-if和v-show区别
-
v-show 只是简单的控制元素的 display 属性,而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件 为假,元素会被销毁);
-
v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多;
-
v-if 有更高的切换开销,v-show 切换开销小;
-
v-if 有配套的 v-else-if 和 v-else,而 v-show 没有
-
v-if 可以搭配 template 使用,而 v-show 不行
-
如果需要非常频繁地切换,则使用 v-show 较好
Vue组件之间都有哪些通信方式?
因为Vue的优点之一就是组件化开发,采用组件通信是为了解决组件间数据传递问题。组件通信大致可分为以下5类
父组件向子组件通信
props传递(子组件可给定默认值、类型、校验规则)工作中常在封装自定义组件或使用三方组件时常用provide+inject工作中常使用在路由开启缓存keepalive后想要刷新当前缓存额组件时采用(reload)<slot>(插槽传递的是html结构)工作中常在封装自定义组件时使用$children(返回的是一个子组件实例数组,多个情况下返回顺序不确定,Vue3已经废弃)
子组件向父组件通信
- 自定义事件
$emit工作中常在封装自定义组件或使用三方组件时常用 ref(父组件给子组件绑定ref,通过$refs拿到子组件实例对象获取对应的数据)$parent(子组件绑定ref,通过$parent拿到父组件实例对象【离它最近一层的父组件】)props(父组件通过props传递函数,子组件中调用该函数,父组件的函数中就可以拿到对应的数据)
父子组件双向同步数据
Vue2中的: v-model 、 .sync
Vue3中的:v-model、v-model:test
兄弟组件间传递
eventBus全局事件总线 (Vue3弃用,需要借助三方插件mitt)
原理:
因为Vue实例上存在有$emit 、$on、 $off、事件订阅发布的三种方法,所以为了统一管理采用在Vue的显示原型prototype上绑定一个公共属性$bus,该属性的值为Vue的实例对象。从而采用其$emit 、$on、 $off、三种方法进行数据传递。
实现:
绑定$bus
组件1进行事件订阅
组件2进行事件发布
Vuex
跨层级间传递
provide+inject(推荐使用)Props层层传递(不推荐使用,容易造成数据混乱)Vuex
v-if和v-for哪个优先级更高?
- Vue2中v-for优先级高于v-if
- Vue3 中 v-if 优先级高于v-for
v-if和v-for一起使用的问题:
开发中要避免将两者写在一起(逻辑没问题时只是单纯影响性能),开发中常在一个循环列表根据某个条件展示对应列表时常犯该错误。
解决:
- 采用计算属性获取到真正渲染的数据【常用】
- 外层包裹容器标签先判断再循环
- 采用
<template></template>包裹判断使用
- 采用 v-for + v-show一起使用
简述Vue的生命周期以及每个阶段做的事?
因为Vue采用组件化开发思想,所以每个组件会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新dom。这个过程就是生命周期。Vue生命周期总共可以分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期。vue3中新增了三个用于调试和服务端渲染场景。
| 生命周期Vue2 | 生命周期Vue3 | 描述 |
|---|---|---|
| beforeCreate | beforeCreate | 组件实例被创建之初 |
| created | created | 组件实例已经完全创建 |
| beforeMount | beforeMount | 组件挂载之前 |
| mounted | mounted | 组件挂载到实例上去之后 |
| beforeUpdate | beforeUpdate | 组件数据发生变化,更新之前 |
| updated | updated | 数据数据更新之后 |
| beforeDestroy | beforeUnmount | 组件实例销毁之前 |
| destroyed | unmounted | 组件实例销毁之后 |
| activated | activated | keep-alive 缓存的组件激活时 |
| deactivated | deactivated | keep-alive 缓存的组件停用时调用 |
| errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
| - | renderTracked | 调试钩子,响应式依赖被收集时调用 |
| - | renderTriggered | 调试钩子,响应式依赖被触发时调用 |
| - | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用 |
工作中钩子的使用:
- beforeCreate:通常用于插件开发中执行一些初始化任务(没有 this 还在初始化 data methods 等)
- created:组件初始化完毕,可以访问各种数据,获取接口数据等
- mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。
- beforeUpdate:此时view层还未更新,可用于获取更新前各种状态
- updated:完成view层的更新,更新后,所有状态已是最新
- beforeUnmount:实例被销毁前调用,可用于一些定时器或订阅的取消
- unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
Vue3中的区别:
- setup和created谁先执行?
setup
- setup中为什么没有beforeCreate和created?
setup最先执行,此时组件实例在setup内部已经创建,所以created的处理对于setup来讲明显在后面,对于开发者来说已经没有意义, 所以setup中没必要再使用beforeCreate和created。
v-model双向绑定使用和原理
v-model是vue双向数据绑定指令
- Vue2单向数据绑定
:value和 自定义事件@input的语法糖。【可以通过配置 model 属性来修改其 单向数据绑定的值和事件】
使用默认语法糖封装
<template>
<div>
<el-input v-model="text"></el-input>
</div>
</template>
<script>
export default {
props:{
value:{
type:String,
default:''
}
},
data(){
return {
text:''
}
},
watch:{
value:{
immediate:true,
handler(n,o){
this.text = n
}
},
text(){
this.$emit('input',this.text)
}
},
};
</script>
使用自定义事件和属性名封装
<template>
<div>
<el-input v-model="text"></el-input>
</div>
</template>
<script>
export default {
model:{
prop:'test',
event:'changeTest'
},
props:{
test:{
type:String,
default:''
}
},
data(){
return {
text:''
}
},
watch:{
test:{
immediate:true,
handler(n,o){
this.text = n
}
},
text(){
this.$emit('changeTest',this.text)
}
},
};
</script>
- Vue3单向数据绑定:
modelValue和自定义事件$emit('update:modelValue')的语法糖
使用默认语法糖封装
<template>
<el-input v-model="input" placeholder="Please input" />
</template>
<script setup>
import { reactive, ref} from "@vue/reactivity";
import { watch ,} from "@vue/runtime-core";
const { modelValue } = defineProps({ // 结构出来的数据不具有响应式
modelValue: {
type: String,
default: "",
},
});
// 定义语法糖Emit事件
const emit = defineEmits(['update:modelValue'])
const input = ref("");
// 根据props绑定组件传入的value
watch(
() => modelValue, // 利用getter方法将其变成响应式(不是响应式则新旧值都为undefined)
(n, o) => {
input.value = n
},
{ immediate: true }
);
// 根据输入的改变向外层传递数据
watch(
input,
(n,o)=>{
emit('update:modelValue',n)
}
)
</script>
<style scoped>
</style>
<HelloWorld v-model="text" />
使用自定义事件和属性名封装
<template>
<el-input v-model="input" placeholder="Please input" />
</template>
<script setup>
import { reactive, ref} from "@vue/reactivity";
import { watch ,} from "@vue/runtime-core";
const { test } = defineProps({ // 结构出来的数据不具有响应式
test: {
type: String,
default: "",
},
});
// 定义语法糖Emit事件
const emit = defineEmits(['update:test'])
const input = ref("");
// 根据props绑定组件传入的value
watch(
() => test, // 利用getter方法将其变成响应式(不是响应式则新旧值都为undefined)
(n, o) => {
input.value = n
},
{ immediate: true }
);
// 根据输入的改变向外层传递数据
watch(
input,
(n,o)=>{
emit('update:test',n)
}
)
</script>
<style scoped>
</style>
<HelloWorld v-model:test="text" />
说说nextTick的使用和原理?
提供的原因:
nextTick是Vue全局提供的API,由于Vue异步更新策略导致我们数据修改时不会立即更新DOM(diff算法等)
什么时候用:
当我们想要第一时间获取更新后的DOM时就要用这个方法。【如弹窗开启销毁操作,且打开弹窗后自动聚焦到input输入框上】
实现原理
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。会拿到我们传入的回调函数,利用Promise让其成为异步操作
watch和computed的区别以及选择?
- computed 会根据依赖的数据返回一个新值,对依赖的数据具有缓存作用只有变化了才重新计算。一般常用于模板中出现太多逻辑会使模板变得臃肿不易维护,那就抽离一个新的值进行操作。
- watch模板中出现太多逻辑会使模板变得臃肿不易维护 监视当前已有的值,根据当前值的变化进行操作,没有缓存。
怎么缓存当前的组件?缓存后怎么更新?
-
方法: 缓存组件使用keep-alive组件
-
强制更新缓存组件:
1、利用 privide + inject 书写 reload 方法
<keep-alive>
<router-view v-if="refresh"></router-view>
</keep-alive>
<script setup>
import {ref} from "@vue/reactivity";
import { nextTick} from "@vue/runtime-core";
const refresh = ref(true)
const reload = () => {
refresh.value = false
nextTick(() => {
refresh.value = true
})
}
</script>
2、利用动态绑定不同的key值
<HelloWorld :key="key" />
<script setup>
import {ref} from "@vue/reactivity";
import { nextTick} from "@vue/runtime-core";
const key = ref(0)
const reload = () => {
key.value++
}
</script>
你写过自定义指令吗?使用场景有哪些?
使用自定义指令分为定义、注册和使用三步
- 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted和updated时执行
- 注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册
- 使用时在注册名称前加上v-即可,比如v-focus
项目中常用到一些自定义指令,例如:
- 复制粘贴 v-copy
- 长按 v-longpress
- 防抖 v-debounce
- 图片懒加载 v-lazy
- 按钮权限 v-premission
- 页面水印 v-waterMarker
- 拖拽指令 v-draggable
自定义指令➕Vuex 实现按钮权限控制
- 根据后端USf返回的权限列表获取全部的code码数组放在vuex中
- 书写自定义指令
- 根据自定义指令传入的code码和vuex中的做对比然后进行展示隐藏
// 注册一个全局自定义指令 v-author
Vue.directive('author', {
// 当绑定元素插入到 DOM 中。
inserted: function (el, binding, vnode) {
const codeList = [100,200,300] // 模拟后端返回的数据
const {value} = binding
if(!codeList.includes(value)) {
el.remove()
}
},
})
<template>
<div id="app">
<el-button v-author="code['编辑']">点击</el-button>
</div>
</template>
<script>
const CODE = {
'编辑':100
}
export default {
name: "App",
data() {
return {
code:CODE
};
},
methods: {
},
};
</script>
说下$attrs和$listeners的使用场景
常在基于三方组件进行二次封装时使用,相当于批量绑定属性和方法
$attrs:只能和v-bind使用不可简写
$listeners:只能和v-on使用不可简写
<template>
<div>
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
</div>
</template>
<script>
export default {
};
</script>
<HelloWorld placeholder="测试" @change="change"></HelloWorld>
v-once的使用场景有哪些?
v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。- 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用
v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。 - 我们只需要作用的组件或元素上加上v-once即可。
- vue3.2之后,又增加了
v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。 - 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
什么是递归组件?举个例子说明下?
- 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
- 实际开发中类似Tree、Menu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
- 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件
name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent,这样实际获取的组件就是当前组件本身。
- tree Item 组件
<template>
<li>
<!-- 点击折叠展开 -->
<div @click="toggle">
<!-- 显示内容 -->
{{model.title}}
<!-- 显示折叠展开的图标,如果没有下级目录的话,则不显示 -->
<span v-if="isFolder">[{{open?'-':'+'}}]</span>
</div>
<!-- 控制是否显示下级目录 -->
<ul v-show="open" v-if="isFolder">
<!-- 重点代码,调用自身,实现递归,绑定数据 -->
<Item v-for="model in model.children" :model="model" :key="model.title"></Item>
</ul>
</li>
</template>
<script>
export default {
name: "Item",
// 如果想使用此组件,则需要传递的数据
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
// 默认不显示下级目录
open: false
};
},
computed: {
// 控制是否有下级目录和显示下级目录
isFolder() {
return this.model.children && this.model.children.length;
}
},
methods: {
// 点击折叠展开的方法
toggle() {
if (this.isFolder) {
this.open = !this.open;
}
}
}
};
</script>
<template>
<div>
<ul>
<!-- 使用这个组件,并绑定数据 -->
<Item :model="treeData"></Item>
</ul>
</div>
</template>
<script>
// 导入递归组件
import Item from "./views/Item";
export default {
name: "App",
data() {
return {
// 需要实现递归的数据,上面已经给出
treeData: {
title: "Web全栈架构师",
children: [
{
title: "Java架构师"
},
{
title: "JS高级",
children: [
{
title: "ES6"
},
{
title: "动效"
}
]
},
{
title: "Web全栈",
children: [
{
title: "Vue训练营",
expand: true,
children: [
{
title: "组件化"
},
{
title: "源码"
},
{
title: "docker部署"
}
]
},
{
title: "React",
children: [
{
title: "JSX"
},
{
title: "虚拟DOM"
}
]
},
{
title: "Node"
}
]
}
]
}
};
},
components: {
// 注册组件
Item
}
};
</script>
你知道哪些vue3新特性
- api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
- 另外,Vue3.0在框架层面也有很多亮眼的改进:
-
更快
- 虚拟DOM重写
- 编译器优化:静态提升、patchFlags、block等
- 基于Proxy的响应式系统
-
更小:更好的摇树优化
-
更容易维护:TypeScript + 模块化
-
更容易扩展
- 独立的响应化模块
- 自定义渲染器
ref和reactive异同
-
ref()可用于任何数据类型
ref()加工的数据会生成一个新的引用数据对象,即响应式数据。内部通过判断数据类型对象的动态给定响应式的方法,其中基本数据类型则采用
Object.defineProperty()复合数据类型则采用Proxy代理进行处理。但是要修改ref()生成的新的响应式数据,需要修改它的.value属性值。 -
reactive()只能用于复合数据类型,采用
Proxy代理进行处理可以直接修改。
watch和watchEffect异同
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。- 从实现上来说,
watchEffect(fn)相当于watch(fn,fn,{immediate:true})
Vue3.0 性能提升主要是通过哪几方面体现的?
- 代码层面:采用全选式的API基于Prox代理,使其初始化时间和占用内存均大幅改进
- 编译层面:更多的编辑优化处理,如静态提升、动态内容标记、事件缓存、区块等可以有效跳过diff过程
- 打包时更好的支持 tree-shaking 因此整体体积更更小,加载更快。
Composition API和Options API有何不同?
Composition API是一组API,包含了 reactiveAPI、生命周期钩子等,主要模仿了react的hooks,使用时用户可以通过导入函数的方式书写组件,使其复用性、可读性更强。Options API则是通过声明式选项的对象形式编写组件比较固定化、代码比较臃肿。Composition API中的hooks解决了mixins中的命名冲突、来源不明确的问题。Composition API对TS更友好
你如果想要扩展某个Vue组件时会怎么做?
- mixins
- slots
- extends
子组件可以直接改变父组件的数据么,说明原因
组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题。 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告【但不影响使用】。实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更
Vue性能优化方法?
- 路由懒加载
keep-alive缓存页面- 使用
v-show复用DOM:避免重复创建组件 - 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
- 图片懒加载
- 第三方插件按需引入
- 服务端渲染
SPA、SSR的区别是什么?
- SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。
- SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。
- SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题。
vue项目中的错误处理的步骤?
1、区分错误类型,看是接口错误还是页面错误 2、页面错误查看报错信息定位报错界面文件 3、分块注释代码逐渐找出错误 4、debuge和结合devtoos
说一说你对vue响应式理解?
- 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
- mvvm框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
- Vue通过数据响应式加上虚拟DOM和diff算法,可以使我们只需要操作数据,完全不用接触繁琐的dom操作,从而大大提升开发效率,降低开发难度。
- vue2中的数据响应式采用
Object.defineProperty()的方式定义数据拦截,在Vue初始化时会先去读取data中的数据,递归给当前配置的对象添加get/set方法。其中包含创建dep和watch方法,根据消息订阅与发布的原理来通过视图或数据是否更新。当视图更新时,他会利用一套正则解析算法,拿到更新的数据,并生成虚拟DOM再利用Diff算法去判断是否转成真实DOM。当数据更新时则是同通知视图去发生改变。 Object.defineProperty()的方式定义数据拦截不能检测到数组的变化,所以Vue内部重写了数组方法,而且Object.defineProperty()检查不到新增或删除的属性,所以出现了Vue.set``Vue.delete这样特殊的api来解决这一个bug。所有Vue3则重写了这一代理机制,利用proxy方法实现,该方法则可以监测到增删改查的操作。另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,我们甚至不需要引入vue都可以体验。
说说你对虚拟 DOM 的理解?
- 虚拟dom它本身就是一个
JavaScript对象,只不过它是通过不同的属性去描述一个视图结构 - 通过引入vdom将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能。
- 通过引入vdom方便实现跨平台,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等。 Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
你了解vue中的diff算法吗?
-
虚拟DOM要想转化为真实DOM就需要通过pVue中的diff算法转换
-
Vue中diff通过传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。
-
diff过程是一个递归过程,遵循深度优先、同层比较的策略:
1、首先判断两个节点是否为相同同类节点,不同则删除重新创建
2、如果双方都是文本则更新文本内容
3、如果双方都是元素节点则递归更新子元素,同时更新元素属性
4、更新子节点时又分了几种情况:
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
你知道key的作用吗?
- key的作用主要是为了更高效的更新虚拟DOM。
- vue在diff过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,影响性能。
- key尽量使用唯一id,展示性的列表可以使用index,但是操作性的输入框,则不可以用index,因为在对列表进行操作时会因为数组的key值不变但元素发生改变,造成内容错乱。
说说从 template 到 render 处理过程
在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们成为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。
Vue实例挂载的过程中发生了什么?
- 挂载过程指的是app.mount()过程,这个是个初始化过程,整体上做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态、创建各种响应式数据
- 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。