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 检查状态的变化。
如果我们想实现这个功能的话,可以利用 MutationObserver 去监听 element 和其 ancestor element 的 style 和 class 属性变化。
每当属性变更,就重新调用一次 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);