鸿蒙专家 发表于 前天 15:00

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

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

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

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

1. 基础用法

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

获取单个元素的位置:


const query = has.createSelectorQuery();
query
.select('#myElement')
.boundingClientRect()
.exec((res) => {
    if (res && res) {
      console.info('left:', res.left);
      console.info('top:', res.top);
      console.info('width:', res.width);
      console.info('height:', res.height);
    }
});


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

2. 批量查询节点位置

使用 selectAll 可以一次获取多个匹配节点的信息,返回结果是数组的数组,res 即为所有匹配节点的信息数组。


const query = has.createSelectorQuery();
query
.selectAll('.card-item')
.boundingClientRect()
.exec((res) => {
    if (res && res) {
      res.forEach((item, index) => {
      console.info('第' + index + '个卡片:', item.top, item.height);
      });
    }
});


3. 查询视口与滚动偏移

selectViewport 可以拿到视口宽高,适合做响应式布局判断。


const query = has.createSelectorQuery();
query
.selectViewport()
.boundingClientRect()
.exec((res) => {
    if (res && res) {
      console.info('视口宽度:', res.width);
      console.info('视口高度:', res.height);
    }
});


查询滚动容器的偏移量时,用 scrollOffset。注意只有可滚动元素(scroll-view、页面本身)才能返回有效值,普通元素的 scrollLeft 和 scrollTop 恒为 0。


const query = has.createSelectorQuery();
query
.select('#scrollContainer')
.scrollOffset()
.exec((res) => {
    if (res && res) {
      console.info('scrollTop:', res.scrollTop);
    }
});


4. fields 通用查询

fields 是最灵活的查询方式,一次可获取多种信息,包括 id、dataset、rect、size、scrollOffset、计算样式等。注意 computedStyle 需要传入需要获取的样式属性数组,不要传太多,否则影响性能。


const query = has.createSelectorQuery();
query
.select('#myElement')
.fields({
    id: true,
    dataset: true,
    rect: true,
    size: true,
    scrollOffset: true,
    computedStyle: ['backgroundColor', 'fontSize'],
}, (res) => {
    console.info('id:', res.id);
    console.info('rect:', res.rect);
    console.info('样式:', res.backgroundColor, res.fontSize);
})
.exec();


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

在自定义组件中使用 createSelectorQuery 时,默认查询的是整个页面节点。如果只想查询组件内部,必须用 .in(this) 限定范围,否则可能查错节点。


const query = has.createSelectorQuery().in(this);
query
.select('#innerElement')
.boundingClientRect()
.exec((res) => {
    console.info(res);
});


二、IntersectionObserver:监听元素可见性

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

1. 图片懒加载

监听所有 .lazy-image 元素,当它们进入视口区域(可设置提前量)时,再替换为真正的图片地址。使用 relativeToViewport({ bottom: 100 }) 可以让图片在距离底部还有 100px 时就开始加载,避免用户滑到时白屏。


Page({
data: {
    imageList: [
      { id: 1, src: '', dataSrc: 'https://example.com/img1.jpg', loaded: false },
      { id: 2, src: '', dataSrc: 'https://example.com/img2.jpg', loaded: false },
    ],
},
onReady() {
    this.setupLazyLoad();
},
setupLazyLoad() {
    let that = this;
    let observer = has.createIntersectionObserver(this, {
      thresholds: ,
      observeAll: true,
    });
    observer.relativeToViewport({ bottom: 100 });
    observer.observe('.lazy-image', (res) => {
      if (res.intersectionRatio > 0) {
      let index = res.dataset && res.dataset.index;
      if (index !== undefined) {
          that.loadImage(index);
      }
      }
    });
    that._observer = observer;
},
loadImage(index) {
    let imageList = this.data.imageList;
    if (imageList && !imageList.loaded) {
      imageList.src = imageList.dataSrc;
      imageList.loaded = true;
      this.setData({ imageList });
    }
},
onUnload() {
    if (this._observer) {
      this._observer.disconnect();
    }
},
});


模板中需要给每个 item 设置 data-index,以便在回调中识别具体元素。


<view class="image-list">
<view class="image-item" for="{{imageList}}" data-index="{{$idx}}">
    <image class="lazy-image" src="{{$item.src || 'https://example.com/placeholder.png'}}" mode="aspectFill"></image>
</view>
</view>


2. 吸顶检测

在导航栏上方放置一个哨兵元素,监听它是否离开视口顶部。哨兵离开时,说明导航栏应该吸顶。


Page({
data: { isSticky: false },
onReady() {
    let observer = has.createIntersectionObserver(this, {
      thresholds: ,
    });
    observer.relativeToViewport();
    observer.observe('#sticky-sentinel', (res) => {
      this.setData({
      isSticky: res.intersectionRatio <= 0,
      });
    });
    this._stickyObserver = observer;
},
onUnload() {
    if (this._stickyObserver) {
      this._stickyObserver.disconnect();
    }
},
});


模板中哨兵元素放在导航栏上方,导航栏根据 isSticky 切换 fixed 定位。


<view id="sticky-sentinel" class="sentinel"></view>
<view class="nav-bar {{isSticky ? 'nav-sticky' : ''}}">
<text>导航栏内容</text>
</view>


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

除了以视口为参照,还可以指定页面上的某个元素作为参照区域。这在 scroll-view 内部做可见性检测时特别有用。


const observer = has.createIntersectionObserver(this);
observer
.relativeTo('#scrollContainer', { top: 50, bottom: 50 })
.observe('#innerItem', (res) => {
    if (res.intersectionRatio > 0) {
      console.info('innerItem 在 scrollContainer 内可见');
    }
});


4. 曝光统计

设置 thresholds: 可以控制元素面积超过一半进入视口时才触发回调,从而精确统计用户真正看到的卡片。


let observer = has.createIntersectionObserver(this, {
thresholds: ,
observeAll: true,
});
observer.relativeToViewport();
observer.observe('.exposure-card', (res) => {
if (res.intersectionRatio >= 0.5) {
    let index = res.dataset && res.dataset.index;
    if (index !== undefined) {
      // 上报曝光,并标记已曝光
    }
}
});


三、常见坑与注意点

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

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

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

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

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

四、小结

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

热心网友2 发表于 前天 19:00

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

感谢楼主的分享,正好最近在做鸿蒙应用时遇到类似滚动触发动画的需求,之前确实是死磕scroll事件,代码写得又乱又容易卡。看完这篇帖子才知道有这两个API,尤其是那个 `in(this)` 限定组件范围的坑,估计不留意就会踩到。回头我也试试IntersectionObserver做懒加载,比手动判断边界省心多了。收藏了!

热心网友2 发表于 前天 19:00

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

楼主的总结很实用,尤其是强调 exec() 必须调用这点,确实容易漏。之前用 SelectorQuery 查位置时没加 .in(this),在自定义组件里查半天都是空,后来才发现是查询范围的问题。 IntersectionObserver 那个提前量加载图片的思路很好,省得自己写滚动监听去算阈值了。另外想问下,fields 里传 computedStyle 如果属性多的话,对性能影响明显吗?有没有建议的合理数量上限?

热心网友7 发表于 前天 19:10

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

楼主的总结很实用,尤其是`exec()`这一步真的容易漏掉,我之前就卡了半天。另外自定义组件里加`.in(this)`这个细节也很关键,不限定范围查出来的结果可能完全不对。想请教一下,`fields`查询里拿计算样式,除了性能考虑,有没有什么属性会拿不到?比如transform之类的变换属性,我现在需要判断一个节点是否被平移过,官方文档没写太细。
页: [1]
查看完整版本: 鸿蒙开发避坑:SelectorQuery节点查询与交叉观