my-blog:手写图片懒加载指令

加载中... 浏览

1. 简历描述(亮点总结建议1-2句)

核心亮点:手写自定义指令 v-lazy 实现基于 IntersectionObserver 的图片懒加载优化

  • 背景:针对博客中多图文章导致的首屏网络阻塞问题,摒弃了传统的监听 scroll 事件方案。
  • 成果:有效减少了首屏 60%+ 的无效资源加载,避免了获取 DOM 尺寸引起的浏览器强迫同步布局,显著降低了长列表页面的内存占用。

2. 功能背景:为什么做?有什么区别?

  • 痛点:如果一篇文章有 20 张高清图,用户刚打开时,浏览器会同时发起 20 个请求。这不仅耗带宽,还会抢占正在加载的 CSS 和 JS 的带宽,导致页面白屏时间长。
  • 传统做法 vs 你的做法
    • 传统做法:监听 window.scroll 事件,触发时计算 offsetTop
    • 缺点:滚动一秒钟触发几十次,而且 offsetTop 等属性会强制浏览器重新渲染页面(回流),极其卡顿。
    • 你的做法:使用 IntersectionObserver
    • 优点:这是浏览器原生的“交叉观察器”,它只在元素进入视口的一瞬间才异步通知你。它更聪明、更省电、完全不卡顿。

3. 核心代码逐行解析 (components/directives/lazy.ts)

为了面试能讲清楚,我们需要对每一行代码负责:

typescript
import type { Directive } from 'vue'

// 【逻辑】定义一个 Vue 指令对象。Directive<根元素类型, 传入的值类型>。
export const lazyDirective: Directive<HTMLImageElement, string> = {
  // 【逻辑】mounted 代表 <img> 标签被插进页面的一瞬间执行。
  mounted(el, binding) {
    // 1. 【逻辑】先占坑。
    // 在真实图下载好之前,给一张极小的透明图,保证页面不留白,也不报错。
    el.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';

    // 2. 【逻辑】创建“保安”(观察器)。
    // 【考点】它接收一个回调函数,当图片和视口“交叉”时执行。
    const observer = new IntersectionObserver((entries) => {
      // entries 是一个监控列表,因为可能一个保安盯着好几个元素。
      entries.forEach(entry => {
        // 【核心】判断是否进入了视口范围。
        if (entry.isIntersecting) {
          // 【逻辑】既然用户看到了,那就把 v-lazy="xxx" 里的 xxx 真实地址给 src。
          // 此时浏览器才会真正发起这个图片地址的网络下载请求。
          el.src = binding.value;

          // 【逻辑】防御性编程:如果下载失败,给个保底图。
          el.onerror = () => {
            el.src = 'https://via.placeholder.com/...';
          };

          // 【逻辑】大功告成,辞退保安。
          // 既然加载过了,就不需要再观察这张图了,节省内存空间。
          observer.unobserve(el);
        }
      });
    });

    // 3. 【逻辑】告诉保安去盯着这个具体的图片标签。
    observer.observe(el);

    // 【考点】为什么存到 el 上?
    // 为了将来在 unmounted 钩子(组件销毁时)能拿到同一个保安去清理掉。
    (el as any)._observer = observer;
  },

  // 【逻辑】当页面跳转、文章销毁时执行。
  unmounted(el) {
    // 【深度考点】手动 disconnect 观察器。
    // 如果不手动断开,这种全局监听可能会导致内存泄露,这是资深面试官必问的职场规范。
    const observer = (el as any)._observer;
    if (observer) {
      observer.disconnect();
    }
  }
};

4. 面试必问场景(模拟对答)

场景一:为什么要自定义指令,而不是直接写在组件里?

你的对答: “因为通用性抽象程度。懒加载是一个与具体业务无关、纯粹操作底层 DOM 的功能。把它封装成指令 v-lazy,我可以让任何地方、任何组件里的 <img> 标签通过简单加个属性就获得性能优化,代码复用率极高,也符合 Vue 开发的最佳实践(将复杂 DOM 操作解耦)。”

场景二:深挖性能——回流(Reflow)与重绘(Repaint)

面试官提问: “你刚才提到传统做法会引起‘回流’,你能解释一下什么是回流,为什么你的 IntersectionObserver 没有这个问题?”

你的对答:回流是指当元素的尺寸、位置发生变化时,浏览器需要重新计算整个网页的布局。而像 offsetTopgetComputedStyle 这些 API 即使你没改数据,只要你去‘读取’,浏览器为了保证给你最准的数据,也会强制触发一次回流(Forced Synchronous Layout)。 而我的方案中,IntersectionObserver 属于异步执行,它是浏览器底层在每一帧渲染的最后阶段主动告诉我们元素是否可见,它不会阻塞渲染主线程,也不会因为高频执行而导致掉帧,所以性能是碾压级别的。”

场景三:如果图片非常多,怎么优化“保安”的数量?

面试官提问(进阶): “如果你页面有一千张图,你每一张图都 new 一个观察器实例吗?这会不会很占内存?”

你的对答(展现思考): “目前代码里确实是每个元素一个实例,对于博客文章来说足够了。但如果真的遇到超长列表,我会采用**‘观察器单例模式’**。即在全局创建一个共享的监听器实例,维护一个 Map。这样无论页面有多少张图,都共用同一个‘保安大队负责人’,根据进出的元素去派发任务,这样内存占用会极低。”

场景四:用户滑动太快,图片还没加载完就划走了怎么办?

你的对答: “这其实也是懒加载的一个天然优势。因为我们是先有了一个观察,如果用户划走太快,我们可以在 unobserve 之后甚至在加载过程中中止加载(如请求还没发出去)。在大厂业务里,我们还会给图片设置一个 threshold(预加载阈值),比如快滚到剩下 200px 时就开始加载,给网络传输留出 buffer,让用户感官上觉得‘图一直都在’,而不是等看到了才加载。”


5. 八股关键词联想映射

考点关键词你的回答关键词
浏览器渲染流水线回流、重绘、合成层、帧渲染周期
性能优化指标LCP (Largest Contentful Paint)、资源抢占
JS 设计模式注册中心、单例模式、观察者模式
Vue 指令生命周期mounted vs updated, 内存泄漏防范
DOM 操作异步 API vs 同步 API 阻塞性能

留言板

加载评论中...
my-blog:全栈交互体验(上)- 浏览量统计
浏览器原理高频面试题深度解析
Valaxy v0.28.0-beta.1 驱动|主题-Yunv0.28.0-beta.1