惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
美团技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
WordPress大学
WordPress大学
月光博客
月光博客
宝玉的分享
宝玉的分享
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
Apple Machine Learning Research
Apple Machine Learning Research
A
About on SuperTechFans
J
Java Code Geeks
博客园_首页
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
Vercel News
Vercel News
博客园 - 聂微东

博客园 - 兴杰

JavaScript Library – Astro 工具 - pnpm 前端基础几何计算 – basic geometric tan sin cos CSS & JS Effect – 用 SVG 画曲线和箭头 CSS & JS Effect – 画直角三角形与边框 CSS & JS Effect – 画箭头 C# – Nullable Reference Types ASP.NET Core Library – Magick.NET CSS & JS Effect – Smooth scrollIntoView JavaScript Library – Embla Carousel 富文本编辑器 ProseMirror 第二篇 の Schema 富文本编辑器 ProseMirror 第一篇 の 初探 JavaScript – Pipeline Operator 工具 – Playwright 工具 – Rsbuild JavaScript Library – Markdown CSS & JS Effect – 大杂烩 BOM – Clipboard API OData – 大杂烩 OData – 基础语法 Basic
DOM – checkVisibility
兴杰 · 2025-03-28 · via 博客园 - 兴杰

介绍

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

View Code