开发图片预加载框架
HTML5学堂:在此前的一篇文章当中,我们讲解了图片预加载,对图片预加载的知识以及原理等内容均进行了一些讲解。对于我们开发人员来说,几乎每个移动端的项目(专题类和游戏类)均需要使用到图片预加载,那么如何让自己不再每次都重新书写图片预加载的代码呢?一起来看“开发图片预加载框架”
本文会依照“产生需求——>实现需求——>优化代码”的过程来讲解,主要原因在于:我们是要依据我们的需求而构思代码,而不是分析一段成品代码。因此在最初并不给出最终成品代码。
关于成品代码
关于成品代码,利利已经上传到GitHub当中,各位可以访问并下载->下载图片预加载框架,点击打开的页面中的“Download ZIP”即可,除了基本功能JS之外,利利还上传了API文档(README.md文件,可以用Sublime编辑器打开)以及相关demo,辅助大家理解。
功能需求
功能需求:在原有代码基础上做优化,并针对代码进行封装,提升代码的复用性。(为了方便大家查看,我已经把之前的成品代码放置在了步骤一中),如果想要了解具体的“图片预加载”的知识,可以访问此处——>图片预加载
基本功能需求分析与实现流程
1 调整代码并调整变量名称(此步骤纯粹是为了防止变量名对大家产生的影响)
2 思考预加载时需要哪些属性和方法,进行封装
2.1 思考必要的属性和方法(主要是为了复用性,此处通常会考虑函数封装)
2.2 函数封装与参数控制(通常使用参数进行不同功能的控制)
2.3 利用对象优化参数
3 功能优化以及bug排查
3.1 防止闪屏出现
3.2 newImg.src的位置 - 兼容问题
3.3 onload方法赋值为null
4 高级优化 - 混合模式封装
4.1 混合模式封装的基本原理
4.2 混合模式封装的需求原因
4.3 关于混合模式方面的相关知识
4.4 混合模式的代码实现
4.5 如何看待混合模式这些高大上的知识点
5 访问层面的优化
5.1 防止全局作用域受影响
5.2 工程师更方便的访问
5.3保证全局下能够访问
能看出来吗?其实今天讲解的最主要的内容并非是简单的知识点,而是一段代码的“优化”过程~用我经常逗我学生的一句话就是:“如何让你把自己写的一段代码,从读得懂优化修改成你完全看不懂~”(代码优化以及扩展性、复用性的提升,其实是JS层面的灵魂)
1 之前的代码与基本变量名称修改
之前的代码
-
var loadImg = ['test (1).jpg', 'test (2).jpg'];
-
var imgsNum = loadImg.length;
-
var nowNum = 0;
-
var nowPercentage = 0;
-
for (var i = 0; i < imgsNum; i++) {
-
-
var newImg = new Image();
-
newImg.onload = (function() {
-
nowNum++;
-
if (nowNum == imgsNum) {
-
complete();
-
};
-
progress();
-
})();
-
newImg.src = loadImg[i];
-
};
-
function complete() {
-
// 全部加载完毕之后要运行的代码
-
}
-
function progress() {
-
// 每加载完一张需要运行的代码
-
}
我们针对这段代码修改一下,这样更有利于大家看懂我书写的框架(此处主要是变量名的更换,并不涉及到什么其他方面的东西)
-
var fileArr = ['test (1).jpg', 'test (2).jpg'];
-
var current = 0;
-
var percentage = 0;
-
for (var i = 0; i < fileArr.length; i++) {
-
-
var newImg = new Image();
-
newImg.onload = (function() {
-
current++;
-
if (current == fileArr.length) {
-
complete();
-
};
-
progress();
-
})();
-
newImg.src = fileArr[i];
-
};
-
function complete() {
-
// 全部加载完毕之后要运行的代码
-
}
-
function progress() {
-
// 每加载完一张需要运行的代码
-
}
2 提取预加载时需要的属性和方法,进行功能封装
2.1 需要的基本属性和方法(每次调用都不同的)
2.1.1 不难想象,预加载,需要有预加载的对象,也就是那些图片,这个属性必不可少。
2.1.2 每一张图片加载完成之后通常都需要执行一些东西(如修改loading条、修改页面中的百分比值等)
2.1.3 当所有图片加载完毕之后,必然要执行一些功能。
每一次在实现预加载时,上面的三点需求必然都不相同,那么我们此处能够想到的就是针对封装后的函数,传递不同的参数来实现。
这个部分,决定着我们调用这个函数的方法以及函数中必须有属性来接收这几个“参数”。因此我们进行函数封装。
2.2 函数封装以及参数的使用
-
function filePreLoad(files, progress, complete) {
-
var fileArr = files;
-
var current = 0;
-
var percentage = 0;
-
for (var i = 0; i < fileArr.length; i++) {
-
-
var newImg = new Image();
-
newImg.onload = (function() {
-
current++;
-
progress();
-
if (current == fileArr.length) {
-
complete();
-
};
-
})();
-
newImg.src = fileArr[i];
-
};
-
}
2.3 参数的再度优化
三个参数实在是太复杂了,因此我们可以调整一下,将三个参数合并到一个对象当中,之后修改我们里面的各类变量名即可。
-
var obj = {
-
files : [],
-
progress : function(precent, currentImg) {
-
// 具体代码 - HTML5学堂
-
},
-
complete : function() {
-
// 具体代码 - HTML5学堂
-
}
-
}
-
filePreLoad(obj);
-
function filePreLoad(obj) {
-
var fileArr = obj.files;
-
var current = 0;
-
var percentage = 0;
-
for (var i = 0; i < fileArr.length; i++) {
-
-
var newImg = new Image();
-
newImg.onload = (function() {
-
current++;
-
var precentage = parseInt(current / obj.files.length * 100);
-
obj.progress(precentage, newImg);
-
if (current == fileArr.length) {
-
obj.complete();
-
};
-
})();
-
newImg.src = fileArr[i];
-
};
-
}
此段代码中需要注意一点,对于progress,在执行时,应当能够了解到当前的加载进度情况以及当前加载的具体图像,因此我们增加了计算百分比的语句,并修改了实参,即:
-
var precentage = parseInt(this.current / this.files.length * 100);
3 功能优化以及bug排查
当前的代码的确已经能够实现图片预加载的功能。但是却存在着一定的问题。
3.1 闪屏的可能
我们采用上面的这种方式,会引发一个问题,当我们没有把创建img标签插到页面时,会在切换图片的时候出现“一闪”的现象,因此我们需要拿一个容器把这些需要预加载的图片放置于页面当中。
此时需要注意,盛放这些图片的元素是不能够出现在可视窗口中的,不然岂不是页面乱了套?
于是在filePreload函数中增加如下代码
-
var box = document.createElement('div');
-
box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
-
document.body.appendChild(box);
在newImg.onload函数当中增加如下代码
-
box.appendChild(newImg);
3.2 关于newImg.src的位置
在ie和opera下,先赋值src,再赋值onload,因为是缓存图片,就错过了onload事件的触发)。应该 先绑定onload事件,然后再给src赋值
3.3 在图片加载完成之后,需要将onload方法赋值为null
经过测试,该步骤不适用于当前的这种函数封装,适用于第四步之后的原型封装,请各位务必注意。
不将onload方法赋值为null的坏处
1 此处创建了一个临时匿名函数来作为图片的onload事件处理函数,形成了闭包。ie下的内存泄漏中有一种是“循环引用”,闭包就有保存外部运行环境的能力(依赖于作用域链的实现),所以newImg.onload这个函数内部又保存了对newImg的引用,这样就形成了循环引用,导致内存泄漏。(这种模式的内存泄漏只存在低版本的ie6中,打过补丁 的ie6以及高版本的ie都解决了循环引用导致的内存泄漏问题)。
2 另外,当我们遇到gif这种动态图的加载时,可能会多次触发onload。
解决办法
为了解决上面这两个问题,我们会在图片下载完成之后先将newImg.onload设置为null。这样既能解决内存泄漏的问题,又能避免动态图片的事件多次触发问题。
回调函数与newImg.onload = null的位置:之前看过很多框架,大部分的框架都是在callback运行以后,才将newImg.onload设置为null,这样虽然能解决循环引用的问题,但是对于动态图片来说,如果callback运行比较耗时的话,还是有多次触发的隐患的。
代码调整
-
newImg.onload = function() {
在上面这段代码之后,增加这样一行代码:
-
newImg.onload = null;
于是,经过我们的修改,代码变成了这个样子
-
// HTML5学堂提示:上面的调用过程在此省略
-
function filePreLoad(obj) {
-
var fileArr = obj.files;
-
var current = 0;
-
var percentage = 0;
-
-
// 新增代码
-
var box = document.createElement('div');
-
box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
-
document.body.appendChild(box);
-
-
for (var i = 0; i < fileArr.length; i++) {
-
var newImg = new Image();
-
newImg.onload = (function() {
-
// newImg.onload = null; 需注释 会有问题
-
current++;
-
box.appendChild(newImg); // 新增代码
-
var precentage = parseInt(current / obj.files.length * 100);
-
obj.progress(precentage, newImg);
-
if (current == fileArr.length) {
-
obj.complete();
-
};
-
})();
-
newImg.src = fileArr[i]; // 注意位置
-
};
-
}
到此为止,功能的封装已经实现。这个框架也可以正常使用了~
4 更进一步? - 混合模式封装
实现混合模式的部分,如果没有接触过面向对象、this、原型知识的童鞋,大家可以简单的理解为:“将for循环位置开始的功能,封装拆解,分别存储在了3个函数当中,并相互调用访问。”
利利温馨提醒:该文章从此步骤开始,难度系数加大,学习该知识的先决条件为面向对象、原型继承(混合模式)以及图片预加载。建议在这几个方面存在盲点的童鞋,先掌握这几个知识,再看这篇文章,不然非常难看懂(毕竟知识是有阶梯性的)。
4.1 混合模式封装原理
属性使用构造模式写法,而方法挂载在原型上,并且将嵌套的函数拆分掉。
4.2 为何要再写成面向对象形式
可能有人会问:对于预加载这个例子来说,普通的构造模式完全够用了,一个对象解决,换句话说空间也还是这个对象,没必要使用原型,复用的话可以在内部定义一个初始化函数。
对于此处的问题,如果我们基于当前功能往后封装,就是JS的核心功能库,几十行,放一个js,没啥必要,比如希望将移动端的一些功能封装在一起,此时就需要用到混合模式了。换句话说,当从一个效果的复用性提升到一个个项目核心功能代码的复用性时,就需要考虑这个层面的东西。
4.3 关于混合模式的知识点
面向对象系列
4.4 混合模式封装的代码
-
function filePreLoad(obj) {
-
this.files = obj.files;
-
this.progress = obj.progress;
-
this.complete = obj.complete;
-
// 当前加载数量为0
-
this.current = 0;
-
// 容器设置
-
this.box = document.createElement('div');
-
this.box.style.cssText = 'overflow:hidden; position: absolute; left: -9999px; top: 0; width: 1px; height: 1px;';
-
document.body.appendChild(this.box);
-
this.getFiles();
-
}
-
-
// 获取每一个图片
-
filePreLoad.prototype.getFiles = function() {
-
var fileArr = [];
-
for (var i = 0; i < this.files.length; i++) {
-
fileArr[i] = this.files[i];
-
this.loadImg(fileArr[i]);
-
};
-
}
-
-
// 加载图像
-
filePreLoad.prototype.loadImg = function(file) {
-
var _this = this;
-
var newImg = new Image();
-
-
newImg.onload = function(){
-
newImg.onload = null;
-
_this.loadFtn(newImg);
-
}
-
-
newImg.src = file;
-
}
-
-
// 执行相关回调
-
filePreLoad.prototype.loadFtn = function(currentImg) {
-
this.current++;
-
this.box.appendChild(currentImg);
-
if (this.progress) {
-
var precentage = parseInt(this.current / this.files.length * 100);
-
this.progress(precentage, currentImg); // 需要返回些什么呢?
-
};
-
if (this.current == this.files.length) {
-
if (this.complete) {
-
this.complete();
-
};
-
};
-
}
Plus: 可能有人看完代码之后想问 —— 为何要多拆分一个功能函数?
本着面向对象代码中,不会有函数嵌套函数的现象,其实这段代码并不需要拆解成这么多的函数(getFiles和loadImg),此处利利主要是考虑以后可能会做“其他类型文件”的预加载处理,因此拆分出来,便于之后代码的书写
4.5 利利想啰嗦两句~~~
利利表示,自己在书写这个功能时,直接写成了混合模式,然后又要讲解,于是只能一步一步往回倒。顺便也想提提这个方面的东西:很多人学东西都会比较“急于求成”,我自己有时候也是这样。不过,话说回来,哪个攻城狮不希望能够实现高大上的东西的呢?哪个又不希望自己写出来的代码很流弊呢?
在大概3年前,利利仅仅对面向对象有所了解,能够去写个什么用户、属性、方法,但是却不知道如何应用,对于面向对象的了解浮于表面,那时候很希望能够直接上手就能够写出“面向对象”的代码,但是这个“想法”在那时是不能够实现的,因为我根本不知道从何下手;后来大概过了一年,到了2014年年初,自己突然对面向对象有了比较清晰的理解,开始逐渐的看明白this、想明白了意义;到了2014年中下旬吧,自己发现自己能够去书写面向对象的东西了,小效果、小游戏,逐渐玩儿转原型;而今,写代码竟然能够直接写出一套“面向对象”的代码,说实在的,自己也有点儿小惊喜。
说了这么多,其实利利只是想告诉当前看到下面这段代码,感到“不知从何下手”的童鞋:“知识是有一定的关系的,很多知识是其他知识的前置条件,看到一个东西我们希望能够立即实现固然没有错,但是,却需要我们拥有一定的基础和沉淀。对于面向对象,this、作用域、函数封装、原型都是其基础,如果在这个方面并不是很了解,不建议纠结于上面这个步骤的代码,而更建议先学习这些基础知识”,我自己在之前整理过相关的开发经验,在HTML5学堂官网里都能够搜索到,会比较方便大家对“面向对象”的理解,感兴趣的可以查看一下。
5 防止全局作用域受影响并让工程师能够更方便的访问
5.1 防止全局作用域受影响,可以使用匿名函数
5.2 希望工程师能够更方便的访问,可以采用类似于JQ简化调用的方法,具体详见代码
5.3 为了保证全局下能够访问,可以将函数赋值给window下的某个属性
基于以上三点,我们针对代码做如下变化
-
(function(){
-
// HTML5学堂提醒:此处先放置上个步骤中的所有代码
-
-
// 如下代码为5.2的需求。[涉及知识:构造函数、函数返回值]
-
function preload(obj) {
-
return new filePreLoad(obj);
-
}
-
// 5.3的需求
-
window.preload = preload;
-
})();
具体成品代码,最开始利利就说过了,已经上传到GitHub当中,评论区的链接,即可查看下载。
从代码成品,到GitHub的维护,再到文章的书写和案例的拆解,耗时14h……HTML5学堂小编-利利。
欢迎沟通交流~HTML5学堂
HTML5学堂微信~欢迎扫码关注