简单实现图片懒加载、预加载、分批加载

图片我这里是随便找了十张图片,起名 0~9 的名字,方便获取。

懒加载:原理就是先放一批空图片占位,当图片进入可视区,再来赋予图片src值,让其显示。
预加载:提前请求一批图片资源。下载缓存里。到真的渲染图片时候就可以快速获取到图片。用 new Image() 来请求资源,就不用先创建dom。
分批加载:设定一个距离值,在滑动到距离底部小于这个距离值,则请求下一批图片。

分批加载可以与其他两者混用,懒加载与预加载应该不混用了,一个减低服务器压力,一个是增加服务器压力换取体验。

<!DOCTYPE>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>lazyload</title>
</head>
<style type="text/css">
  #box {
    width: 100%;
    background: black;
    border-radius: 10px;
  }
  ul {
    padding: 0;
    margin: 0;
  }
  li {
    margin: 5px;
    background: gray;
    text-align: center;
  }
  img {
    height: 300px;
    width: auto;
  }
</style>
<body>
<script type="text/javascript">
// 简单节流
function throttle(fn, time) {
  let timer
  return (ev) => {
    if (timer) {
      return
    }
    timer = setTimeout(() => {
      timer = null
      fn(ev)
    }, time)
  }
}
function getNum(numMin, numMax) {
  return Math.round( Math.random()*(numMax-numMin) + numMin );
}
function getImg (len) {
  const arr = []
  for (let i = 0; i < len; i++) {
    arr.push(getNum(0, 9))
  }
  // 模拟异步获取图片资源
  return Promise.resolve(arr)
}
window.onload = function () {
  const $ui = document.querySelector('#box ul')
  const loading = './img/loading.gif'
  const distance = 20
  function initImg() {
    return getImg(5).then((imgs) =>{
      imgs.forEach((img) => {
        const url = `./img/${img}.JPG`
        // 先预加载,演示一下,不然这里懒加载没什么意义了,都是发了请求
        // 用一个img对象来先对资源进行请求
        const imgObj = new Image()
        imgObj.src = url
        // 创建图片dom
        const $li = document.createElement('li')
        $ui.appendChild($li)
        const $img = document.createElement('img')
        $img.src = loading
        $img.dataset.src = `./img/${img}.JPG`
        $li.appendChild($img)
      })
    })    
  }
  function checkImg() {
    const $imgs = document.querySelectorAll('#box li img')
    $imgs.forEach((dom) => {
      const dataSrc = dom.getAttribute('data-src')
      // 此top为该元素距离窗口顶部距离,
      // 所以直接判断该值是否小于窗口可视区的值,即在可视区内
      const { top } = dom.getBoundingClientRect()
      // 先判断有没有data-src,防止不断修改图片src
      if (dataSrc && (top < window.innerHeight)) {
        dom.src = dataSrc
        dom.removeAttribute('data-src')
      }
    })
  }
  window.addEventListener('scroll', throttle(function(ev) {
    // 距离底部还有20px
    const { scrollTop, clientHeight, offsetHeight } = document.documentElement
    if (offsetHeight - ( scrollTop + clientHeight ) < distance) {
      initImg()
    }
    checkImg()
  }, 200))
  // 进行首次的资源获取
  initImg().then(() => {
    checkImg()
  })
}
</script>
<div id="box">
  <ul></ul>
</div>
</body>
</html>

除此之外还有个 api 专门来处理这种可视回调的IntersectionObserver
具体可参阅下阮老师的文章

自己实现call,apply,bind

介绍

这三个都是改变函数里的 this 指向
第一个参数都是用来替换 this 的对象,后面是原来函数的参数,不定个数
当第一个参数为 null 或 undefined,则里面 this 指向 window

call,apply用法一样,区别在于

func.call(obj, a, b, c, ...) // 后面的参数是一个一个
func.apply(obj, []) // 后面的参数是一个数组
// 这里有个帅气的找出数组最大值
Math.max.apply(null, [1,2,3,4,1,2,3])

bind 比较特别,返回的是个函数,不执行

// 后面的参数也是一个一个,同时会被锁住,成为原函数的前几个参数
const newFn = bind.call(obj, a, b, c, ...)
// 再调用的时候,参数为原函数的第n+1个参数
newFn(e, f, g, ...)

上码

var name = 'windowname'
var age = 'windowage'  
const boy = {
  name: 'xiaoming',
  age: 18,
  show (a, b, c) {
    console.log(`${this.name} is ${this.age}`, a, b, c)
  }
}
const girl = {
  name: 'xiaohong',
  age: 16
}
boy.show(7, 8, 9) // xiaoming is 18 7 8 9

// 先来正常的结果
console.log('------------------- 对比 -------------------')
boy.show.call(girl, 17, 18, 19, 10) // xiaohong is 16 17 18 19
boy.show.apply(girl, [27, 28, 29, 20]) // xiaohong is 16 27 28 29
boy.show.call(null, 17, 18, 19, 10) // windowname is windowage 17 18 19
boy.show.apply(undefined, [27, 28, 29, 20]) // windowname is windowage 27 28 29

const bindFn1 = boy.show.bind(girl, 17, 18, 19, 10)
bindFn1() // xiaohong is 16 17 18 19
bindFn1(1, 2, 3) // xiaohong is 16 17 18 19

const bindFn2 = boy.show.bind(girl, 27)
bindFn2() // xiaohong is 16 27 undefined undefined
bindFn2(1, 2, 3) // xiaohong is 16 27 1 2

const bindFn3 = boy.show.bind(null, 27)
bindFn3() // windowname is windowage 27 undefined undefined
bindFn3(12, 13, 14) // windowname is windowage 27 12 13

console.log('------------------- 分割线 -------------------')
// 通俗来讲就是哪个对象调用了函数,this就指向谁,当前面没有谁调用则是window
// 绑定对象赋予一个字段为函数,来调用这个函数
// 当绑定对象为 null 或者 undefined,则让这个函数单独运行,自然 this 会指向window
// 随机数的为了不要不小心覆盖到绑定对象原来的属性
// 不能绑定后对象多了个新属性,要删掉多出来的字段函数,雁过不留痕
Function.prototype._call = function(context, ...param) {
  // 随便的随机名字
  const functionName = Math.floor(Math.random() * 100000)
  let result
  if (context) {
    context[functionName] = this
    result = context[functionName](...param)
    delete context[functionName]
  } else {
    result = this(...param)
  }
  return result
}

// 当不能用 ES6 的解构时候,就用字符串拼接起来,然后用 eval 触发
Function.prototype._call2 = function() {
  const functionName = Math.floor(Math.random() * 100000)
  const params = []
  let _context
  for (let i = 1, l = arguments.length; i < l; i++) {
    params.push(arguments[i])
  }
  if (arguments[0]) {
    _context = arguments[0]
    _context[functionName] = this
    paramStr = `_context[functionName](${params.join(', ')})`
  } else {
    _context = this
    paramStr = `_context(${params.join(', ')})`
  }
  const result = eval(paramStr)
  delete _context[functionName]
  return result
}

Function.prototype._apply = function(context, arr) {
  const functionName = Math.floor(Math.random() * 100000)
  let result
  if (context) {
    context[functionName] = this
    result = context[functionName](...arr)
    delete context[functionName]
  } else {
    result = this(...arr)
  }
  return result
}

Function.prototype._bind = function(context, ...oldParam) {
  const fn = this
  if (context) {
    return (...newParam) => {
      const functionName = Math.floor(Math.random() * 100000)
      context[functionName] = fn
      const result = context[functionName](...oldParam, ...newParam)
      delete context[functionName]
      return result
    }
  }
  return (...newParam) => {
    return fn(...oldParam, ...newParam)
  }
}

boy.show._call(girl, 17, 18, 19, 10)
boy.show._apply(girl, [27, 28, 29, 20])
boy.show._call(null, 17, 18, 19, 10)
boy.show._apply(undefined, [27, 28, 29, 20])

const _bindFn1 = boy.show._bind(girl, 17, 18, 19, 10)
_bindFn1()
_bindFn1(1, 2, 3)

const _bindFn2 = boy.show._bind(girl, 27)
_bindFn2()
_bindFn2(1, 2, 3)

const _bindFn3 = boy.show._bind(null, 27)
_bindFn3()
_bindFn3(12, 13, 14)

关于缓存的几个关键词

缓存可以看成浏览器开辟了一个区域来放缓存的东西。有些放在内存有些放在硬盘。
从缓存返回的状态码为304。缓存分两种

强缓存:

设定一个期限,期限的重复访问都会去缓存获取,有两个字符来表示这个期限:
__http1__的是__Expires__,Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
__http1.1__的是__Cache-Control__,当Cache-Control:max-age=300,表示5分钟缓存时间。还有其他几个值。直接看另外一个比较有影响的:no-cache。这个看做就是不强缓存,继续去判断协商缓存。

Cache-Control优先级高于Expires,现在基本都是http1.1,Expires也就是兼容用。

协商缓存:

首次访问资源,响应头会返回标识符(__Last-Modified/ETag__),下次访问请求头会带上标识符(__If-Modified-Since/If-None-Match__),去服务器查询是否启用缓存。也就是请求还是要发,但是至少东西是省发了。

响应头和请求头的缓存标识符字段名不一样,但值一样。

Last-Modified和If-Modified-Since,就是资源最后修改时间,发送到服务器去判断有没有更新,无则返回304。
缺点就很明显,如果文件有被打开或者别的什么改动了时间,那缓存失效。同时最低也是精确到秒,一秒内的改动判断不出来。

ETag和If-None-Match
Etag可以看成根据文件算出的哈希,唯一的,文件一改肯定就改。
缺点就是要消耗资源去计算。

当然ETag明显精度高于Last-Modified,所以优先级也是ETag高。

应用中的策略

设定一个期限很大的强缓存,如 max-age=31536000 (一年)。文件通过打包工具生成出来都带上哈希。只要文件内容不改哈希就不改,请求的地址就不改,命中强缓存。改了内容自然连请求地址都改了,就会返回新文件。

代码的角度,

看到的资料几乎都是服务端的,比如node的express可以在请求资源的返回里设置响应头

app.get('/foo.js', function (req, res) {
  res.set({
    'Content-Type': 'text/javascript',
    'Cache-Control': 'no-cache',
    'ETag': '12345'
  })
  res.sendFile('foo.js')
})

当然每个都这样搞未免太麻烦了,在静态资源那里可以统一设置

const options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now())
  }
}

app.use(express.static('public', options))

上面是默认设置,自己也可以修改。

至于前端的设置,似乎只有

<meta http-equiv="expires" content="Wed, 20 Jun 2007 22:33:00 GMT">
<meta http-equiv="cache-control" content="no-cache">

尝试设置为强缓存好像没生效。而且这还是只是针对该份html,其他的资源似乎无能为力,可能也是了解不够。

其他

上面提到的利用打包工具来做这个哈希名字的事,首先想到的当然是webpack

module.exports = {
  output:{
    path:path.join(__dirname, '/dist/js'),
    filename: '[name].[chunkhash].js',
  }
}

都知道输出文件名字可配置成带哈希字符的,也就是上面的 [chunkhash] 部分,这里有三个值可以写:
hash、chunkhash、contenthash

__hash:__利用整个工程内容来计算,所以改一点点全部都改。
__chunkhash:__计算的单独chunk(这个是webpack的概念,相当于每个要输出的文件,一般一个entry对应一个chunk)的哈希。
__contenthash:__在使用抽离的css插件的时候,抽离的css文件会与引用它的js共用一份chunkhash,那js一改css也得改。当然反过来也是。此时用contenthash就只针对该份文件内容做哈希,就不会互相影响到了。

HTML几个资源异步属性

async

异步加载,乱序,只要它加载完了就会立刻执行。不阻塞页面解析。在window.onload前执行完。

defer

异步加载,DOMContentLoaded 事件触发之前完成,并且是安装加载顺序运行,相当于把js放在body最后。

prefetch

用于告诉浏览器,这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低。也就是说prefetch通常用于加速下一次导航,而不是本次的。被标记为prefetch的资源,将会被浏览器在空闲时间加载。

preload

通常用于本页面要用到的关键资源,包括关键js、字体、css文件。preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度。

__注:__这个影响到浏览器下载资源的优先级,具体可参阅下这篇文章:
https://www.cnblogs.com/xiaohuochai/p/9183874.html
css的优先级居然比js高…