目录
Object.defineProperty和Proxy的优缺点?
什么是MVVM?
Model-View-ViewModel 模式,最早在 2005 年微软推出的基于 Windows 的⽤户界⾯框架 WPF 中提出,而最早采用 MVVM 的前端框架是 2010 年发布的 Knockout。
Model 层
对应数据层的域模型,主要用来做域模型的同步。
通过 Ajax
、fetch
等 API 完成客户端和服务端业务模型的同步。
在分层关系中,它主要⽤于抽象出 ViewModel 层中视图的 Model。
View 层
作为视图模板存在,其实在 MVVM 中整个 View 就是⼀个动态模板。
除了用于定义结构和布局之外,它还展示了 ViewModel 层的数据和状态。
View 层并不负责状态的实际处理,它只是做:数据绑定声明、 指令声明、 事件绑定声明。
ViewModel 层
负责暴露数据给 View 层,并对 View 层中的数据绑定声明、 指令声明、 事件绑定声明进行实际的业务逻辑。
ViewModel 底层会做好绑定属性的监听,当 ViewModel 中的数据变化时,View 层会自动进行更新;⽽当 View 中声明了数据的双向绑定(表单元素),框架也会监听 View 层(表单元素)值的变化,⼀旦变化,则 View 层绑定的 ViewModel 中的数据也会得到⾃动更新。
MVVM的优缺点有哪些?
优点
-
实现了视图(View)和模型(Model)的分离,降低代码耦合、提⾼视图或逻辑的复⽤性
⽐如:View 可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定于不同的 "View",当 View 发生变化时 Model 一定会随之改变,而当 Model 变化时则 View 可以不变。我们可以把⼀些视图逻辑放在⼀个 ViewModel ⾥,以此让多个 View 重⽤这段视图逻辑。
-
提⾼了可测试性:ViewModel 的存在可以帮助开发者更好地编写测试代码
-
能⾃动更新 DOM:利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动操作 DOM 中解放出来
缺点
-
Bug 很难被调试:因为使⽤了双向绑定的模式,当我们看到界⾯发生异常了,有可能是 View 的代码产生的 Bug,也有可能是Model 代码的问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 模版中的,它们没办法打断点进行 Debug
-
在⼀个⼤的模块中 Model 也会很⼤,虽然使⽤上来说⽅便了,也能很容易的保证了数据的⼀致性,但如果⻓期持有不释放内存,就会造成更多的内存消耗
-
vue3如果你没做特别处理,它不帮你双向绑定
-
对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼
谈谈对Vue生命周期的理解?
生命周期的概念
每个 Vue 实例都有⼀个完整的⽣命周期:
-
开始创建
-
初始化数据
-
编译模版
-
挂载 DOM
-
渲染、更新数据 => 重新渲染
-
卸载
这⼀系列过程我们称之为 Vue 的⽣命周期。
各个生命周期的作用
生命周期 | 执行时机 |
---|---|
beforeCreate | 在组件实例被创建之初、组件的属性⽣效之前被调用 |
created | 在组件实例已创建完毕。此时属性也已绑定,但真实DOM还未⽣成,$el 还不可⽤ |
beforeMount | 在组件挂载开始之前被调⽤。相关的 render 函数⾸次被调⽤ |
mounted | 在 el 被新建的 vm.$el 替换并挂载到实例上之后被调用 |
beforeUpdate | 在组件数据更新之前调⽤。发⽣在虚拟 DOM 打补丁之前 |
updated | 在组件数据更新之后被调用 |
activited | 在组件被激活时调⽤(使用了 <keep-alive> 的情况下) |
deactivated | 在组件被隐藏时调⽤(使用了 <keep-alive> 的情况下) |
beforeDestory | 在组件销毁前调⽤ |
destoryed | 在组件销毁后调⽤(中断了数据的渲染) |
生命周期示意图
在Vue中网络请求应该放在哪个生命周期中发起?
可以在 mounted
或 created
这两个⽣命周期中调⽤发起网络请求。
beforeCreate也可以,发送请求,请求完可以访问vue渲染后的dom
div ref='dev'
beforeCreate(){
this.$nextTick(()=>{})
xxx().then(()=>{
访问vue渲染后的dom
this.$refs.dev
})
}
Vue组件之间如何进行通信?
ref传值
子组件标签加入ref属性 this.$refs.ref值===子组件的实例对象(this)
子组件内访问父组件 this.$parent
props
和 $emit + v-on
**
通过 props
将数据在组件树中进行⾃上⽽下的传递;
通过 $emit
和 v-on
来作信息的向上传递。
组件传值
props与emit
父组件
son
父传子(父传子属性)
传: son 属性名=值
收:
子组件内
props:{
属性名:{
type:[String,Number],
default:如果是原始值,直接定义,如果是引用值,()=>{return 引用值},
required:true, 必传
validator:(value)=>{
return value>10
return boolean值 true:正常使用,false:报错
}
}
}
属性特点:单向数据流(原始值不可修改,引用值,只要不修改它的引用地址,它的堆可以随便修改)
栈不可修改,堆随便修改,只适用于vue2.0
全不可修改 vue3.0
子传父(子触发父方法)
绑定 son @子组件方法名=“父组件方法名”
触发:子组件内触发
this.$emit('子组件方法名',参数)
EventBus
可通过 EventBus 进⾏信息的发布与订阅。
用于所有组件间的传值
用法:
1:Vue.prototype.$bus=new Vue()
2:监听:
this.$bus.$on('方法名',(形参)=>{...代码})
3:触发:
this.$bus.$emit('方法名',实参)
4:销毁
this.$bus.$off('方法名')
Vuex
全局状态管理库。可通过它来进行全局数据流的管理。
基本使用:
1:安装 npm i vuex
2:导入 import Vuex from 'vuex'
3:注册 Vue.use(Vuex)
4:实例化
import user from './modules/user'
const store=new Vuex.Store({
state:{},
mutations:{},
actions:{},
getters:{},
modules:{user}
})
5:暴露出去 export default store
6:挂载
new Vue({
store
})
1:src/store/modules/模块名.js
模块名.js
const state={
userInfo:"老王"
// 基本使用:this.$store.state.user.userInfo
// map使用
1:导入 import {mapState} from 'vuex'
2:定义
computed:{
...mapState('模块名(user)',['字段名(userInfo)'])
}
}
const mutations={
方法名(setUserInfo)(state,value){
state.userInfo=value
this.commit('模块名/方法名2',参数值)
this===this.$store
},
方法名2(state,value){
}
基本使用:this.$store.commit('模块名(user)/方法名(setUserInfo)',参数值)
map使用: 1:导入 import {mapMutations} from 'vuex'
2:定义
methods:{ ...mapMutations(['模块名/方法名'])} this['模块名/方法名'](参数值)
methods:{ ...mapMutations(‘模块名’,['方法名'])} this.方法名(参数值)
}
const actions={
方法名getUserInfo(store,value){
store:commit,dispatch,state,rootState,getters,rootGetters
xxx().then(res=>{
数据修改userInfo
store.commit('setUserInfo',res)
// 调用本模块的mutations store.commit('本模块的mutations方法名',参数值)
// 调其它模块的mutations store.commit('模块名/其它模块mutations方法名',参数值,{root:true})
// 调用本模块的actions store.dispatch('本模块的actions方法名',参数值)
// 调用其它模块的actions方法 store.dispatch('模块名/其它模块actions',参数值,{root:true})
// 本模块数据 store.state
// 其它模块数据 store.rootState.模块名.字段名
// getters方法调用
本模块getters store.getters.方法名
其它模块的getters store.rootGetters['模块名/其它模块的getters方法名']
})
}
}
export default {
namespaced:true,
state,
mutaions,
actions
}
2:
$attrs
和 $listeners
在 Vue 2.4 版本中加⼊的 $attrs
和 $listeners
可以用来作为跨级组件之间的通信机制。
$attrs:非props属性,父组件传值子组件,子组件没有使用props接收的属性,它会加入到$attrs内
$listeners:子组件标签身上绑定的所有的方法
father <son xxx=123 @abc="..." />
son
<sonson v-bind="$attrs" v-on="$listeners" />
<div v-bind="$attrs" v-on="$listeners" ></div>
provide
和 inject
由于 provide
和 inject
可以允许⼀个祖先组件向它的所有⼦孙组件注⼊⼀个依赖(不论组件层次有多深),并在其上下游关系成⽴的时间⾥始终⽣效,因此这种机制也就成为了一种跨组件通信的手段。
父辈向后代组件传值
用法:
app.vue div v-if="bol"
data(){
return {
bol:true,
abc:{
a:10
}
}
}
methods:{
fn(参数值){
....
},
reLoad(){
this.bol=false
this.$nextTick(()=>{
this.bol=true
})
}
}
provide(){
return {
abc:this.abc,
fn:this.fn,
reLoad:this.reLoad
}
}
}
app.vue的provide属性访问的机会都没有
所有app.vue后代组件都可使用inject,单向数据流
inject:['abc'],
inject:{
xxx:{
from:'abc',
default:默认值
},
fn:{
default:...
},
reLoad:{
default:...
}
}
this.fn(123)
this.reLoad()
另外还有一些方式使用场景有限,在此不介绍了。
可以阅读参考文章:Vue中的8种组件通信方式
computed和watch的区别是什么?
computed
-
它是计算属性。主要用于值的计算并一般会返回一个值。所以它更多⽤于计算值的场景
-
它具有缓存性。当访问它来获取值时,它的 getter 函数所计算出来的值会进行缓存,只有当它依赖的属性值发生了改变,那下⼀次再访问时才会重新调⽤ getter 函数来计算
-
它适⽤于计算⽐较消耗性能的计算场景
watch:某个值change事件
-
它更多的是起到 “观察” 的作⽤,类似于对数据进行变化的监听并执行回调。主要⽤于观察
props
、$emit
或本组件的值,当这些值发生变化时,执⾏回调 -
它不具有缓存性。当⻚⾯重新渲染时,即使值没发生变化也会执⾏
建议
-
当目的是进⾏数值计算,且依赖于其他数据,那么推荐使用
computed
-
当需要在某个数据发生变化的同时做⼀些稍复杂的逻辑操作,那么推荐使⽤
watch
Vue双向绑定原理?
在 Vue 2.x 中,利⽤的是 Object.defineProperty
去劫持对象的访问器(Getter、Setter),当对象属性值发⽣变化时可获取变化,然后根据变化来作后续响应;
数据更新了,修改视图,Object.defineProperty 对数据实现set监听,数据修改了就触发set,set就会将视图使用该数据的所有地方更新
视图更新了修改数据,dom绑定了相应事件做相应处理
vue2.x:新增属性无法实现双向绑定
在 Vue 3.0 中,则是通过 Proxy
代理对象进⾏类似的操作。
proxy:监听该对象的所有属性,proxy不修改原有数据,只是做了代理,产生了一个新的对象对原有数据进行代理
Object.defineProperty和Proxy的优缺点?
Proxy
-
可以直接监听整个对象,⽽⾮是对象的属性
-
可以直接监听数组的变化
-
拦截⽅法丰富:多达13种,不限于
apply
、ownKeys
、deleteProperty
、has
等。比Object.defineProperty
强大很多 -
返回的是⼀个新对象,可以在不影响原对象的情况下,只操作新对象来达到⽬的;⽽
Object.defineProperty
只能遍历原对象属性并直接修改原对象 -
受到各浏览器⼚商的重点持续性能优化,能享受到作为新标准的性能红利
Object.defineProperty
-
兼容性较好(可⽀持到 IE9)
如何理解Vue的响应式系统?
(考察MVVM) M: model数据模型, V:view视图模型, VM: viewModel视图数据模型
双向:
-
视图变化了, 数据自动更新 => 监听原生的事件即可, 输入框变了, 监听输入框input事件
-
数据变化了, 视图要自动更新 => vue2 和 vue3
基本原理
vue2.0 数据劫持: Object.defineProperty (es5)
vue3.0 数据劫持: Proxy (es6)
分析 :此题考查 Vue的 MVVM 原理
解答: Vue的双向绑定原理其实就是 MVVM 的基本原理, Vuejs官网已经说明, 实际就是通过 Object.defineProperty方法 完成了对于Vue实例中数据的 劫持
, 通过对于 data中数据 进行set的劫持监听, 然后通过观察者模式
, 通知 对应的绑定节点 进行节点数据更新, 完成数据驱动视图的更新
简单概述 : 通过Object.defineProperty 完成对于数据的劫持, 通过观察者模式, 完成对于节点的数据更新
观察者模式
观察者模式: 当对象间存在 一对多 关系时,则使用观察者模式(Observer Pattern)。
比如,当一个对象或者数据被修改时,则会自动通知依赖它的对象。
意图:定义对象间的一种 一对多的依赖关系
,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
拍卖会的时候,大家相互叫价,拍卖师(Dep)会观察最高标价(Object.defineProperty),
一旦最高价变化了, 然后通知给其它竞价者(watcher观察者 - 订阅者, 订阅了价格的变化),这就是一个观察者模式
下图为Vue框架在数据初始化中使用观察者模式的示意图:
Dep要进行 依赖收集,并通过一个subs数组, 记录观察者Watcher,
Watcher 分为渲染 watcher、计算属性 watcher、侦听器 watcher三种
收集依赖: 简单点说就是谁借了我的钱,我就把那个人 记下来 ,以后我的钱少了 我就通知他们说我没钱了
<div>
<p>{{ msg }}</p> // Watcher1(渲染), 依赖于msg
</div>
<div>
<h1>{{ car }}</h1> // Watcher2(渲染), 依赖于car
</div>
<div>
<h1>{{ myMsg }}</h1> // Watcher3(渲染), 依赖于myMsg
</div>
computed: {
myMsg () {
console.log('计算属性重新计算了')
return this.msg + '20' // Watcher4(计算属性中), 依赖于msg, msg变了重新计算
}
}
watch: {
msg (newValue) {
console.log('新的msg', newValue) // Watcher5(侦听器), 将来msg变化, 这边要执行这个函数
}
}
------------------------------------------------------------------
// 收集依赖 (dep结构有点类似于二维数组, (Map结构)) arr.type="msgDep"
dep: [
msgDep: [Watcher5(侦听器依赖), Watcher4(计算属性依赖), Watcher1(渲染)],
carDep: [Watcher2(渲染)],
myMsgDep: [Watcher3(渲染)]
]
// Watcher
{
callback: Function, (数据变化后, 需要执行的回调)
isRenderWatcher: Boolean, (是否是render的watcher, 是否要触发视图的更新, 往后放, 最后统一虚拟dom对比, 统一更新)
...
}
比如: 假定数据 money 变了, 那么没有任何与money相关的观察者, 就不需要进行任何更新操作, 也不需要执行任何的监视函数
然而: 假定数据 msg 变了, 就会通知到相关的Watcher, 且优先通知侦听器Watcher和计算属性Watcher, 后进行统一的渲染更新
-
通知侦听器Watcher, 立刻执行配置的函数, console.log('新的msg', newValue)
-
通知计算属性Watcher, 计算属性依赖的值变了, 需要重新计算 且更新后, myMsg 变化了, 需要进行进行视图的渲染 (render) (--- 要更新, 等着---)
-
通过到watcher1, 渲染Watcher (---要更新---)
-
最后统一进行, 新旧虚拟dom的对比, 完成视图的更新
当数据状态发生改变时,会被 Object.defineProperty 监听劫持到, 会通知到 Dep, 并根据收集的依赖关系,
让订阅者Watcher进行数据更新(update)操作 , 派发更新
总结概述: vue采用的是观察者模式, 是一种一对多的关系, 一上来vue在解析渲染时, 会进行依赖收集, 会将渲染 watcher、计算属性 watcher、侦听器 watcher, 都收集到dep中,将来Object.defineProperty 监听到数据变化, 就根据依赖关系, 派发更新
Vue中的key到底有什么用?
key
是为 Vue 中的虚拟 DOM 节点(vNode)标记唯⼀性的 id。
作用: 给虚拟dom添加标识, (优化复用对比策略, 优化渲染性能)
主要考察:
-
vue 的更新机制 (差异化更新) 对比新旧虚拟dom 进行更新视图
为什么对比虚拟dom, 而不对比真实的dom ? 真实的dom太复杂, 对比起来性能太差
-
虚拟dom: 使用 js 对象的方式, 模拟真实的 dom 结构
属性的量大大的减少了, 没有真实dom的那么多无效的属性, 对比起来性能高很多
-
diff 算法: 默认的对比(diff) 机制, 同层兄弟元素, 是按照下标进行对比的, 但是加了 key, 就相当于给虚拟dom加了个标识
对比策略, 就是对相同key的元素进行对比了, 在列表v-for中, key的使用尤为常见, 可以用于优化渲染性能
diff算法
diff 算法的基本策略 (对比新旧虚拟dom差异的一种算法):
-
由于dom结构是一个树形结构, 就算是对比新旧虚拟dom, 一样是在对比树形结构
而树形结构, 每往下都一层, 遍历成本就越高
-
为了避免一些无效的对比, diff算法, (tree diff) 优先比较树的根节点
如果根节点元素类型(组件), 是一致的, 才考虑复用元素内部的结构
如果根节点元素类型(组件), 不一致, 直接销毁, 重新构建新的结构
旧 <div> <span>我是内容</span> </div> 新 <li> <a href="#">我是内容</a> <span>我是内容</span> </li>
大大的提升了对比的效率
-
同一根节点下, 同级兄弟元素, 默认的对比策略: 按照下标对比
旧 <div class="box"> <h1>大标题</h1> <p>内容</p> </div> 新 <div class="message"> <h1>测试标题</h1> <p>内容</p> </div>
-
同一根节点下, 同级兄弟元素, 如果配置 key 属性, 那么对比的策略, 就不是按照下标, 而是按照key进行 一一对比
旧 <div class="box"> <h1 key="201">大标题</h1> <p key="202">内容</p> </div> 新 <div class="message"> <h1 key="200">大标题</h1> // 新结构 <p key="202">内容</p> // 对比出来, 原来结构有key="202", 进行对比, 完成了复用 </div>
如果你希望一个结构, 并不进行复用, 就是希望某个组件某个结构, 能够重新构建, 可以设置一个不同的 key
key的常见应用场景
key 的常见应用场景 => v-for, v-for 遍历的列表中的项的顺序, 非常的容易改变
1 往后面加, 默认的对比策略, 按照下标, 没有任何问题
// 旧
<ul>
<li>张三</li>
<li>李四</li>
</ul>
// 新
<ul>
<li>张三</li>
<li>李四</li>
<li>王五</li>
</ul>
2 往前面加, 由于下标变了, 如果按照之前的下标对比, 元素是混乱的, 策略: 加上key
一旦加上了key, 就是按照 key 进行新旧dom的对比了
// 旧
<ul>
<li key="17">张三</li>
<li key="31">李四</li>
</ul>
// 新 [ { id: 17, name: '张三' }, ... ]
<ul>
<li key="52">王五</li>
<li key="17">张三</li>
<li key="31">李四</li>
</ul>
总结: key 就是给 虚拟dom 添加了一个 标识, 优化了对比策略!!!