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

Svelte Action详解:use:指令的基本用法与自定义操作

2024-07-052.9k 阅读

Svelte Action 简介

在 Svelte 框架中,Action 是一种非常强大且独特的功能。它允许开发者为 DOM 元素附加自定义行为,这些行为可以在元素插入到 DOM 中、更新时以及从 DOM 中移除时执行特定的逻辑。通过 Action,我们能够实现一些用普通的 Svelte 声明式语法难以达成的复杂交互和功能,为前端开发带来更多的灵活性和控制力。

Action 通常使用 use: 指令来应用到 Svelte 组件的 DOM 元素上。这个指令后面跟着一个函数或者一个对象,这个函数或对象定义了 Action 的具体行为。

use: 指令的基本用法

简单的点击计数 Action

首先,让我们来看一个简单的示例,通过 Action 来实现一个点击计数的功能。假设我们有一个按钮,每次点击按钮时,我们想要统计点击的次数并显示出来。

<script>
    let count = 0;
    function clickCounter(node) {
        const handleClick = () => {
            count++;
        };
        node.addEventListener('click', handleClick);
        return {
            destroy() {
                node.removeEventListener('click', handleClick);
            }
        };
    }
</script>

<button use:clickCounter>{count} 次点击</button>

在上述代码中:

  1. 我们定义了一个名为 clickCounter 的函数,这个函数就是我们的 Action。它接收一个 node 参数,这个 node 就是应用了该 Action 的 DOM 元素(在这里就是按钮)。
  2. clickCounter 函数内部,我们定义了一个 handleClick 函数,用于处理按钮的点击事件,每次点击时将 count 变量加 1。
  3. 然后我们使用 node.addEventListener('click', handleClick) 为按钮添加点击事件监听器。
  4. 最后,clickCounter 函数返回一个对象,对象中有一个 destroy 方法。这个 destroy 方法会在 DOM 元素从页面移除时被调用,我们在其中使用 node.removeEventListener('click', handleClick) 移除之前添加的点击事件监听器,以避免内存泄漏。

聚焦输入框 Action

另一个常见的场景是在页面加载时自动聚焦到某个输入框。我们可以通过 Action 来轻松实现这个功能。

<script>
    function focusInput(node) {
        node.focus();
        return {
            update(newValue) {
                if (newValue) {
                    node.focus();
                }
            }
        };
    }
</script>

<input type="text" use:focusInput>

在这个例子中:

  1. focusInput 函数作为 Action 接收 node 参数,在函数内部直接调用 node.focus() 使输入框获得焦点。
  2. 函数返回的对象中有一个 update 方法。这个 update 方法会在 Action 接收到新的值时被调用(通过 use:action={value} 的形式传递值)。在这里,如果传递了新的值且为真,就再次聚焦输入框。这种机制使得我们可以根据外部条件来动态控制输入框的聚焦行为。

传递参数给 Action

有时候,我们的 Action 需要接收一些参数来动态调整其行为。比如,我们想要创建一个可以控制动画时长的淡入淡出 Action。

<script>
    function fade(node, { duration = 1000 }) {
        node.style.opacity = 0;
        const fadeIn = () => {
            const interval = setInterval(() => {
                const opacity = parseFloat(node.style.opacity);
                if (opacity < 1) {
                    node.style.opacity = opacity + 0.01;
                } else {
                    clearInterval(interval);
                }
            }, duration / 100);
        };
        fadeIn();
        return {
            destroy() {
                const interval = setInterval(() => {
                    const opacity = parseFloat(node.style.opacity);
                    if (opacity > 0) {
                        node.style.opacity = opacity - 0.01;
                    } else {
                        clearInterval(interval);
                    }
                }, duration / 100);
            }
        };
    }
</script>

<div use:fade={{ duration: 2000 }}>淡入淡出的内容</div>

在上述代码中:

  1. fade Action 函数接收 node 和一个包含 duration 属性的对象作为参数。duration 用于控制动画的时长,默认值为 1000 毫秒。
  2. 在函数内部,首先将元素的透明度设置为 0,然后通过 setInterval 实现淡入效果,每次增加 0.01 的透明度,直到透明度达到 1 时清除定时器。
  3. 在返回的对象中,destroy 方法实现了淡出效果,同样通过 setInterval 逐渐减少透明度,直到透明度为 0 时清除定时器。
  4. 在使用时,通过 use:fade={{ duration: 2000 }} 传递了一个自定义的 duration 值为 2000 毫秒,从而可以动态控制淡入淡出动画的时长。

自定义操作

创建一个拖拽 Action

现在我们来创建一个更复杂的自定义 Action,实现元素的拖拽功能。

<script>
    function draggable(node) {
        let isDragging = false;
        let startX;
        let startY;
        let initialLeft;
        let initialTop;

        const handleMouseDown = (event) => {
            isDragging = true;
            startX = event.clientX;
            startY = event.clientY;
            initialLeft = parseInt(node.style.left) || 0;
            initialTop = parseInt(node.style.top) || 0;
        };

        const handleMouseMove = (event) => {
            if (isDragging) {
                const dx = event.clientX - startX;
                const dy = event.clientY - startY;
                node.style.left = initialLeft + dx + 'px';
                node.style.top = initialTop + dy + 'px';
            }
        };

        const handleMouseUp = () => {
            isDragging = false;
        };

        node.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);

        return {
            destroy() {
                node.removeEventListener('mousedown', handleMouseDown);
                document.removeEventListener('mousemove', handleMouseMove);
                document.removeEventListener('mouseup', handleMouseUp);
            }
        };
    }
</script>

<div style="position: relative; width: 200px; height: 200px;">
    <div use:draggable style="position: absolute; background-color: lightblue; width: 100px; height: 100px;"></div>
</div>

在这个拖拽 Action 的实现中:

  1. draggable 函数接收 node 参数,定义了一些变量来跟踪拖拽状态,如 isDragging 表示是否正在拖拽,startXstartY 记录鼠标按下时的坐标,initialLeftinitialTop 记录元素初始的位置。
  2. handleMouseDown 函数在鼠标按下时被调用,设置 isDraggingtrue,记录鼠标坐标和元素初始位置。
  3. handleMouseMove 函数在鼠标移动时被调用,如果正在拖拽,则根据鼠标移动的距离更新元素的位置。
  4. handleMouseUp 函数在鼠标松开时被调用,设置 isDraggingfalse,结束拖拽。
  5. 为了实现拖拽功能,我们为 node 添加 mousedown 事件监听器,为 document 添加 mousemovemouseup 事件监听器。
  6. 在返回的对象的 destroy 方法中,移除添加的事件监听器,以确保在元素从 DOM 移除时不会产生内存泄漏。

防抖输入框 Action

在处理输入框的输入事件时,防抖是一种常见的需求,以避免频繁触发某些操作。下面我们实现一个防抖的 Action。

<script>
    function debounceInput(node, { delay = 300 }) {
        let timer;
        const handleInput = (event) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                // 这里可以执行防抖后的逻辑,例如发送 AJAX 请求
                console.log('防抖后的输入:', event.target.value);
            }, delay);
        };
        node.addEventListener('input', handleInput);
        return {
            destroy() {
                node.removeEventListener('input', handleInput);
                clearTimeout(timer);
            }
        };
    }
</script>

<input type="text" use:debounceInput={{ delay: 500 }} placeholder="输入内容,500ms 防抖">

在这个防抖 Action 中:

  1. debounceInput 函数接收 node 和一个包含 delay 属性的对象作为参数,delay 表示防抖的延迟时间,默认值为 300 毫秒。
  2. 定义了一个 timer 变量来存储定时器。handleInput 函数在输入框触发 input 事件时被调用,首先清除之前的定时器,然后设置一个新的定时器,在延迟 delay 毫秒后执行相应的逻辑(这里只是简单地在控制台打印输入内容,实际应用中可以是发送 AJAX 请求等操作)。
  3. 为输入框添加 input 事件监听器。在返回的对象的 destroy 方法中,移除事件监听器并清除定时器,以防止内存泄漏。

Action 的生命周期

Action 有其自身的生命周期,主要包括初始化、更新和销毁三个阶段。

初始化阶段

当 DOM 元素被插入到页面中并且应用了 Action 时,Action 函数会被调用,这就是初始化阶段。在这个阶段,我们可以执行一些初始化的操作,比如添加事件监听器、设置元素的初始状态等。例如在前面的点击计数 Action 中,在初始化阶段我们为按钮添加了点击事件监听器。

更新阶段

如果 Action 接收了动态的值(通过 use:action={value} 的形式传递),当这个值发生变化时,Action 的 update 方法会被调用。这个方法可以接收新的值作为参数,我们可以根据新的值来更新 Action 的行为。例如在聚焦输入框 Action 中,当 update 方法接收到新的值且为真时,会再次聚焦输入框。

销毁阶段

当 DOM 元素从页面中移除时,Action 返回的对象中的 destroy 方法会被调用。在这个阶段,我们需要清理在初始化阶段添加的资源,比如移除事件监听器,以避免内存泄漏。在前面的所有示例中,我们都在 destroy 方法中移除了相应的事件监听器。

在组件中使用 Action

Action 不仅可以直接应用到普通的 DOM 元素上,还可以在 Svelte 组件中使用。

封装一个可复用的拖拽组件

假设我们想要封装一个可复用的拖拽组件,我们可以这样做:

<script>
    function draggable(node) {
        let isDragging = false;
        let startX;
        let startY;
        let initialLeft;
        let initialTop;

        const handleMouseDown = (event) => {
            isDragging = true;
            startX = event.clientX;
            startY = event.clientY;
            initialLeft = parseInt(node.style.left) || 0;
            initialTop = parseInt(node.style.top) || 0;
        };

        const handleMouseMove = (event) => {
            if (isDragging) {
                const dx = event.clientX - startX;
                const dy = event.clientY - startY;
                node.style.left = initialLeft + dx + 'px';
                node.style.top = initialTop + dy + 'px';
            }
        };

        const handleMouseUp = () => {
            isDragging = false;
        };

        node.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);

        return {
            destroy() {
                node.removeEventListener('mousedown', handleMouseDown);
                document.removeEventListener('mousemove', handleMouseMove);
                document.removeEventListener('mouseup', handleMouseUp);
            }
        };
    }
</script>

{#each Array.from({ length: 5 }, (_, i) => i) as item}
    <div use:draggable style="position: absolute; background-color: lightblue; width: 100px; height: 100px; left: {Math.random() * 300}px; top: {Math.random() * 300}px;">
        拖拽元素 {item + 1}
    </div>
{/each}

在这个例子中:

  1. 我们定义了 draggable Action 函数,和之前实现的拖拽 Action 逻辑相同。
  2. 使用 #each 块创建了 5 个可拖拽的元素,每个元素都应用了 draggable Action。这样就实现了一个可复用的拖拽组件的效果,每个元素都可以独立地进行拖拽操作。

在嵌套组件中传递 Action

有时候我们可能需要在嵌套组件中传递 Action。例如,我们有一个父组件和一个子组件,父组件定义了一个 Action,然后将其传递给子组件使用。

<!-- Parent.svelte -->
<script>
    import Child from './Child.svelte';
    function highlight(node) {
        node.style.backgroundColor = 'yellow';
        return {
            destroy() {
                node.style.backgroundColor = 'white';
            }
        };
    }
</script>

<Child use:highlight text="传递 Action 的子组件内容" />
<!-- Child.svelte -->
<script>
    export let text;
</script>

<div>{text}</div>

在上述代码中:

  1. Parent.svelte 中,我们定义了一个 highlight Action,当应用到元素上时,会将元素的背景颜色设置为黄色,在销毁时恢复为白色。
  2. 然后我们将 highlight Action 通过 use: 指令传递给 Child 组件,并传递了一个 text 属性。
  3. Child.svelte 组件中,它接收 text 属性并在 div 中显示内容,同时应用了从父组件传递过来的 highlight Action,这样 Child 组件的 div 元素就会应用 highlight Action 的行为。

与其他 Svelte 特性结合使用

Action 可以与 Svelte 的其他特性,如响应式、事件绑定等很好地结合使用,进一步增强应用的功能。

Action 与响应式数据结合

假设我们有一个可以根据响应式数据动态改变颜色的 Action。

<script>
    let color = 'lightblue';
    function changeColor(node) {
        const updateColor = () => {
            node.style.backgroundColor = color;
        };
        updateColor();
        return {
            update(newColor) {
                color = newColor;
                updateColor();
            }
        };
    }
</script>

<button on:click={() => color = color === 'lightblue'? 'lightgreen' : 'lightblue'}>
    切换颜色
</button>
<div use:changeColor={color}>根据响应式数据变色的区域</div>

在这个例子中:

  1. 我们定义了一个 color 响应式变量,初始值为 lightblue
  2. changeColor Action 函数接收 node 参数,在函数内部定义了 updateColor 函数用于更新元素的背景颜色。在初始化时调用 updateColor 设置初始颜色。
  3. 返回的对象中的 update 方法接收新的颜色值,更新 color 变量并再次调用 updateColor 更新元素颜色。
  4. 通过按钮的点击事件改变 color 的值,由于 Action 与响应式数据的结合,div 元素的背景颜色会随之动态改变。

Action 与事件绑定结合

我们可以将 Action 与事件绑定结合起来,实现更复杂的交互逻辑。例如,我们有一个可以根据点击状态改变样式的 Action,同时结合点击事件来控制这个状态。

<script>
    let isClicked = false;
    function clickStyle(node) {
        const updateStyle = () => {
            node.style.border = isClicked? '2px solid red' : '1px solid gray';
        };
        updateStyle();
        return {
            update(newIsClicked) {
                isClicked = newIsClicked;
                updateStyle();
            }
        };
    }
</script>

<button use:clickStyle={isClicked} on:click={() => isClicked =!isClicked}>
    点击改变样式
</button>

在这个示例中:

  1. 定义了一个 isClicked 响应式变量来表示按钮是否被点击。
  2. clickStyle Action 函数接收 node 参数,内部的 updateStyle 函数根据 isClicked 的值来设置按钮的边框样式。初始化时调用 updateStyle 设置初始样式。
  3. 返回对象的 update 方法接收新的点击状态值,更新 isClicked 并再次调用 updateStyle 更新样式。
  4. 通过按钮的点击事件绑定,每次点击时切换 isClicked 的值,从而触发 Action 的 update 方法,实现按钮样式的动态改变。

注意事项

  1. 内存泄漏:在 Action 的 destroy 方法中一定要清理在初始化阶段添加的所有资源,特别是事件监听器。如果不这样做,当元素从 DOM 移除时,事件监听器仍然会存在,可能会导致内存泄漏,使得应用随着时间推移性能下降。
  2. 性能问题:在 Action 中如果进行频繁的 DOM 操作,可能会导致性能问题。尤其是在 update 方法中,如果每次更新都进行大量的 DOM 改变,会影响应用的流畅性。尽量优化 Action 的逻辑,减少不必要的 DOM 操作。
  3. 兼容性:虽然 Svelte 本身在现代浏览器中有很好的兼容性,但在 Action 中使用一些特定的 DOM API 或 JavaScript 特性时,要注意浏览器兼容性。可以使用 polyfill 等技术来确保在不同浏览器中都能正常工作。

通过深入理解 Svelte Action 的基本用法和自定义操作,开发者能够更加灵活地构建复杂且交互性强的前端应用,充分发挥 Svelte 框架的强大功能。无论是简单的点击计数,还是复杂的拖拽、动画等功能,Action 都为我们提供了一种优雅且高效的实现方式。同时,合理地与 Svelte 的其他特性结合使用,并注意相关的注意事项,能够让我们开发出性能良好、健壮的前端应用。