本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
各位有没有做过分页,做分页一定会涉及到上一页,下一页,我们为了页面的流畅性,一般都是只请求当前页的数据,当点击上下页按钮的时候,才会请求对应的数据。
这个策略没错,但是用户一般都很调皮,不会老老实实,只点下一页,或者只点上一页,有时候可能会来回,上一页,下一页疯狂点击,那么这种情况就会疯狂请求数据。
页面很可能会卡死,那有没有什么方法能解决这种调皮鬼行为呢?
有时候我们可以增加点击限制,数据没返回之前,不允许第二次点击,但是这样界面交互性就不太好,有没有什么方法能即满足用户,又提升页面交互?
那就需要今天的这种设计模式,备忘录模式了。
备忘录模式
在不破坏对象封装性的前提下,在对象之外捕获,并保存该对象的内部状态以便日后对象使用或者对象恢复到以前的某个状态 。
大家先来看一下下面的代码:
//获取数据
function getPageData() {
//异步请求获取数据
//...
showPage(page, data);
}
//展示某页逻辑
function showPage(page, data) {
//展示某页逻辑
}
//下一页按钮点击事件
$('#next_page').click(function() {
//获取列表内容元素
var $list = $('#list_content').
//获取列表当前页
page = $list.data('page');
//获取并展示列表
getPageData(page, function() {
//修正列表内容元素当前页数据
$list.data('page', page + 1);
})
});
//上一页按钮点击事件
$('#next_page').click(function() {
//上一页逻辑
//...
});
各位仔细看,上面代码有什么问题?
上面代码没有任何问题。
大伙别打我,代码是没问题,但是如果用户点完下一页,又点一下上一页,这是不是就造成浪费了,第一次我们已经获取到数据了,不需要再浪费请求。而且移动端也会造成不必要的流量浪费。
解决这一问题,我们可以创建一个缓存器来存放请求到的数据
//Page备忘录类
var Page = function() {
//信息缓存对象
var cache = {
};
/**
* 主函数
* 参数 page 页码
* 参数 fn 成功回调函数
*/
return function(page, fn) {
//判断该页数据是否在缓存中
if(cache[page]) {
//恢复到该页状态,显示该页内容
showPage(page, cache[page]);
//执行成功回调函数
fn && fn();
} else {
//若缓存 cache 中无该页数据
$.post('./data/getData.php', {
page: page
}, function(res) {
//成功返回
if(res.errNo == 0) {
//显示该页数据
showPage(page, res.data);
//将该数据存入缓存
cache[page] = res.data;
//执行成功回调
fn && fn();
} else {
//处理异常
}
})
}
}
} ()
Page 类里面缓存了每次请求的数据,就不需要每次都请求新的重复的数据了。对于上一页下一页的处理逻辑,可以直接用 Page 类替换掉原来的 getPageData 方法即可。
//下一页按钮点击事件
$('#next_page').click(function() {
//获取列表内容元素
var $list = $('#list_content').
//获取列表当前页
page = $list.data('page');
//获取并展示列表
Page(page, function() {
//修正列表内容元素当前页数据
$list.data('page', page + 1);
})
});
简简单单的改动,就能优化出这么理想的效果,设计模式的应用改变的是开发的核心思想。
迭代器模式
在不暴露对象内部结构的同时,可以顺序的访问聚合对象内部的元素 。
只要是写程序,就没有不用循环的,有时候循环能帮我们节约时间,但是有时候,循环多了会让代码显得很臃肿。所以循环是把双刃剑,本节就和大家一起研究研究怎么把循环玩出门道。
举个例子,我们写一个轮播图,有的要求淡入淡出,有的要求滑动,不同需求,总会有不同的特点,这时候我们就可以创建一个迭代器,来帮我们实现。
//迭代器
vat Iterator = function(items, container) {
//获取父容器,若 container 参数存在,并且可以获取该元素则获取,否则获取 document
var container = container && document.getElementById(container) || document,
//获取元素
items = container.getElementsByTagName(items),
//获取元素长度
length = items.length,
//当前索引 默认:0
index = 0;
.//缓存原生数组 splice 方法
var splice = [].splice;
return {
//获取第一个元素
first: function() {
},
//获取最后一个元素
second: function() {
},
//获取前一个元素
pre: function() {
},
//获取后一个元素
next: function() {
},
//获取某一个元素
get: function() {
},
//对每一个元素执行某一个方法
dealEach: function() {
},
//对某一元素执行某一方法
dealItem: function() {
},
//排他方式处理某一元素
exclusive: function() {
}
}
}
实现迭代器如下:
//获取第一个元素
first: function() {
index = 0;
return items[index];
},
//获取最后一个元素
second: function() {
index = length - 1;
return item[index];
},
对于前一个后一个元素的获取方法稍微复杂点,前一个元素要判断当前是不是第一个,是的话就返回空,并且索引重置为 0 ,后一个同样返回空,但索引要变为 length - 1 。
//获取前一个元素
pre: function() {
if(--index > 0) {
return items[index];
} else {
index = 0;
return null;
}
},
//获取后一个元素
next: function() {
if(++index < length) {
return items[index];
} else {
index = length - 1;
return null;
}
},
对于获取某一个元素的方法,需要注意,如果参数大于等于 0 ,则直接对 length 取模获取对应的元素,并设置索引值,否则对 length 取模后还要加上 length 才能获取对应的索引值,此时是从后向前搜索元素。
//获取某一个元素
get: function(num) {
//如果 num 大于 0 再正向获取,否则逆向获取
index = num >= 0 ? num % length : num % length + length;
//返回对应元素
return items[index];
},
对于每一个元素执行某一方法,可以通过我们之前提到过的访问者模式使回调函数在每一个元素的作用域中执行一次,此时如果传入的参数大于一个,则将多余参数作为回调函数的参数传递。
//对每一个元素执行某一个方法
dealEach: function(fn) {
//第二个参数开始为回调函数中参数
var args = splice.call(arguments, 1);
//遍历元素
for(var i = 0; i < length; i++) {
//对元素执行回调函数
fn.apply(items[i], args);
}
},
对于处理某一元素的方法,实现起来很简单,只需要将回调函数执行时的作用域变为该元素即可,如果传入的参数个数大于两个,则将多余参数作为回调函数的参数传递。
//对某一元素执行某一方法
dealItem: function(fn) {
//对元素执行回调函数,注:1.第三个参数开始为回调函数中参数 2 通过 this.get 方法设置 index 索引
fn.apply(this.get(num), splice.call(arguments, 2))
},
排他方法处理某一元素的思想是综合 dealEach 方法与 dealItem 方法。但要注意,如果传入的参数为数组,是表示处理多个元素,这次要执行一次遍历。
//排他方式处理某一元素
exclusive: function(num, allFn, unmFn) {
//对所有元素执行回调函数
this.dealEach(allFn);
//如果 num 类型为数组
if(Object.prototype.toString.call(num) === '[Object Array]') {
//遍历 num 数组
for(var i = 0, len = num.length; i < len; i++) {
//分别处理数组中每一个元素
this.dealItem(num[i], numFn);
}
} else {
//处理第 num 个元素
this.dealItem(num. numFn);
}
}
有了这个迭代器,船舰轮播图的时候,我们只需要用迭代器创建内部图片数据即可,如果想处理某一张图片,只需要调用迭代器提供的方法即可。比如获取 ul 中的第 4 个 li 元素,就可以创建一个迭代器对象。
var demo = new Iterator('li', 'container');
然后我们想为每一个元素处理一些逻辑,可以这样:
console.log(demo.first()); //<li>1</li>
console.log(demo.pre()); //null
console.log(demo.next()); //<li>2</li>
console.log(demo.get(2000)); //<li>1</li>
//处理所有元素
demo.dealEach(function(text, color) {
this.innerHTML = text; //设置内容
this.style.background = color //设置背景
}, 'test', 'pink');
//排他思想处理第3和第4个元素
demo.exclusive([2, 3], function() {
this.innerHTML = '被排除的';
this.style.background = 'green';
}, function() {
this.innerHTML = '选中的';
this.style.background = 'red';
})
通过迭代器接口方法就能轻易的访问聚合对象的每一个元素,甚至不需要知道聚合对象的内部结构。
迭代器方法在 JS 应用中还是比较常见的,比如数组迭代器方法,一次对每一个元素遍历,并将该元素的索引与索引值传入回调函数中。
//数组迭代器
var eachArray = function(arr, fn) {
var i = 0,
len = arr.length;
for(; i < len; i++) {
//一次执行回调函数,注意回调函数中传入的参数第一个为索引,第二个为索引对应的值
if(fn.call(arr[i], i, arr[i]) === false) {
break;
}
}
}
对象迭代器与数组迭代器比较类似,但传入回调函数中的为对象属性与对象的属性值。
//对象迭代器
var eachObject = function(obj, fn) {
//遍历对象每一个属性
for(var i in obj) {
//一次执行回调函数,注意回调函数中传入的参数第一个为属性,第二个为该属性的值
if(fn.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
迭代器对象创建完了,接下来就是使用了。
//数组迭代器对象使用
let arr = [1, 2, 3, 4, 5];
eachArray(arr, function(i, data) {
console.log(i, data);
});
//测试结果
//0 1
//1 2
//2 3
//3 4
//4 5
//对象迭代器对象使用
let object = {
a: 23, b: 56, c: 79};
eachObject(obj, function(i, data) {
console.log(i, data);
});
//测试结果
//a 23
//b 56
//c 79
迭代器的应用
同步变量迭代器
我们日常操作中,迭代器是非常有用的,比如有时我们需要使用到某些变量的内部属性时,我们不知道该变量的属性是否一级一级的打印了出来,此时通过 点 语法或者 [ ] 来获取是有可能报错的。
比如现在有对象 A : let A = {} 。
获取 A 属性 b 下面的 c 属性: let c = A.b.c (这种方式是不允许的,会报错)。
正确方式是: let c = A && A.b && A.b.c 。
这种方式会显得代码很臃肿,而且需要判断的地方太多了,每次都要实在是过于麻烦。我们可以通过迭代器模式来减少这种情况。
//同步变量
var A = {
//所有用户共有
common: {
},
//客户端数据
client: {
user: {
username: '不见星空',
uid: '123'
}
},
//服务器端数据
server: {
}
}
我们想获取客户端的用户名,可以通过一个同步变量迭代器来实现。
//同步变量迭代取值器
AGetter = function(key) {
//如果不存在 A 则返回未定义
if(!A) retrun undefined
var result = A;
key = key.split('.');
//迭代同步变量 A 对象属性
for(var i = 0, len = key.length; i < len; i++) {
//如果第 i 层属性存在对应的值则迭代该属性
if(result[key[i]] !== undefined) {
result = result[key[i]];
} else {
//如果不存在则返回未定义
return undefined;
}
}
//返回获取的结果
return result;
}
//获取用户名数据
console.log(AGetter('client.user.username')); //不见星空
//获取服务器数据
console.log(AGetter('server.lang.local')); //undefined
分支循环嵌套问题
当我们用 canvas 来绘制一些特殊图片的时候。如下:
window.onload = function() {
var canvas = document.getElementsByTagName('canvas')[0],
img = document.images[0],
width = (canvas.width = img.width * 2) / 2,
height = canvas.height = img.height,
ctx = canvas.getContext('2d');
ctx.drawImag(img, 0, 0);
//绘制特效图片
function dealImage() {
}
//添加特效
dealImage('gray', 0, 0, width, height, 255);
dealImage('gray', 100, 50, 300, 200, 100);
dealImage('gray', 150, 100, 200, 100, 255);
}
对于绘制特效图片方法 dealImage 在处理图片数据时需要两个步骤,第一步是迭代每一个图片像素数据,第二步是根据给定特效类型选择不同算法处理像素数据。
/**
* 绘制特效图片
* param t 特效类型
* param x x坐标
* param y y坐标
* param w 宽度
* param h 高度
* param a 透明度
*/
function dealImage(t, x, y, w, h, a) {
//获取画布图片数据
var canvasData = ctx.getImageData(x, y, w, h);
//获取像素数据
var data = canvasData.data;
//遍历每组像素数据(四个数据表示一个像素点,分别是红色,绿色,蓝色,透明度)
for(var i = 0, len = data.length; i < len; i += 4) {
switch(t) {
//红色滤镜,将绿色蓝色取值为 0
case 'red':
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = a;
break;
//绿色滤镜,将红色蓝色取值为 0
case 'green':
data[i] = 0;
data[i + 2] = 0;
data[i + 3] = a;
break;
//蓝色滤镜,将绿色红色取值为 0
case 'blue':
data[i] = 0;
data[i + 1] = 0;
data[i + 3] = a;
break;
//平均值灰色滤镜,取三色平均值
case 'gray':
var num = parseInt((data[i] + data[i + 1] + data[i + 2]) / 3);
data[i] = num;
data[i + 1] = num
data[i + 2] = num;
data[i + 3] = a;
break;
//其他方案
}
}
//绘制处理后的图片
ctx.putImageData(canvasData, width + x, y);
}
上面这种情况在处理多个像素点的时候,会很吃力,一张 1000*1000 的图片就有, 1000000 个像素点,假设有 200 种特效,最糟糕的情况下,我们需要做 200000000 次无用分支判断,为了减少不必要的消耗,我们需要抽象一个迭代器出来。
function dealImage(t, x, y, w, h, a) {
var canvasData = ctx.getImageData(x, y, w, h),
data = canvasData.data;
var Deal = function() {
var method = {
//默认类型--平均灰度特效
'default': function(i) {
return method['gray'](i);
},
//红色特效
'red': function(i) {
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = a;
},
//平均灰度特效
'gray': function(i) {
var num = parseInt((data[i] + data[i + 1] + data[i + 2]) / 3);
data[i] = data[i + 1] = data[i + 2] = parseInt((data[i] + data[i + 1] + data[i + 2]) / 3);
data[i + 3] = a;
}
};
//主函数,通过给定类型回应过滤算法
return function(type) {
return method[type] || method['default'];
}
} ();
//迭代器处理数据
function eachData(fn) {
for(var i = 0, len = data.length; i < len; i+= 4) {
//处理一组像素数据
fn(i);
}
}
//处理数据
eachData(Deal(t));
ctx.putImageData(canvasData, width + x, y);
}
本节例子比较多,主要是这个迭代器模式平常使用的情况会非常多,别的模式可以暂时放放,这个迭代器,一定要学会哈。