imagesloaded源码分析

来源:互联网 发布:移动网络电视好吗? 编辑:程序博客网 时间:2024/06/04 19:31

小白的第一篇源码分析, 选择的是imagesloaded插件(版本4.1.3), 这个被masonry使用, 作者居然是同一个人!

git clone之后, 打开imagesloaded.pkgd.js(这个是将ev-emitter打包进去的, 可以直接使用)

源码结构

  • EvEmitter(观察者模式实现)
  • 工具方法(extend, makeArray等)
  • ImagesLoaded构造函数
  • ImagesLoaded原型, 原型属性, 方法
  • LoadingImage构造函数
  • LoadingImage原型, 原型属性, 方法
  • Background构造函数
  • Background原型, 原型属性, 方法
  • 构造jQuery插件(makeJQueryPlugin方法)

源码上来就是EvEmitter部分, 这我们已经用过太多了, 其实说到底就是观察者模式, 那就先分析下它吧.

EvEmitter(观察者模式实现)

为了避免篇幅过长, EvEmitter源码分析见我的另一篇博客.

工具方法(extend, makeArray等)

extend简单的属性拷贝, 无须多讲,
但是注意, 这会将源对象的原型上的属性也拷贝到目标对象

// extend objectsfunction extend( a, b ) {  for ( var prop in b ) {    a[ prop ] = b[ prop ];  }  return a;}

makeArray的实现

// 将一个element或者nodeList转换为数组// turn element or nodeList into an arrayfunction makeArray(obj) {  var ary = [];  // 如果是数组, 直接获取  if (Array.isArray(obj)) {    // use object if already an array    ary = obj;  }   // 如果是NodeList对象  else if (typeof obj.length == 'number') {    // convert nodeList to array    for (var i=0; i < obj.length; i++) {      ary.push(obj[i]);    }  } else {    // array of single index    ary.push(obj);  }  return ary;}

注意这里的第二个判断else if部分并不十分严格,
个人理解是作者考虑到自身库的使用情况而做的判断.

ImagesLoaded构造函数

为了方便理解, 建议先看完LoadingImage和Background部分再回来看这部分, 因为ImagesLoaded部分要借助LoadingImageBackground.

* @param {Array, Element, NodeList, String} elem* @param {Object or Function} options - if function, use as callback* @param {Function} onAlways - callback function*/function ImagesLoaded(elem, options, onAlways) {  // 保证调用返回为ImagesLoaded的实例  // coerce ImagesLoaded() without new, to be new ImagesLoaded()  if (!(this instanceof ImagesLoaded)) {    return new ImagesLoaded(elem, options, onAlways);  }  // elem参数为选择器  // use elem as selector string  if (typeof elem == 'string') {    elem = document.querySelectorAll(elem);  }  // elements属性为数组  this.elements = makeArray(elem);  // options属性为对象  this.options = extend({}, this.options);  // new ImagesLoaded(elem, function)形式调用  if (typeof options == 'function') {    onAlways = options;  } else {    // new ImagesLoaded(elem, options, function)形式调用    extend(this.options, options);  }  // 指定always回调  if (onAlways) {    this.on('always', onAlways);  }  // 寻找图片  this.getImages();  // 如果有jQuery存在, 添加jqDeferred字段为一个Deferred对象  if ($) {    // add jQuery Deferred object    this.jqDeferred = new $.Deferred();  }  // 检测图片加载图片  // HACK check async to allow time to bind listeners  setTimeout(function() {    this.check();  }.bind(this));}

先看下getImages方法, 存在<img />和背景图两种情况

ImagesLoaded.prototype.getImages = function() {  this.images = [];  // 对elements和循环调用addElementImages  // filter & find items if we have an item selector  this.elements.forEach(this.addElementImages, this);};

addElementImages依赖addImage方法和addElementBackgroundImages方法:

/**  * @param {Node} element  */ImagesLoaded.prototype.addElementImages = function(elem) {  // 图片(<img>)调用addImage方法  // 注意nodeName获取到的是大写, 这里转大写再判断应该更安全  // filter siblings  if (elem.nodeName == 'IMG') {    this.addImage(elem);  }  // 如果还有背景图, 调用addElementBackgroundImages方法  // get background image on element  if (this.options.background === true) {    this.addElementBackgroundImages(elem);  }  // 指定元素白名单, 只有下面三种才继续进行  `    var elementNodeTypes = {      1: true, // Element      9: true, // Document      11: true // DocumentFragment    };  `  // find children  // no non-element nodes, #143  var nodeType = elem.nodeType;  if (!nodeType || !elementNodeTypes[nodeType]) {    return;  }  // 从子结点中找图片  var childImgs = elem.querySelectorAll('img');  // concat childElems to filterFound array  for (var i = 0; i < childImgs.length; i++) {    var img = childImgs[i];    this.addImage(img);  }  // 从子结点中找背景图  // get child background images  if (typeof this.options.background == 'string') {    var children = elem.querySelectorAll(this.options.background);    for (i = 0; i < children.length; i++) {      var child = children[i];      this.addElementBackgroundImages(child);    }  }};

addImage方法

// 往images中添加一个LoadingImage实例ImagesLoaded.prototype.addImage = function(img) {  var loadingImage = new LoadingImage(img);  this.images.push(loadingImage);};

addElementBackgroundImages方法

ImagesLoaded.prototype.addElementBackgroundImages = function(elem) {  // 获取最终应用在元素上的所有CSS属性对象   var style = getComputedStyle(elem);  if (!style) {    // 兼容firefox的bug, 好    // Firefox returns null if in a hidden iframe https://bugzil.la/548397    return;  }  // 获取背景图片地址, 一般形式为url("xxx.jpg"), 引号可不写或者写单引号  // get url inside url("...")  var reURL = /url\((['"])?(.*?)\1\)/gi;  var matches = reURL.exec(style.backgroundImage);  // 亮点  // 因为css3中支持多背景图, 所以这里是while而不是if  while (matches !== null) {    // 分组2(matches[2]), 也就是上述reURL的`(.*?)`部分    var url = matches && matches[2];    if (url) {      this.addBackground(url, elem);    }    matches = reURL.exec(style.backgroundImage);  }};

addBackground方法

// 同addImage思路一致, 不过换成了BackgroundImagesLoaded.prototype.addBackground = function(url, elem) {  var background = new Background(url, elem);  this.images.push(background);};

*到这里可以看出作者思路清晰, 写法严谨.
<img />和背景图分别用对象表示, 然后在添加图片时又层层调用,
对css3也有考虑, 兼容性也有涉及(firefox下的getComputedStyle).
说实话, 若不是看源代码, 谁能知道这些兼容性问题呢, 谁又能说考虑这么全面呢?*

接着说构造函数中的check方法:
建议先看progress方法

ImagesLoaded.prototype.check = function() {  var _this = this;  // 加载完成数  this.progressedCount = 0;  // 是否有加载失败的图片  this.hasAnyBroken = false;  // complete if no images  if (!this.images.length) {    this.complete();    return;  }  function onProgress(image, elem, message) {    // HACK - Chrome triggers event before object properties have changed. #83    setTimeout(function() {      _this.progress(image, elem, message);    });  }  this.images.forEach(function(loadingImage) {    // 对progress事件确保只触发一次    loadingImage.once('progress', onProgress);    // 对每张图都调用自身的check方法    // 无论是背景图或是`<img />`, 这里都抽象为`一张加载中的的图`(参数名loadingImage也起得很好), 保证了check接口统一, 好    loadingImage.check();  });};

progress方法

ImagesLoaded.prototype.progress = function(image, elem, message) {  this.progressedCount++;  this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;  // progress event  // 触发progress回调  // imgLoad.on( 'progress', function( instance, image ) {  //    ...  // });  this.emitEvent('progress', [this, image, elem]);  // 处理以jquery插件的调用方式  // $('#container').imagesLoaded()  // .progress( function( instance, image ) {  //    ...  // });  if (this.jqDeferred && this.jqDeferred.notify) {    this.jqDeferred.notify(this, image);  }  // 查看是否已经加载完成所有图片  // check if completed  if (this.progressedCount == this.images.length) {    this.complete();  }  // 兼容调试  if (this.options.debug && console) {    console.log('progress: ' + message, image, elem);  }};

complete较为简单

ImagesLoaded.prototype.complete = function() {  // 如果没有完全加载完, 都进入到fail回调  var eventName = this.hasAnyBroken ? 'fail' : 'done';  this.isComplete = true;  this.emitEvent(eventName, [this]);  // always回调  this.emitEvent('always', [this]);  // jquery兼容  // $('#container').imagesLoaded()  // .always( function( instance ) {  //   console.log('all images loaded');  // })  // .done( function( instance ) {  //   console.log('all images successfully loaded');  // })  // .fail( function() {  //   console.log('all images loaded, at least one is broken');  // });  if (this.jqDeferred) {    var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';    this.jqDeferred[jqMethod](this);  }};

LoadingImage构造函数

这表示一个加载中的图片:

// 参数img是一个图片dom结点function LoadingImage(img) {  this.img = img;}
// 为LoadingImage指定原型LoadingImage.prototype = Object.create(EvEmitter.prototype);

Object.create不明白的可以看这里.

LoadingImage原型, 原型属性, 方法

原型方法check(检查图片是否加载完成)

LoadingImage.prototype.check = function() {  // 手动检查图片是否加载完成  // If complete is true and browser supports natural sizes,  // try to check for image status manually.  var isComplete = this.getIsImageComplete();  if (isComplete) {    // report based on naturalWidth    this.confirm(this.img.naturalWidth !== 0, 'naturalWidth');    return;  }  // If none of the checks above matched, simulate loading on detached element.  this.proxyImage = new Image();  this.proxyImage.addEventListener('load', this);  this.proxyImage.addEventListener('error', this);  // 这里是说在firefox下, 只用proxyproxyImage会有问题  // #191是说issue编号   // issue讨论地址: https://github.com/desandro/imagesloaded/issues/191  // 相关代码: https://github.com/desandro/imagesloaded/blob/v3.2.0/imagesloaded.js#L287-L289  // bind to image as well for Firefox. #191  this.img.addEventListener('load', this);  this.img.addEventListener('error', this);  this.proxyImage.src = this.img.src;};

再看上述用到的getIsImageCompleteconfirm的具体实现:

// 检测图片加载完成(加载错误也算)LoadingImage.prototype.getIsImageComplete = function() {  return this.img.complete && this.img.naturalWidth !== undefined;};// 确定加载完成(设置标志和触发事件)LoadingImage.prototype.confirm = function( isLoaded, message ) {  this.isLoaded = isLoaded;  this.emit( 'progress', this, this.img, message );};

getIsImageComplete方法用到了图片的complete属性和naturalWidth属性.
complete还算是用过, 它说的是图片是否加载完成, 兼容性还不清楚.
naturalWidth说的是图片的原始大小(加样式之前), 兼容IE9+.

complete在我写这篇文章时, mdn上还没有详情页面, 只在列表中出现, 引用如下:

HTMLImageElement.complete (Read only)
Returns a Boolean that is true if the browser has finished fetching the image, whether successful or not. It also shows true, if the image has no src value.

LoadingImage事件相关原型方法

// ----- events ----- //// 触发指定事件// trigger specified handler for event typeLoadingImage.prototype.handleEvent = function(event) {  var method = 'on' + event.type;  if (this[method]) {    this[method](event);  }};// 处理load事件LoadingImage.prototype.onload = function() {  this.confirm(true, 'onload');  this.unbindEvents();};// 处理error事件LoadingImage.prototype.onerror = function() {  this.confirm(false, 'onerror');  this.unbindEvents();};// 移除事件LoadingImage.prototype.unbindEvents = function() {  this.proxyImage.removeEventListener('load', this);  this.proxyImage.removeEventListener('error', this);  this.img.removeEventListener('load', this);  this.img.removeEventListener('error', this);};

Background部分(代表一张背景图)

这部分借助了上述的LoadingImage, 毕竟也是一张图片嘛.
直接上代码, 相信一看就懂:

// -------------------------- Background -------------------------- //function Background(url, element) {  this.url = url;  this.element = element;  this.img = new Image();}// 亮点// 指定原型为LoadingImage.prototype, 从而一些方法可以继承过来// inherit LoadingImage prototypeBackground.prototype = Object.create(LoadingImage.prototype);// 覆写原型方法Background.prototype.check = function() {  this.img.addEventListener('load', this);  this.img.addEventListener('error', this);  this.img.src = this.url;  // 调用继承过来的方法检测  // check if image is already complete  var isComplete = this.getIsImageComplete();  if (isComplete) {    this.confirm(this.img.naturalWidth !== 0, 'naturalWidth');    this.unbindEvents();  }};// 覆写原型方法Background.prototype.unbindEvents = function() {  this.img.removeEventListener('load', this);  this.img.removeEventListener('error', this);};// 覆写原型方法Background.prototype.confirm = function(isLoaded, message) {  this.isLoaded = isLoaded;  this.emitEvent('progress', [this, this.element, message]);};

jQuery插件

ImagesLoaded.makeJQueryPlugin = function(jQuery) {  jQuery = jQuery || window.jQuery;  if (!jQuery) {    return;  }  // set local variable  $ = jQuery;  // $().imagesLoaded()  $.fn.imagesLoaded = function(options, callback) {    var instance = new ImagesLoaded(this, options, callback);    // 将jquery中promise的属性和方法拷贝到当前选中的元素上来    // jquery中promise方法的实现    // promise: function( obj ) {    //  return obj != null ? jQuery.extend( obj, promise ) : promise;    // }    // 所以有这种调用方式:    // $('#container').imagesLoaded()    //   .always(function(instance) {    //   })    //   .done(function(instance) {    //   })    //   .fail(function() {    //   })    //   .progress(function(instance, image) {    //   });    return instance.jqDeferred.promise($(this));  };};// try making pluginImagesLoaded.makeJQueryPlugin();

欢迎批评指正!

原创粉丝点击