zhuanyongxigua / blog

blog
77 stars 9 forks source link

前端性能优化之渲染之奇技淫巧 #28

Open zhuanyongxigua opened 6 years ago

zhuanyongxigua commented 6 years ago

节流和防抖

节流和防抖其实差不多,从编程的角度说,都是防止一个函数被过多地调用。都是降低频率,只是程度不同,防抖降的很彻底,彻底到只有一次,而节流还是会多次,只不过是我们可以接受的多次。

防抖

把持续发生的事情堵住,只处理最后一次

防抖的应用场景:文本输入验证(连续输入AJAX请求进行验证,验证一次就可以了)。

防抖的最简单的实现

function debounce(method, context) {
  clearTimeout(method.tId);
  method.tId = setTimeout(function() {
    method.call(context);
  }, 1000);
}

function print() {
  console.log('hello world');
}

window.onscroll = function() {
  debounce(print);
};

节流

把持续发生的事情堵住,每隔一段时间处理一次

比如做一个拖拽的交互,如果每移动一个像素都执行一遍回调,这个性能消耗是惊人的。比较好的处理方法就是降低回调的频率(更准确的说是降低拖拽交互的频率,回调还是一直都在放生的)。

节流其他的应用场景:搜索联想(keyup);监听滚动事件判断是否到达页面底部,然后自动加载更多。

节流的实现: 第一种方法,当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。:

function Throttle(method, context) {
  if (method.tId) return;
  method.tId = setTimeout(function() {
    method.call(context);
    clearTimeout(method.tId);
    method.tId = false
  }, 1000);
}

function print() {
  console.log('hello world');
}

window.onscroll = function() {
  Throttle(print);
};

第二种方法,是用时间戳来判断是否已到回调该执行时间,记录上次执行的时间戳,然后每次触发 scroll 事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达 1000ms,如果是,则执行,并更新上次执行的时间戳,如此循环;:

var stamp = Date.now();
function Throttle(method, context) {
  if (Date.now() - stamp < 1000) return;
  method.call(context);
  stamp = Date.now();
}

function print() {
  console.log('hello world');
}

window.onscroll = function() {
  Throttle(print);
};

延迟加载和预加载

延迟加载使用的较多的就是图片的延迟加载,没有访问到的图片不会加载,会给服务器省流量的钱。

一个比较简单的思路:

在HTML标签中,img标签是这样的:

<img src="" data-src="https://.....">

data-src属性中放的是真实的地址,这样图片目前是不会加载的,

具体的执行的函数:

function lazyload() {
  const images = document.getElementsByTagName('img')
  const len = images.length
  let n = 0
  return function() {
    const seeHeight = document.documentElement.clientHeight
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
    for (let i = n; i < len; i++) {
      if (images[i].offsetTop < seeHeight + scrollTop) {
        if (images[i].getAttribute('src') === 'https://8.url.cn/edu/lego_modules/edu-ui/0.0.1/img/nohash/loading.gif') {
          images[i].src = images[i].getAttribute('data-src')
        }
        n = n + 1
      }
    }
  }
}
var loadImages = lazyload()
window.onload = function () {
  loadImages()
  window.addEventListener('scroll', loadImages, false)
}

等到window.onload触发,或者滚动事件触发的时候,把data=src中的路径放到src里面去。

其实还可以做的更好,加一个有效性验证:

function lazyload() {
  const images = document.getElementsByTagName('img')
  const len = images.length
  let n = 0
  return function() {
    const seeHeight = document.documentElement.clientHeight
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
    for (let i = n; i < len; i++) {
      if (images[i].offsetTop < seeHeight + scrollTop) {
        if (images[i].getAttribute('src') === 'https://8.url.cn/edu/lego_modules/edu-ui/0.0.1/img/nohash/loading.gif') {
          // 有效性验证
          var curImg = new Image
          curImg.src = images[i].getAttribute('data-src')
          // 如果地址找不到图片,就不会触发onload
          curImg.onload = function() {
            images[i].src = images[i].getAttribute('data-src')
            // 释放内存
            curImg = null;
          }
        }
        n = n + 1
      }
    }
  }
}
var loadImages = lazyload()
window.onload = function () {
  loadImages()
  window.addEventListener('scroll', loadImages, false)
}

对于预加载,会用到的地方,如”查看更多“的功能,可以在空闲的时候先把“查看更多”的内容拿到,点击了之后直接显示就可以了。

大列表优化

一般是出现在移动端的无线列表。

例子:

var data = [];
var limit = 1e5;
for (var i = 1; i < limit; i++) data.push(i);
var $list = document.getElementById('list');
function update() {
  item_height = 24;
  $list.style.height = (data.length * item_height) + 'px';
  var show_counts = Math.ceil(window.innerHeight / item_height) + 10;
  var update_list = function() {
    var scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
    var start = Math.max(Math.floor(scrollTop / item_height) - 3, 0);
    var show_data = data.slice(start, start + show_counts);
    var html = '';
    // 占位符,用于是滚动条正常使用
    if (start !== 0) {
      html += `<li style="height:${start * item_height}px"></li>`;
    }
    show_data.forEach(v => {
      html += `<li>${v}</li>`;
    });
    $list.innerHTML = html;
  };
  window.onscroll = throttle(update_list, 100);
  update_list();
}

完整的例子在这里

考虑了滚动条的位置。DOM中有一个占位符,在<li>的最上面,这个占位符的高度与已经划过去的列表的高度相同,目的是就是占用之前的列表的位置,从而使当前的滚动条的状态不变,划过去的列表实际上已经不在了,这样做的目的是节省资源,不会渲染太多的东西。

其实就是减少可视区域外的DOM数量。可视区域外的用占位符代替。没有移除的就容易卡顿,如果数据不多的话问题不大。