1、什么是 MVVM?
Model–View–ViewModel (MVVM) 是一个软件架构设计模式。MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。
MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
(1)View 层 View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
(2)Model 层 Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
(3)ViewModel 层 ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。
需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维ViewModel更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现:
(1)View 层
<div id="app">
<p>{{ message }}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
(2)ViewModel 层
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
},
methods: {
showMessage() {
let vm = this;
alert(vm.message);
}
},
created() {
let vm = this;
ajax({
url: '/your/server/data/api',
success(res) {
vm.message = res;
}
});
}
})
(3) Model 层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
2、Vue 是如何实现数据双向绑定的?
Vue 内部通过 Object.defineProperty 方法属性拦截的方式,把 data 对象里每个数据的读写转化成 getter/setter ,当数据变化时通知视图更新。
1、什么是MVVM数据双向绑定 Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
- 输入框内容变化时,
Data 中的数据同步变化。即 View => Data 的变化。 Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以我们本文主要讨论如何根据 Data 变化更新 View。
我们会通过实现以下 4 个步骤,来实现数据的双向绑定:
-
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter 。这样的话,给这个对象的某个值赋值,就会触发 setter ,那么就能监听到了数据变化。 -
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。 -
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。 -
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher ,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
以上四个步骤的流程图表示如下:
3、Proxy 与 Object.defineProperty 优劣对比
Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于
apply 、ownKeys 、deleteProperty 、has 等等是 Object.defineProperty 不具备的; - Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而
Object.defineProperty 只能遍历对象属性直接修改; - Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
4、谈谈虚拟DOM
1、虚拟DOM的优缺点
优点:
- 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
2、虚拟 DOM 实现原理
虚拟 DOM 的实现原理主要包括以下 3 部分:
- 用 JS对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 —— 比较两棵虚拟 DOM 树的差异;
- patch 算法 —— 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
5、介绍一下Vuex ?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store (仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。 (1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 (2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。 主要包括以下几个模块:
State :定义了应用状态的数据结构,可以在这里设置默认的初始状态。Getter :允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将store 中的 getter 映射到局部计算属性。Mutation :是唯一更改 store 中状态的方法,且必须是同步函数。Action :用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。Module :允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。 *
6、vue-router 路由模式有几种?
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
其中,3 种路由模式的说明如下:
-
hash (浏览器环境): 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器; -
history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式; -
abstract (Node.js环境) : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
7、能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?
(1)hash 模式的实现原理
www.test.com/#/ 就是 Hash URL ,当 # 后面的哈希值发生变化时,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何变化,服务端接收到的 URL 请求永远是 www.test.com
window.addEventListener('hashchange', () => {
})
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 ‘#search’:
https://www.baidu.com#search
(2)history 模式的实现原理
History 模式是 HTML5 新推出的功能,主要使用history.pushState 和 history.replaceState 改变 URL。
这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
两种模式对比
Hash 模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
9、Vue 组件间的通信方式?
Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信。
(1)props / $emit 适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref 与 $parent / $children 适用 父子组件通信
ref :如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent / $children :访问父 / 子实例
(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
实现简单的一个事件总线(发布——订阅模式)
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName,callback) {
if (!this.events[eventName]) {
this.events[eventName] = [callback];
} else {
this.events[eventName].push(callback)
}
}
emit(eventName) {
this.events[eventName] && this.events[eventName].forEach(cb => cb());
}
}
(4)$attrs /$listeners 适用于 隔代组件通信
-
$attrs :包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。 -
$listeners :包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
(5)provide / inject 适用于 隔代组件通信 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
(6)Vuex 适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store (仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
参考文章
30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度) 0 到 1 掌握:Vue 核心之数据双向绑定
|