缕析条分Scroll属性 | 京东云技术团队

京东云开发者
• 阅读 170

最近有项目需要使用js原生开发滑动组件,频繁要用到dom元素的各种属性,其中以各种类型的height和top属性居多,名字相近,含义也很容易搞混。因此特地总结归纳了一下常用的知识点,在文末我们来挑战实现一个简易的移动端Scroll组件。

要理解height和top,要从盒模型开始说起,首先我们来认识一下css3中定义的盒模型。dom元素在页面上实际占据的面积可以由下面这张图来说明:

缕析条分Scroll属性 | 京东云技术团队)

我们可以把它想象成一枚鸡蛋,从外到内依次是

橙色区域——外边距margin:鸡蛋壳

黑色区域——边框border:蛋壳膜

绿色区域——内边距padding:蛋白

白色区域——内容content:蛋黄

我们真正关心的部分是内容content,也就是鸡蛋营养最丰富的部分。content外面包裹了这么多层,我们在页面中才会感觉dom结构整体疏密有致,不会被密密麻麻的文字和图片所困扰。为了更好地描述盒模型,web规范中还定义了一些其他接口来描述他们,也就是我们今天要聊到的主角们:

缕析条分Scroll属性 | 京东云技术团队)

在上面这张图中,我们描述了具有嵌套关系的两层dom元素,注意这里的内容区和前述一张图中有所不同,这里的内容区也是一个独立的dom元素(即它也有自己的margin、border、padding和content!,为了简化起见,子元素不再具体绘制出来),子元素由于整体高度很高,甚至超出了父元素的高度,在父元素中不能完全展示出来(超出的不可见部分用灰色表示),也就是两者构成了滚动关系。下面我们来尝试描述以下属性:

一、clientHeight

只读属性。clientHeight实际上就是垂直滚动条的高度,一般情况下,垂直滚动条都是要紧贴上下border的,因此clientHeight = 上下padding + 内容height。别忘了有个特殊情况,当存在水平方向滚动条时,还需要考虑水平滚动条挤占了垂直滚动条的一部分空间,即:clientHeight = 上下padding + 内容height - 水平滚动条高度。用鸡蛋来比喻的话,clientHeight就是剥了壳的鸡蛋。

二、clientTop

只读属性。描述了顶部border的宽度。顶部border容易让我们联想到头发的厚度,下次在镜子前可以对自己说:你的clientTop变少了,不过你也变强了。

三、offsetHeight

只读属性。offsetHeight和clientHeight比较类似,观察可以发现,比clientHeight多了border这一层:offsetHeight = clientHeight + 上下border。现在我们把时光回退到给鸡蛋剥壳的前一刻,鸡蛋从水里捞出来洗干净呈现的样子——offsetHeight。

四、offsetTop

只读属性。它返回当前元素相对于其offsetParent元素的顶部内边距的距离。这段距离是不包含自身和父元素的border宽度的,实际上:offsetTop=自己的上margin + 父元素的上padding,相当于鸡蛋和鸡蛋盒之间挡板的厚度。

五、scrollHeight

只读属性。描述了元素内容高度的度量,包括由于溢出导致的视图中不可见内容。我们可以hack一下,把父元素中不可见的内容也展示出来,子元素在垂直方向可以分为两部分:外层的margin和内部的offsetHeight:

缕析条分Scroll属性 | 京东云技术团队)

对于具有滚动关系的父子元素,其scrollHeight具有不同的含义:

(1)对于子元素而言,由于子元素本身不包含溢出部分,其scrollHeight和clientHeight具有相同的值

(2)对于父元素而言,由于父元素的内容实际上被子元素“撑起来”了,因此其scrollHeight为把内容区域“展开”后的实际高度:父元素scrollHeight = 子元素的offsetHeight + 子元素上下margin + 自己的上下padding

六、scrollTop

读写属性,可以获取或设置一个元素的内容垂直滚动的像素数。这个是目前为止咱们遇到的第一个可以支持设置的属性。

(1)初始状态时,内容垂直方向未滚动,其值为0

(2)当内容垂直方向滚动到底时,由于内容区实际高度为scrollHeight,可显示高度为clientHeight,多出来的部分就是此时的scrollTop值为scrollHeight - clientHeight

因此可以得出结论:0 <= scrollTop <= scrollHeight - clientHeight

七、实战

学习了以上属性和信息,我们模仿京东小程序的scroll-view组件功能,来实现一个H5版滑动组件的常见功能:列表的下拉刷新和上拉加载。要实现这个功能,大概可以分为3个核心要点:

(1)可滚动:需要有一个不可动的外壳和可滑动的内容区。

(2)手势识别:通过移动端的touch属性,我们可以对比touchend和touchstart的手指位置,来简单进行手势识别。

(3)事件触发:根据滑动位置的临界条件,来判断是否应该触发刷新和加载事件。

部分实现代码如下:

// scroller.js

export default class Scroller {
    constructor(el, option) {
        this._el = el
        this._parent = el.parentNode
        this._option = option
        this._pos = 0
        this._handleScrollStart = this.handleScrollStart.bind(this)
        this._handleScroll = this.handleScroll.bind(this)
        this.init()
    }
    init() {
        this._el.addEventListener('touchstart', this._handleScrollStart)
        this._el.addEventListener('touchend', this._handleScroll)
        if(this._option.auto) {
            this.handleRefresh()
        }
    }
    destroy() {
        this._el.removeEventListener('touchstart', this._handleScrollStart)
        this._el.removeEventListener('touchend', this._handleScroll)
    }
    handleScrollStart(e) {
        const touch = e.targetTouches[0] || e.changedTouches[0]
        this._pos = touch.clientY
    }
    handleScroll(e) {
        const touch = e.targetTouches[0] || e.changedTouches[0]
        const delta = touch.clientY - this._pos
        if(delta >= this._option.threhold) {
            // 手势向下,且下行滚动距离超过判定阈值
            if(this._parent.scrollTop == 0) {
                this.handleRefresh(e)
            }
        } else if(this._parent.scrollHeight > this._parent.clientHeight && delta <= -this._option.threhold){
            // 内容可滚动,且上行滚动距离超过判定阈值
            if(this._parent.scrollTop > this._parent.scrollHeight - this._parent.clientHeight - this._option.threhold){
                this.handleLoad(e)
            }
        }
    }
    handleRefresh(...params) {
        this._option.onRefresh && this._option.onRefresh.apply(this, params)
    }
    handleLoad(...params) {
        this._option.onRefresh && this._option.onLoad.apply(this, params)
    }
}

调用demo:

var inner = document.getElementsById('inner')
var list = []
var pageIndex = 1
function getMockData() {
    const newData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => i+(pageIndex-1)*10)
    setTimeout(() => {
        if(pageIndex==1) { list = newData } else if(pageIndex < 5) { list.push(...newData) } else { alert('没有更多内容了') }
        pageIndex++
        inner.innerHTML = list.map(i => `<div class="inner-item">${i}</div>`).join('')
    }, 300)
}
function onRefresh() {
    pageIndex = 1
    getMockData()
}
new Scroller(inner, {
    threhold: 20,
    onRefresh: onRefresh,
    onLoad: getMockData,
    auto: true
})

大家可以自己试用一下,感觉效果还不错~~~欢迎留言区讨论交流。

作者:京东零售 陈震

来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Peter20 Peter20
3年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Stella981 Stella981
2年前
Python Challenge Level 18
初学Python,挑战一下流行的PythonChallenge,很不幸,卡在了18关~~被字符字节码之间的转换搞得焦头烂额,不过终于搞定了还是很happy的~~~主要的问题就是16进制形式的字符如何转成字节码(注意:不是encoding)如:\'89','50','4e','47','0d','0a','1a','0a','00
Easter79 Easter79
2年前
Taro小程序自定义顶部导航栏
微信自带的顶部导航栏是无法支持自定义icon和增加元素的,在开发小程序的时候自带的根本满足不了需求,分享一个封装好的组件,支持自定义icon、扩展dom,适配安卓、ios、h5,全面屏。我用的是京东的Taro多端编译框架写的小程序,原生的也可以适用,用到的微信/taro的api做调整就行,实现效果如下。!在这里插入图片描述(https://i
Wesley13 Wesley13
2年前
2019 年 CNCF 中国云原生调查报告
!头图.jpg(https://ucc.alicdn.com/pic/developerecology/6db0c465111b4d9a96eb1ffe85c00e7a.jpg)中国72%的受访者生产中使用Kubernetes在CNCF,为更好地了解开源和云原生技术的使用,我们定期调查社区。这是第三次中国云原生调查,以中文进行
Stella981 Stella981
2年前
AssemblyScript 入门指南[每日前端夜话0xEB]
每日前端夜话0xEB每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2459 字预计阅读时间:10分钟作者:DannyGuo翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/b880277c594152a503
Wesley13 Wesley13
2年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
CSS 分类 (Classification)
★★CSS分类属性(Classification)★★⑴CSS分类属性允许你控制如何显示元素,设置图像显示于另一元素中的何处,相对于其正常位置来定位元素,使用绝对值来定位元素,以及元素的可见度。⑵下面是常用的属性以及描述:!(https://oscimg.oschina.net/oscnet/00cb565
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_