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);
View Code

 

posted @ 2025-03-28 21:09  兴杰  阅读(66)  评论(0)    收藏  举报