1、介绍一下MVVM,和MVC有什么区别
MVVM是Model-View-ViewModel
缩写,也就是把MVC
中的Controller
演变成ViewModel
。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
MVC: MVC是Model-View- Controller的简写。即模型-视图-控制器。M和V指的意思和MVVM中的M和V意思一样。C即Controller指的是页面业务逻辑。使用MVC的目的就是将M和V的代码分离。MVC是单向通信。
MVVM原理:https://github.com/DMQ/mvvm
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图 4、mvvm入口函数,整合以上三者
数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
2、ViewModel有什么好处
ViewModel监听模型数据的改变和控制视图行为、处理用户交互、简单理解就是一个同步View和Model的对象,连接View和Model
3、nextTick是如何实现的
在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列
Vue的核心流程大体可以分成以下几步
- 遍历属性为其增加get,set方法,在get方法中会收集依赖(
dev.subs.push(watcher)
),而set方法则会调用dev的notify方法,此方法的作用是通知subs中的所有的watcher并调用watcher的update方法,我们可以将此理解为设计模式中的发布与订阅 - 默认情况下update方法被调用后会触发
queueWatcher
函数,此函数的主要功能就是将watcher实例本身加入一个队列中(queue.push(watcher)
),然后调用nextTick(flushSchedulerQueue)
flushSchedulerQueue
是一个函数,目的是调用queue中所有watcher的watcher.run
方法,而run
方法被调用后接下来的操作就是通过新的虚拟dom与老的虚拟dom做diff算法后生成新的真实dom- 只是此时我们
flushSchedulerQueue
并没有执行,第二步的最终做的只是将flushSchedulerQueue
又放进一个callbacks队列中(callbacks.push(flushSchedulerQueue)
),然后异步的将callbacks遍历并执行(此为异步更新队列) - 如上所说
flushSchedulerQueue
在被执行后调用watcher.run()
,于是你看到了一个新的页面
4、父子组件挂载时,生命周期的顺序是怎么样的
父组件先创建,然后子组件创建;子组件先挂载,然后父组件挂载。
测试:写一个有父子嵌套关系的组件,分别在他们的钩子函数中打印日志,观察执行顺序。
组件的调用顺序都是先父后子
,渲染完成的顺序是先子后父
。
组件的销毁操作是先父后子
,销毁完成的顺序是先子后父
。
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
5、Vue的双向绑定是如何实现的
vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作
Object.defineProperty( )是用来做什么的?它可以来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举,
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
6、Vue2中关于数组和对象数据观察时有做什么特别处理吗
需要深度监听,7种改变原数组的方法,Vue.set方法/this.$set方法
object.assign()
7、defineProperty和proxy有什么区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N99BPCEN-1618738578622)(C:\Users\张子浩\AppData\Roaming\Typora\typora-user-images\image-20210409083949375.png)]
8、Vue中的数据为什么频繁变化但只会更新一次
这是因为vue的dom更新是一个异步操作,在数据更新后会首先被set
钩子监听到,但是不会马上执行dom更新,而是在下一轮循环
中执行更新。
具体实现是vue中实现了一个queue
队列用于存放本次事件循环
中的所有watcher
更新,并且同一个watcher
的更新只会被推入队列一次,并在本轮事件循环的微任务
执行结束后执行此更新(UI Render
阶段),这就是dom只会更新一次的原因。
- 检测到数据变化
- 开启一个队列
- 在同一事件循环中缓冲所有数据改变
- 如果同一个
watcher (watcherId相同)
被多次触发,只会被推入到队列中一次
不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run
优化后:执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)
9、什么是状态管理,为什么需要状态管理
1、多个组件依赖同一状态时,对于多层嵌套的组件的传参将会非常繁琐,并且对兄弟组件之间的状态传递无能为力
2、来自不同组件的行为需要变更同一状态。以往采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。这种模式非常脆弱,维护困难
10、介绍一下Vuex或Redux/区别
vuex可以理解为一种开发模式或框架,1、应用级的状态集中放在store中,2、改变状态的方式是提交mutation,同步3、异步逻辑封装在action中
11、如果让你实现一个简单的状态管理,要如何实现
1.在项目中新建一个store.js文件,然后按官网推荐方法将需要管理的状态和方法放在里面,然后export暴露出来。
export var store = {
debug: true,
state: { /*需要管理的状态*/
count: 0,
name: 'store'
},
setNewCount(newVal) { /*修改状态的方法*/
this.state.count = newVal;
},
setNewName(newVal) { /*修改状态的方法*/
this.state.name = newVal;
}
}
2.在需要使用该状态的组件中引入此文件:
import {store} from '../../store.js'
3.然后把需要的数据放到组件的data中,或者在方法中直接使用即可:
name: store.state.name
4.如果需要修改store中的状态,只需要直接调用store中的修改方法:
chgName() {
let newName = '222';
this.name = newName;
store.setNewName(newName);
},
这里有一个问题,如果只执行store中的修改方法,本组件中的name是不会实时修改的,添加watch监听也不行。必须要单独进行赋值修改。这个问题我还没有弄清楚,有知道的希望能指点一下。
5.现在进入其他引入store进行状态管理的组件就可以看到新的值了。
12、父子组件如何进行通信
- 父子组件通信:
props
;$parent
/$children
;provide
/inject
;ref
;$attrs
/$listeners
- 父组件通过
props
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。对于$emit
我自己的理解是这样的:$emit
绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。 - $children / p a r e n t 。 parent 。 parent。children 的值是数组,而$parent是个对象
provide
/inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。
13、爷孙(跨级)组件如何进行通信
- 跨级通信:
eventBus
;Vuex;provide
/inject
、$attrs
/$listeners
provide
/inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。这里不论子组件嵌套有多深, 只要调用了inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. Vuex 解决了
多个视图依赖于同一状态
和来自不同视图的行为需要变更同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上
14、兄弟组件如何进行通信
eventBus
又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。- vuex
15、Virtual DOM是什么
用JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了。
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。
https://juejin.cn/post/6844903902429577229#heading-0
16、为什么需要Virtual Dom
既然我们已经有了DOM,为什么还需要额外加一层抽象?
首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.
其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.
最后,也是Virtual DOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为Virtual DOM本身是JavaScript对象.
- 具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
- 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)
- 提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新
17、Vue的Virtual Dom解决了什么问题
虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。
其实虚拟DOM在Vue.js主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图
18、介绍一下Vue的diff策略
由于在浏览器中操作DOM的代价是非常“昂贵”的,所以才在Vue引入了Virtual DOM,Virtual DOM是对真实DOM的一种抽象描述,不懂的朋友可以自行查阅相关资料。
即使使用了Virtual DOM来进行真实DOM的渲染,在页面更新的时候,也不能全量地将整颗Virtual DOM进行渲染,而是去渲染改变的部分,这时候就需要一个计算Virtual DOM树改变部分的算法了,这个算法就是Diff算法。
Diff算法是逐层进行比较,只比较同一层次的节点,大大降低了复杂度,具体如下图。在后面的内容中也会介绍Vue中同层节点比较的具体实现。

不同类型节点的比较
如果发现新旧两个节点类型不同时,Diff算法会直接删除旧的节点及其子节点并插入新的节点,这是由于前面提出的不同组件产生的DOM结构一般是不同的,所以可以不用浪费时间去比较。注意的是,删除节点意味着彻底销毁该节点,并不会将该节点去与后面的节点相比较。
相同类型节点的比较
若是两个节点类型相同时,Diff算法会更新节点的属性实现转换。
列表节点的比较
列表节点的操作一般包括添加、删除和排序,列表节点需要我们给它一个key才能进行高效的比较
19、key 有什么用
key 是为 Vue 中的标记,通过这个 它,diff 操作可以更准确、更快速。
Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们的新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 对比中可以避免就地复用的情况,所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
20、Vue computed是如何实现的 /看源码
computed 本质是一个惰性求值的观察者computed watcher
。其内部通过 this.dirty
属性标记计算属性是否需要重新求值。
- 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher
通过this.dep.subs.length
判断有没有订阅者, - 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性
最终计算的值
发生变化时才会触发渲染 watcher
重新渲染,本质上是一种优化。) - 没有的话,仅仅把
this.dirty = true
(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
21、watch是如何实现的 /看源码
watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中
的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听
注意:Watcher : 观察者对象 , 实例分为渲染 watcher
(render watcher),计算属性 watcher
(computed watcher),侦听器 watcher
(user watcher)三种
22、computed的时候可以引用其他computed属性,是如何实现的
Vue 实例在建立的时候会运行一系列的初始化操作,其中数据的初始化包括initProps、initMethods、initData、initComputed、initWatch。
在初始化computed的时候(initComputed),会监测数据是否已经存在data或props上,如果存在则抛出警告,否则调用defineComputed函数对数据进行数据劫持。
如果你computed中的数据a被使用了(调用了getter),那么会设置Dep.target,如果getter中含有被监听了的数据(如b),那么会把Dep.target追加到b的deps中(没有的话),这样当b改变的时候,会遍历计算所有的deps(包括a的getter)。
23、再说一下Computed和Watch
Computed
本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
Watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true
选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式
监听,如果没有写到组件中,不要忘记使用unWatch手动注销
哦。
24、组件中的data为什么是一个函数?
Watch
Computed
本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
Watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true
选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式
监听,如果没有写到组件中,不要忘记使用unWatch手动注销
哦。
24、组件中的data为什么是一个函数?
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数
。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。