本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
各位,本节是看不懂系列的最后一篇文章了,非常感谢张容铭老师的 《JavaScript设计模式》 这本书我从头到尾完整的读了三遍,对我个人的提升真的非常巨大,也推荐各位有机会也读一读。
本节要介绍的设计模式,也是书中的最后一章,是目前公认的最优框架模式, MVVM ,从名字就可以看出, MVVM 也是从 MVC 模式演变过来的,准确的说是从 MVP 模式演变的。
MVC 演变到 MVP 是因为每次新增需求,要修改视图层和控制器层。那 MVP 演变到 MVVM 是因为什么呢?
有需求才有发展,大家感觉一下,上一节的 MVP 模式有什么缺点不?
好像没什么缺点吧,除了不太好理解以外,它只要修改管理器就能灵活的增加需求。
其实“不好理解”,就是 MVP 模式的缺点。一个优秀的框架最核心的一定是要满足快速开发, MVP 不能大规模应用的原因就在于它必须要在理解原理的基础下,才能进行管理器层的相关操作,这就导致很多初学者前期投入成本太高,增加了整个项目的投入。
这对于这一问题,就衍生出了 MVVM 模式。
MVVM 模式
MVVM 即模型(Model)—视图(View)—视图模型(ViewModel)。
为视图层( View )量身定做一套视图模型( ViewModel ),并在视图模型中( ViewModel )中创建属性和方法,为视图层( View )绑定数据( Model )并实现交互。
各位思考一个问题,上一节我们将视图独立出来,通过管理器来控制视图,那我们能不能反过来,通过创建视图来控制管理器呢?
当然可以了,创建视图就是创建页面内的视图,因此本质上就是在页面里写 HTML 。如果将视图作用提升,通过在页面中直接书写 HTML 代码创建视图组件,让控制器或管理器去监听这些视图组件,并处理这些组件完成预期功能,这种实现方式是不是可以让那些只熟悉 HTML 的开发人员轻松完成一些功能需求。
这就是江湖上被各路英雄豪杰所赞誉的 MVVM 模式。
创建一个实例来展示试试,这里我们创建一个滑动条与进度条。
我们预期的目标是通过以下方式实现。
<div class="first" data-bind="type : 'slider', data: demo1"></div>
<div class="second" data-bind="type : 'slider', data: demo2">test</div>
<div class="third" data-bind="type : 'progressbar', data: demo3"></div>
上面的代码我们分析以下,首先 HTML 代码即是 MVVM 中的视图( V )层,三个组件可以分为两类。
第一类是前两个,滑动组件,第二类是最后一个,进度条组件。通过自定义属性 data-bind 中的 type 值可以看出。而 data-bind 属性中的内容格式和对象非常相似,只是少一对大括号。后面的 data 值代表的是组件所需的数据模型,当然这些数据要在数据模型中给出。
我们的核心目的是通过 data-bind 自定义属性值为元素绑定 JavaScript 行为。
对于视图( V )层内的元素是要被视图模型( VM )层监听的,因此我们在视图模型( VM )层中实现对这些元素的监听,并为他们绑定行为。因此我们接下来要做的就是创建 VM 对象,不过在此之前我们要先创建一个 VM 环境。
视图模型层
//屏蔽压缩报错
~(function() {
//在闭包中获取全局变量
var window = this || (0, eval)('this');
//获取页面字体大小,作为创建页面 UI 尺寸参照物
var FONTSIZE = function() {
//获取页面 body 元素字体大小并转化成整数
return parseInt(document.body.currentStyle ? document.body.currentStyle['fontSize'] :
getComputedStyle(document.body, false)['fontSize']);
} ();
//视图模型对象
var VM = function() {};
//将视图模型对象绑定在 Window 上,供外部获取
window.VM = VM;
}) ();
在 VM 环境中,我们首先获取全局变量 Window (当然对于非浏览器环境下全局变量并非 Window ),并获取页面 body 元素中字号作为创建页面 UI 组件的参考尺寸。最后将模型视图( VM )层对象绑定在 Window 上供外部操作。
接下来我们要做的就是处理视图模型对象 VM 。在该对象中我们保存实例化组件方法,由于创建每类组件方法不同,因此我们将创建 UI 组件方法作为策略对象的方法保存,这样日后更容易拓展其他 UI 组件。
var VM = function() {
//组件创建策略对象
var Method = {
//进度条组件创建方法
progressbar: function() {};
//滑动条组件创建方法
slider: function() {}
}
} ();
由于实例中只有两个组件,我们在策略对象中只创建两个组件策略方法, progressbar 可以创建进度条, slider 可以创建滑动条。下面我们依次实现这两个策略方法。
这部分属于 VM 的核心,日后我们增加其他组件只需要在策略对象中添加创建组件的策略方法就可以了。
当然我们也可以为 Method 对象添加拓展方法,这样我们即可在外部拓展该策略对象以创建出更复杂多样的 UI 组件了。
/**
* 进度条组件创建方法
* dom 进度条容器
* data 进度条数据模型
*/
progressbar: function (dom, data) {
//进度条完成容器
var progress = document.createElement('div'),
//数据模型数据,结构 {position: 50}
param = data.data;
//设置进度完成容器尺寸
progress.style.width = (param.position || 100) + '%';
//为进度条组件添加 UI 样式
dom.className += ' ui-progressbar';
//进度完成容器元素插入进度条容器中
dom.appendChild(progress);
}
有了创建 progressbar 组件的经验我们再创建 slider 组件便轻松许多。我们观察滑动条,发现他们都有一个深灰色的滑动拨片。滑动容器以及滑动容器内部的深灰色的进度容器,当然对于复杂滑动条来说,它还要拥有容量提示信息与滑动拨片信息,因此我们按照示例中的滑动条的结构将滑动条创建出来。
/**
* 滑动条组件创建方法
* dom 滑动条内容
* data 滑动条数据模型
*/
slider: function(dom, data) {
//滑动条拨片
var bar = document.createElement('span'),
//滑动条进度容器
progress = document.createElement('div'),
//滑动条容量提示信息
totleText = null,
//滑动条拨片提示信息
progressText = null,
//数据模型数据, 结构 {position: 60, totle: 200}
param = data.data,
//容器元素宽度
width = dom.clientWidth,
//容器元素横坐标
left = dom.offsetLeft,
//拨片位置(以模型数据中 position 数据计算)
realWidth = (param.position || 100) * width / 100;
//清空滑动条容器,为创建滑动条做准备
dom.innerHTML = '';
//如果模型数据中提供容器总量信息(param.totle),则创建滚动条提示文案
if(param.totle) {
//容器总量提示文案
text = document.createElement('b');
//拨片位置提示文案
progressText = document.createElement('em');
//设置容器总量提示文字
text.innerHTML = param.totle;
//将容器总量提示文案元素添加到滑动条组件中
dom.appendChild(text);
//将拨片位置提示文案元素添加到滑动条组件中
dom.appendChild(progressText);
}
//设置滑动条
setStyle(realWidth);
//为滑动条组件添加样式
dom.className += ' ui-slider';
//将进度容器添加到滑动条组件中
dom.appendChild(progress);
//将拨片添加到滑动条组件中
dom.appendChild(bar);
//设置滑动条
function setStyle(w) {
//设置进度容器宽度
progress.style.width = w + 'px';
//设置拨片横坐标
bar.style.left = w - FONTSIZE / 2 + 'px';
//如果有拨片提示文案
if(progressText) {
//设置拨片提示文案横坐标
progressText.style.left = w - FONTSIZE / 2 * 2.4 + 'px';
//设置拨片提示文案内容
progressText.innerHTML = parseFloat(w / width * 100).toFixed(2) + '%';
}
}
}
滚动条创建出来后,我们还需要为它添加交互事件,这样才能使用,比如当拖拽滚动条拨片时,滚动条提示文案与拨片位置及进度容器宽度都要随之改变,当鼠标松开时停止交互。
slider: function(dom, data) {
//创建组件逻辑
//...
//按下鼠标拨片
bar.onmousedown = function() {
//移动拨片(光标在页面中滑动,事件绑定给 document 是为了优化交互体验,使光标可以在页面中自由滑动)
document.onmousedown = function(event) {
//获取事件源
var e = event || window.event;
//鼠标光标相对于滑动条容器位置原点移动的横坐标
var w = e.clientX - left;
//设置滑动条
setStyle(w < width ? (w > 0 ? W : 0) : width);
}
//阻止页面滑动选取事件
document.onselectstart = function() {
return false;
}
}
//停止滑动交互(鼠标按键松开)
document.onmouseup = function() {
//取消文档鼠标光标移动事件
document.onmousemove = null;
//取消文档滑动选取事件
document.onselectstart = null;
}
}
创建组件的方法是 VM 对象中偏重于视图方面的逻辑,当然完整的 VM 对象还需要对模型数据的处理,因此我们要有一个获取数据的方法 getBindData 通过元素中给定的数据映射标识获取对应数据来渲染我们的视图。
var VM = function() {
var Method = {
//...
}
/**
* 获取视图层组件渲染数据的映射信息
* dom 组件元素
*/
function getBindData(dom) {
//获取自定义属性 data-bing 值
var data = dom.getAttribute('data-bind');
//将自定义属性 data-bind 值转化为对象
return !!data && (new Function("return ({" + data + "})")) ();
}
} ();
有了创建视图组件的方法以及获取组件映射数据的方法,对我们实现 VM 对象功能来说如虎添翼,所以,我们在 VM 中只需要获取页面中那些需要渲染的组件即可。
var VM = function() {
var Method = {
//...
}
function getBindData(dom) {
//...
}
//组件实例化方法
return function() {
//获取页面中所有元素
var doms = document.body.getElementsByClassName('*'),
//元素自定义数据句柄
ctx = null;
//ui 处理是会向页面中插入元素,此时 doms.length 会改变,此时动态获取 dom.length
for(var i = 0; i < doms.length; i++) {
//获取元素自定义数据
ctx = getBindData(doms[i]);
//如果元素是 UI 组件,则根据自定义属性中组件类型,渲染该组件
ctx.type && Method[ctx.type] && Method[ctx.type](doms[i], ctx);
}
}
} ();
此时 VM 部分基本完成,最后我们要为 M 数据模型层添加一些数据。
//数据模型层中获取到的组件渲染数据
//带有提示文案的滑动条
var demo1 = {
position: 60,
totle: 200
},
//简易版滑动条
demo2 = {
position: 20
},
//进度条
demo3 = {position: 50};
渲染组件很容易。
window.onload = function() {
//渲染组件
VM();
}
当然了,为了完善组件,我们可以为页面添加滚动条事件与窗口尺寸重置事件来优化组件交互。
总个小结
MVVM 与 MVC 和 MVP 模式的不同在于 MVVM 模式使视图更灵活,可以独立于数据模型层,视图模型层而独立修改,自由创建。当然这也使得数据模型层可以独立变化,甚至一个视图模型层可以对应多个视图层或者数据模型层。
MVVM 开发模式一个最重要的特征就是视图层独立开发,这就让那些不熟悉 JavaScript 的人,只要了解了 HTML 就可以按照视图层规范格式完成一个复杂页面的开发,而且测试问题也变得很容易,只需要针对视图模型层撰写测试代码即可。
本篇到此,《看不懂》系列也就到此结束了,感谢各位的陪伴,设计模式的总结还远没有停止,所有这些模式都是前辈们在实际开发中总结出的技巧,经验。这些经验都是不断丰富,不断发展的,各位也可以总结自己的设计模式,只要够靓,就会成为经典。
江湖路远,有缘再见~