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:守护函数,结果节点的父节点必须符合该函数指定的条件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 不同框架对于DOM节点的检索优化方式会有所不同。以下是几种常见的框架及其优化方式: 1. jQuery:jQuery的选择器引擎Sizzle优化了节点检索,通过尽可能减少比较操作来提高性能。Sizzle会先尝试使用浏览器原生的querySelectorAll方法,如果不支持则使用自己的实现。 2. React:React采用了虚拟DOM技术,即将真实的DOM节点抽象成一个虚拟的JavaScript对象,使得对DOM的操作变成了对JavaScript对象的操作,从而减少了对真实DOM的访问次数,提高了性能。 3. AngularJS:AngularJS通过数据绑定机制使得DOM操作最小化,只有在数据变化时才进行DOM操作。此外,AngularJS还提供了一些指令(如ng-repeat)来优化循环操作,避免重复的DOM操作。 4. Vue.js:Vue.js也采用了虚拟DOM技术,但是与React不同的是,Vue.js对于每个组件都维护了一个独立的虚拟DOM树,使得只有当组件数据发生变化时才会重新渲染,从而提高了性能。 总的来说,不同框架的优化方式都是为了减少对真实DOM的访问次数,尽可能减少不必要的DOM操作,从而提高性能。 ### 回答2: 在页面往服务器端传输期间,如果需要检索DOM对象中每个节点的值,不同框架会采取不同的优化方式。 首先,DOM(文档对象模型)是用于表示和操作HTML或XML文档的标准。在传统的JavaScript中,通过getElementById、getElementsByTagName等方法来获取DOM对象,然后逐个节点进行值的检索。这种方式简单直接,但效率较低,因为需要遍历整个DOM树。 为了优化DOM节点的检索效率,一些现代前端框架引入了虚拟DOM(Virtual DOM)的概念。虚拟DOM是在内存中对DOM的抽象表示,通过算法比较虚拟DOM的差异,然后只更新变化的部分。这种方式减少了DOM操作的数量,提高了性能。 另外,一些框架还提供了选择器引擎,如jQuery的Sizzle引擎,可以根据CSS选择器快速定位和检索DOM节点。通过这种方式,可以更方便地获取特定条件下的节点值。 除了选择器引擎外,还有一些优化方法也可以使用。例如,在某些情况下,可以将DOM节点的值存储在数据结构中进行缓存,减少对DOM树的频繁访问。此外,可以合理利用Event Delegation机制,将事件处理程序注册在DOM的父级节点上,从而减少事件处理程序的数量,提高性能。 总之,不同框架对DOM对象的检索优化方式不尽相同,但都旨在提高效率。通过引入虚拟DOM、选择器引擎和其他一些优化技术,可以减少对整个DOM树的遍历,从而提升页面性能和用户体验。 ### 回答3: 在页面往服务器端传输数据时,我们经常需要检索DOM对象中每个节点的值。不同框架在这方面的检索优化方法可以有以下几种: 1. 缓存节点查询结果:为了避免多次查询同一个节点,在第一次查询节点时,可以将查询结果缓存在某个变量中,以后再次需要查询该节点时,直接使用缓存结果,避免了不必要的查询操作。 2. 使用选择器引擎:许多框架提供了选择器引擎,通过使用选择器来选择和查询DOM节点,可以提高检索的效率。这是因为选择器引擎底层会使用更高效的算法来查询和匹配DOM节点,相比于手动编写查询代码,使用选择器引擎可以简化操作,提高性能。 3. 使用事件委托:事件委托是一种优化DOM操作的方法,它利用了事件冒泡的机制。通过将事件绑定到父节点上,而不是绑定到子节点上,可以减少事件绑定的数量,从而提高性能。当子节点触发事件时,事件会冒泡至父节点,通过判断触发事件的子节点来执行相应的操作,避免了对每个子节点都进行事件绑定。 4. 使用虚拟DOM:一些现代框架,如React和Vue,使用虚拟DOM来优化DOM操作。虚拟DOM是在内存中对真实DOM的一种抽象表示,通过比对虚拟DOM和真实DOM的差异,最小化对真实DOM的操作,从而提高性能。 总之,不同框架通过缓存结果、使用选择器引擎、使用事件委托和使用虚拟DOM等方法来优化DOM对象的检索操作,提高性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值