鸿蒙开发避坑: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 可以覆盖大多数滚动联动、懒加载和曝光统计需求。建议在代码中统一封装工具方法,并在页面销毁时做好观察器的清理工作,避免内存问题。
Re: 鸿蒙开发避坑:SelectorQuery节点查询与交叉观
感谢楼主的分享,正好最近在做鸿蒙应用时遇到类似滚动触发动画的需求,之前确实是死磕scroll事件,代码写得又乱又容易卡。看完这篇帖子才知道有这两个API,尤其是那个 `in(this)` 限定组件范围的坑,估计不留意就会踩到。回头我也试试IntersectionObserver做懒加载,比手动判断边界省心多了。收藏了!Re: 鸿蒙开发避坑:SelectorQuery节点查询与交叉观
楼主的总结很实用,尤其是强调 exec() 必须调用这点,确实容易漏。之前用 SelectorQuery 查位置时没加 .in(this),在自定义组件里查半天都是空,后来才发现是查询范围的问题。 IntersectionObserver 那个提前量加载图片的思路很好,省得自己写滚动监听去算阈值了。另外想问下,fields 里传 computedStyle 如果属性多的话,对性能影响明显吗?有没有建议的合理数量上限?Re: 鸿蒙开发避坑:SelectorQuery节点查询与交叉观
楼主的总结很实用,尤其是`exec()`这一步真的容易漏掉,我之前就卡了半天。另外自定义组件里加`.in(this)`这个细节也很关键,不限定范围查出来的结果可能完全不对。想请教一下,`fields`查询里拿计算样式,除了性能考虑,有没有什么属性会拿不到?比如transform之类的变换属性,我现在需要判断一个节点是否被平移过,官方文档没写太细。
页:
[1]