这个来自以前作的培训,删减了一些业务相关的,参考了不少资料( 参考资料列表),谢谢前辈们,么么哒 😘
随着前端技术的发展,前端框架也在不断的改变。前端
DOM(Document Object Model,文档对象模型)将 HTML 文档表达为树结构,并定义了访问和操做 HTML 文档的标准方法。jquery
前端开发基本上都会涉及到HTML页面,也就避免不了和DOM打交道。web
最先期的Web前端,就是一个静态的黄页,网页上的内容不能更新。算法
慢慢的,用户能够在Web页面上进行一些简单操做了,好比提交表单,文件上传。可是整个页面的部分或者总体的更新,仍是靠刷新页面来实现的。编程
随着AJAX技术的出现,前端页面上的用户操做愈来愈多,愈来愈复杂,因此就进入了对DOM元素的直接操做时代。要对DOM元素操做,就要使用DOM API,常见的DOM API有:json
类型 | 方法 |
---|---|
节点查询 | getElementById、getElementsByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll |
节点建立 | createElement、createDocumentFragment、createTextNode、cloneNode |
节点修改 | appendChild、replaceChild、removeChild、insertBefore、innerHTML |
节点关系 | parentNode、previousSibling、childNodes |
节点属性 | innerHTML、attributes、getAttribute、setAttribure、getComputedStyle |
内容加载 | XMLHttpRequest、ActiveX |
使用DOM API能够完成前端页面中的任何操做,可是随着网站应用的复杂化,使用原生的API很是低效。因此 jQuery 这个用来操做DOM的交互框架就诞生了。segmentfault
jQuery 为何能成为在这个时代最流行的框架呢?主要是他帮前端开发人员解决了太多问题:设计模式
除了解决了上面这些问题,jQuery还拥有良好的生态,海量的插件拿来即用,让前端开发比之前流畅不少。尤为是在IE六、IE7时代,没有jQuery,意味着无穷的兼容性处理。浏览器
// DOM API:
document.querySelectorAll('#container li');
// jQuery
$('#container').find('li');复制代码
随着HTML5技术的发展,jQuery提供的不少方法已经在原生的标准中实现了,慢慢的,jQuery的必要性在逐渐下降。youmightnotneedjquery.com/bash
渐渐地,SPA(Single Page Application,单页面应用)开始被普遍承认,整个应用的内容都在一个页面中并彻底经过异步交互来加载不一样的内容,这时候使用 jQuery 直接操做DOM的方式就不容易管理了,页面上事件的绑定会变得混乱,在这种状况下,迫切须要一个能够自动管理页面上DOM和数据之间交互操做的框架。
MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它经过分离关注点来改进代码的组织方式。
单纯从概念上,很难区分和感觉出来这三种模式在前端框架中有什么不一样。咱们经过一个例子来体会一下:有一个能够对数值进行加减操做的组件:上面显示数值,两个按钮能够对数值进行加减操做,操做后的数值会更新显示。
Model层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。这里咱们把须要用到的数值变量封装在Model中,并定义了add、sub、getVal三种操做数值方法。
var myapp = {}; // 建立这个应用对象
myapp.Model = function() {
var val = 0; // 须要操做的数据
/* 操做数据的方法 */
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};复制代码
View做为视图层,主要负责数据的展现。
myapp.View = function() {
/* 视图元素 */
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
/* 渲染数据 */
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
};复制代码
这里,经过Model&View完成了数据从模型层到视图层的逻辑。但对于一个应用程序,这远远是不够的,咱们还须要响应用户的操做、同步更新View和Model。
MVC(Model View Controller)是一种很经典的设计模式。用户对View的操做交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操做,一旦Model发生变化便通知相关视图进行更新。
Model层用来存储业务的数据,一旦数据发生变化,模型将通知有关的视图。
// Model
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
/* 观察者模式 */
var self = this,
views = [];
this.register = function(view) {
views.push(view);
};
this.notify = function() {
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};复制代码
Model和View之间使用了观察者模式,View事先在此Model上注册,进而观察Model,以便更新在Model上发生改变的数据。
View和Controller之间使用了策略模式,这里View引入了Controller的实例来实现特定的响应策略,好比这个栗子中按钮的 click 事件:
// View
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
/* 绑定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};复制代码
控制器是模型和视图之间的纽带,MVC将响应机制封装在Controller对象中,当用户和应用产生交互时,控制器中的事件触发器就开始工做了。
// Controller
myapp.Controller = function() {
var model = null,
view = null;
this.init = function() {
/* 初始化Model和View */
model = new myapp.Model();
view = new myapp.View(this);
/* View向Model注册,当Model更新就会去通知View啦 */
model.register(view);
model.notify();
};
/* 让Model更新数值并通知View更新视图 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};复制代码
这里咱们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View作更新。
能够明显感受到,MVC模式的业务逻辑主要集中在Controller,而前端的View其实已经具有了独立处理用户事件的能力,当每一个事件都流经Controller时,这层会变得十分臃肿。并且MVC中View和Controller通常是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的链接让Controller的复用性成了问题,若是想多个View共用一个Controller该怎么办呢?
MVP(Model-View-Presenter)是MVC模式的改良。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。
在MVC里,View是能够直接访问Model的。而MVP中的View并不能直接使用Model,而是经过为Presenter提供接口,让Presenter去更新Model,再经过观察者模式更新View。
与MVC相比,MVP模式经过解耦View和Model,彻底分离视图和模型使职责划分更加清晰;因为View不依赖Model,能够将View抽离出来作成组件,它只须要提供一系列接口提供给上层操做。
// Model
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};复制代码
Model层依然是主要与业务相关的数据和对应处理数据的方法,很简单。
// View
myapp.View = function() {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
this.init = function() {
var presenter = new myapp.Presenter(this);
$incBtn.click(presenter.increase);
$decBtn.click(presenter.decrease);
};
};复制代码
MVP定义了Presenter和View之间的接口,用户对View的操做都转移到了Presenter。好比这里的View暴露setter接口(render方法)让Presenter调用,待Presenter通知Model更新后,Presenter调用View提供的接口更新视图。
// Presenter
myapp.Presenter = function(view) {
var _model = new myapp.Model();
var _view = view;
_view.render(_model);
this.increase = function() {
_model.add(1);
_view.render(_model);
};
this.decrease = function() {
_model.sub(1);
_view.render(_model);
};
};复制代码
Presenter做为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码须要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。若是Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也须要改动。
MVVM(Model-View-ViewModel)最先由微软提出。ViewModel指 "Model of View"——视图的模型。
MVVM把View和Model的同步逻辑自动化了。之前Presenter负责的View和Model同步再也不手动地进行操做,而是交给框架所提供的数据绑定功能进行负责,只须要告诉它View显示的数据对应的是Model哪一部分便可。
咱们使用Vue来完成这个栗子。
在MVVM中,咱们能够把Model称为数据层,由于它仅仅关注数据自己,不关心任何行为(格式化数据由View的负责),这里能够把它理解为一个相似json的数据对象。
// Model
var data = {
val: 0
};复制代码
和MVC/MVP不一样的是,MVVM中的View经过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会经过数据绑定更新到View。
<!-- View -->
<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>复制代码
ViewModel大体上就是MVC的Controller和MVP的Presenter了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心就是数据绑定。与MVP不一样的是,没有了View为Presente提供的接口,以前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。
new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v;
}
},
sub(v) {
if(this.val > 0) {
this.val -= v;
}
}
}
});复制代码
总体来看,比MVC/MVP精简了不少,不只仅简化了业务与界面的依赖,还解决了数据频繁更新(以前用jQuery操做DOM很繁琐)的问题。由于在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式可使开发过程更加容易,提升应用的可重用性。
在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。其实双向数据绑定,能够简单地理解为一个模版引擎,可是会根据数据变动实时渲染。
有人还不要脸的申请了专利:
不一样的MVVM框架中,实现双向数据绑定的技术有所不一样。目前一些主流的实现数据绑定的方式大体有如下几种:
手动触发指令绑定是比较直接的实现方式,主要思路是经过在数据对象上定义get()方法和set()方法,调用时手动触发get ()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的从新渲染功能。
Angularjs是典型的使用脏检测机制的框架,经过检查脏数据来进行View层操做更新。
脏检测的基本原理是在ViewModel对象的某个属性值发生变化时找到与这个属性值相关的全部元素,而后再比较数据变化,若是变化则进行Directive 指令调用,对这个元素进行从新扫描渲染。
数据劫持是目前使用比较普遍的方式。其基本思路是使用 Object.defineProperty 和 Object.defineProperies 对ViewModel数据对象进行属性get ()和set()的监听,当有数据读取和赋值操做时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就能够了。
Vue就是典型的采用数据劫持和发布订阅模式的框架。
以前咱们说过 Proxy 实现数据劫持的方法:
总结来看,前端框架从直接DOM操做到MVC设计模式,而后到MVP,再到MVVM框架,前端设计模式的改进原则一直向着高效、易实现、易维护、易扩展的基本方向发展。虽然目前前端各种框架也已经成熟并开始向高版本迭代,可是尚未结束,咱们如今的编程对象依然没有脱离DOM编程的基本套路,一次次框架的改进大大提升了开发效率,可是DOM元素运行的效率仍然没有变。对于这个问题的解决,有的框架提出了Virtual DOM的概念。
MVVM的前端交互模式大大提升了编程效率,自动双向数据绑定让咱们能够将页面逻辑实现的核心转移到数据层的修改操做上,而再也不是在页面中直接操做DOM。尽管MVVM改变了前端开发的逻辑方式,可是最终数据层反应到页面上View层的渲染和改变还是经过对应的指令进行DOM操做来完成的,并且一般一次ViewModel的变化可能会触发页面上多个指令操做DOM的变化,带来大量的页面结构层DOM操做或渲染。
好比一段伪代码:
<ul>
<li repeat="list">{{ list.value }}</li>
</ul>
let viewModel = new VM({
data:{
list:[{value: 1},{value: 2},{value: 3}]
}
})复制代码
使用MVVM框架生成一个数字列表,此时若是须要显示的内容变成了 [{value: 1}, {value: 2}, {value: 3}, {value: 4}]
,在MVVM框架中通常会从新渲染整个列表,包括列表中无须改变的部分也会从新渲染一次。 但实际上若是直接操做改变DOM的话,只须要在<ul>子元素最后插入一个新的<li>元素就能够了。但在通常的MVVM框架中,咱们一般不会这样作。毫无疑问,这种状况下MVVM的View层更新模式就消耗了更多不必的性能。
那么该如何对ViewModel进行改进,让浏览器知道实际上只是增长了一个元素呢?经过对比
[{value: 1},{value: 2},{value: 3}]
和 [{value: 1}, {value: 2}, {value: 3}, {value: 4}]
其实只是增长了一个 {value: 4}
,那么该怎样将这个增长的数据反映到View层上呢?能够将新的Model data 和旧的Model data 进行对比,而后记录ViewModel的改变方式和位置,就知道了此次View 层应该怎样去更新,这样比直接从新渲染整个列表高效得多。
这里其实能够理解为,ViewModel 里的数据就是描述页面View 内容的另外一种数据结构标识,不过须要结合特定的MVVM描述语法编译来生成完整的DOM结构。
能够用JavaScript对象的属性层级结构来描述上面HTML DOM对象树的结构,当数据改变时,新生成一份改变后的Elements,并与原来的Elemnets结构进行对比,对比完成后,再决定改变哪些DOM元素。
刚才例子里的 ulElement 对象能够理解为VirtualDOM。一般认为,Virtual DOM是一个可以直接描述一段HTMLDOM结构的JavaScript对象,浏览器能够根据它的结构按照必定规则建立出肯定惟一的HTML DOM结构。总体来看,Virtual DOM的交互模式减小了MVVM或其余框架中对DOM的扫描或操做次数,而且在数据发生改变后只在合适的地方根据JavaScript对象来进行
最小化的页面DOM操做,避免大量从新渲染。
Virtual-DOM的执行过程:
用JS对象模拟DOM树 -> 比较两棵虚拟DOM树的差别 -> 把差别应用到真正的DOM树上
在Virtual DOM中,最主要的一环就是经过对比找出两个Virtual DOM的差别性,获得一个差别树对象。
对于Virtual DOM的对比算法其实是对于多叉树结构的遍历算法。可是找到任意两个树之间最小的修改步骤,通常会循环递归对节点进行依次对比,算法复杂度达到 O(n^3),这个复杂度很是高,好比要展现1000多个节点,最悲观要依次执行上十亿次的比较。因此不一样的框架采用的对比算法实际上是一个略简化的算法。
拿React来讲,因为web应用中不多出现将一个组件移动到不一样的层级,绝大多数状况下都是横向移动。所以React尝试逐层的对比两棵树,一旦出现不一致,下层就再也不比较了,在损失较小的状况下显著下降了比较算法的复杂度。
前端框架的演进很是快,因此只有知道演进的缘由,才能去理解各个框架的优劣,从而根据应用的实际状况来选择最合适的框架。对于其余技术也是如此。