简述MVVM
什么是MVVM
首先是 MVC ,MVC 是最常见的软件架构之一,在这个架构中,它将软件分为了3个部分:View、Controller、Model,各部分的通信方式如下: View 将用户指定发送到 Controller,Controller 完成业务逻辑后,让 Model 改变状态,Model 改变状态后将新数据传给 View,整个的过程都是单向通信。 随着业务量的扩大,Controller 需要处理各种逻辑,维护成本越来越高,也就衍生出了一个新的软件架构,即 MVVM。
MVVM 就是把 MVC 中的 Controller 的数据和逻辑处理部分从中抽离出来,用一个专门的对象去管理,即 ViewModel,是 Model 和 Controller 之间的一座桥梁。 MVVM 架构也是将软件分为了3个部分:View、ViewModel、Model,各部分通信方式如下: View 与 ViewModel 双向绑定,View 的变动自动绑定在 ViewModel 中,然后 ViewModel 与 Model 进行双向通信,Model 就不与 View 直接进行通信。
Vue 生命周期
vue 生命周期就是 vue 实例从创建到销毁的过程,可以将此过程分为四个阶段:
创建阶段,此阶段有 beforeCreate 和 created 两个钩子函数: beforeCreate 是new Vue()之后触发的第一个钩子,发生在实例创建之前,此时 data,methods 等数据和方法都还没初始化,还不能访问; created 发生在实例创建之后,此时的 data 和 methods 已经初始化好了,可以进行调用或者操作,可以进行异步数据请求,例如数据初始化。
挂载阶段,此阶段有 beforeMount 和 mounted 两个钩子函数: beforeMount 发生发生在挂载前,此时虚拟 Dom 已创建完成,可以对数据进行更改但是不会渲染到页面上; mounted 发生在挂载后,此时真实的 Dom 挂载完毕,可以访问到 DOM 节点了,vue实例也完成了初始化,此时用户可以在浏览器上看到页面内容了。
运行阶段,此阶段有 beforeUpdate 和 updated 两个钩子函数: beforeUpdate 发生在状态或者数据更新前,此时 data 中的数据状态已经发生变化了,但还没开始渲染 dom 树 updated 发生在更新后,此时 dom 节点被重新渲染了,页面显示的也是更新后的数据。但是要避免在这个阶段进行更改数据,可能会陷入死循环,可以用计算属性或者 watcher 进行状态改变。
销毁阶段,此阶段有 beforeDestroy 和 destroyed 两个钩子函数: beforeDestroy 发生在实例销毁前,这个时候实例还没被销毁,vue 实例还能使用 destroyed 发生在实例销毁之后,这个时候 vue 实例已不存在,所有的子实例也被销毁,所有的指令被解绑,事件监听被移除。
每个生命周期适合哪些场景?生命周期钩子的一些使用方法:
beforecreate : 可以在这加个loading事件,在加载实例时触发
created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
mounted : 挂载元素,获取到DOM节点
updated : 如果对数据统一处理,在这里写上相应函数
beforeDestroy : 可以做一个确认停止事件的确认框
nextTick : 更新数据后立即操作dom
Vue 如何实现组件间通信?
vue 组件的通信分为两种方式:一个是父子组件之间的通信,一个是非父子组件之间的通信(bus)
父子组件
props / emits 这是Vue跨组件通信最常用,也是基础的一个方案。
父组件通过 prop 向子组件传值
父组件:
<template>
<div>
<h3>this is father</h3>
<!-- :传递到子组件的数据名="需要传递的数据" -->
<Child :shop="shopList" />
</div>
</template>
<script>
import Child from './child.vue';
export default {
name: 'Father',
components: {
Child
},
setup() {
const shopList = '三包烟, 两瓶啤酒, 一斤花生米';
return {
shopList
};
}
};
</script>
子组件:
<template>
<div>
<p>this is child</p>
{{ shop }}
</div>
</template>
<script>
export default {
name: 'Child',
props: ['shop']
};
</script>
子组件通过 emit 出发父组件的时间执行
父组件
<template>
<div>
<h1>【case1】父传子</h1>
<p>父:好大儿,去帮为父买几个东西吧,东西通过 props 传给你了</p>
<!-- :传递到子组件的数据名="需要传递的数据" -->
<Child :shop="shopList" @giveClick="giveMoney" />
<p>父:给给给,一共 {{ number }}</p>
</div>
</template>
<script>
import { ref } from 'vue';
import Child from './child.vue';
export default {
name: 'Father',
components: {
Child
},
setup() {
const shopList = '三包烟, 两瓶啤酒, 一斤花生米';
let number = ref();
const giveMoney = msg => {
console.log(msg);
number.value = msg;
console.log(number);
};
return {
shopList,
giveMoney,
number
};
}
};
</script>
子组件
<template>
<div>
<p>子:好的爸爸,这是我收到的东西:{{ shop }}</p>
<h1>【case2】子传父</h1>
<p>子:爸爸我没钱了</p>
<button @click="moneyClick">伸手要钱</button>
</div>
</template>
<script>
export default {
name: 'Child',
props: ['shop'],
setup(props, { emit }) {
const moneyClick = () => {
// 触发父组件中的方法,方法名要与父组件中的监听事件名一样
emit('giveClick', '1000');
};
return {
moneyClick
};
}
};
</script>
expose / ref
父组件获取子组件的属性或者调用子组件方法 defineExpose someMethod ref="comp"
attrs
包含父作用域里除 class 和 style 除外的非 props 属性集合
v-model
可以支持多个数据双向绑定
爷孙组件
provide / inject
适用于隔代通信,例如爷孙组件 provide / inject 为依赖注入
provide:可以让我们指定想要提供给后代组件的数据或
inject:在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用
任意组件
Mitt
mitt 的用法:通过 on 方法添加事件,off 方法移除,clear 清空所有,emit
Vuex
Vuex 实现原理
概念 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,每个 Vuex 应用的核心就是 store,store 本质上就是一个容器,存储着应用中大部分的状态。
Vuex 的状态和属性
state:定义了应用状态的数据结构,可以在这里设置默认的初始状态,数据是响应式的;
getter:可以对 state 进行计算操作,通过 commit 提交方法,主要用来过滤一些数据,可以在多组件之间复用;
mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
module:当应用变得庞大复杂,拆分store为具体的module模块。
vuex 流程
第一步:在vue组件里面,通过 dispatch 来触发 actions 提交修改数据的操作。
第二步:然后再通过 actions 的 commit 来触发 mutations 来修改数据。
第三步:mutations 接收到 commit 的请求,就会自动通过 Mutate 来修改 state(数据中心里面的数据状态)里面的数据。
第四步:最后由 store 触发每一个调用它的组件的更新
应用场景
vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据
vue实现双向数据绑定原理是什么
传统的 dom 操作: 数据变化时更新视图:先获取到目标节点,然后将改变后的值放入节点中 视图变化时修改数据:需要绑定事件修改数据
vue数据双向绑定是通过 数据劫持 结合 发布者-订阅者模式 的方式来实现的, 数据劫持是利用 ES5 的 Object.defineProperty(obj,key,val) 方法来劫持每个属性的 getter 和 setter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
第一步:实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
第二步:实现一个解析器 Compile :解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
第三步:实现一个订阅者 Watcher :Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
第四步:实现一个订阅器 Dep :订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
v-model 的实现以及它的实现原理
在表单输入元素或组件上创建双向绑定。
vue 中实现双向绑定是一个指令 v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值,使用 v-model 可以减少大量繁琐的事件处理代码,提高开发效率,代码可读性也更好;
vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,
v-model 本质上是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件,以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
vue-router 实现原理
前端路由的本质就是监听 URL 的变化,然后匹配路由规则,显示相应的⻚⾯,并且⽆须刷新。 ⽬前单⻚⾯使⽤的路由就只有 hash 和 history 两种实现⽅式。
Vue hash 路由和 history 路由的区别
hash 模式
www.test.com/#/ 就是 Hash URL,使用 hashchange 事件来监听 hash 值的变化,从而进行页面跳转。
hash 虽然出现URL中,但不会被包含在HTTP请求中,URL 中 hash 值只是客户端的一种状态,也就是说当向服务端发出请求时,hash 部分不会被发送;
hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
使用方法:可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
history 模式
history 利用了HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法来实现 URL 的变化,这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
什么是 ref( )
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性性 .value。 ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。 它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
watch 和 computed 区别
watch:watch 是监听数据,用于声明在数据更改时调用的侦听回调; computed:omputed 是计算属性,用来描述依赖响应式状态的复杂逻辑;
运作模式
computed 有缓存,如果 computed 属性依赖的属性没有变化,那么 computed 属性就不会重新计算,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
computed:当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算,例:购物车商品结算功能
watch:当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的,例:搜索数据
data () { a: { b: 1 } }; 如何用 watch 监听 b 的变化
keep-alive的实现
是 Vue 的一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
概念
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态 —— 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
配置属性
include 字符串或正则表达式。只有名称匹配的组件会被缓存 exclude 字符串或正则表达式。任何名称匹配的组件都不会被缓存 max 数字、最多可以缓存多少组件实例
缓存实例的生命周期
一个持续存在的组件可以通过 activated 和 deactivated 选项来注册相应的两个状态的生命周期钩子:
export default {
activated() {
// 在首次挂载、
// 以及每次从缓存中被重新插入的时候调用
},
deactivated() {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
}
}
实现原理
Vue.js 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( pruneCache 与 pruneCache )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。
nextTick的实现
等待下一次 DOM 更新刷新的工具方法。
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
count: 0
}
},
methods: {
async increment() {
this.count++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
}
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
Vue 渲染机制
虚拟 dom
Vue 的渲染系统是基于虚拟 DOM 的概念构建的,虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
虚拟 dom 实现步骤
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档中; 当状态变更的时候,重新构造一棵树的对象树,然后用新的树和旧的树进行对比,记录两棵树差异; 把所记录的差异应用到所构建的真正的 DOM 树上,视图就更新了;
Vue 渲染管线
从高层面的视角看,Vue 组件挂载后发生了如下这几件事:
编译:Vue 模板被编译为了渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
插槽是什么 怎么使用的(0)
常用指令列举
v-if:判断是否隐藏,条件渲染;
v-for:数据循环出来,循环渲染;
v-bind:绑定一个属性;
v-model:表单双向绑定;
v-on: 绑定一个事件。
v-show 和 v-if 有什么区别
v-if
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染,也就是才会在 dom 树上出现,用法:
<h1 v-if="awesome">Vue is awesome!</h1>
v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
v-show
v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。 用法:
<h1 v-show="ok">Hello!</h1>
v-show 不支持在 'template' 元素上使用。
v-show 使元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。
总的来说:
v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。 因此,如果需要频繁切换,则使用 v-show 较好; 如果在运行时绑定条件很少改变,则 v-if 会更合适。
v-bind 的实现与原理
动态的绑定一个或多个 attribute,也可以是组件的 prop。
用途 用于绑定 class 或 style attribute,v-bind 支持额外的值类型如数组或对象。
v-for 中 key 的作用
key 的作用主要是为了高效的更新虚拟DOM,对比虚拟 DOM 中每个节点是否是相同节点;
Vue 在 patch 过程中判断两个节点是否是相同节点,key 是一个必要条件,渲染一组列表时,key 往往是唯一标识,所以如果不定义 key 的话,Vue 只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个 patch 过程比较低效,影响性能;
从源码中可以知道,Vue 判断两个节点是否相同时主要判断两者的 key 和元素类型等,因此如果不设置 key,它的值就是 undefined,则可能永远认为这是两个相同的节点,只能去做更新操作,这造成了大量的 dom 更新操作,明显是不可取的。
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。 默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:
Vue3 新特性与影响
性能提升
更小巧、更快速;支持自定义渲染器;支持摇树优化:一种在打包时去除无用代码的优化手段;支持Fragments和跨组件渲染。
API变动
模板语法99%保持不变;原生支持基于 class 的组件,并且无需借助任何编译及各种 stage 阶段的特性;在设计时也考虑TypeScript的类型推断特性;重写虚拟DOM可以期待更多的编译时提示来减少运行时的开销;优化插槽生成可以单独渲染父组件和子组件;静态树提升降低渲染成本;基于 Proxy 的观察者机制节省内存开销
不兼容IE11
检测机制更加全面、精准、高效,更具可调试式的响应跟踪
其他知识点
vue 为什么要用 template
Vue 数据响应式怎么做到的?
如何解决 vue 初始化页面闪动问题
vue 首屏渲染优化有哪些
Vue.set 是做什么用的?
vue动态传参以及获取
Eventbus具体是怎么实现的
nuxt 怎样配置路由,如何自定义路由,自定义的和约定路由哪个优先级高
new Vue后整个的流程
vue 的模板编译原理
实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何
vnode的理解,compiler和patch的过程
你都做过哪些Vue的性能优化
vue中的data为什么返回的是一函数
Vue的父子组件生命周期钩子函数执行顺序
什么是组件以及如何使用组件