You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React复刻MDN触摸手势日志器遇两问题求助

React版MDN触摸事件日志器问题修复

问题1:日志输出无内容

原因

React会自动转义HTML字符串,直接渲染包含<br>的logText会将标签当作纯文本显示,而非解析为换行。此外,enableLog函数中的逻辑错误,错误地将logText赋值给testText,导致目标元素显示异常。

修复方案

  1. 使用dangerouslySetInnerHTML渲染日志内容,让React解析HTML标签:
    // 替换原日志显示的div
    <div dangerouslySetInnerHTML={{ __html: logText }}></div>
    
  2. 修正enableLog函数逻辑,仅切换日志记录状态:
    const enableLog = () => {
      setLogEvents(prev => !prev);
    };
    

问题2:双指捏合缩放功能未触发

原因

  1. 触摸点缓存逻辑错误:在touchstart时,检测到双指触摸后未清空原有缓存,而是追加新触摸点,导致缓存中存在无效数据,无法匹配后续触摸事件的identifier。
  2. 捏合判断逻辑错误:原MDN通过计算两个触摸点之间的距离变化判断捏合动作,当前代码错误地计算单个触摸点的X轴位移,且else分支位置错误,导致缓存清空逻辑触发时机不正确。
  3. State异步更新问题:直接依赖当前tpCache状态更新可能获取不到最新值,需使用函数式更新保证状态的正确性。

修复方案

  1. 修正startHandler中的触摸点缓存逻辑,清空缓存后存储双指触摸点的坐标(而非整个事件对象):
    const startHandler = (ev) => {
      ev.preventDefault();
      if (ev.targetTouches.length === 2) {
        setTpCache(() => {
          const newCache = [];
          for (let i = 0; i < ev.targetTouches.length; i++) {
            newCache.push({
              identifier: ev.targetTouches[i].identifier,
              clientX: ev.targetTouches[i].clientX,
              clientY: ev.targetTouches[i].clientY
            });
          }
          return newCache;
        });
      }
      if (logEvents) {
        log("touchStart", ev, true);
      }
      updateBackground(ev);
    };
    
  2. 修正handlePinchZoom函数,改为计算双指距离变化判断捏合:
    const handlePinchZoom = (ev) => {
      if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
        let point1 = -1, point2 = -1;
        for (let i = 0; i < tpCache.length; i++) {
          if (tpCache[i].identifier === ev.targetTouches[0].identifier) point1 = i;
          if (tpCache[i].identifier === ev.targetTouches[1].identifier) point2 = i;
        }
        if (point1 >= 0 && point2 >= 0) {
          // 计算初始双指距离
          const initialDistance = Math.sqrt(
            Math.pow(tpCache[point1].clientX - tpCache[point2].clientX, 2) +
            Math.pow(tpCache[point1].clientY - tpCache[point2].clientY, 2)
          );
          // 计算当前双指距离
          const currentDistance = Math.sqrt(
            Math.pow(ev.targetTouches[0].clientX - ev.targetTouches[1].clientX, 2) +
            Math.pow(ev.targetTouches[0].clientY - ev.targetTouches[1].clientY, 2)
          );
          const PINCH_THRESHHOLD = ev.target.clientWidth / 10;
          // 判断距离变化是否超过阈值
          if (Math.abs(currentDistance - initialDistance) >= PINCH_THRESHHOLD) {
            ev.target.style.background = "green";
          }
        } else {
          setTpCache([]);
        }
      }
    };
    
  3. 调整moveHandler中的逻辑,确保双指触摸时不覆盖背景色:
    const moveHandler = (ev) => {
      ev.preventDefault();
      if (logEvents) {
        log("touchMove", ev, false);
      }
      // 仅当触摸点数量不是2时更新背景色
      if (ev.targetTouches.length !== 2) {
        updateBackground(ev);
      }
      ev.target.style.outline = "dashed";
      handlePinchZoom(ev);
    };
    

修复后的完整代码

import React, { useState, useEffect } from 'react';

const TouchEventsContainer = () => {
  const [logEvents, setLogEvents] = useState(false);
  const [tpCache, setTpCache] = useState([]);
  const [logText, setLogText] = useState("");
  const [testText, setTestText] = useState("Tap Swipe");

  const enableLog = () => {
    setLogEvents(prev => !prev);
  };
  const clearLog = () => setLogText('');

  const log = (name, ev, printTargetIds) => {
    let s = `${name}: touches = ${ev.touches.length} ; targetTouches = ${ev.targetTouches.length} ; changedTouches = ${ev.changedTouches.length}`;
    if (printTargetIds) {
      for (var i = 0; i < ev.targetTouches.length; i++) {
        s += `... id = ${ev.targetTouches[i].identifier} <br>`;
      }
    }
    setLogText((prevLogText) => prevLogText + s + "<br>");
  };

  const updateBackground = (ev) => {
    switch (ev.targetTouches.length) {
      case 1:
        ev.target.style.background = "yellow";
        break;
      case 2:
        ev.target.style.background = "pink";
        break;
      default:
        ev.target.style.background = "lightblue";
    }
  }

  const handlePinchZoom = (ev) => {
    if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
      let point1 = -1, point2 = -1;
      for (let i = 0; i < tpCache.length; i++) {
        if (tpCache[i].identifier === ev.targetTouches[0].identifier) point1 = i;
        if (tpCache[i].identifier === ev.targetTouches[1].identifier) point2 = i;
      }
      if (point1 >= 0 && point2 >= 0) {
        const initialDistance = Math.sqrt(
          Math.pow(tpCache[point1].clientX - tpCache[point2].clientX, 2) +
          Math.pow(tpCache[point1].clientY - tpCache[point2].clientY, 2)
        );
        const currentDistance = Math.sqrt(
          Math.pow(ev.targetTouches[0].clientX - ev.targetTouches[1].clientX, 2) +
          Math.pow(ev.targetTouches[0].clientY - ev.targetTouches[1].clientY, 2)
        );
        const PINCH_THRESHHOLD = ev.target.clientWidth / 10;
        if (Math.abs(currentDistance - initialDistance) >= PINCH_THRESHHOLD) {
          ev.target.style.background = "green";
        }
      } else {
        setTpCache([]);
      }
    }
  }

  const startHandler = (ev) => {
    ev.preventDefault();
    if (ev.targetTouches.length === 2) {
      setTpCache(() => {
        const newCache = [];
        for (let i = 0; i < ev.targetTouches.length; i++) {
          newCache.push({
            identifier: ev.targetTouches[i].identifier,
            clientX: ev.targetTouches[i].clientX,
            clientY: ev.targetTouches[i].clientY
          });
        }
        return newCache;
      });
    }
    if (logEvents) {
      log("touchStart", ev, true);
    }
    updateBackground(ev);
  };

  const moveHandler = (ev) => {
    ev.preventDefault();
    if (logEvents) {
      log("touchMove", ev, false);
    }
    if (ev.targetTouches.length !== 2) {
      updateBackground(ev);
    }
    ev.target.style.outline = "dashed";
    handlePinchZoom(ev);
  };

  const endHandler = (ev) => {
    ev.preventDefault();
    if (logEvents) {
      log(ev.type, ev, false);
    }
    if (ev.targetTouches.length === 0) {
      ev.target.style.background = "white";
      ev.target.style.outline = "1px solid black";
    }
  };

  const setHandlers = (name) => {
    const el = document.getElementById(name);
    el.ontouchstart = startHandler;
    el.ontouchmove = moveHandler;
    el.ontouchcancel = endHandler;
    el.ontouchend = endHandler;
  };

  useEffect(() => {
    setHandlers("target1");
    setHandlers("target2");
    setHandlers("target3");
    setHandlers("target4");
  }, []);

  return (
    <>
      <h1>Multi-touch interaction</h1>
      <div id="target1">{testText}</div>
      <div id="target2"> Tap, Hold or Swipe me 2</div>
      <div id="target3"> Tap, Hold or Swipe me 3</div>
      <div id="target4"> Tap, Hold or Swipe me 4</div>

      <button id="log" onClick={enableLog}>Start/Stop event logging</button>
      <button id="clearlog" onClick={clearLog}>Clear the log</button>
      <p></p>
      <div dangerouslySetInnerHTML={{ __html: logText }}></div>
    </>
  );
};

export default TouchEventsContainer;

内容的提问来源于stack exchange,提问作者DDD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 12:40:21