VueBloghyhero6

挖新的坑, jQuery offset 实现(还是笔记加上自己查阅资料和群里提问的一些问题)

2021-04-09 / 2021-04-09 / 338次浏览

近期也是有点萧索,一些乱七八糟的小事,困意十足,但是技术不能停止,不得不继续向前更新。脚步不能停呀。

哎,生活。好了不瞎BB了,进入主题。

大佬原文的意思就是,我们不背诵API,只实现API。

这个话题演变自今日头条某部门面试题。当时面试官提问:“如何获取文档中任意一个元素距离文档 document 顶部的距离?”

这个方法,它返回或者设置匹配元素相对于文档的偏移(位置)。这个方法返回的对象包含俩个整型属性:top 和 left,以像素计。如果使用JQuery,
我们可以直接调取该API获得结果。 但是通过原生 JavaScript 实现。

再次之前需要科普下API,DOM1 级定义了一个Node接口,该接口将由DOM中所有节点类型实现。这个 Node 接口在 JavaScript 中是作为Node类型实现的;除了了IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript 中的所有节点类型都继承自Node类型,因此所有节点都共享着基本属性和方法。

每一个节点都有一个nodeType属性,用于表明节点的类型。节点类型由在Node类型中定义的下列常量来表示,任何节点类型必居其一:
分别是:

Node.Element_Node(1); 元素节点
Node.Attribute_Node(2); 属性节点
Node.Text_Node(3); 文本节点
Node.CDATA_SECTION_NODE(4);

Node.ENTITY_REFERENCE_NODE(5); ( 翻译说叫实体参考节点)
Node.ENTITY_NODE(6); (翻译说是叫做实体节点)
Node.PROCESSING_INSTRUCTION_NODE(7);  (翻译说是处理指令节点)
Node.COMMENT_NODE(8); (注释节点)
Node.DOCUMENT_NODE(9);  (这个貌似不难理解就是DOM 节点)
Node.DOCUMENT_TYPE_NODE(10);  (这个是文件类型节点)
Node.DOCUMENT_FRAGMENT_NODE(11); (这个是碎片节点)
Node.Notation_Node(12); (翻译出来是叫做符号节点)

写代码首先要排除浏览器不支持的情况,

if(someNode.nodeType == Node.ELEMENT_NODE){ // 在IE中无效
    alert('Node is an element');
}

常量比较如果俩者相等,意味着someNode 确实是一个元素。由于IE没有公开Node类型的构造函数,因此上面的代码在IE中会导致错误。为了浏览器兼容。

开发人员常用的俩个节点,元素节点和文本节点。

要了解节点的具体信息需要知道俩条属性,nodeName 和 nodeValue 属性。要取的这个俩个值,需要判断当前的节点为元素节点

如果是元素节点才能有上述的属性。

关于节点关系

相当于文档树比喻成家谱。在HTML中,可以将元素看成是元素的子元素;相对应地,也就可以将元素看成是元素的父元素。而元素,则可以看成是元素的同胞元素,因为它们都是同一个父元素的直接子元素。

然后,每一个节点都有一个childNodes 属性,其中保存着一个NodeList对象。NodeList是一种数组对象,用于保存一组有序的节点,可以通过位置来访问这些及节点。请注意的是,虽然可以通过方括号语法来访问NodeList的值,而且这个对象也有lenhth属性,但它并不是Array的实例。

function convertToArray(nodes){
    var array = null;
    try{
       array = Array.prototype.slice.call(nodes, 0);  // 针对非IE 浏览器
    } catch (ex) {
        array = new Array();
        for(var i = 0,len = nodes.length; i < len; i++){
            array.push(nodes[i])
        }
    }
    return array;
}

以下是节点属性的关系图鉴
(摘选自高级程序设计)

接下来我们看实现代码

const offset = ele => {
    let result = {
        top: 0,
        left: 0
    }

const getOffset = (node, init) => {
        if (node.nodeType !== 1) {
            return
        }

        position = window.getComputedStyle(node)['position']

        if (typeof(init) === 'undefined' && position === 'static') {
            getOffset(node.parentNode)
            return
        }

        result.top = node.offsetTop + result.top - node.scrollTop
        result.left = node.offsetLeft + result.left - node.scrollLeft

        if (position === 'fixed') {
            return
        }

        getOffset(node.parentNode)
    }

    // 当前 DOM 节点的 display === 'none' 时, 直接返回 {top: 0, left: 0}
    if (window.getComputedStyle(ele)['display'] === 'none') {
        return result
    }

    let position

    getOffset(ele, true)

    return result

}

上述代码不难理解,使用递归实现。如果节点 node.nodeType 类型不是Element(1), 则跳出;如果相关节点的 position 属性为 static,则不计入计算,进入下一个节点(其父节点)的递归。如果相关属性的 display 属性 为 none,则应该直接返回 0 作为结果。

关于 position 属性不为static 的问题

我专门对大佬进行了提问。

原因是:在计算时,使用了 offsetTop,offsetTop 是一个只读属性,它返回当前元素相对于其 offsetParent 元素的顶部的距离。(敲黑板:!!!相对于其 offsetParent 元素的顶部!!!)

注意上面说的是“相对于其 offsetParent 元素的顶部的距离”。

offsetParent 是什么意思呢?它是指一个指向最近的(closest,指包含层级上的最近)包含该元素的定位元素。如果当前 node 定位 position:static 那么这个 node 就不能叫一个定位元素。——所以我们计算的时候就要将这个 node 排除。

理解的关键点:
1)offsetTop Api  的精确含义。可以参考:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetTop
2)offsetParent 的含义,可以参考:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetParent 」
- - - - - - - - - - - - - - -

下面这俩个图也是我在高程上面找到的

讲的是 offsetTop 属性的一些小的基础知识

我们换一个API ,getBoundingClientRect 方法

getBoundingClientRect 方法用来描述一个元素的具体位置,该位置下面的四个属性都是相对于视口左上角的位置而言的。对某一节点执行该方法,它的返回值是一个 DOMRect 类型的对象。这个对象表示一个矩形盒子,它含有:left、top、right 和 bottom 等只读属性。

请参考实现代码:

const offset = ele => {
    let result = {
        top: 0,
        left:0
    }
    // 当前为IE11 以下,直接返回 { top:0, left:0 }
    if(!ele.getClientRects().length){
        return result
    }
    // 当前 DOM 节点的 display === 'none' 时,直接返回 {top:0, left: 0}
    if(window.getComputedStyle(ele)['display'] === 'none'){
       return result
    }
    result = ele.getBoundingClientRect()
    var docElement = ele.ownerDocument.documentElement
    
    return {
        top: result.top + window.pageYOffset - docElement.clientTop,
        left: result.left + window.pageXOffset - docElement.clientLeft
    }
}

该实现需要注意的细节

node.ownerDocument.documentElement 的用法可能大家比较陌生,ownerDocument 是DOM节点的一个属性,它返回当前节点顶层的 document对象。ownerDocument是文档,documentElement 是根节点。事实上,ownerDocument 下含 2个节点:
<!DocType>
documentElement

docElement.clientTop, clientTop 是一个元素顶部边框的宽度,不包括顶部外边距或内边距。

除此之外,这个位置挖个坑 clientTop 有另外的几种JS API 可以查看下(暂时挖坑)

哎,路漫漫兮啊