查看: 209|回复: 0

鸿蒙开发避坑:SelectorQuery节点查询与交叉观

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在鸿蒙应用开发中,经常需要处理这类需求:用户滚动到某个位置时触发动画、图片滑入可见区域再加载、导航栏吸顶后切换样式。很多开发者第一反应是监听 scroll 事件并手动计算位置,代码冗长且容易出错。实际上,ASCF 框架已经提供了两个专门的能力:has.createSelectorQuery 用于节点查询,has.createIntersectionObserver 用于交叉观察。合理使用它们,可以大幅简化逻辑。

一、SelectorQuery:解决 ASCF 中的节点查询问题

传统 Web 开发中,document.querySelector 配合 getBoundingClientRect 可以轻松拿到元素位置。但 ASCF 没有直接操作 DOM 的接口,所以需要用 has.createSelectorQuery 来查询节点的位置、尺寸、滚动偏移、自定义属性等信息。

1. 基础用法

调用 has.createSelectorQuery() 会返回一个 SelectorQuery 对象,之后可以链式调用 select、boundingClientRect 等方法,最后必须调用 exec() 才会真正执行查询。很多初次使用的开发者容易忘记 exec(),导致回调一直不触发。

获取单个元素的位置:
  1. const query = has.createSelectorQuery();
  2. query
  3.   .select('#myElement')
  4.   .boundingClientRect()
  5.   .exec((res) => {
  6.     if (res && res[0]) {
  7.       console.info('left:', res[0].left);
  8.       console.info('top:', res[0].top);
  9.       console.info('width:', res[0].width);
  10.       console.info('height:', res[0].height);
  11.     }
  12.   });
复制代码

res 是一个数组,每个元素对应一次查询结果。只查一个节点时,用 res[0] 读取即可。

2. 批量查询节点位置

使用 selectAll 可以一次获取多个匹配节点的信息,返回结果是数组的数组,res[0] 即为所有匹配节点的信息数组。
  1. const query = has.createSelectorQuery();
  2. query
  3.   .selectAll('.card-item')
  4.   .boundingClientRect()
  5.   .exec((res) => {
  6.     if (res && res[0]) {
  7.       res[0].forEach((item, index) => {
  8.         console.info('第' + index + '个卡片:', item.top, item.height);
  9.       });
  10.     }
  11.   });
复制代码

3. 查询视口与滚动偏移

selectViewport 可以拿到视口宽高,适合做响应式布局判断。
  1. const query = has.createSelectorQuery();
  2. query
  3.   .selectViewport()
  4.   .boundingClientRect()
  5.   .exec((res) => {
  6.     if (res && res[0]) {
  7.       console.info('视口宽度:', res[0].width);
  8.       console.info('视口高度:', res[0].height);
  9.     }
  10.   });
复制代码

查询滚动容器的偏移量时,用 scrollOffset。注意只有可滚动元素(scroll-view、页面本身)才能返回有效值,普通元素的 scrollLeft 和 scrollTop 恒为 0。
  1. const query = has.createSelectorQuery();
  2. query
  3.   .select('#scrollContainer')
  4.   .scrollOffset()
  5.   .exec((res) => {
  6.     if (res && res[0]) {
  7.       console.info('scrollTop:', res[0].scrollTop);
  8.     }
  9.   });
复制代码

4. fields 通用查询

fields 是最灵活的查询方式,一次可获取多种信息,包括 id、dataset、rect、size、scrollOffset、计算样式等。注意 computedStyle 需要传入需要获取的样式属性数组,不要传太多,否则影响性能。
  1. const query = has.createSelectorQuery();
  2. query
  3.   .select('#myElement')
  4.   .fields({
  5.     id: true,
  6.     dataset: true,
  7.     rect: true,
  8.     size: true,
  9.     scrollOffset: true,
  10.     computedStyle: ['backgroundColor', 'fontSize'],
  11.   }, (res) => {
  12.     console.info('id:', res.id);
  13.     console.info('rect:', res.rect);
  14.     console.info('样式:', res.backgroundColor, res.fontSize);
  15.   })
  16.   .exec();
复制代码

5. 自定义组件内的查询范围

在自定义组件中使用 createSelectorQuery 时,默认查询的是整个页面节点。如果只想查询组件内部,必须用 .in(this) 限定范围,否则可能查错节点。
  1. const query = has.createSelectorQuery().in(this);
  2. query
  3.   .select('#innerElement')
  4.   .boundingClientRect()
  5.   .exec((res) => {
  6.     console.info(res[0]);
  7.   });
复制代码

二、IntersectionObserver:监听元素可见性

has.createIntersectionObserver 用来监听一个元素与视口(或另一个元素)的交叉状态。它非常适合图片懒加载、吸顶检测、曝光统计、无限滚动等场景。

1. 图片懒加载

监听所有 .lazy-image 元素,当它们进入视口区域(可设置提前量)时,再替换为真正的图片地址。使用 relativeToViewport({ bottom: 100 }) 可以让图片在距离底部还有 100px 时就开始加载,避免用户滑到时白屏。
  1. Page({
  2.   data: {
  3.     imageList: [
  4.       { id: 1, src: '', dataSrc: 'https://example.com/img1.jpg', loaded: false },
  5.       { id: 2, src: '', dataSrc: 'https://example.com/img2.jpg', loaded: false },
  6.     ],
  7.   },
  8.   onReady() {
  9.     this.setupLazyLoad();
  10.   },
  11.   setupLazyLoad() {
  12.     let that = this;
  13.     let observer = has.createIntersectionObserver(this, {
  14.       thresholds: [0],
  15.       observeAll: true,
  16.     });
  17.     observer.relativeToViewport({ bottom: 100 });
  18.     observer.observe('.lazy-image', (res) => {
  19.       if (res.intersectionRatio > 0) {
  20.         let index = res.dataset && res.dataset.index;
  21.         if (index !== undefined) {
  22.           that.loadImage(index);
  23.         }
  24.       }
  25.     });
  26.     that._observer = observer;
  27.   },
  28.   loadImage(index) {
  29.     let imageList = this.data.imageList;
  30.     if (imageList[index] && !imageList[index].loaded) {
  31.       imageList[index].src = imageList[index].dataSrc;
  32.       imageList[index].loaded = true;
  33.       this.setData({ imageList });
  34.     }
  35.   },
  36.   onUnload() {
  37.     if (this._observer) {
  38.       this._observer.disconnect();
  39.     }
  40.   },
  41. });
复制代码

模板中需要给每个 item 设置 data-index,以便在回调中识别具体元素。
  1. <view class="image-list">
  2.   <view class="image-item" for="{{imageList}}" data-index="{{$idx}}">
  3.     <image class="lazy-image" src="{{$item.src || 'https://example.com/placeholder.png'}}" mode="aspectFill"></image>
  4.   </view>
  5. </view>
复制代码

2. 吸顶检测

在导航栏上方放置一个哨兵元素,监听它是否离开视口顶部。哨兵离开时,说明导航栏应该吸顶。
  1. Page({
  2.   data: { isSticky: false },
  3.   onReady() {
  4.     let observer = has.createIntersectionObserver(this, {
  5.       thresholds: [0, 1],
  6.     });
  7.     observer.relativeToViewport();
  8.     observer.observe('#sticky-sentinel', (res) => {
  9.       this.setData({
  10.         isSticky: res.intersectionRatio <= 0,
  11.       });
  12.     });
  13.     this._stickyObserver = observer;
  14.   },
  15.   onUnload() {
  16.     if (this._stickyObserver) {
  17.       this._stickyObserver.disconnect();
  18.     }
  19.   },
  20. });
复制代码

模板中哨兵元素放在导航栏上方,导航栏根据 isSticky 切换 fixed 定位。
  1. <view id="sticky-sentinel" class="sentinel"></view>
  2. <view class="nav-bar {{isSticky ? 'nav-sticky' : ''}}">
  3.   <text>导航栏内容</text>
  4. </view>
复制代码

3. 以其他元素为参照:relativeTo

除了以视口为参照,还可以指定页面上的某个元素作为参照区域。这在 scroll-view 内部做可见性检测时特别有用。
  1. const observer = has.createIntersectionObserver(this);
  2. observer
  3.   .relativeTo('#scrollContainer', { top: 50, bottom: 50 })
  4.   .observe('#innerItem', (res) => {
  5.     if (res.intersectionRatio > 0) {
  6.       console.info('innerItem 在 scrollContainer 内可见');
  7.     }
  8.   });
复制代码

4. 曝光统计

设置 thresholds: [0.5] 可以控制元素面积超过一半进入视口时才触发回调,从而精确统计用户真正看到的卡片。
  1. let observer = has.createIntersectionObserver(this, {
  2.   thresholds: [0.5],
  3.   observeAll: true,
  4. });
  5. observer.relativeToViewport();
  6. observer.observe('.exposure-card', (res) => {
  7.   if (res.intersectionRatio >= 0.5) {
  8.     let index = res.dataset && res.dataset.index;
  9.     if (index !== undefined) {
  10.       // 上报曝光,并标记已曝光
  11.     }
  12.   }
  13. });
复制代码

三、常见坑与注意点

1. 忘记调用 exec()。SelectorQuery 的链式调用最后必须执行 exec(),否则所有查询都不会触发回调。

2. 查询结果为 null。可能是节点未渲染、id 拼写错误,或者自定义组件内未使用 .in(this)。建议在回调中加空值判断。

3. observeAll 与 observe 的区别。observeAll 可以同时监听多个元素,但回调中必须依赖 dataset 来区分具体元素,所以模板里一定要设置 data-index 之类的标识。

4. disconnect() 必须在 onUnload 中调用。否则交叉观察器会持续监听,造成内存泄漏。

5. thresholds 数组的含义。它接受 0~1 之间的数组,例如 [0, 0.5, 1] 表示交叉比例分别达到 0%、50%、100% 时触发回调。阈值设得越多,回调触发越频繁,性能开销越大,建议按需设置。

四、小结

SelectorQuery 和 IntersectionObserver 是 ASCF 框架中处理节点信息和可见性检测的官方方案。相比手动监听 scroll 计算位置,它们更简洁、更稳定。在实际开发中,这两个 API 可以覆盖大多数滚动联动、懒加载和曝光统计需求。建议在代码中统一封装工具方法,并在页面销毁时做好观察器的清理工作,避免内存问题。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-26 16:27 , Processed in 0.024930 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部