DOM操作算法002-寻找指定DOM节点的上一个或下一个节点—— getDomNode

当我们需要寻找指定DOM节点的上一个节点或下一个节点时,我们可能第一时间会想到下面两个API:

  • node.previousSibling
  • node.nextSibling

但是这俩 API 只是针对上一个【兄弟节点】或下一个【兄弟节点】的,如果你确定指定DOM节点前后是有兄弟节点的,那可以使用这俩API,但是如果不确定,比如指定DOM节点可能是某个节点的第一个子节点,那它的上一个节点可能就是它父节点的上一个兄弟节点,或父节点的上一个兄弟节点的最后一个子节点。比如下面的代码:

注意,这里说的是节点,不是元素,意味着我们获取到的也包括文本节点(包括回车、换行、空格等)和注释节点。在正常的前端页面处理中,我们往往是忽略这些节点的,但是在富文本编辑器中,我们却不能听之任之,所以这里我们是需要获取节点而不是元素。

所以,下面的例子中,我们将在html中去除一部分格式化,因为那会引入回车节点,不利于讲解。


<div class="root"><div class="f1"><div class="f11">f11</div><div class="f12">f12</div></div><div class="f2"><div class="f21">f21</div><div class="f22">f22</div></div></div>

为了方便理解,下面是实际结构

节点f12的下一个节点是它父节点的兄弟节点f2,而f21的上一个节点则是f1;

这样的情况,我们就无法使用上面的API了,需要我们自己来实现。

1.0 版 —— 简单遍历

  • 首先,确定方法名,这里将其命名为 getDomNode;
  • 然后,确定参数:
    • 源节点:node,告诉我们要找哪一个节点的下一个或上一个节点
    • 查找方向: ltf,告诉我们向前查找还是向后查找(上一个还是下一个),为了方便操作,这个值可以直接传previousSiblingnextSibling
  • 确定算法:

  • 代码实现
function getDomNode(node, ltr) {while(node) {if (node) {if (node[ltr]) {return node[ltr];} else {if (node.parentNode) {node = node.parentNode;} else {return null;}}} else {return null;}}
}

针对上面的html示例测试一下:

<script>const f21node = document.querySelector('.f21');const preNode = getDomNode(f21node, 'previousSibling');console.log('f21的上一个节点:', preNode);const f12node = document.querySelector('.f12');const nextNode = getDomNode(f12node, 'nextSibling');console.log('f12的下一个节点:',nextNode);</script>

结果:

我们上面的实现,有太多的if-else 嵌套,可以适当优化一下:

function getDomNode(node, ltr) {var tmpNode, parent;node && (tmpNode = node[ltr]);while(!tmpNode && (parent = (parent || node).parentNode)) {tmpNode = parent[ltr];}!tmpNode && (tmpNode = null);return tmpNode;
}

我们引入了两个临时变量:

  • tmpNode: 结果节点
  • parent: 当前父节点

上面的逻辑可表述为:

  1. 如果node 存在,则尝试将node的下一个兄弟节点赋值给结果节点
  2. 如果第1步后,结果节点有值,则不触发while循环,直接返回这个结果节点
  3. 如果第1步后,结果节点仍为未定义,则尝试将当前节点父节点赋值给当前父节点
    (parent = (parent || node).parentNode)
    这一句的意思是:

    • 第一次循环时,parent未定义,则取node的父节点赋值给当前父节点
    • 后面的每次循环,parent已经有了值,则取parent的父节点赋值给当前父节点,实现不断向上层DOM树的遍历
  4. 如果经过遍历循环后, 结果节点仍旧为未定义,则为其赋值null
  5. 返回结果节点

2.0 考虑最外层为 body 的情形

上面的示例中,如果我们查找root节点的上一个节点呢?

</head><body><div class="root"><script>const froot = document.querySelector('.root');const preNode = getDomNode(froot, 'previousSibling');console.log('root的上一个节点:', preNode);</script>

从这个结构,也可以看出,root的节点没有兄弟节点,那就会向上找到body节点的上一个兄弟节点,那就是head节点:

但是实际上,我们在开发中,查找到body 如果还没查找到兄弟节点,就到此结束了,并不需要继续查找head 节点

所以,我们的代码还需要优化一下, 遇到父节点是body还没找到时,就直接返回null

function getDomNode(node, ltr) {var tmpNode, parent;node && (tmpNode = node[ltr]);while(!tmpNode && (parent = (parent || node).parentNode)) {if (parent.tagName == 'BODY') {return null;}tmpNode = parent[ltr];}!tmpNode && (tmpNode = null);return tmpNode;}

3.0 节点过滤

前面我们说过,previousSiblingnextSibling 查找到的是节点,而非元素,里面包含了回车、空行、注释、文本等节点。
如果我们不需要这些节点怎么办呢?
那么我们可以提供一个函数,用来根据指定条件(比如nodeType === 1 )来对查找过程中遇到到节点进行过滤,过滤掉那些不需要的节点类型,保留我们需要的节点类型。

function getDomNode(node, ltr, fn) {var tmpNode, parent;node && (tmpNode = node[ltr]);while(!tmpNode && (parent = (parent || node).parentNode)) {if (parent.tagName == 'BODY') {return null;}tmpNode = parent[ltr];}if (tmpNode && fn && !fn(tmpNode)) {return  getDomNode(tmpNode, ltr, fn);}!tmpNode && (tmpNode = null);return tmpNode;}

上面代码中,我们加入的部分:

if (tmpNode && fn && !fn(tmpNode)) {return  getDomNode(tmpNode, ltr, fn);
}

如果查找到了结果节点,但是结果节点并不符合我们过滤函数指定的条件,那么我们继续从这个结果节点开始,向相同方向继续查找。

现在,我们的html 可以恢复格式化了:

<body><div class="root"><div class="f1"><div class="f11">f11</div><div class="f12">f12</div></div><div class="f2"><div class="f21">f21</div><div class="f22">f22</div></div></div></body>

我们首先来看,不传过滤函数的结果:

const f21 = document.querySelector('.f21');const preNode = getDomNode(f21, 'previousSibling');console.log('f21的上一个节点:', preNode);

然后再传入一个过滤函数,再看看:

const f21 = document.querySelector('.f21');const preNode = getDomNode(f21, 'previousSibling', function (node) {return node.nodeType !== 3;});console.log('f21的上一个节点:', preNode);

这里我们将nodeType ==3 作为结果节点的必备条件,而上面我们看到换行节点的nodeType就是3,就不满足我们的条件,就会被过滤掉,从而继续向上查找。

到这里,其实这个工具方法就已经可以实现我们的目标了。

我们还可以根据这个方法进一步封装两个方法,一个用来获取上一个节点,一个用来获取下一个节点:

function getPreNode(node, fn) {return getDomDode(node, 'previousSibling', fn)
}function getNextNode(node, fn) {return getDomDode(node, 'nextSibling', fn)
}

4. 扩展版

ueditor 编辑器的源码中,这个工具方法是这样的:

function getDomNode(node, start, ltr, startFromChild, fn, guard) {var tmpNode = startFromChild && node[start],parent;!tmpNode && (tmpNode = node[ltr]);while (!tmpNode && (parent = (parent || node).parentNode)) {if (parent.tagName == 'BODY' || guard && !guard(parent)) {return null;}tmpNode = parent[ltr]; }if (tmpNode && fn && !fn(tmpNode)) {return  getDomNode(tmpNode, start, ltr, false, fn);}return tmpNode;
}

可以看到,除了能实现我们上面的目标外,它还支持以下参数来实现更多的功能:

  • start 开始查找的节点,有两个取值: firstChild-从第一个字节点开始查找,lastChild 从最后一个子节点开始查找
  • startFromChild: 查找过程是否从其子节点开始,布尔值
  • guard:守护函数,结果节点的父节点必须符合该函数指定的条件

DOM算法系列002-寻找指定DOM节点的上一个或下一个节点相关推荐

  1. 算法系列15天速成——第十三天 树操作【下】

    今天说下最后一种树,大家可否知道,文件压缩程序里面的核心结构,核心算法是什么?或许你知道,他就运用了赫夫曼树. 听说赫夫曼胜过了他的导师,被认为"青出于蓝而胜于蓝",这句话也是我比 ...

  2. Linux节点之间无密码问题,Linux下多节点SSH无密码互联实现

    需求:有3个主机192.168.0.191.192.168.0.192.192.168.0.193,需要实现无密码ssh互联访问 我使用的是root用户进行操作的: 1.每个节点分别检查是否安装ope ...

  3. Linux 使用sed指令插入到指定的行的上一行或者下一行

    1.匹配到指定行,然后在上一行插入在/etc/config/wireless文件中匹配到'sta'字符串,然后在改行的上一行插入config wifi-iface 'ap' sed -i " ...

  4. 深度剖析:如何实现一个 Virtual DOM 算法

    作者:戴嘉华 链接:https://github.com/livoras/blog/issues/13 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4 ...

  5. 《两日算法系列》之第四篇:隐马尔可夫模型HMM

    目录 1. 定义与假设 2. 相关概念的表示 3. 三个基本问题 3.1. 概率计算问题 3.2. 学习问题 3.3. 预测问题 总结 1. 定义与假设 李雷雷所在城市的天气有三种情况,分别是:晴天. ...

  6. [算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营

    [算法系列] 深入递归本质+经典例题解析--如何逐步生成, 以此类推,步步为营 本文是递归系列的第三篇, 第一篇介绍了递归的形式以及递归设计方法(迭代改递归),;第二篇以递归为引子, 详细介绍了快排和 ...

  7. 算法系列之二:三只水桶等分水问题

    算法系列之二: 三只水桶等分水问题 有一个容积为8升的水桶里装满了水,另外还有一个容积为3升的空桶和一个容积为5升的空桶,如何利用这两个空桶等分8升水?附加条件是三个水桶都没有体积刻度,也不能使用其它 ...

  8. 基本算法系列15天速成

    算法系列15天速成--第一天 七大经典排序[上] 算法系列15天速成--第二天 七大经典排序[中] 算法系列15天速成--第三天 七大经典排序[下] 算法系列15天速成--第四天 五大经典查找[上] ...

  9. java算法判断链表有没有闭环_前端算法系列之二:数据结构链表、双向链表、闭环链表、有序链表...

    前言 上一次我们讲到了数据结构:栈和队列,并对他们的运用做了一些介绍和案例实践:我们也讲到了怎么简单的实现一个四则运算.怎么去判断标签是否闭合完全等等,anyway,今天接着和大家介绍一些数据结构: ...

最新文章

  1. Javascript内置对象新增接口列表
  2. getdate函数_SQL日期函数和GETDATE解释为带有语法示例
  3. CentOS笔记:yum使用说明
  4. UVALive - 7511 Multiplication Table(暴力+模拟)
  5. 跨站点脚本(XSS)
  6. EF Core3.0+ 通过拦截器实现读写分离与SQL日志记录
  7. 博士生找工作的真相!就问一声:你是否足够强大?
  8. 输入一个年份,并判断是否为闰年
  9. 作为一只爬虫,如何科学有效地处理短信验证码?
  10. pidgin安装_如何在Ubuntu中禁用Pidgin通知
  11. 50个最新TypeScript面试题合集 – TypeScript开发教程
  12. 【06月03日】预分红股息率最高排名
  13. win10 powershell无法激活conda v4.9环境
  14. 从客户端(userName=hr /)中检测到有潜在危险的 Request.Form 值
  15. 2019.9-电赛国赛-基于FDC2214的纸张计数显示装置
  16. 华中科技大学计算机课程设计,华中科技大学计算机学院操作系统课程设计资料报告材料[1].doc...
  17. tensoflow2.6训练自己的图像分类
  18. 中国三大港进入全球最高效港口排名前十,新兴市场物流竞争力中国第一,印度和南亚或继续充当西方买家采购替代市场 | 美通社头条...
  19. IDEA设置中的 Hard wrap 和 Soft wrap
  20. 问渠哪得清如许,为有源头活水来

热门文章

  1. python中pixels函数_Python的PIL库中getpixel方法的使用
  2. python变成exe1023无标题_GitHub - Qing1023/Python-100-Days: Python - 100天从新手到大师
  3. Hello, world?Hello, world
  4. iOS - UILabel点击选中文字,部分高亮,YYLabel
  5. visual studio 下载地址和安装方法
  6. 圣科车衣,给予您的爱车完美守护
  7. skewx 字体模糊_为什么网站设计宋体消除锯齿要用无,而其他字体如黑体用平滑,还有英文字体要用那种消除锯齿方式?...
  8. UOS下使用HHDESK文本编辑功能
  9. [工具书]常用软件注册表位置
  10. 3天10万,5天20万!中山公园这场摄影大赛是如何做到的?