原生JS实现瀑布流插件


Posted in Javascript onFebruary 06, 2018

瀑布流布局中的图片有一个核心特点—等宽不定等高,瀑布流布局在国内网网站都有一定规模的使用,比如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 张图片呢?这时候把第 6 张图片和在它上面的图片当作是一个整体后,思路和上述是一致的。代码实现如下:
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 属性,此外还要理解 offsetHeightclientHeightoffsetTopscrollTop 等属性的区别,才能比较好的理解这个项目。css 代码简单如下:

.waterfall-box {
 float: left;
 width: 200px;
 padding-left: 10px;
 padding-bottom: 10px;
}
scroll、resize 事件监听的实现

实现了初始化函数 init 以后,下一步就要实现对 scroll 滚动事件进行监听,从而实现当滚到父节点的底部有源源不断的图片被加载出来的效果。这时候要考虑一个点,是滚动到什么位置时触发加载函数呢?这个因人而异,我的做法是当满足 父容器高度 + 滚动距离 > 最后一张图片的 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)
 }
}

因为父节点可能自定义节点,所以提供了对监听 scroll 函数的封装,代码如下:

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)
  },
 }

resize 事件的监听与 scroll 事件监听大同小异,当触发了 resize 函数,调用 init 函数进行重置就行。

使用发布-订阅模式和继承实现监听绑定

既然以开发插件为目标,不能仅仅满足于功能的实现,还要留出相应的操作空间给开发者自行处理。联想到业务场景中瀑布流中下拉加载的图片一般都来自 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 隔离了子类和父类,关于继承更多方面的细节,可以另写一篇文章了,此处点到为止。

小优化

为了防止 scroll 事件触发多次加载图片,可以考虑用函数防抖与节流实现。在基于发布-订阅模式的基础上,定义了个 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()
})
源码地址:https://github.com/MuYunyun/waterfall

项目简陋,不足之处在所难免,欢迎留下你们宝贵的意见。

Javascript 相关文章推荐
js弹出框轻量级插件jquery.boxy使用介绍
Jan 15 Javascript
基于jquery实现的可编辑下拉框实现代码
Aug 02 Javascript
jquery在ie7下选择器的问题导致append失效的解决方法
Jan 10 Javascript
原生js的数组除重复简单实例
May 24 Javascript
各式各样的导航条效果css3结合jquery代码实现
Sep 17 Javascript
BootStrap Table 设置height表头与内容无法对齐的问题
Dec 28 Javascript
jQuery页面弹出框实现文件上传
Feb 09 Javascript
详解Angular4中路由Router类的跳转navigate
Jun 09 Javascript
详解Vue.js搭建路由报错 router.map is not a function
Jun 27 Javascript
vue 兄弟组件的信息传递的方法实例详解
Aug 30 Javascript
基于vue实现图片验证码倒计时60s功能
Dec 10 Javascript
原生js实现ajax请求和JSONP跨域请求操作示例
Mar 14 Javascript
JS实现的将html转为pdf功能【基于浏览器端插件jsPDF】
Feb 06 #Javascript
20行JS代码实现粘贴板复制功能
Feb 06 #Javascript
JS中offset和匀速动画详解
Feb 06 #Javascript
Bootstrap实现的表格合并单元格示例
Feb 06 #Javascript
JavaScript实现获取select下拉框中第一个值的方法
Feb 06 #Javascript
AngularJS实时获取并显示密码的方法
Feb 06 #Javascript
详解使用React进行组件库开发
Feb 06 #Javascript
You might like
如何将一个表单同时提交到两个地方处理
2006/10/09 PHP
PHP编实现程动态图像的创建代码
2008/09/28 PHP
php 验证码实例代码
2010/06/01 PHP
常见php数据文件缓存类汇总
2014/12/05 PHP
简要剖析PHP的Yii框架的组件化机制的基本知识
2016/03/17 PHP
Laravel中如何增加自定义全局函数详解
2017/05/09 PHP
jquery 框架使用教程 AJAX篇
2009/10/11 Javascript
javascript 打开页面window.location和window.open的区别
2010/03/17 Javascript
使用JS 清空File控件的路径值
2013/07/08 Javascript
javascript计算星座属相(十二生肖属相)示例代码
2014/01/09 Javascript
JavaScript中实现Map的示例代码
2015/09/09 Javascript
jquery实现简单的遮罩层
2016/01/08 Javascript
js实现有过渡渐变效果的图片轮播相册(兼容IE,ff)
2016/01/19 Javascript
基于jQuery.validate及Bootstrap的tooltip开发气泡样式的表单校验组件思路详解
2016/07/18 Javascript
jQuery删除当前节点元素
2016/12/07 Javascript
swiper动态改变滑动内容的实现方法
2018/01/17 Javascript
微信小程序学习笔记之跳转页面、传递参数获得数据操作图文详解
2019/03/28 Javascript
vue动态循环出的多个select出现过的变为disabled(实例代码)
2019/11/10 Javascript
vue项目中使用eslint+prettier规范与检查代码的方法
2020/01/16 Javascript
javascript设计模式 ? 享元模式原理与用法实例分析
2020/04/15 Javascript
详解vue 中 scoped 样式作用域的规则
2020/09/14 Javascript
vue使用vue-quill-editor富文本编辑器且将图片上传到服务器的功能
2021/01/13 Vue.js
Python基于高斯消元法计算线性方程组示例
2018/01/17 Python
python使用mysql的两种使用方式
2018/03/07 Python
pytorch cnn 识别手写的字实现自建图片数据
2018/05/20 Python
Python线程下使用锁的技巧分享
2018/09/13 Python
Python 画出来六维图
2019/07/26 Python
python 两个数据库postgresql对比
2019/10/21 Python
django实现模板中的字符串文字和自动转义
2020/03/31 Python
Python使用jupyter notebook查看ipynb文件过程解析
2020/06/02 Python
CSS3中几个新增加的盒模型属性使用教程
2016/03/01 HTML / CSS
特步官方商城:Xtep
2017/03/21 全球购物
YSL圣罗兰美妆俄罗斯官网:Yves Saint Lauret RU
2020/09/23 全球购物
日语专业推荐信
2013/11/12 职场文书
优秀员工演讲稿
2019/06/21 职场文书
2019暑期安全倡议书!
2019/06/27 职场文书