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

Svelte自定义Action:从零开始构建复杂DOM行为

2024-01-182.8k 阅读

理解 Svelte 中的 Action

在 Svelte 框架中,Action 是一种强大的机制,它允许我们在 DOM 元素上附加自定义行为。这意味着我们可以在不直接操作 DOM 的情况下,为特定元素添加额外的功能。Action 本质上是一个函数,当一个元素被创建时,这个函数会被调用,并传入该 DOM 元素作为参数。

Action 的基本结构

一个基本的 Svelte Action 函数接受两个参数:node,即要应用该 Action 的 DOM 元素;以及options(可选),这是一个包含配置选项的对象。以下是一个简单的示例:

<script>
    function myAction(node, options) {
        // 这里可以对 node 进行操作
        node.textContent = `Options: ${JSON.stringify(options)}`;
        return {
            destroy() {
                // 清理逻辑,当元素被销毁时调用
                node.textContent = '';
            }
        };
    }
</script>

<div use:myAction="{{message: 'Hello, Svelte!'}}">
    This div will have custom text.
</div>

在上述代码中,myAction 函数接收 nodeoptions。我们使用 options 中的数据更新 nodetextContent。返回的对象中的 destroy 函数用于在元素从 DOM 中移除时清理操作。

创建自定义 Action 的步骤

确定需求

在构建自定义 Action 之前,需要明确要实现的功能。例如,我们想要创建一个能够自动聚焦输入框的 Action,或者一个在元素滚动到视口时触发动画的 Action。

编写 Action 函数

以自动聚焦输入框为例,我们可以编写如下 Action 函数:

<script>
    function autoFocus(node) {
        node.focus();
        return {
            destroy() {
                // 这里可以添加清理逻辑,例如取消聚焦
            }
        };
    }
</script>

<input type="text" use:autoFocus>

在这个 autoFocus Action 中,当输入框元素被创建时,node.focus() 会立即将焦点设置到该输入框上。

处理配置选项

如果我们希望这个 autoFocus Action 可以延迟聚焦,就需要引入配置选项。

<script>
    function autoFocus(node, {delay = 0}) {
        setTimeout(() => {
            node.focus();
        }, delay);
        return {
            destroy() {
                // 清理逻辑
            }
        };
    }
</script>

<input type="text" use:autoFocus="{{delay: 1000}}">

这里我们添加了一个 delay 选项,默认值为 0。通过 setTimeout,我们可以根据 delay 的值延迟聚焦操作。

构建复杂 DOM 行为的 Action

实现元素拖拽功能

  1. 确定行为逻辑

    • 当鼠标按下时,记录初始位置。
    • 当鼠标移动时,根据鼠标移动的距离更新元素位置。
    • 当鼠标松开时,停止移动。
  2. 编写 Action 函数

<script>
    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            isDragging = true;
            initialX = event.clientX;
            initialY = event.clientY;
            originalLeft = parseInt(getComputedStyle(node).left) || 0;
            originalTop = parseInt(getComputedStyle(node).top) || 0;
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function 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: 100px; height: 100px; background-color: lightblue" use:draggable>
    Drag me!
</div>

在上述代码中,draggable Action 为元素添加了拖拽功能。通过监听 mousedownmousemovemouseup 事件,我们实现了元素的拖拽逻辑。在 destroy 函数中,我们移除了添加的事件监听器,以避免内存泄漏。

实现元素视口可见性检测

  1. 确定行为逻辑

    • 使用 IntersectionObserver 来检测元素是否进入或离开视口。
    • 当元素进入视口时,触发特定的回调函数。
  2. 编写 Action 函数

<script>
    function onViewportEnter(node, {callback}) {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    callback();
                }
            });
        });

        observer.observe(node);

        return {
            destroy() {
                observer.unobserve(node);
            }
        };
    }
</script>

<script let:count = 0>
    function incrementCount() {
        count++;
    }
</script>

<div style="height: 2000px">
    <div style="height: 100px; background-color: lightgreen" use:onViewportEnter="{{callback: incrementCount}}">
        This div will increment count when it enters the viewport.
    </div>
    <p>Count: {count}</p>
</div>

在这个 onViewportEnter Action 中,我们利用 IntersectionObserver 来检测元素是否进入视口。当元素进入视口时,会调用传入的 callback 函数,这里是 incrementCount 函数,从而实现了视口可见性检测并触发相应操作的功能。在 destroy 函数中,我们取消对元素的观察,以释放资源。

嵌套 Action

在 Svelte 中,我们可以在一个元素上应用多个 Action,甚至可以嵌套使用 Action。例如,我们可以在一个可拖拽的元素上同时应用自动聚焦的 Action。

<script>
    function autoFocus(node) {
        node.focus();
        return {
            destroy() {
                // 清理逻辑
            }
        };
    }

    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            isDragging = true;
            initialX = event.clientX;
            initialY = event.clientY;
            originalLeft = parseInt(getComputedStyle(node).left) || 0;
            originalTop = parseInt(getComputedStyle(node).top) || 0;
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function 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>

<input type="text" use:draggable use:autoFocus style="position: relative; left: 100px; top: 100px">

在上述代码中,输入框元素同时应用了 draggableautoFocus Action。这展示了 Svelte Action 在复杂场景下的灵活性,我们可以根据需求组合不同的 Action 来实现更加丰富的 DOM 行为。

Action 与响应式数据

我们可以让 Action 与 Svelte 的响应式数据进行交互。例如,我们可以根据一个响应式变量来动态改变 Action 的行为。

<script>
    let enableDragging = true;

    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            if (enableDragging) {
                isDragging = true;
                initialX = event.clientX;
                initialY = event.clientY;
                originalLeft = parseInt(getComputedStyle(node).left) || 0;
                originalTop = parseInt(getComputedStyle(node).top) || 0;
            }
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function handleMouseUp() {
            isDragging = false;
        }

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

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

<button on:click={() => enableDragging =!enableDragging}>
    {enableDragging? 'Disable Dragging' : 'Enable Dragging'}
</button>

<div style="position: relative; width: 100px; height: 100px; background-color: lightblue" use:draggable>
    Drag me if enabled!
</div>

在这个例子中,enableDragging 是一个响应式变量。通过在 draggable Action 中添加 update 函数,我们可以根据 enableDragging 的变化动态开启或禁用元素的拖拽功能。当按钮被点击时,enableDragging 的值会改变,从而影响 draggable Action 的行为。

处理不同的 DOM 事件

在构建复杂 DOM 行为的 Action 时,我们经常需要处理各种 DOM 事件。除了常见的 mousedownmousemovemouseup 等鼠标事件,还有 keydownkeyupscroll 等事件。

处理键盘事件

假设我们要创建一个 Action,当按下特定键时,对元素执行某个操作。例如,当按下回车键时,将输入框的值打印到控制台。

<script>
    function onEnterPress(node) {
        function handleKeyDown(event) {
            if (event.key === 'Enter') {
                console.log(node.value);
            }
        }

        node.addEventListener('keydown', handleKeyDown);

        return {
            destroy() {
                node.removeEventListener('keydown', handleKeyDown);
            }
        };
    }
</script>

<input type="text" use:onEnterPress>

在上述代码中,onEnterPress Action 监听输入框的 keydown 事件。当检测到按下的键是回车键时,会将输入框的值打印到控制台。在 destroy 函数中,我们移除了事件监听器,以确保在元素被销毁时不会产生内存泄漏。

处理滚动事件

如果我们想要在元素滚动到一定位置时触发某个操作,可以编写如下 Action:

<script>
    function onScrollThreshold(node, {threshold = 100}) {
        let hasTriggered = false;

        function handleScroll() {
            if (node.scrollTop >= threshold &&!hasTriggered) {
                console.log('Reached scroll threshold');
                hasTriggered = true;
            }
        }

        node.addEventListener('scroll', handleScroll);

        return {
            destroy() {
                node.removeEventListener('scroll', handleScroll);
            }
        };
    }
</script>

<div style="height: 200px; overflow-y: scroll; border: 1px solid black" use:onScrollThreshold="{{threshold: 50}}">
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
    <p>Scroll me...</p>
</div>

onScrollThreshold Action 中,我们监听元素的 scroll 事件。当元素的 scrollTop 达到设定的 threshold 且尚未触发过操作时,会在控制台打印消息。同样,在 destroy 函数中,我们移除了事件监听器。

优化 Action 的性能

在构建复杂 DOM 行为的 Action 时,性能优化是至关重要的。以下是一些优化的建议:

减少不必要的 DOM 操作

尽可能减少在事件处理函数中对 DOM 的直接操作。例如,在拖拽 Action 中,我们可以通过 requestAnimationFrame 来批量处理位置更新,而不是在每次 mousemove 事件触发时都立即更新 DOM。

<script>
    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;
        let requestId;

        function handleMouseDown(event) {
            isDragging = true;
            initialX = event.clientX;
            initialY = event.clientY;
            originalLeft = parseInt(getComputedStyle(node).left) || 0;
            originalTop = parseInt(getComputedStyle(node).top) || 0;
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;

                if (!requestId) {
                    requestId = requestAnimationFrame(() => {
                        node.style.left = `${originalLeft + dx}px`;
                        node.style.top = `${originalTop + dy}px`;
                        requestId = null;
                    });
                }
            }
        }

        function handleMouseUp() {
            isDragging = false;
            if (requestId) {
                cancelAnimationFrame(requestId);
                requestId = null;
            }
        }

        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);
                if (requestId) {
                    cancelAnimationFrame(requestId);
                }
            }
        };
    }
</script>

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

在这个优化后的 draggable Action 中,我们使用 requestAnimationFrame 来确保位置更新在浏览器的下一次重绘之前进行,从而提高性能并减少不必要的重排和重绘。

合理使用事件委托

如果需要监听多个子元素的相同事件,可以使用事件委托。例如,在一个包含多个按钮的列表中,我们可以在父元素上监听 click 事件,并根据事件目标来判断点击的是哪个按钮。

<script>
    function handleButtonClick(node) {
        function handleClick(event) {
            if (event.target.tagName === 'BUTTON') {
                console.log(`Clicked button: ${event.target.textContent}`);
            }
        }

        node.addEventListener('click', handleClick);

        return {
            destroy() {
                node.removeEventListener('click', handleClick);
            }
        };
    }
</script>

<div use:handleButtonClick>
    <button>Button 1</button>
    <button>Button 2</button>
    <button>Button 3</button>
</div>

在上述代码中,handleButtonClick Action 在父 div 元素上监听 click 事件。通过检查 event.targettagName,我们可以判断是否点击了按钮,并执行相应的操作。这样可以减少事件监听器的数量,提高性能。

缓存计算结果

如果在 Action 中需要进行一些复杂的计算,并且这些计算结果不会频繁变化,可以考虑缓存这些结果。例如,在计算元素位置时,如果元素的初始位置不会改变,我们可以在 Action 初始化时计算并缓存该位置。

<script>
    function positionBasedAction(node) {
        const initialPosition = {
            x: parseInt(getComputedStyle(node).left),
            y: parseInt(getComputedStyle(node).top)
        };

        function handleSomeEvent() {
            // 使用缓存的初始位置进行计算
            const newX = initialPosition.x + 10;
            const newY = initialPosition.y + 10;
            node.style.left = `${newX}px`;
            node.style.top = `${newY}px`;
        }

        node.addEventListener('someEvent', handleSomeEvent);

        return {
            destroy() {
                node.removeEventListener('someEvent', handleSomeEvent);
            }
        };
    }
</script>

<div style="position: relative; left: 50px; top: 50px" use:positionBasedAction>
    This div will be repositioned on some event.
</div>

positionBasedAction Action 中,我们在初始化时计算并缓存了元素的初始位置。在事件处理函数中,我们直接使用缓存的位置进行计算,避免了每次都重新获取元素位置的开销。

与其他 Svelte 特性结合

Action 与组件

我们可以在 Svelte 组件中使用 Action,为组件的 DOM 元素添加自定义行为。例如,我们创建一个可拖拽的按钮组件。

<script>
    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            isDragging = true;
            initialX = event.clientX;
            initialY = event.clientY;
            originalLeft = parseInt(getComputedStyle(node).left) || 0;
            originalTop = parseInt(getComputedStyle(node).top) || 0;
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function 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>

<button style="position: relative" use:draggable>Drag me</button>

在这个按钮组件中,我们应用了 draggable Action,使得按钮可以被拖拽。这样,我们可以将复杂的 DOM 行为封装在组件中,提高代码的复用性。

Action 与响应式声明

我们可以结合 Svelte 的响应式声明来动态控制 Action 的行为。例如,根据一个响应式变量来决定是否应用某个 Action。

<script>
    let shouldEnableDragging = true;

    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            if (shouldEnableDragging) {
                isDragging = true;
                initialX = event.clientX;
                initialY = event.clientY;
                originalLeft = parseInt(getComputedStyle(node).left) || 0;
                originalTop = parseInt(getComputedStyle(node).top) || 0;
            }
        }

        function handleMouseMove(event) {
            if (isDragging) {
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function handleMouseUp() {
            isDragging = false;
        }

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

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

<button on:click={() => shouldEnableDragging =!shouldEnableDragging}>
    {shouldEnableDragging? 'Disable Dragging' : 'Enable Dragging'}
</button>

<div style="position: relative; width: 100px; height: 100px; background-color: lightblue" use:draggable>
    Drag me if enabled!
</div>

在这个例子中,shouldEnableDragging 是一个响应式变量。通过在 draggable Action 中添加 update 函数,我们可以根据 shouldEnableDragging 的值动态开启或禁用元素的拖拽功能。

错误处理与调试

在编写复杂的自定义 Action 时,错误处理和调试是必不可少的环节。

错误处理

  1. 参数验证:在 Action 函数开始时,对传入的参数进行验证。例如,如果 Action 期望一个特定类型的 options 对象,可以检查对象的结构和属性。
<script>
    function myAction(node, options) {
        if (!options || typeof options.message!=='string') {
            throw new Error('Invalid options. Expected an object with a "message" string property.');
        }
        node.textContent = options.message;
        return {
            destroy() {
                node.textContent = '';
            }
        };
    }
</script>

<div use:myAction="{{message: 'Hello, Svelte!'}}">
    This div will have custom text.
</div>

在上述代码中,myAction 函数在开始时检查 options 是否有效。如果无效,会抛出一个错误,这样可以避免在后续代码中出现难以调试的错误。

  1. 事件处理中的错误:在事件处理函数中,使用 try - catch 块来捕获可能出现的错误。
<script>
    function myAction(node) {
        function handleClick() {
            try {
                // 可能会出错的代码
                const nonExistentVariable = someNonExistentValue;
                console.log(nonExistentVariable);
            } catch (error) {
                console.error('Error in click handler:', error);
            }
        }

        node.addEventListener('click', handleClick);

        return {
            destroy() {
                node.removeEventListener('click', handleClick);
            }
        };
    }
</script>

<button use:myAction>Click me</button>

handleClick 函数中,我们使用 try - catch 块来捕获可能出现的错误,并将错误信息打印到控制台,以便于调试。

调试技巧

  1. 使用 console.log:在 Action 函数的关键位置添加 console.log 语句,输出变量的值和执行流程。
<script>
    function draggable(node) {
        let isDragging = false;
        let initialX;
        let initialY;
        let originalLeft;
        let originalTop;

        function handleMouseDown(event) {
            console.log('Mouse down event');
            isDragging = true;
            initialX = event.clientX;
            initialY = event.clientY;
            originalLeft = parseInt(getComputedStyle(node).left) || 0;
            originalTop = parseInt(getComputedStyle(node).top) || 0;
            console.log(`Initial position: x = ${initialX}, y = ${initialY}`);
        }

        function handleMouseMove(event) {
            if (isDragging) {
                console.log('Mouse move event while dragging');
                const dx = event.clientX - initialX;
                const dy = event.clientY - initialY;
                console.log(`Delta: dx = ${dx}, dy = ${dy}`);
                node.style.left = `${originalLeft + dx}px`;
                node.style.top = `${originalTop + dy}px`;
            }
        }

        function handleMouseUp() {
            console.log('Mouse up event');
            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: 100px; height: 100px; background-color: lightblue" use:draggable>
    Drag me!
</div>

通过在不同的事件处理函数中添加 console.log 语句,我们可以了解拖拽操作的执行流程和变量的变化情况,有助于调试问题。

  1. 使用浏览器开发者工具:利用浏览器的开发者工具,如 Chrome DevTools。可以在 Sources 面板中设置断点,逐步调试 Action 函数的执行过程。在 Elements 面板中,可以查看元素的样式和属性变化,以及是否正确应用了 Action。

例如,在拖拽 Action 中,我们可以在 handleMouseMove 函数处设置断点,然后通过拖拽元素来观察变量的值和 DOM 操作的过程。这样可以直观地发现代码中可能存在的问题。

通过以上对 Svelte 自定义 Action 的深入探讨,我们了解了如何从零开始构建复杂的 DOM 行为。从基本概念到实际应用,以及性能优化、与其他特性结合、错误处理和调试等方面,都为构建强大且高效的前端交互提供了有力的支持。在实际项目中,合理运用自定义 Action 可以极大地提升用户体验和代码的可维护性。