DOM – checkVisibility
介绍
checkVisibility 是一个比较新的功能,需要 IOS 17.4 才能支持。

它主要的作用是拿来检测一个 element 是否可见。
我们有 4 种方法让一个 element 隐藏起来,checkVisibility 可以依据不同的隐藏方式,判断出 element 是否被隐藏了,或者是可见的。
display:none
<h1 style="display: none;">Hello World</h1>
h1 是 display:none
调用 checkVisibility
const h1 = document.querySelector<HTMLElement>('h1')!;
console.log('is visible?', h1.checkVisibility());
效果

很好理解,display: none 自然 "不可见"。
visibility: hidden
<h1 style="visibility: hidden;">Hello World</h1>
效果

默认情况下,visibility: hidden 被视为 "可见"。
如果我们不这么认为的话,可以在调用 checkVisibility 时传入一个 config
console.log('is visible?', h1.checkVisibility({ visibilityProperty: true }));
这样 visibility: hidden 就会被视为 "不可见"。
效果

注:checkVisibilityCSS 是 visibilityProperty 的别名 alias。
opacity: 0
<h1 style="opacity: 0;">Hello World</h1>
效果

和 visibility 一样,opacity: 0 默认情况下被视为 "可见"。
我们同样可以透过 config 去调节。
console.log('is visible?', h1.checkVisibility({ opacityProperty: true }));
这样 opacity: 0 就会被视为 "不可见"。
效果

注1:opacity > 0 就一定被视为 “可见”。
注2:checkOpacity 是 opacityProperty 的别名 alias。
content-visibility: hidden
<div style="content-visibility: hidden;"> <h1>Hello World</h1> </div>
效果

content-visibility: hidden 和 display: none 效果一样,均视为 "不可见"。
content-visibility: auto
<div style="content-visibility: auto;"> <h1>Hello World</h1> </div>
效果

默认情况下,不管 h1 是否被渲染出来,checkVisibility 都是 true "可见",这和 visibility: hidden 效果雷同。
如果我们不认同这个默认行为,可以透过 config 去调节它
console.log('is visible?', h1.checkVisibility({ contentVisibilityAuto: true }));
调节后,只有当 h1 被渲染出来 (通常是当用户 scroll 到 h1 这个区域的时候),checkVisibility 才返回 true "可见",否则会返回 false "不可见"。
Polyfill and Workaround
checkVisibility 很好用,但需要 IOS 17.4 才支持。
也就是说至少要 2017 年尾发布的 iPhone X 才能支持。
如果想兼容更老旧的设备,我们就需要 polyfill。
但很遗憾,没有现成的 polyfill 可以用。
我们只能自己写了,幸好也不难写。
只要透过 computedStyle 获取 element 的 display, visibility 和 opacity 属性,
然后查看 value 是不是 none, hidden, 0,如果是的话,那就表示 "不可见"。
除了查看当前 element,我们也需要查看其所有 ancestor elements,因为 ancestor element "不可见" 也会导致 descendant element "不可见"。
注:我刻意忽略了 content-visibility 的检查,这是因为 content-visibility 要 IOS 18 才能支持,如果设备都已经能支持 content-visibility 了,那要求更低的 checkVisibility 自然是支持的,也就不需要 polyfill 了。
watch visibility status
checkVisibility 只能检查当前 element 的状态。它不像 matchMedia 那样可以 addEventListener 检查状态的变化。
如果我们想要监听 visibility 变更,没有 built-in 方案。
我们只能考一些间接的方式,比如利用 MutationObserver 去监听 element 和其 ancestor element 的 style 和 class 属性变化,或者 matchMedia 去监听 breakpoint(都是一些间接的手法)。
只要这些间接行为发生,我们就重新调用一次 checkVisibility 做检查。
演示代码
const h1 = document.querySelector<HTMLElement>('h1')!;
interface VisibilityState {
readonly visible: boolean;
readonly visibleChange$: Observable<boolean>;
}
function checkVisibility(
element: HTMLElement,
options?: Pick<CheckVisibilityOptions, 'opacityProperty' | 'visibilityProperty' | 'contentVisibilityAuto'>,
): VisibilityState {
const visible = internalCheckVisibility(element, options);
const visibleChange$ = defer(() => {
const selfAndAncestors = [element, ...getAncestors(element)];
const mo = new StgMutationObserver();
const styleChange$ = merge(
...selfAndAncestors.map(element =>
mo.observe(element, { attributes: true, attributeFilter: ['class', 'style'] }),
),
);
return styleChange$.pipe(
debounce(() => of(undefined).pipe(observeOn(asapScheduler))),
map(() => internalCheckVisibility(element, options)),
);
});
return {
visible,
visibleChange$,
};
function internalCheckVisibility(
element: HTMLElement,
options?: Pick<CheckVisibilityOptions, 'opacityProperty' | 'visibilityProperty' | 'contentVisibilityAuto'>,
): boolean {
if (typeof element.checkVisibility === 'function') {
return element.checkVisibility(options);
}
const selfAndAncestors = [element, ...getAncestors(element)];
const anyHidden = selfAndAncestors.some(element => {
const computedStyle = window.getComputedStyle(element);
return (
computedStyle.display === 'none' ||
(options?.visibilityProperty && computedStyle.visibility === 'hidden') ||
(options?.opacityProperty && computedStyle.opacity === '0')
);
});
return !anyHidden;
}
function getAncestors(element: HTMLElement): HTMLElement[] {
const ancestors: HTMLElement[] = [];
let loopElement = element;
while (true) {
const parent = loopElement.parentElement;
if (parent === null) break;
ancestors.push(parent);
loopElement = parent;
}
return ancestors;
}
}
type StgMutationRecord<TElement = Node> = Omit<MutationRecord, 'target'> & { target: TElement };
class StgMutationObserver<TElement extends Node = Node> {
private readonly mutationObserverSubscriberMap = new Map<
MutationObserver,
Subscriber<StgMutationRecord<TElement>[]>
>();
observe(target: TElement, options?: MutationObserverInit): Observable<StgMutationRecord<TElement>[]> {
return new Observable(subscriber => {
const mo = new MutationObserver(records => {
subscriber.next(records as StgMutationRecord<TElement>[]);
});
mo.observe(target, options);
this.mutationObserverSubscriberMap.set(mo, subscriber);
return () => {
mo.disconnect();
this.mutationObserverSubscriberMap.delete(mo);
};
});
}
disconnect() {
for (const [mo, subscriber] of this.mutationObserverSubscriberMap.entries()) {
mo.disconnect();
subscriber.complete();
this.mutationObserverSubscriberMap.delete(mo);
}
}
}
const state = checkVisibility(h1, { opacityProperty: true });
console.log('now', state.visible);
state.visibleChange$.subscribe(visible => console.log('future', visible));
window.setTimeout(() => {
h1.parentElement!.style.opacity = '0';
}, 2000);

浙公网安备 33010602011771号