1.什么是MVVM?
MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
衍生问题1:MVVM跟MVC有什么区别?
mvvm它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变,因此开发者只需要专注对数据的维护操作即可。
MVC是单向通信。也就是View跟Model,必须通过Controller来承上启下。
mvvm 主要解决了 mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到 View 。
2.vue 的双向绑定的原理是什么?(v-model)
所谓双向绑定,指的就是我们在js中的vue实例中的data与其渲染的dom元素上的内容保持一致,两者无论谁被改变,另一方也会相应的更新为相同的数据。
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
3.vue的虚拟dom怎么实现的?diff算法?时间复杂度?
什么是虚拟dom?
虚拟dom就是用一个js对象来描述dom节点。可以通过该对象来生成真实的dom节点。实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。
对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM
的一个过程
Virtual DOM
并没有完全实现DOM
,Virtual DOM
最主要的还是保留了Element
之间的层次关系和一些基本属性. 你给我一个数据,我根据这个数据生成一个全新的Virtual DOM
,然后跟我上一次生成的Virtual DOM
去 diff
,得到一个Patch
,然后把这个Patch
打到浏览器的DOM
上去。
我们可以通过javascript
对象表示的树结构来构建一棵真正的dom
树,当数据状态发生变化时,可以直接修改这个javascript
对象,接着对比修改后的javascript
对象,记录下需要对页面做的dom
操作,然后将其应用到真正的dom
树,实现视图的更新,这个过程就是Virtual DOM
的核心思想。
怎么实现虚拟dom的(虚拟dom的原理)?
使用js根据dom
树的结构来构建一个对象,当数据状态发生变化时,可以直接修改这个js
对象,接着对比修改后的js
对象,记录下需要对页面做的dom
操作,然后将其应用到真正的dom
树,实现视图的更新,这个过程就是Virtual DOM
的核心思想。
diff算法是干嘛的?
要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
我们先根据真实DOM生成一颗virtual DOM
,当virtual DOM
某个节点的数据改变后会生成一个新的Vnode
,然后Vnode
和oldVnode
作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode
的值为Vnode
。
diff的过程就是调用名为patch
的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。
Vue的核心是双向绑定和虚拟DOM(下文我们简称为vdom)
Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。
diff 算法包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了
diff算法的时间复杂度为O(n)。
衍生问题1:Vue采用虚拟DOM的目的是什么?
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。
虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。
其实虚拟DOM在Vue.js中主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图
对两个虚拟节点进行对比是虚拟DOM中最核心的算法即patch,patch算法的核心是diff算法,它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。
衍生问题2:为什么虚拟DOM可以提高渲染速度?
传统方式用js操作DOM会有很多额外的DOM操作,例如,一个ul标签下有很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,其实除了那个发生变化的li节点之外,其他节点都不需要重新渲染。由于DOM操作比较慢,所以这些DOM操作在性能上会有一定的浪费,避免这些不必要的DOM操作会提升很大一部分性能(减少重排重绘从而节省浏览器的性能开销)。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无任何改动的DOM。
其实虚拟DOM在Vue.js中主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图
对两个虚拟节点进行对比是虚拟DOM中最核心的算法即patch,patch算法的核心是diff算法,它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。
衍生问题3:浏览器渲染过程?
4.讲讲Vue的生命周期?
详细看:https://blog.csdn.net/Newbie___/article/details/105347733
vue中的生命周期,总共分为三个阶段:初始化、运行中、销毁。
首先创建一个vue
实例,进行第一次初始化操作,这次初始化仅仅是初始化自己的事件和自己的生命周期,初始化完成之后,开始第二次初始化,此时会将data
、computed
、methods
等等的数据观测 (data observer
),属性和方法的运算,event/watch
事件回调进行初始化注入,完成之后,会检查是否指定了el选项,如果指定了,或没用指定但调用了vm.$mount("el")
时,继续查看是否指定了template
模板。如果没有指定,则把外部的HTML
作为template
编译,如果指定了,则将template
渲染到render
函数中(当然我们也可以不使用template
,直接使用render
函数创建模板),然后创建vm.$el
,替换掉el
,此时便已经挂载完毕了。如果数据改变,则虚拟DOM
重新渲染,并应用更新。当调用销毁方法时,解除绑定,销毁子组件及事件监听器。
衍生问题1:el跟vm.$el是什么?
el
是Vue实例的挂载目标。在实例挂载之后,元素可以用 vm.$el
访问。
挂载阶段还没开始的时候,$el
属性是不可见的。Vue生命周期mounted阶段,el
被新创建的vm.$el
替换,这个时候Vue实例的挂载目标确定, DOM渲染完毕。在这个Vue实例当中,也就可以使用vm.$el
访问到el
了。具体参考Vue文档API
衍生问题2:说说vue的生命周期钩子?
1.beforeCreate
在实例初始化之后,数据观测 (data observer
) 和 event/watch
事件配置之前被调用。
<div id="app">
{{ name }}
</div>
const vm = new Vue({
el:"#app",
data:{
name:"monk"
},
beforeCreate(){
console.log(this.name);
console.log(this.handleFunction)
console.log("--beforeCreate--")
},
methods: {
handleFunction(){
console.log("我是一个方法!");
}
},
watch: {
name:{
handler(){
console.log("我已经开始监听侦听name属性啦");
},
immediate:true
}
},
})
打印顺序:在实例初始化之后,数据观测 (data observer) 和 event/watch 事件配置之前被调用。
undefined
undefined
--beforeCreate--
我已经开始监听侦听name属性啦
2.created
在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,event/watch 事件回调。
如果要在第一时间调用methods中的方法,或者操作data中的数据,可在此钩子中进行操作。需要注意的是,执行此钩子时,挂载阶段还未开始,$el 属性目前不可见。此时,可以进行数据请求,将请求回来的值赋值给data中的数据。
<div id="app">
{{ name }}
</div>
const vm = new Vue({
el:"#app",
data:{
name:"monk"
},
created(){
console.log(this.name);
console.log(this.handleFunction)
console.log(this.$el);
console.log('----------created-------');
},
methods: {
handleFunction(){
console.log("我是一个方法!");
}
},
watch: {
name:{
handler(){
console.log("我已经开始监听侦听name属性啦");
},
immediate:true
}
},
})
打印顺序:此时已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。
我已经开始监听侦听name属性啦
monk
ƒ handleFunction(){console.log("我是一个方法!");}
undefined
----------created-------
3.beforeMount
在挂载开始之前被调用,此时模板已经编译完成,只是未将生成的模板替换el
对应的元素。在此钩子函数中,可以获取到模板最初始的状态。此时,可以拿到vm.$el,只不过为旧模板。
4.mounted
el
被新创建的 vm.$el
替换,并挂载到实例上去之后调用该钩子。在该钩子函数中的vm.$el
为新模板。执行完该钩子函数后,代表实例已经被完全创建好。如果要在第一时间,操作页面上的dom节点时,可以在此钩子函数中操作
5.beforeUpdate
数据更新时调用,发生在虚拟 DOM 打补丁之前。此时数据已经更新,但是DOM还未更新
6.updated
数据更改导致DOM重新渲染后,会执行该钩子函数。此时数据和dom同步。感觉不需要举例,嘿嘿嘿
7.beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。可以在该钩子函数中,清除定时器。
8.destroyed
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除。
5.vue 父子组件通信
1. prop实现通信
子组件的props选项能够接收来自父组件数据。没错,仅仅只能接收,props是单向绑定的,即只能父组件向子组件传递,不能反向。而传递的方式也分为两种:
(1)静态传递
子组件通过props选项来声明一个自定义的属性,然后父组件就可以在嵌套标签的时候,通过这个属性往子组件传递数据了。
(2)动态传递
我们已经知道了可以像上面那样给 props 传入一个静态的值,但是我们更多的情况需要动态的数据。这时候就可以用 v-bind 来实现。通过v-bind绑定props的自定义的属性,传递去过的就不是静态的字符串了,它可以是一个表达式、布尔值、对象等等任何类型的值。
2.通过$ref 实现通信
对于ref官方的解释是:ref 是被用来给元素或子组件注册引用信息的。引用信息将会注册在父组件的 $refs 对象上。
3.通过emit实现通信
上面两种示例主要都是父组件向子组件通信,而通过emit 实现通信**上面两种示例主要都是父组件向子组件通信,而通过emit实现通信∗∗上面两种示例主要都是父组件向子组件通信,而通过emit 实现子组件向父组件通信。
$emit 绑定一个自定义事件event,当这个这个语句被执行到的时候,就会将参数arg传递给父组件,父组件通过@event监听并接收参数。
6.vue中的computed属性的原理?
详细看:https://www.jianshu.com/p/d95a7b8afa06
下面这段话的理解详细看:https://www.cnblogs.com/eret9616/p/12863102.html
在initComputed的时候,会将computed对象中的每一个key创建一个watcher(computed wacther)并将自己的lazy属性设置为true(表明computed
是有缓存的,是惰性的,也就是里面的通过this.访问的data属data性值不会触发getter),watcher的getter就是你写的函数,当依赖变化的时候(也就是你写的函数中的数据发生了改变的时候,会通知watcher,执行watcher的update函数,又因为在.prototype.update方法中会判断 if(this.lazy){this.dirty = true},),这个watcher 会把dirty属性变为true(注意并不会计算,只是把dirty属性变为true了)。 此时并不计算自己的值, 然后将computed的key通过defineComputed方法将getter和setter设置到vm上。同时,让组件的渲染watcher(render-watcher)也收集依赖,当依赖变化的时候,触发渲染watcher的Update方法, 会判断computed watcher是否为dirty,如果为dirty,那么计算,拿到值,再把dirty设为false,否则直接拿值。
对比侦听器 watch
当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的 ,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
computed 和 watch 的差异:
- computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm
上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props) - computed 计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算,而watch 则是当数据发生变化便会调用执行函数
- 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;
7.Vuex实现原理?
在vue初始化的时候在所有组件的beforeCreate生命周期注入了设置this.$store这样一个对象,其本质就是将传入的state作为一个隐藏的vue组件的data,也就是说commit操作,本质上其实是修改这个组件的data值。(通过修改被defineReactive代理的对象值后,会将其收集到的依赖的watcher中的dirty设置为true,等到下一次访问该watcher中的值后重新获取最新值。)因此我们在 Vue 的组件中可以通过 this.$store.xxx
访问到 Vuex 的各种数据和状态。
一句话总结vuex的工作原理:vuex中的store本质就是没有
template的隐藏着的vue组件;
这样就能解释了为什么vuex中的state的对象属性必须提前定义好,如果该state中途增加一个属性,因为该属性没有被defineReactive,所以其依赖系统没有检测到,自然不能更新。
8.action和mutations区别联系?
action的功能和mutation是类似的,都是去变更state,不过action和mutation有两点不同:
1、action主要处理的是异步的操作,mutation必须同步执行,执行完action之后会返回一个promise,也就是说action中我们既可以处理同步,也可以处理异步的操作。
2、action改变状态,最后都是通过提交mutation来完成的
9.如果让你实现一下类似 vuex 的工具 , 你会怎么想呢?
我认为 vuex , 可以理解为一个全局对象 。在全局对象里面修改状态。
class Vuex {
constructor() {
this.state = {};
}
getter() {
}
mutations() {
}
actions () {
}
}
衍生问题1:vuex解决了什么问题?
解决两个问题:
- 多个组件依赖于同一状态时,对于多层嵌套的组件的传参将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
- 不同组件的行为需要变更同一状态。以往采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
衍生问 题2:什么时候用Vuex?
当项目遇到以下两种场景时:
- 多个组件依赖于同一状态时。
- 来自不同组件的行为需要变更同一状态。
衍生问题3:Vuex中5个核心属性?
- state:存储状态(变量)
- getters:对数据获取之前的再次编译,可以理解为state的计算属性。
getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 - mutations:修改状态,并且是同步的。在组件中使用$store.commit('',params)。这个和我们组件中的自定义事件类似。
- actions:异步操作。在组件中使用是$store.dispath('')
- modules:store的子模块,为了开发大型项目,方便状态管理而使用的。这里我们就不解释了,用起来和上面的一样。
衍生问题4:mixins和vuex的区别?
vuex:用来做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。
Mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的,值的修改在组件中不会相互影响。