MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Svelte生命周期全解:从onMount到onDestroy的完整流程

2021-06-113.7k 阅读

Svelte 生命周期概述

在 Svelte 应用的开发过程中,理解组件的生命周期是至关重要的。生命周期钩子函数提供了在组件不同阶段执行代码的能力,这些阶段涵盖了从组件创建、插入到 DOM、更新,再到最终销毁的整个过程。通过合理利用这些钩子函数,开发者能够更好地控制组件的行为,处理副作用,如数据获取、事件绑定与解绑等操作。

onMount 钩子函数

onMount 基本概念

onMount 是 Svelte 组件生命周期中的一个重要钩子函数,它会在组件首次渲染到 DOM 后立即执行。这意味着当组件的 HTML 结构已经被插入到页面中,并且相关的样式和初始状态都已设置好时,onMount 内的代码将会被触发。这个时机非常适合执行那些依赖于 DOM 存在的操作,例如初始化第三方库(像 Chart.js 创建图表,或者初始化一些需要操作 DOM 元素的 UI 组件库),获取 DOM 元素的尺寸,以及设置一些仅在首次渲染后才需要的事件监听器等。

onMount 代码示例

假设我们要创建一个简单的 Svelte 组件,当组件渲染到页面后,在控制台打印出组件对应的 DOM 元素的宽度。

<script>
  import { onMount } from'svelte';

  let myDiv;

  onMount(() => {
    if (myDiv) {
      console.log('The width of the div is:', myDiv.offsetWidth);
    }
  });
</script>

<div bind:this={myDiv}>
  This is a div inside the Svelte component.
</div>

在上述代码中,我们首先从 svelte 模块中导入 onMount 函数。然后声明了一个变量 myDiv 用于引用我们要操作的 DOM 元素。通过 bind:this 指令,我们将模板中的 <div> 元素绑定到 myDiv 变量上。在 onMount 回调函数中,我们检查 myDiv 是否存在(确保 DOM 元素已经渲染),然后打印出它的宽度。

再来看一个使用 onMount 初始化第三方库的例子,这里以初始化一个简单的滑块(slider)组件为例,假设我们使用的是一个虚构的 SimpleSlider 库。

<script>
  import { onMount } from'svelte';
  import SimpleSlider from 'SimpleSlider-library';

  let sliderEl;

  onMount(() => {
    if (sliderEl) {
      new SimpleSlider(sliderEl, {
        min: 0,
        max: 100,
        value: 50
      });
    }
  });
</script>

<div bind:this={sliderEl}>
  <!-- This div will be the container for the slider -->
</div>

在此示例中,我们导入了 SimpleSlider 库,并在 onMount 钩子函数中对其进行初始化,传入滑块容器的 DOM 元素以及相关配置选项。只有在组件成功渲染到 DOM 后,我们才能确保 sliderEl 存在并进行初始化操作,这正是 onMount 发挥作用的场景。

组件更新相关钩子

beforeUpdate 钩子函数

  1. beforeUpdate 基本概念 beforeUpdate 钩子函数会在组件的响应式数据发生变化,并且即将重新渲染 DOM 之前被调用。这为开发者提供了一个在 DOM 更新前执行某些操作的机会,例如取消正在进行的异步操作(防止不必要的重复请求),或者对即将更新的数据进行预处理。当组件的某个响应式变量值改变时,Svelte 会触发重新渲染流程,在这个流程正式开始前,beforeUpdate 函数内的代码会首先执行。
  2. beforeUpdate 代码示例 考虑一个简单的计数器组件,每次点击按钮计数器增加,同时我们在 beforeUpdate 中记录旧的计数器值。
<script>
  import { beforeUpdate } from'svelte';
  let count = 0;
  let oldCount;

  beforeUpdate(() => {
    oldCount = count;
  });

  function increment() {
    count++;
  }
</script>

<button on:click={increment}>Increment</button>
<p>Current count: {count}</p>
<p>Old count before last update: {oldCount}</p>

在上述代码中,当点击按钮触发 increment 函数,count 值改变,从而触发重新渲染。在重新渲染 DOM 之前,beforeUpdate 钩子函数会将当前的 count 值保存到 oldCount 变量中,这样我们就可以在更新后对比新旧值。

afterUpdate 钩子函数

  1. afterUpdate 基本概念 afterUpdate 钩子函数在组件的 DOM 已经更新完成后执行。这对于那些依赖于最新 DOM 状态的操作非常有用,比如重新计算元素的尺寸,因为在 afterUpdate 执行时,DOM 已经反映了最新的数据变化。与 beforeUpdate 相对应,afterUpdate 是在重新渲染流程结束,新的 DOM 结构已经生效后触发。
  2. afterUpdate 代码示例 假设我们有一个可扩展的文本区域组件,当文本内容改变时,我们希望自动调整文本区域的高度以适应内容。
<script>
  import { afterUpdate } from'svelte';
  let text = '';

  afterUpdate(() => {
    const textarea = document.querySelector('textarea');
    if (textarea) {
      textarea.style.height = 'auto';
      textarea.style.height = textarea.scrollHeight + 'px';
    }
  });
</script>

<textarea bind:value={text}></textarea>

在这个例子中,当 text 值发生变化,文本区域重新渲染后,afterUpdate 钩子函数获取到最新的 <textarea> 元素。我们首先将其高度设置为 auto,然后再将高度设置为其滚动高度(scrollHeight),从而实现自动调整高度以适应文本内容的效果。

onDestroy 钩子函数

onDestroy 基本概念

onDestroy 钩子函数会在组件从 DOM 中移除,即将被销毁时执行。这个钩子函数主要用于清理在组件生命周期中创建的副作用,例如解绑事件监听器,取消未完成的异步请求,或者释放占用的资源等。如果在组件的 onMount 中绑定了事件监听器或者发起了异步请求,那么在组件销毁时,应该在 onDestroy 中对这些操作进行清理,以避免内存泄漏等问题。

onDestroy 代码示例

假设我们在组件中添加了一个全局的 window 滚动事件监听器,当组件销毁时,我们需要移除这个监听器。

<script>
  import { onMount, onDestroy } from'svelte';

  let scrollPosition;

  onMount(() => {
    window.addEventListener('scroll', () => {
      scrollPosition = window.scrollY;
      console.log('Current scroll position:', scrollPosition);
    });
  });

  onDestroy(() => {
    window.removeEventListener('scroll', () => {
      scrollPosition = window.scrollY;
      console.log('Current scroll position:', scrollPosition);
    });
  });
</script>

<p>This component has a scroll event listener that logs the scroll position.</p>

在上述代码中,onMount 钩子函数为 window 对象添加了一个滚动事件监听器,当用户滚动页面时,会在控制台打印出当前的滚动位置。而在 onDestroy 钩子函数中,我们移除了这个事件监听器,确保在组件销毁后,不会再有不必要的事件处理函数占用资源。

再看一个关于取消异步请求的例子,假设我们使用 fetch 进行数据获取,并且在组件销毁时需要取消这个请求。

<script>
  import { onMount, onDestroy } from'svelte';
  let data;
  let controller;

  onMount(() => {
    controller = new AbortController();
    const signal = controller.signal;

    fetch('https://example.com/api/data', { signal })
    .then(response => response.json())
    .then(result => {
        data = result;
      })
    .catch(error => {
        if (error.name === 'AbortError') {
          console.log('Request aborted');
        } else {
          console.error('Error fetching data:', error);
        }
      });
  });

  onDestroy(() => {
    if (controller) {
      controller.abort();
    }
  });
</script>

{#if data}
  <p>Data fetched: {JSON.stringify(data)}</p>
{/if}

在这个示例中,onMount 钩子函数发起一个 fetch 请求,并创建了一个 AbortController 用于控制请求。在 onDestroy 钩子函数中,如果 controller 存在,我们调用 abort 方法取消请求,防止在组件销毁后请求继续执行,从而避免潜在的内存泄漏和不必要的资源消耗。

父子组件生命周期交互

父组件更新对子组件生命周期的影响

当父组件的状态发生变化,导致父组件重新渲染时,子组件也会受到影响。如果父组件传递给子组件的属性(props)发生了变化,子组件会触发更新流程,即 beforeUpdateafterUpdate 钩子函数会被调用。这是因为 Svelte 会检测到子组件输入的变化,并重新渲染子组件以反映这些变化。 例如,我们有一个父组件 Parent.svelte 和一个子组件 Child.svelte

// Parent.svelte
<script>
  import Child from './Child.svelte';
  let count = 0;

  function increment() {
    count++;
  }
</script>

<button on:click={increment}>Increment in Parent</button>
<Child value={count} />
// Child.svelte
<script>
  import { beforeUpdate, afterUpdate } from'svelte';
  export let value;

  beforeUpdate(() => {
    console.log('Child is about to update due to prop change in Parent');
  });

  afterUpdate(() => {
    console.log('Child has updated due to prop change in Parent');
  });
</script>

<p>Value from Parent: {value}</p>

在上述代码中,当父组件中的 count 值改变并传递给子组件时,子组件会触发更新,beforeUpdateafterUpdate 钩子函数中的日志会在控制台打印出来。

子组件销毁与父组件的关系

当子组件从 DOM 中移除(例如通过条件渲染控制子组件的显示与隐藏),子组件的 onDestroy 钩子函数会被调用。父组件通常不需要直接处理子组件的销毁逻辑,但在某些情况下,父组件可能需要根据子组件的销毁状态来调整自身的状态。例如,假设父组件维护一个子组件列表,当某个子组件被销毁时,父组件可以从列表中移除对应的引用。

// Parent.svelte
<script>
  import Child from './Child.svelte';
  let showChild = true;
  let childList = [];

  function toggleChild() {
    showChild =!showChild;
    if (!showChild) {
      // 假设子组件有一个唯一标识 id
      const childToRemove = childList.find(c => c.id === someChildId);
      if (childToRemove) {
        childList = childList.filter(c => c.id!== someChildId);
      }
    }
  }
</script>

<button on:click={toggleChild}>Toggle Child</button>
{#if showChild}
  <Child bind:this={childRef} id={someChildId} />
{/if}
// Child.svelte
<script>
  import { onDestroy } from'svelte';
  export let id;

  onDestroy(() => {
    // 可以通过某种方式通知父组件自己被销毁
    console.log(`Child with id ${id} is being destroyed`);
  });
</script>

<p>This is a child component</p>

在这个例子中,当父组件通过 toggleChild 函数控制子组件的显示与隐藏时,子组件销毁时会在控制台打印日志,同时父组件可以根据子组件的销毁情况(通过 id 标识)从 childList 中移除相应的子组件引用。

嵌套组件的生命周期顺序

多层嵌套组件创建时的生命周期顺序

当创建多层嵌套的 Svelte 组件时,生命周期钩子函数的执行顺序是从最外层组件开始,逐步向内层组件执行 onMount 钩子函数。例如,假设有一个 App.svelte 组件,它包含一个 Parent.svelte 组件,而 Parent.svelte 又包含一个 Child.svelte 组件。

// App.svelte
<script>
  import Parent from './Parent.svelte';
</script>

<Parent />
// Parent.svelte
<script>
  import Child from './Child.svelte';
</script>

<Child />
// Child.svelte
<script>
  import { onMount } from'svelte';

  onMount(() => {
    console.log('Child onMount');
  });
</script>

<p>This is the child component</p>

在这种情况下,首先 App.svelteonMount(如果有)会执行,然后是 Parent.svelteonMount,最后是 Child.svelteonMount。这是因为组件的渲染是自上而下进行的,只有外层组件渲染到一定阶段,内层组件才会开始渲染并触发其 onMount 钩子函数。

多层嵌套组件销毁时的生命周期顺序

与创建时相反,当多层嵌套组件被销毁时,生命周期钩子函数的执行顺序是从最内层组件开始,逐步向外层组件执行 onDestroy 钩子函数。继续以上面的例子为例,当这些组件被销毁时,首先 Child.svelteonDestroy 会执行,然后是 Parent.svelteonDestroy,最后是 App.svelteonDestroy。这种顺序确保了内层组件的资源先被清理,然后再清理外层组件的资源,避免了外层组件依赖内层组件资源而导致的潜在问题。

特殊场景下的生命周期处理

动态组件切换时的生命周期

在 Svelte 中,当使用动态组件(通过 {#if}{#each} 等指令动态显示或隐藏组件)时,组件的生命周期会相应地触发。例如,通过 {#if} 条件判断来切换两个不同的组件 ComponentAComponentB

<script>
  import ComponentA from './ComponentA.svelte';
  import ComponentB from './ComponentB.svelte';
  let showA = true;

  function toggleComponent() {
    showA =!showA;
  }
</script>

<button on:click={toggleComponent}>Toggle Component</button>
{#if showA}
  <ComponentA />
{:else}
  <ComponentB />
{/if}

showA 的值发生变化时,即将被隐藏的组件会触发 onDestroy 钩子函数,而即将显示的组件会触发 onMount 钩子函数。这使得开发者可以在组件切换时进行必要的清理和初始化操作。

组件在路由变化时的生命周期

在使用 Svelte 进行路由开发(例如使用 svelte - routing 库)时,路由变化会导致组件的挂载和卸载。当用户导航到一个新的路由,对应的组件会被挂载并触发 onMount 钩子函数,而当前路由对应的组件会被卸载并触发 onDestroy 钩子函数。例如,假设我们有两个路由组件 Home.svelteAbout.svelte

// main.js (using svelte - routing)
import { Router, Route } from'svelte - routing';
import Home from './Home.svelte';
import About from './About.svelte';

const router = new Router({
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
});

当用户从 / 导航到 /about 时,Home.svelte 组件会触发 onDestroy,而 About.svelte 组件会触发 onMount。开发者可以利用这些生命周期钩子函数来处理与路由相关的副作用,比如在进入新路由时获取特定的数据,或者在离开当前路由时保存状态等操作。

通过深入理解 Svelte 组件的生命周期,从 onMountonDestroy 的完整流程,开发者能够更好地编写健壮、高效且易于维护的前端应用程序,灵活处理各种复杂的业务逻辑和交互场景。无论是处理 DOM 操作、管理异步任务,还是协调组件间的关系,生命周期钩子函数都提供了强大的功能和灵活性。