动作Actions #

一、Actions 概述 #

Actions 是一种增强 DOM 元素的方式,当元素挂载时执行函数,销毁时执行清理。

text
┌─────────────────────────────────────────────────────────────┐
│                    Actions 工作原理                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <div use:action={options}>                         │   │
│  │    Content                                          │   │
│  │  </div>                                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                         ↓                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  function action(node, options) {                   │   │
│  │    // node: DOM 元素                                │   │
│  │    // options: 传递的参数                           │   │
│  │                                                     │   │
│  │    // 初始化逻辑                                    │   │
│  │                                                     │   │
│  │    return {                                         │   │
│  │      update(options) { },  // 参数更新时调用        │   │
│  │      destroy() { }          // 元素销毁时调用       │   │
│  │    };                                               │   │
│  │  }                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、基本用法 #

2.1 简单 Action #

svelte
<script>
  function focus(node) {
    node.focus();
  }
</script>

<input use:focus placeholder="Auto-focused input" />

2.2 带参数的 Action #

svelte
<script>
  function clickOutside(node, callback) {
    function handleClick(event) {
      if (!node.contains(event.target)) {
        callback?.();
      }
    }
    
    document.addEventListener('click', handleClick, true);
    
    return {
      destroy() {
        document.removeEventListener('click', handleClick, true);
      }
    };
  }
  
  let showModal = true;
</script>

{#if showModal}
  <div class="modal" use:clickOutside={() => showModal = false}>
    <p>Click outside to close</p>
  </div>
{/if}

2.3 完整 Action 结构 #

svelte
<script>
  function tooltip(node, { text = '', position = 'top' }) {
    let tooltipEl;
    
    function create() {
      tooltipEl = document.createElement('div');
      tooltipEl.textContent = text;
      tooltipEl.className = `tooltip tooltip-${position}`;
      document.body.appendChild(tooltipEl);
    }
    
    function position() {
      const rect = node.getBoundingClientRect();
      tooltipEl.style.left = rect.left + rect.width / 2 + 'px';
      tooltipEl.style.top = rect.top - tooltipEl.offsetHeight - 10 + 'px';
    }
    
    function show() {
      tooltipEl.style.opacity = '1';
      position();
    }
    
    function hide() {
      tooltipEl.style.opacity = '0';
    }
    
    create();
    
    node.addEventListener('mouseenter', show);
    node.addEventListener('mouseleave', hide);
    
    return {
      update({ text: newText }) {
        tooltipEl.textContent = newText;
      },
      destroy() {
        node.removeEventListener('mouseenter', show);
        node.removeEventListener('mouseleave', hide);
        tooltipEl.remove();
      }
    };
  }
</script>

<button use:tooltip={{ text: 'Click me!', position: 'top' }}>
  Hover me
</button>

三、常用 Actions #

3.1 点击外部 #

svelte
<script>
  function clickOutside(node, handler) {
    const onClick = (event) => {
      if (node && !node.contains(event.target) && !event.defaultPrevented) {
        handler();
      }
    };
    
    document.addEventListener('click', onClick, true);
    
    return {
      destroy() {
        document.removeEventListener('click', onClick, true);
      }
    };
  }
</script>

<div use:clickOutside={() => console.log('Clicked outside')}>
  Click outside this div
</div>

3.2 长按 #

svelte
<script>
  function longpress(node, { duration = 500, onLongpress }) {
    let timer;
    
    function handleMouseDown() {
      timer = setTimeout(() => {
        onLongpress?.();
      }, duration);
    }
    
    function handleMouseUp() {
      clearTimeout(timer);
    }
    
    node.addEventListener('mousedown', handleMouseDown);
    node.addEventListener('mouseup', handleMouseUp);
    node.addEventListener('mouseleave', handleMouseUp);
    
    return {
      update({ duration: newDuration }) {
        duration = newDuration;
      },
      destroy() {
        node.removeEventListener('mousedown', handleMouseDown);
        node.removeEventListener('mouseup', handleMouseUp);
        node.removeEventListener('mouseleave', handleMouseUp);
      }
    };
  }
</script>

<button use:longpress={{ duration: 1000, onLongpress: () => alert('Long pressed!') }}>
  Hold me
</button>

3.3 懒加载 #

svelte
<script>
  function lazyload(node, { onLoad, threshold = 0.1 }) {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            onLoad?.();
            observer.unobserve(node);
          }
        });
      },
      { threshold }
    );
    
    observer.observe(node);
    
    return {
      destroy() {
        observer.unobserve(node);
      }
    };
  }
  
  let loaded = false;
</script>

<div use:lazyload={{ onLoad: () => loaded = true }}>
  {#if loaded}
    <img src="image.jpg" alt="Lazy loaded" />
  {:else}
    <div class="placeholder">Loading...</div>
  {/if}
</div>

3.4 自动调整大小 #

svelte
<script>
  function autosize(node) {
    function resize() {
      node.style.height = 'auto';
      node.style.height = node.scrollHeight + 'px';
    }
    
    node.addEventListener('input', resize);
    resize();
    
    return {
      destroy() {
        node.removeEventListener('input', resize);
      }
    };
  }
</script>

<textarea use:autosize placeholder="Auto-resizing textarea"></textarea>

3.5 拖拽 #

svelte
<script>
  function draggable(node, { bounds = null, onDragStart, onDrag, onDragEnd }) {
    let isDragging = false;
    let startX, startY;
    let initialX, initialY;
    
    function handleMouseDown(event) {
      isDragging = true;
      startX = event.clientX;
      startY = event.clientY;
      
      const rect = node.getBoundingClientRect();
      initialX = rect.left;
      initialY = rect.top;
      
      node.style.position = 'fixed';
      node.style.zIndex = '1000';
      
      onDragStart?.({ x: initialX, y: initialY });
      
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }
    
    function handleMouseMove(event) {
      if (!isDragging) return;
      
      const deltaX = event.clientX - startX;
      const deltaY = event.clientY - startY;
      
      let newX = initialX + deltaX;
      let newY = initialY + deltaY;
      
      if (bounds) {
        newX = Math.max(bounds.left, Math.min(newX, bounds.right - node.offsetWidth));
        newY = Math.max(bounds.top, Math.min(newY, bounds.bottom - node.offsetHeight));
      }
      
      node.style.left = newX + 'px';
      node.style.top = newY + 'px';
      
      onDrag?.({ x: newX, y: newY });
    }
    
    function handleMouseUp() {
      isDragging = false;
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
      onDragEnd?.();
    }
    
    node.addEventListener('mousedown', handleMouseDown);
    
    return {
      destroy() {
        node.removeEventListener('mousedown', handleMouseDown);
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      }
    };
  }
</script>

<div 
  use:draggable={{ 
    bounds: { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight },
    onDragStart: () => console.log('Drag started'),
    onDrag: (pos) => console.log(pos),
    onDragEnd: () => console.log('Drag ended')
  }}
>
  Drag me
</div>

四、Actions 与 TypeScript #

4.1 类型定义 #

typescript
import type { Action } from 'svelte/action';

interface TooltipOptions {
  text: string;
  position?: 'top' | 'bottom' | 'left' | 'right';
}

const tooltip: Action<HTMLElement, TooltipOptions> = (node, options) => {
  // Implementation
  
  return {
    update(newOptions) {
      // Update logic
    },
    destroy() {
      // Cleanup
    }
  };
};

4.2 完整类型示例 #

svelte
<script lang="ts">
  import type { Action } from 'svelte/action';
  
  interface ClickOutsideOptions {
    handler: () => void;
    enabled?: boolean;
  }
  
  const clickOutside: Action<HTMLElement, ClickOutsideOptions> = (
    node, 
    { handler, enabled = true }
  ) => {
    function onClick(event: MouseEvent) {
      if (enabled && !node.contains(event.target as Node)) {
        handler();
      }
    }
    
    document.addEventListener('click', onClick, true);
    
    return {
      update({ enabled: newEnabled }) {
        enabled = newEnabled;
      },
      destroy() {
        document.removeEventListener('click', onClick, true);
      }
    };
  };
</script>

五、Actions 库 #

5.1 svelte-actions 库 #

svelte
<script>
  import { clickOutside, longpress, lazyload } from 'svelte-actions';
</script>

<div use:clickOutside={() => console.log('outside')}>
  Click outside
</div>

<button use:longpress={{ duration: 500 }} on:longpress={handleLongpress}>
  Long press
</button>

<img use:lazyload data-src="image.jpg" />

5.2 自定义 Actions 库 #

javascript
export function clickOutside(node, handler) {
  const onClick = (event) => {
    if (!node.contains(event.target)) {
      handler();
    }
  };
  
  document.addEventListener('click', onClick, true);
  
  return {
    destroy() {
      document.removeEventListener('click', onClick, true);
    }
  };
}

export function longpress(node, { duration = 500, handler }) {
  let timer;
  
  const onmousedown = () => {
    timer = setTimeout(handler, duration);
  };
  
  const onmouseup = () => {
    clearTimeout(timer);
  };
  
  node.addEventListener('mousedown', onmousedown);
  node.addEventListener('mouseup', onmouseup);
  node.addEventListener('mouseleave', onmouseup);
  
  return {
    destroy() {
      node.removeEventListener('mousedown', onmousedown);
      node.removeEventListener('mouseup', onmouseup);
      node.removeEventListener('mouseleave', onmouseup);
    }
  };
}

export function lazyload(node, { threshold = 0.1, onLoad }) {
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting) {
        onLoad?.();
        observer.unobserve(node);
      }
    },
    { threshold }
  );
  
  observer.observe(node);
  
  return {
    destroy() {
      observer.unobserve(node);
    }
  };
}

六、Actions 最佳实践 #

6.1 命名规范 #

svelte
<script>
  function useClickOutside(node, handler) {
    // ...
  }
  
  function useLongpress(node, options) {
    // ...
  }
</script>

<div use:useClickOutside={handler}>
  <!-- ... -->
</div>

6.2 参数验证 #

svelte
<script>
  function tooltip(node, options) {
    if (typeof options === 'string') {
      options = { text: options };
    }
    
    const { text = '', position = 'top' } = options;
    
    // ...
  }
</script>

<button use:tooltip="Simple tooltip">Hover</button>
<button use:tooltip={{ text: 'Advanced', position: 'bottom' }}>Hover</button>

6.3 派发事件 #

svelte
<script>
  function longpress(node, { duration = 500 }) {
    let timer;
    
    function handleMouseDown() {
      timer = setTimeout(() => {
        node.dispatchEvent(new CustomEvent('longpress'));
      }, duration);
    }
    
    function handleMouseUp() {
      clearTimeout(timer);
    }
    
    node.addEventListener('mousedown', handleMouseDown);
    node.addEventListener('mouseup', handleMouseUp);
    node.addEventListener('mouseleave', handleMouseUp);
    
    return {
      destroy() {
        node.removeEventListener('mousedown', handleMouseDown);
        node.removeEventListener('mouseup', handleMouseUp);
        node.removeEventListener('mouseleave', handleMouseUp);
      }
    };
  }
</script>

<button use:longpress={{ duration: 1000 }} on:longpress={() => alert('Long pressed!')}>
  Hold me
</button>

七、完整示例 #

7.1 可调整大小 #

svelte
<script>
  function resizable(node, { minWidth = 100, minHeight = 100, maxWidth, maxHeight }) {
    let startX, startY, startWidth, startHeight;
    
    const handle = document.createElement('div');
    handle.className = 'resize-handle';
    handle.style.cssText = `
      position: absolute;
      right: 0;
      bottom: 0;
      width: 10px;
      height: 10px;
      background: #ccc;
      cursor: se-resize;
    `;
    
    node.style.position = 'relative';
    node.appendChild(handle);
    
    function handleMouseDown(event) {
      startX = event.clientX;
      startY = event.clientY;
      startWidth = node.offsetWidth;
      startHeight = node.offsetHeight;
      
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }
    
    function handleMouseMove(event) {
      let newWidth = startWidth + event.clientX - startX;
      let newHeight = startHeight + event.clientY - startY;
      
      newWidth = Math.max(minWidth, maxWidth ? Math.min(newWidth, maxWidth) : newWidth);
      newHeight = Math.max(minHeight, maxHeight ? Math.min(newHeight, maxHeight) : newHeight);
      
      node.style.width = newWidth + 'px';
      node.style.height = newHeight + 'px';
    }
    
    function handleMouseUp() {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    }
    
    handle.addEventListener('mousedown', handleMouseDown);
    
    return {
      destroy() {
        handle.removeEventListener('mousedown', handleMouseDown);
        handle.remove();
      }
    };
  }
</script>

<div 
  use:resizable={{ minWidth: 100, minHeight: 100, maxWidth: 500, maxHeight: 500 }}
  style="width: 200px; height: 150px; border: 1px solid #ccc;"
>
  Resize me
</div>

八、总结 #

方法 说明
use:action 应用 Action
use:action={options} 带参数的 Action
update() 参数更新时调用
destroy() 元素销毁时调用

Actions 要点:

  • Action 是增强 DOM 元素的函数
  • 接收 node 和 options 参数
  • 返回 update 和 destroy 方法
  • 适合封装可复用的 DOM 操作
  • 可以派发自定义事件
  • 与 TypeScript 配合获得类型安全
最后更新:2026-03-28