前言

ES6 注意!!

最近在优化个人博客前端,翻看到了自己图片懒加载/预加载的远古代码(通过watch监听实现的),虽说实际效果勉强还行,但总觉得不够 “Vue”,功能上也有所不足。
考虑到现有的 vue-lazyload 插件将懒加载指令化了,于是我想能不能自己也写一个。搜索一波后发现是可行的,并且还挺简单的,这篇文章给了我很多参考,在此之上我又优化并增添了额外功能,最后也不超一百行代码,奥利给,造TA就完了

问:为什么不用现成的插件?
答:代码还是自己撸的爽,vue-lazyload 据说不支持分别指定占位图,而我实现了

图片懒加载

这里给小白的简单介绍一下,熟知此概念的大佬可以跳过,还是看不懂的小白转百度或谷歌

图片的懒加载是指对于首屏加载后未在视野的图片容器 / 视野之外新增的图片容器,先给容器一个缩略图或者默认占位图(size 很小);待容器进入或即将进入视野时在后台下载原图,原图准备好后才替换。效果如下,左右分别是懒加载完成前后的效果

懒加载效果图

这样用户在原图到来之前至少还有缩略图看看,一定程度减缓其不耐烦的心情从而优化用户体验;同时按需加载的特性能够节省流量,这对服务器和用户都是一件好事

所以对于小水管服务器和面对网络拥堵时候,懒加载就显得特别有用

实现关键

首先给数组原型加两个自定义方法,后面会用上,这段代码放在指令调用前即可

// 移除数组指定的元素
if (!Array.prototype.remove){
  Array.prototype.remove = function (item) {
    if (!this.length) return;
    let index = this.indexOf(item);
    if (index > -1) {
      this.splice(index,1);
      return this;
    }
  }
}
// 推入数组当且仅当该数组没有该元素(针对string)
if (!Array.prototype.pushIfNew){
  Array.prototype.pushIfNew = function (...item) {
    for (let i of item)
      if (this.indexOf(i)===-1)
        this.push(i);
    return this
  }
}

位置判断

懒加载关键的之一就是判断该图片容器是否在视野之内,这里要用到节点的 getBoundingClientRect() 方法,返回值是 DOMRect 对象,包含该元素块边框相对于视野左上角的距离,各属性如下

rect

如果视野高度为 screenHeight,结合以上属性,我们很容易判断元素是否在视野之内

let top  = el.getBoundingClientRect().top;
let screenHeight = window.innerHeight || document.documentElement.clientHeight;
if (top < screenHeight + 50 && top > -50){
// 不一定要严格地进入视野,可以适当“扩大”视野,能够判断“即将进入”的情况,更符合实际要求
}

后台加载

懒加载关键的之二是后台加载原图,实现起来很简单,当 img 元素的 src 属性被赋值时,加载就会发生,加载成功后执行其 onload 方法,失败时执行 onerror 方法。利用这个特性,当目标进入视野时,可以创建一个临时 img(不用插入document),定义其加载成功和失败的行为,然后给他的 src 赋值即可

let img = new Image();
img.onload = ()=>{
// 成功后替换缩略图
//...
}
img.onerror = ()=>{
// 失败后可以显示 error 图片
// 或什么都不做维持之前的缩略图
// ...
}
img.src = 'original imgSrc'

监听追踪

关键之三就是对目标的监听和追踪了,可以定义两个数组,listenList 存放追踪目标,imgCacheList 存放已加载(已缓存)图片的 src。

当一个 img 元素被新插入文档后,以下操作按序三选一

  • 如果其原图在 imgCacheList 中,直接 src 赋值为原图
  • 如果该 img 在视野之内,开始触发后台加载,加载成功后其 src 加入 imgCacheList
  • 如果该 img 在视野之外,将其加入 listenList 中进行监听

对于 listenList, 我们会绑定全局滚动事件,窗口一滚动就对 listenList 中的所有目标进行位置判断

  • 如果在视野内,触发后台加载,加载成功后其 src 加入 imgCacheList,同时将目标从 listenList 中移除
  • 如果在视野外,什么都不干

当然直接绑定滚动时间会超频繁的触发函数,这里可以对函数做防抖处理

指令注册

因为要对 listenListimgCacheList 进行共享和管理,所以不能简单地进行全局指令注册 Vue.directive(),而是要在其之外开辟一个区域存放这些共享的数据,这就要以插件形式进行指令的注册了

同时指令有多个钩子函数,考虑到 img 要插入文档后才能通过 getBoundingClientRect() 获取位置信息,这里选择 inserted 钩子函数

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)

虽然跟要求有偏差,但这是最接近要求的钩子函数了,实际使用上又没什么问题,就选他吧

/*-------lazyload.js-------*/
export default (Vue,options)=>{
    let listenList = [];
    let imgCacheList = [];
    //.....
    Vue.directive('lazyload',{
        inserted:(el,binding)=>{

        }
    }
}
/*-------main.js-------*/
import LazyLoad from './lazyload';
Vue.use(LazyLoad)

使用

了解了这几个关键点我想最终实现也应该有个大概了,剩下一些细节以注释给出,详看下面的完整代码

// ----lazyload.js----
// 防抖
function throttle(func, wait) {
  let context, args;
  let previous = 0;
  return function() {
    let now = +new Date();
    context = this;
    args = arguments;
    if (now - previous > wait) {
      func.apply(context, args);
      previous = now;
    }
  }
}
export default (Vue,options={})=>{
  //默认设置,可以传入options覆盖
  //preloadClass: 占位状态(原图未加载进来)的 class,可以利用他配合 css 加模糊效果
  //loadErrorClass: 图片加载失败后赋予的 class
  //default: 默认占位图透明
  //error: 出错后显示的图片默认透明,要启用错误处理才生效
  let init = {
    preloadClass:'lazyload-preload',
    loadErrorClass:'lazyload-status-fail',
    default:'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==',
    error:`data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==`,
    ...options
  };
  let listenList = [];
  let imgCacheList = [];

  // 判断图片是否已经缓存
  const isAlreadyLoad = (imgSrc)=>{
    return imgCacheList.indexOf(imgSrc) > -1;
  };

  // 如果在视野内,触发后台加载返回true,否则返回false
  const tryLoad = (item)=>{
    let {el,src} = item;
    let top  = el.getBoundingClientRect().top;
    let screenHeight = window.innerHeight || document.documentElement.clientHeight;
    if (top < screenHeight + 50 && top > -50){
      let img = new Image();
      //后台加载完:进行替换,加入缓存,移除监听,更新class
      img.onload = ()=>{
        el.src = src;
        el.classList.remove(init.preloadClass);
        imgCacheList.pushIfNew(src);
        listenList.remove(item);
      };
      //如果出错:更新class,移除监听
      img.onerror = ()=>{
        if (item.errorHandle){
          el.src = init.error;
          el.style.objectFit = 'none';
        }
        el.classList.remove(init.preloadClass);
        el.classList.add(init.loadErrorClass);
        listenList.remove(item);
      };
      //出发后台加载
      img.src = src;
      return true;
    }else{
      return false;
    }
  };

  //用于标记监听状态,确保只会 addEventListener(由第一张插入的图片触发)
  let listenStatus = false;
  const listenScroll = ()=>{
    if (!listenStatus){
      window.addEventListener('scroll',throttle(()=>{
        let len = listenList.length;
        for (let i = 0; i < len; i++){
          tryLoad(listenList[i])
        }
      },200));
      listenStatus = true;
    }
  };

  Vue.directive('lazyload',{
    inserted:(el,{value,modifiers})=>{
      let imgSrc,placeholder;
      // 两种方式传参数
      if (typeof value==='string'){
        imgSrc = value;
        placeholder = init.default;
      }else{
        imgSrc = value[0];
        placeholder = value[1]||init.default;
      }
      // 如果已经有缓存,直接使用
      if (isAlreadyLoad(imgSrc)){
        el.src = imgSrc;
        return false;
      }
      let item = {
        el:el,
        src:imgSrc,
        errorHandle:!!modifiers.rude //是否开启错误处理
      };
      // 先给占位图和占位 class
      el.src = placeholder;
      el.classList.add(init.preloadClass);
      if (tryLoad(item)){
        return;
      }
      // 如果在视野外,加入监听
      listenList.pushIfNew(item);
      // 第一张插入的图片负责 addEventListener
      !listenStatus && listenScroll();
    },
    //图片被移除,取消监听
    unbind:(el)=>{
      for(let item of listenList)
        if (item.el===el){
          listenList.remove(item);
          //console.log('remove')
        }
    }
  })
}

使用上和 Vue 装插件一样

/*-------main.js-------*/

import LazyLoad from './lazyload';
Vue.use(LazyLoad)

/*-------xxx.vue-------*/
// 两种方式传参数,指定原图和占位图/只指定原图,占位图默认
<img v-lazyload="[originSrc,thumbnailSrc]">
<img v-lazyload="originSrc">
// 启用错误处理
<img v-lazyload.rude="[originSrc,thumbnailSrc]">

其他

目前该指令只支持 img 标签的懒加载,对于 background-image 这种背景图并未支持(因为自己博客用得少),但我想实现起来也不难 “通过指令的修饰区别两种情况,改一下 tryLoad 函数……” 应该就行了

同时也不支持动态响应的参数(我不知道这样说对不对),也就是如果传入指令的 imgSrc 发生变动,被绑定的元素并不会更新。所以目前该指令只适用于插入一次后不再变更的元素

目前想到的问题就上面两个,如果有什么实用的功能也可以提出来,正好我也想把这个指令做得更精一些

以上

    添加新评论 | #

    Markdown Supported
    简单数学题:NaN + NaN =
    表情

    Comments | ?? 条评论

    • 单身为狗 22 年

    • 朝循以始 夜继以终

    • Blog: Von by Bersder

    • Alive: 0 天 0 小时 0 分

    Support:

    frame ssr server DNS music player

    © 2019-2021 ᛟᛊᚺᛁᚾᛟ

    back2top