资讯专栏INFORMATION COLUMN

向Zepto学习关于"偏移"的那些事

hzx / 1155人阅读

摘要:获得当前元素相对于的位置。返回一个对象含有和当给定一个含有和属性对象时,使用这些值来对集合中每一个元素进行相对于的定位。获取对象集合中第一个元素相对于其的位置。结尾以上就是中与偏移相关的几个的解析,欢迎指出其中的问题和有错误的地方。

前言
这篇文章主要想说一下Zepto中与"偏移"相关的一些事,很久很久以前,我们经常会使用offsetpositionscrollTopscrollLeft等方式去改变元素的位置,他们之间有什么区别,是怎么实现的呢?接下来我们一点点去扒开他们的面纱。

原文链接

源码仓库

offsetParent

offsetposition两个api内部的实现都依赖offsetParent方法,我们先看一下它是怎么一回事。

找到第一个定位过的祖先元素,意味着它的css中的position 属性值为“relative”, “absolute” or “fixed” #offsetParent

我们都知道css属性position用于指定一个元素在文档中的定位方式,其初始值是static, css3中甚至还增加了sticky等属性,不过目前貌似浏览器几乎还未支持。

看一下这个例子

html

css


javascript

console.log($(".child3").offsetParent()) // child1
console.log(document.querySelector(".child3").offsetParent) // child1

既然原生已经有了一个offsetParentmdn offsetParent属性供我们使用,为什么Zepto还要自己实现一个呢?其实他们之间还是有些不同的,比如同样是上面的例子,如果child3的display属性设置为了none,原生的offsetParent返回的是null,但是Zepto返回的是包含body元素的Zepto对象。

源码分析

offsetParent: function () {
  return this.map(function () {
    var parent = this.offsetParent || document.body
    while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
      parent = parent.offsetParent
    return parent
  })
}

实现逻辑还是比较简单,通过map方法遍历当前选中的元素集合,结果是一个数组,每个项即是元素的最近的定位祖先元素。

首先通过offsetParent原生DOM属性去获取定位元素,如果没有默认是body节点,这里其实就能解释前面的child3设置为display:none,原生返回null,但是Zepto得到的是body了

var parent = this.offsetParent || document.body

再通过一个while循环如果

parent元素存在

parent元素不是html或者body元素

parent元素的display属性是static,则再次获取parent属性的offsetParent再次循环。

offset
获得当前元素相对于document的位置。返回一个对象含有: top, left, width和height

当给定一个含有left和top属性对象时,使用这些值来对集合中每一个元素进行相对于document的定位。

offset() ⇒ object

offset(coordinates) ⇒ self v1.0+

offset(function(index, oldOffset){ ... }) ⇒

#offset

源码

offset: function (coordinates) {
  if (coordinates) return this.each(function (index) {
    var $this = $(this),
      coords = funcArg(this, coordinates, index, $this.offset()),
      parentOffset = $this.offsetParent().offset(),
      props = {
        top: coords.top - parentOffset.top,
        left: coords.left - parentOffset.left
      }
    if ($this.css("position") == "static") props["position"] = "relative"
    $this.css(props)
  })

  if (!this.length) return null
  if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
    return { top: 0, left: 0 }
  var obj = this[0].getBoundingClientRect()
  return {
    left: obj.left + window.pageXOffset,
    top: obj.top + window.pageYOffset,
    width: Math.round(obj.width),
    height: Math.round(obj.height)
  }
}

和Zepto中的其他api类似遵循get one, set all原则,我们先来看看获取操作是如何实现的。

if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
  return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
  left: obj.left + window.pageXOffset,
  top: obj.top + window.pageYOffset,
  width: Math.round(obj.width),
  height: Math.round(obj.height)
}

!this.length如果当前没有选中元素,自然就没有往下走的必要了,直接return掉

当前选中的集合中不是html元素,并且也不是html节点子元素。直接返回{ top: 0, left: 0 }

接下来的逻辑才是重点。首先通过getBoundingClientRect获取元素的大小及其相对于视口的位置,再通过pageXOffset、pageYOffset获取文档在水平和垂直方向已滚动的像素值,相加既得到我们最后想要的值。

再看设置操作如何实现之前,先看下面这张图,或许会有助于理解

if (coordinates) return this.each(function(index) {
  var $this = $(this),
      coords = funcArg(this, coordinates, index, $this.offset()),
      parentOffset = $this.offsetParent().offset(),
      props = {
        top: coords.top - parentOffset.top,
        left: coords.left - parentOffset.left
      }

  if ($this.css("position") == "static") props["position"] = "relative"
  $this.css(props)
})

还是那个熟悉的模式,熟悉的套路,循环遍历当前元素集合,方便挨个设置,通过funcArg函数包装一下,使得入参既可以是函数,也可以是其他形式。

通过上面那张图,我们应该可以很清晰的看出,如果要将子元素设置到传入的coords.left的位置,那其实

父元素(假设父元素是定位元素)相对文档的左边距(parentOffset.left)

子元素相对父元素的左边距(left)

相加得到的就是入参coords.left

那再做个减法,就得到我们最终通过css方法需要设置的left和top值啦。

需要注意的是如果元素的定位属性是static,则会将其改为relative定位,相对于其正常文档流来计算。

position
获取对象集合中第一个元素相对于其offsetParent的位置。
position: function() {
  if (!this.length) return

  var elem = this[0],
    offsetParent = this.offsetParent(),
    offset = this.offset(),
    parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
  offset.top -= parseFloat($(elem).css("margin-top")) || 0
  offset.left -= parseFloat($(elem).css("margin-left")) || 0
  parentOffset.top += parseFloat($(offsetParent[0]).css("border-top-width")) || 0
  parentOffset.left += parseFloat($(offsetParent[0]).css("border-left-width")) || 0
  return {
    top: offset.top - parentOffset.top,
    left: offset.left - parentOffset.left
  }
}

先看一个例子

html

css

.parent{
  width: 400px;
  height: 400px;
  border: solid 1px red;
  padding: 10px;
  margin: 10px;
  position: relative;
}

.child{
  width: 200px;
  height: 200px;
  border: solid 1px green;
  padding: 20px;
  margin: 20px;
}

console.log($(".child").position()) // {top: 10, left: 10}

下面分别是父子元素的盒模型以及标注了需要获取的top的值

接下来我们来看它怎么实现的吧,come on!!!

第一步

var offsetParent = this.offsetParent(),
// Get correct offsets
// 获取当前元素相对于document的位置
offset = this.offset(),
// 获取第一个定位祖先元素相对于document的位置,如果是根元素(html或者body)则为0, 0
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()

第二步

// 相对于第一个定位祖先元素的位置关系不应该包括margin的举例,所以减去
offset.top -= parseFloat($(elem).css("margin-top")) || 0
offset.left -= parseFloat($(elem).css("margin-left")) || 0

第三步

// 祖先定位元素加上border的宽度
parentOffset.top += parseFloat($(offsetParent[0]).css("border-top-width")) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css("border-left-width")) || 0

第四步

// 相减即结果
return {
  top: offset.top - parentOffset.top,
  left: offset.left - parentOffset.left
}

整体思路还是用当前元素相对于文档的位置减去第一个定位祖先元素相对于文档的位置,但有两点需要注意的是position这个api要计算出来的值,不应该包括父元素的border长度以及子元素的margin空间长度。所以才会有第二和第三步。

scrollLeft
获取或设置页面上的滚动元素或者整个窗口向右滚动的滚动距离。
scrollLeft: function (value) {
  if (!this.length) return
  var hasScrollLeft = "scrollLeft" in this[0]
  if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
  return this.each(hasScrollLeft ?
    function () { this.scrollLeft = value } :
    function () { this.scrollTo(value, this.scrollY) })
}

首先判断当前选中的元素是否支持scrollLeft特性。

如果value没有传进来,又支持hasScrollLeft特性,就返回第一个元素的hasScrollLeft值,不支持的话返回第一个元素的pageXOffset值。

pageXOffset是scrollX的别名,而其代表的含义是返回文档/页面水平方向滚动的像素值

传进来了value就是设置操作了,支持scrollLeft属性,就直接设置其值即可,反之需要用到scrollTo,当然设置水平方向的时候,垂直方向还是要和之前的保持一致,所以传入了scrollY作为

scrollTop
获取或设置页面上的滚动元素或者整个窗口向下滚动的距离。
scrollTop: function(value) {
  if (!this.length) return
  var hasScrollTop = "scrollTop" in this[0]
  if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
  return this.each(hasScrollTop ?
    function() { this.scrollTop = value } :
    function() { this.scrollTo(this.scrollX, value) })
},

可以看出基本原理和模式与scrollLeft一致,就不再一一解析。

结尾
以上就是Zepto中与"偏移"相关的几个api的解析,欢迎指出其中的问题和有错误的地方。
参考

读Zepto源码之属性操作

scrollTo

scrollLeft

pageXOffset

...

文章记录

ie模块

Zepto源码分析之ie模块(2017-11-03)

data模块

Zepto中数据缓存原理与实现(2017-10-03)

form模块

zepto源码分析之form模块(2017-10-01)

zepto模块

这些Zepto中实用的方法集(2017-08-26)

Zepto核心模块之工具方法拾遗 (2017-08-30)

看zepto如何实现增删改查DOM (2017-10-2)

Zepto这样操作元素属性(2017-11-13)

向Zepto学习关于"偏移"的那些事(2017-12-10)

event模块

mouseenter与mouseover为何这般纠缠不清?(2017-06-05)

向zepto.js学习如何手动触发DOM事件(2017-06-07)

谁说你只是"会用"jQuery?(2017-06-08)

ajax模块

原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92204.html

相关文章

  • 关于Javascript中"use strict"那些

    摘要:作用范围这样都会应用上模式。如果你仅想在一个函数中使用的特性检查对象中的重复键这段代码会抛出一个错误因为出现了两次。未声明变量在模式下,给未声明的变量赋值会抛出的警告。重复的参数注意出现了两次,因此会抛出一个错误。 use strict作用范围 // file.js use strict function doStuff(){ // use strict is enabled ...

    icyfire 评论0 收藏0
  • 点透问题及解决

    摘要:问题封装的事件由和实现,事件在端无效使用中事件在中该版本的点透问题已经解决可以放心使用,但是端仍然无效。一、问题描述实际学习与工作中可能会有这样的需求:在移动web中给有重叠的两个元素都添加了点击事件,当触发上方的元素的时候同时也会透过该元素触发下面的元素。这就是点透,然而这并不是我想要的效果。二、例子下面通过多种方式来模拟感受点透:<divclass="tap"&...

    番茄西红柿 评论0 收藏0
  • Python标准库time使用方式详解

      小编写这篇文章的主要目的,是给大家进行一个解答,解答关于标准库times的使用方式一些,具体的操作,下面就给大家进行一个解答。  1、time库  时间戳(timestamp)的方式:通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量  结构化时间(struct_time)方式:struct_time元组共有9个元素  格式化的时间字符串(format_strin...

    89542767 评论0 收藏0

发表评论

0条评论

hzx

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<