Native JS реализует плагин водопада

JavaScript

У изображений в макете водопада есть основная особенность - одинаковая ширина и переменная высота Макет водопада используется в определенном масштабе на отечественных и зарубежных веб-сайтах, таких какpinterest,Лепесткии т.п. Затем, основываясь на этой особенности, начните тур по исследованию потока водопада.

Чтобы обеспечить актуальность и точность контента, вы можете обратить внимание наличный блог

Реализация основной функции

Сначала мы определяем контейнер с 20 изображениями,

<body>
  <style>
    #waterfall {
      position: relative;
    }
    .waterfall-box {
      float: left;
      width: 200px;
    }
  </style>
</body>
<div id="waterfall">
    <img src="images/1.png" class="waterfall-box">
    <img src="images/2.png" class="waterfall-box">
    <img src="images/3.png" class="waterfall-box">
    <img src="images/4.png" class="waterfall-box">
    <img src="images/5.png" class="waterfall-box">
    <img src="images/6.png" class="waterfall-box">
    ...
  </div>

Из-за неизвестных очков знаний CSS девушка с самыми длинными чулками занимает все место внизу. . .

Рядом с текстом, если в каждой строке 5 столбцов, как показано на картинке выше, то 6-я картинка должна появиться под какой из первых 5 картинок? Разумеется, он расположен абсолютно под картинкой с наименьшей высотой из первых 5 картинок.

А что на 7 картинке? На этот раз, после того, как мы сделали шестой снимок и снимки над ним в целом, идея та же, что и выше. Код реализован следующим образом:

Waterfall.prototype.init = function () {
  ...
  const perNum = this.getPerNum() // 获取每排图片数
  const perList = []              // 存储第一列的各图片的高度
  for (let i = 0; i < perNum; i++) {
    perList.push(imgList[i].offsetHeight)
  }

  let pointer = this.getMinPointer(perList) // 求出当前最小高度的数组下标

  for (let i = perNum; i < imgList.length; i++) {
    imgList[i].style.position = 'absolute' // 核心语句
    imgList[i].style.left = `${imgList[pointer].offsetLeft}px`
    imgList[i].style.top = `${perList[pointer]}px`

    perList[pointer] = perList[pointer] + imgList[i].offsetHeight // 数组最小的值加上相应图片的高度
    pointer = this.getMinPointer(perList)
  }
}

Внимательные друзья могут обнаружить, что используется высота картинки, полученная в кодеoffsetHeightЭтот атрибут, сумма высот этого атрибута равна图片高度 + 内边距 + 边框, из-за этого мы используем padding вместо margin для установки расстояния между изображениями. В дополнение кoffsetHeightсвойства, помимо пониманияoffsetHeight,clientHeight,offsetTop,scrollTopРазницу между свойствами и другими атрибутами можно лучше понять, чтобы понять проект. Код css такой же простой, как и следующий:

.waterfall-box {
  float: left;
  width: 200px;
  padding-left: 10px;
  padding-bottom: 10px;
}

На данный момент базовая компоновка потока водопада завершена, и рендеринг выглядит следующим образом:

Реализация прослушивателей событий прокрутки и изменения размера

После реализации функции инициализации init следующим шагом является отслеживание события прокрутки, чтобы добиться эффекта непрерывной загрузки изображений при прокрутке до нижней части родительского узла. В это время нам нужно рассмотреть момент, когда положение прокрутки должно запускать функцию загрузки? Это варьируется от человека к человеку, мой подход заключается в том, чтобы удовлетворить父容器高度 + 滚动距离 > 最后一张图片的 offsetTopЭто условие, то есть оранжевая линия + фиолетовая линия > синяя линия, запускает функцию загрузки, код выглядит следующим образом:

window.onscroll = function() {
  // ...
  if (scrollPX + bsHeight > imgList[imgList.length - 1].offsetTop) {// 浏览器高度 + 滚动距离 > 最后一张图片的 offsetTop
    const fragment = document.createDocumentFragment()
    for(let i = 0; i < 20; i++) {
      const img = document.createElement('img')
      img.setAttribute('src', `images/${i+1}.png`)
      img.setAttribute('class', 'waterfall-box')
      fragment.appendChild(img)
    }
    $waterfall.appendChild(fragment)
  }
}

Поскольку родительский узел может настраивать узел, он обеспечивает инкапсуляцию функции прокрутки монитора.Код выглядит следующим образом:

  proto.bind = function () {
    const bindScrollElem = document.getElementById(this.opts.scrollElem)
    util.addEventListener(bindScrollElem || window, 'scroll', scroll.bind(this))
  }

  const util = {
    addEventListener: function (elem, evName, func) {
      elem.addEventListener(evName, func, false)
    },
  }

Мониторинг события изменения размера аналогичен мониторингу события прокрутки.Когда функция изменения размера запускается, вызовите функцию инициализации, чтобы сбросить ее.

Реализовать привязку прослушивателя с использованием шаблона публикации-подписки и наследования.

Поскольку цель разработки подключаемых модулей состоит в том, чтобы удовлетвориться реализацией функций, соответствующее рабочее пространство должно быть зарезервировано для разработчиков, чтобы они могли справиться с ними самостоятельно. Думается, что картинки, которые вытягиваются и загружаются в водопадном потоке в бизнес-сценарии, вообще получаются асинхронно от Ajax, поэтому загружаемые данные не должны записываться насмерть в библиотеку, а могут быть реализованы следующие вызовы (здесь для справки).waterfallПрименение),

const waterfall = new Waterfall({options})

waterfall.on("load", function () {
  // 此处进行 ajax 同步/异步添加图片
})

Наблюдая за вызывающим методом, нетрудно подумать об использовании модели публикации/подписки для его реализации.Что касается модели публикации/подписки, мы обсудилиNode.js Асинхронный анекдотК нему есть введение. Основная идея состоит в том, чтобы добавить функцию в кеш через функцию подписки, а затем реализовать асинхронный вызов через функцию публикации.Реализация кода приведена ниже:

function eventEmitter() {
  this.sub = {}
}

eventEmitter.prototype.on = function (eventName, func) { // 订阅函数
  if (!this.sub[eventName]) {
    this.sub[eventName] = []
  }
  this.sub[eventName].push(func) // 添加事件监听器
}

eventEmitter.prototype.emit = function (eventName) { // 发布函数
  const argsList = Array.prototype.slice.call(arguments, 1)
  for (let i = 0, length = this.sub[eventName].length; i < length; i++) {
    this.sub[eventName][i].apply(this, argsList) // 调用事件监听器
  }
}

Затем, чтобы разрешить Waterfall использовать модель публикации/подписки, просто позвольте Waterfall унаследовать функцию eventEmitter, и код будет реализован следующим образом:

function Waterfall(options = {}) {
  eventEmitter.call(this)
  this.init(options) // 这个 this 是 new 的时候,绑上去的
}

Waterfall.prototype = Object.create(eventEmitter.prototype)
Waterfall.prototype.constructor = Waterfall

Способ написания наследования поглощает преимущества наследования на основе конструктора и наследования на основе цепочки прототипов и используетObject.createИзолируйте подкласс и суперкласс.Для более подробной информации о наследовании вы можете написать другую статью, поэтому я остановлюсь здесь.

Небольшая оптимизация

Чтобы событие прокрутки не запускало многократную загрузку изображений, вы можете рассмотреть возможность реализации функции анти-дрожания и дросселирования. На основе модели публикации-подписки параметр isLoading определяется, чтобы указать, загружается ли он, и определяется, следует ли загружать, в соответствии с его логическим значением.Код выглядит следующим образом:

let isLoading = false
const scroll = function () {
  if (isLoading) return false // 避免一次触发事件多次
  if (scrollPX + bsHeight > imgList[imgList.length - 1].offsetTop) { // 浏览器高度 + 滚动距离 > 最后一张图片的 offsetTop
    isLoading = true
    this.emit('load')
  }
}

proto.done = function () {
  this.on('done', function () {
    isLoading = false
    ...
  })
  this.emit('done')
}

В это время нужно добавить место, откуда осуществляется звонокwaterfall.done, чтобы сообщить, что текущее изображение было загружено, код выглядит следующим образом:

const waterfall = new Waterfall({})
waterfall.on("load", function () {
  // 异步/同步加载图片
  waterfall.done()
})

адрес проекта

адрес проекта

Использование этого плагина в проектах React

Проект простой, недочеты неизбежны, оставляйте свои ценные комментарии.