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

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

tags: [zepto源码分析, javascript, zepto, 源码分析]

前言

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

原文链接

源码仓库

offsetParent

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

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

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

看一下这个例子

html

<div class="wrap">
  <div class="child1">
    <div class="child2">
      <div class="child3"></div>
    </div>
  </div>
</div>

css

<style>
  .wrap{
    width: 400px;
    height: 400px;
    border: solid 1px red;
  }

  .child1{
    width: 300px;
    height: 300px;
    border: solid 1px green;
    position: relative;
    padding: 10px;
  }

  .child2{
    width: 200px;
    height: 200px;
    border: solid 1px bisque;
  }

  .child3{
    width: 100px;
    height: 100px;
    border: solid 1px goldenrod;
    position: absolute;
    left: 0;
    top: 0;
  }
</style>

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循环如果

  1. parent元素存在
  2. parent元素不是html或者body元素
  3. parent元素的display属性是static,则再次获取parent属性的offsetParent再次循环。

offset

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

  1. offset() ? object
  2. offset(coordinates) ? self v1.0+
  3. 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)
}
  1. !this.length如果当前没有选中元素,自然就没有往下走的必要了,直接return掉
  2. 当前选中的集合中不是html元素,并且也不是html节点子元素。直接返回{ top: 0, left: 0 }
  3. 接下来的逻辑才是重点。首先通过getBoundingClientRect获取元素的大小及其相对于视口的位置,再通过pageXOffsetpageYOffset获取文档在水平和垂直方向已滚动的像素值,相加既得到我们最后想要的值。

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

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的位置,那其实

  1. 父元素(假设父元素是定位元素)相对文档的左边距(parentOffset.left)
  2. 子元素相对父元素的左边距(left)
  3. 相加得到的就是入参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

<div class="parent">
  <div class="child"></div>
</div>

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!!!

  1. 第一步
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()
  1. 第二步
// 相对于第一个定位祖先元素的位置关系不应该包括margin的举例,所以减去
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
  1. 第三步
// 祖先定位元素加上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

...

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端说吧

JS-DOM对象知识点汇总(慕课)

2657
来自专栏GreenLeaves

Jquery 遍历数组之grep()方法介绍

grep()方法用于数组元素过滤筛选。 grep(array,callback,boolean);方法参数介绍。 array   ---待处理数组 callba...

1795
来自专栏余生开发

插件集--页面滚动scrollreveal.js

scrollReveal.js 不依赖其他任何文件。不支持 IE10 以下 基本方法

1504
来自专栏水击三千

JavaScript正则表达式

正则表达式是对字符串(包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为“元字符”))操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字...

27510
来自专栏前端知识分享

第29天:js-数组添加删除、数组和字符串相互转换

一、添加数组 var arr=[1,3,5]; arr.push(7,9);//添加7和9到数组arr后面,得到[1,3,5,7,9] 1、push();可向数...

731
来自专栏idba

*args 和 **kwargs的用法

一 简介 *args 和 **kwargs 主要用于函数定义。 当我们需要定义的函数的传入参数个数不确定时,可以使用*args 和 **kwargs ...

673
来自专栏JetpropelledSnake

Python入门之函数的嵌套/名称空间/作用域/函数对象/闭包函数

本篇目录:     一、函数嵌套     二、函数名称空间与作用域     三、函数对象     四、闭包函数 ========================...

35610
来自专栏xingoo, 一个梦想做发明家的程序员

【AngularJS】—— 8 自定义指令

AngularJS支持用户自定义标签属性,在不需要使用DOM节点操作的情况下,添加自定义的内容。 前面提到AngularJS的四大特性:   1 MVC ...

1779
来自专栏Python小屋

Python中修饰器的定义与使用

修饰器(decorator)是函数嵌套定义的另一个重要应用。修饰器本质上也是一个函数,只不过这个函数接收其他函数作为参数并对其进行一定的改造之后使用新函数替换原...

2745
来自专栏blackheart的专栏

[C#2] 3-局部类型、属性访问器保护级别、命名空间别名限定符

1. 局部类型 C#1.0中,一个类只可以放在一个文件中。C#2.0中用了一个关键字"partial", 可以把一个类分成两个部分[即一个类的实现可以在多个文件...

1795

扫码关注云+社区