Ref与DOM操作 #

一、Ref 概述 #

1.1 什么是 Ref #

Ref 提供了一种访问 DOM 元素或组件实例的方式,绕过典型的 Props 数据流。

1.2 使用场景 #

场景 说明
焦点管理 自动聚焦输入框
文本选择 选择输入文本
媒体控制 播放/暂停视频
动画 触发动画
第三方库 集成非 Preact 库

1.3 Ref vs State #

方面 Ref State
更新触发 不触发渲染 触发渲染
用途 DOM/实例引用 组件数据
可变性 可直接修改 通过 setter

二、useRef #

2.1 基本用法 #

jsx
import { useRef } from 'preact/hooks';

function TextInput() {
  const inputRef = useRef(null);

  const focus = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus Input</button>
    </div>
  );
}

2.2 访问 DOM 元素 #

jsx
function MeasureElement() {
  const divRef = useRef(null);

  const measure = () => {
    if (divRef.current) {
      const { width, height } = divRef.current.getBoundingClientRect();
      console.log(`Width: ${width}, Height: ${height}`);
    }
  };

  return (
    <div>
      <div 
        ref={divRef} 
        style={{ width: '200px', height: '100px', background: '#f0f0f0' }}
      >
        Content
      </div>
      <button onClick={measure}>Measure</button>
    </div>
  );
}

2.3 存储可变值 #

jsx
function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    if (intervalRef.current) return;
    
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  const reset = () => {
    stop();
    setTime(0);
  };

  return (
    <div>
      <p>{time} seconds</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

2.4 获取前一次的值 #

jsx
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

三、createRef(类组件) #

3.1 基本用法 #

jsx
import { Component, createRef } from 'preact';

class TextInput extends Component {
  inputRef = createRef();

  focus = () => {
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.focus}>Focus</button>
      </div>
    );
  }
}

3.2 回调 Ref #

jsx
class TextInput extends Component {
  inputElement = null;

  setInputRef = (element) => {
    this.inputElement = element;
  };

  focus = () => {
    if (this.inputElement) {
      this.inputElement.focus();
    }
  };

  render() {
    return (
      <div>
        <input ref={this.setInputRef} type="text" />
        <button onClick={this.focus}>Focus</button>
      </div>
    );
  }
}

四、常见使用场景 #

4.1 自动聚焦 #

jsx
function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="Auto focused" />;
}

4.2 文本选择 #

jsx
function SelectableInput({ value }) {
  const inputRef = useRef(null);

  const selectAll = () => {
    inputRef.current?.select();
  };

  return (
    <div>
      <input ref={inputRef} value={value} readOnly />
      <button onClick={selectAll}>Select All</button>
    </div>
  );
}

4.3 视频控制 #

jsx
function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  const play = () => {
    videoRef.current?.play();
  };

  const pause = () => {
    videoRef.current?.pause();
  };

  const restart = () => {
    if (videoRef.current) {
      videoRef.current.currentTime = 0;
      videoRef.current.play();
    }
  };

  return (
    <div>
      <video ref={videoRef} src={src} />
      <div>
        <button onClick={play}>Play</button>
        <button onClick={pause}>Pause</button>
        <button onClick={restart}>Restart</button>
      </div>
    </div>
  );
}

4.4 滚动控制 #

jsx
function ScrollContainer() {
  const containerRef = useRef(null);

  const scrollToTop = () => {
    containerRef.current?.scrollTo({
      top: 0,
      behavior: 'smooth'
    });
  };

  const scrollToBottom = () => {
    if (containerRef.current) {
      containerRef.current.scrollTo({
        top: containerRef.current.scrollHeight,
        behavior: 'smooth'
      });
    }
  };

  return (
    <div>
      <div 
        ref={containerRef}
        style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}
      >
        {/* Long content */}
        {Array.from({ length: 20 }, (_, i) => (
          <p key={i}>Item {i + 1}</p>
        ))}
      </div>
      <button onClick={scrollToTop}>Scroll to Top</button>
      <button onClick={scrollToBottom}>Scroll to Bottom</button>
    </div>
  );
}

4.5 Canvas 操作 #

jsx
function CanvasDrawing() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 2;
  }, []);

  const startDrawing = (e) => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    const rect = canvas.getBoundingClientRect();
    
    ctx.beginPath();
    ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
    setIsDrawing(true);
  };

  const draw = (e) => {
    if (!isDrawing) return;
    
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    const rect = canvas.getBoundingClientRect();
    
    ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
    ctx.stroke();
  };

  const stopDrawing = () => {
    setIsDrawing(false);
  };

  const clear = () => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };

  return (
    <div>
      <canvas
        ref={canvasRef}
        width={400}
        height={300}
        style={{ border: '1px solid #ccc' }}
        onMouseDown={startDrawing}
        onMouseMove={draw}
        onMouseUp={stopDrawing}
        onMouseLeave={stopDrawing}
      />
      <button onClick={clear}>Clear</button>
    </div>
  );
}

五、转发 Ref #

5.1 forwardRef #

jsx
import { forwardRef } from 'preact';

const FancyInput = forwardRef((props, ref) => {
  return (
    <div class="fancy-input">
      <input ref={ref} {...props} class="input" />
      <span class="icon">🔍</span>
    </div>
  );
});

// 使用
function Form() {
  const inputRef = useRef(null);

  const focus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Search..." />
      <button onClick={focus}>Focus</button>
    </div>
  );
}

5.2 useImperativeHandle #

jsx
import { forwardRef, useImperativeHandle, useRef } from 'preact';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
    clear: () => {
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    },
    getValue: () => {
      return inputRef.current?.value;
    }
  }));

  return <input ref={inputRef} {...props} />;
});

// 使用
function Form() {
  const inputRef = useRef(null);

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
      <button onClick={() => inputRef.current?.clear()}>Clear</button>
      <button onClick={() => console.log(inputRef.current?.getValue())}>
        Log Value
      </button>
    </div>
  );
}

六、集成第三方库 #

6.1 集成 jQuery 插件 #

jsx
function jQueryDatePicker({ onSelect }) {
  const inputRef = useRef(null);

  useEffect(() => {
    const $input = $(inputRef.current);
    
    $input.datepicker({
      onSelect: (date) => {
        onSelect?.(date);
      }
    });

    return () => {
      $input.datepicker('destroy');
    };
  }, [onSelect]);

  return <input ref={inputRef} type="text" />;
}

6.2 集成 D3.js #

jsx
function D3Chart({ data }) {
  const svgRef = useRef(null);

  useEffect(() => {
    const svg = d3.select(svgRef.current);
    
    svg.selectAll('*').remove();

    svg.selectAll('circle')
      .data(data)
      .enter()
      .append('circle')
      .attr('cx', d => d.x)
      .attr('cy', d => d.y)
      .attr('r', d => d.r)
      .attr('fill', 'steelblue');

  }, [data]);

  return <svg ref={svgRef} width={400} height={300} />;
}

6.3 集成 Chart.js #

jsx
function Chart({ type, data, options }) {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);

  useEffect(() => {
    if (chartRef.current) {
      chartRef.current.destroy();
    }

    const ctx = canvasRef.current.getContext('2d');
    chartRef.current = new Chart(ctx, {
      type,
      data,
      options
    });

    return () => {
      chartRef.current?.destroy();
    };
  }, [type, data, options]);

  return <canvas ref={canvasRef} />;
}

七、最佳实践 #

7.1 避免过度使用 #

jsx
// ❌ 不应该用 ref 来做这个
function BadExample() {
  const inputRef = useRef(null);

  const getValue = () => {
    return inputRef.current?.value;
  };

  return <input ref={inputRef} />;
}

// ✅ 应该使用 state
function GoodExample() {
  const [value, setValue] = useState('');

  return (
    <input 
      value={value} 
      onInput={(e) => setValue(e.target.value)} 
    />
  );
}

7.2 Ref 回调时机 #

jsx
function RefCallbackExample() {
  const [show, setShow] = useState(false);

  const setRef = (element) => {
    console.log('Ref callback:', element);
    // element 可能为 null(卸载时)
  };

  return (
    <div>
      <button onClick={() => setShow(!show)}>Toggle</button>
      {show && <div ref={setRef}>Content</div>}
    </div>
  );
}

7.3 检查 Ref 是否存在 #

jsx
function SafeRefUsage() {
  const ref = useRef(null);

  const doSomething = () => {
    // ✅ 安全检查
    if (ref.current) {
      ref.current.focus();
    }

    // ✅ 可选链
    ref.current?.focus();
  };

  return <input ref={ref} />;
}

八、总结 #

要点 说明
useRef 函数组件中使用
createRef 类组件中使用
forwardRef 转发 ref 给子组件
useImperativeHandle 自定义暴露的方法

核心原则:

  • 优先使用 state,必要时使用 ref
  • 记得检查 ref.current 是否存在
  • 清理第三方库实例
  • 使用 forwardRef 暴露组件方法
最后更新:2026-03-28