R28第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

DOM 操作与焦点管理

DOM 操作与焦点管理 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

焦点、滚动这类 DOM 状态,React 的声明式数据流管不到,需要 ref 接管。这一课你会在对话框场景里完成一套完整的焦点闭环:打开自动聚焦、Esc 关闭、关闭后焦点还给触发按钮。

问题场景

「新增课程」对话框第一版:点「打开」弹出对话框,但输入框没有焦点,键盘用户必须先用鼠标点一下输入框;关闭后焦点直接丢到页面上,Tab 得从头再来一遍。

function CourseDialog({ onClose }) {
  // 打开后没人管焦点,全靠用户自己找
  return (
    <div className="dialog">
      <input placeholder="课程名称" />
      <button onClick={onClose}>关闭</button>
    </div>
  );
}

对鼠标用户只是「不顺手」,对键盘和读屏用户则是不可用。

核心解释

焦点是 DOM 的命令式状态:它不体现在 JSX 里,只能通过调用 DOM 方法改变。React 管数据流,DOM 命令交给 ref,两者配合的固定套路是:

  1. 用 useRef(null) 建引用,绑到 JSX 元素上;
  2. 在 useEffect 里操作它——Effect 在 DOM 提交之后运行,此刻节点必然已挂载;
  3. 组件卸载前(Effect 清理函数)做善后,比如移除监听、归还焦点。

拿到节点后,node.focus() 聚焦,node.scrollIntoView() 把它滚进可视区,document.activeElement 随时能查「现在焦点在谁身上」——这就是本课的验证手段。

示例拆解

完整的对话框焦点闭环:

function CourseDialog({ onClose }) {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus(); // 挂载后聚焦输入框

    function onKeydown(e) {
      if (e.key === 'Escape') onClose();
    }
    document.addEventListener('keydown', onKeydown);
    return () => {
      document.removeEventListener('keydown', onKeydown); // 卸载前移除监听
    };
  }, [onClose]);

  return (
    <div role="dialog" aria-label="新增课程">
      <input ref={inputRef} placeholder="课程名称" />
      <button onClick={onClose}>关闭</button>
    </div>
  );
}

function Page() {
  const [open, setOpen] = useState(false);
  const triggerRef = useRef(null);

  return (
    <div>
      <button ref={triggerRef} onClick={() => setOpen(true)}>打开对话框</button>
      {open && (
        <CourseDialog
          onClose={() => {
            setOpen(false);
            triggerRef.current?.focus(); // 关闭后焦点还给触发按钮
          }}
        />
      )}
    </div>
  );
}
  • inputRef.current?.focus():条件渲染下节点可能还不存在,?. 判空是标配。
  • Esc 监听挂在 document 上,清理函数必须移除,否则组件卸载后还在响应按键。
  • 焦点归还是父组件在 onClose 里做的:卸载之后子组件的 ref 已失效,能还焦点的只有还活着的触发按钮。

动手任务

打开本课实验区:

  1. 打开对话框后按 Esc,确认对话框关闭且焦点回到「打开对话框」按钮(直接再按 Enter 能重新打开,说明焦点真的在那里)。
  2. 把自动聚焦目标从姓名输入框换成「取消」按钮,验证改动只需要换绑 ref。
  3. 列表区加一个「定位到第 9 条」按钮,用 scrollIntoView + focus 让目标项可见且带高亮样式。

常见错误

  • 在组件函数体里直接调 focus():渲染期间操作 DOM 是副作用,可能执行多次且时机不对;一律放进 useEffect。
  • 对没挂载的节点调用方法:条件渲染下 ref.current 是 null,不判空直接 .focus() 会抛 TypeError。
  • 监听器忘了清理:Esc 监听挂在 document 上,卸载不移除就会重复响应、重复关闭。
  • 只在桌面浏览器验证:移动端软键盘弹起会挤压视口、改变滚动位置,实验区(桌面环境)验证不了,要用真机按同一份清单再过一遍。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 打开自动聚焦、Esc 关闭、焦点归还三步都在实验区验证过
  • 能说清为什么焦点操作写在 useEffect 里(DOM 提交后才运行)
  • 所有挂在 document/window 上的监听都有对应的移除

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

弹窗组件打开后想把焦点自动移到输入框,正确的做法是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...