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

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:守护函数,结果节点的父节点必须符合该函数指定的条件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值