Skip to content

持续追踪

watchPosition 方法是 Geolocation API 提供的持续定位功能,它会自动追踪设备位置的变化,并在位置更新时调用回调函数。与 getCurrentPosition 只获取一次位置不同,watchPosition 适合需要实时跟踪用户移动轨迹的场景,如导航、运动记录、打车应用等。

前置知识

阅读本节前,建议先了解:获取位置信息

基础概念

watchPosition vs getCurrentPosition

特性getCurrentPositionwatchPosition
获取次数一次持续
返回值watch ID
适用场景获取当前位置实时追踪移动
位置更新不更新自动更新
停止方式无需停止clearWatch()

watchPosition 工作流程

调用 watchPosition()

  ├── 立即返回 watchId

  ├── 获取第一个位置 → 调用成功回调

  ├── 位置发生变化 → 再次调用成功回调

  ├── 位置继续变化 → 持续调用成功回调

  └── 调用 clearWatch(watchId) → 停止追踪

语法与 API

基本用法

javascript
// 开始持续追踪
const watchId = navigator.geolocation.watchPosition(
  onSuccess,   // 位置更新回调
  onError,     // 错误回调
  options      // 配置选项(与 getCurrentPosition 相同)
);

// 停止追踪
navigator.geolocation.clearWatch(watchId);

// 成功回调(每次位置更新时调用)
function onSuccess(position) {
  console.log('位置更新:', position.coords.latitude, position.coords.longitude);
}

// 错误回调
function onError(error) {
  console.error('追踪错误:', error.message);
}

// 配置选项
const options = {
  enableHighAccuracy: true,  // 高精度模式
  timeout: 15000,           // 超时时间
  maximumAge: 0             // 不使用缓存
};

watchPosition 返回值

javascript
// watchPosition 返回一个数字 ID
const watchId = navigator.geolocation.watchPosition(onSuccess, onError);

console.log('watch ID:', watchId); // 例如: 1

// 使用 ID 停止追踪
navigator.geolocation.clearWatch(watchId);

详细说明

位置更新频率

watchPosition 的更新频率由浏览器和系统决定,开发者无法直接控制:

设备/环境典型更新间隔说明
GPS(户外)1 秒最频繁
WiFi 定位5-10 秒中等频率
基站定位10-30 秒较低频率
设备静止不更新节省电量
浏览器后台可能停止取决于系统策略

同时使用多个 watch

javascript
// 可以同时创建多个 watch
const highAccuracyWatch = navigator.geolocation.watchPosition(
  onHighAccuracy,
  onError,
  { enableHighAccuracy: true, timeout: 15000 }
);

const lowAccuracyWatch = navigator.geolocation.watchPosition(
  onLowAccuracy,
  onError,
  { enableHighAccuracy: false, timeout: 5000 }
);

// 分别停止
function stopAllWatches() {
  navigator.geolocation.clearWatch(highAccuracyWatch);
  navigator.geolocation.clearWatch(lowAccuracyWatch);
}

追踪轨迹记录

javascript
// 轨迹追踪器
const Tracker = {
  watchId: null,
  trackPoints: [],     // 轨迹点
  startTime: null,     // 开始时间
  totalDistance: 0,    // 总距离

  // 开始追踪
  start(options = {}) {
    this.trackPoints = [];
    this.totalDistance = 0;
    this.startTime = Date.now();

    this.watchId = navigator.geolocation.watchPosition(
      (position) => this.onPositionUpdate(position),
      (error) => this.onError(error),
      {
        enableHighAccuracy: true,
        timeout: 15000,
        maximumAge: 0,
        ...options
      }
    );

    console.log('开始追踪, watchId:', this.watchId);
  },

  // 位置更新处理
  onPositionUpdate(position) {
    const point = {
      lat: position.coords.latitude,
      lng: position.coords.longitude,
      accuracy: position.coords.accuracy,
      speed: position.coords.speed,
      altitude: position.coords.altitude,
      timestamp: position.timestamp
    };

    // 计算与上一个点的距离
    if (this.trackPoints.length > 0) {
      const last = this.trackPoints[this.trackPoints.length - 1];
      const distance = this.calculateDistance(last.lat, last.lng, point.lat, point.lng);
      // 只记录有效移动(过滤 GPS 抖动)
      if (distance > 2 && distance < 100) {
        this.totalDistance += distance;
        this.trackPoints.push(point);
        this.onUpdate(point, distance);
      }
    } else {
      this.trackPoints.push(point);
      this.onUpdate(point, 0);
    }
  },

  // 更新回调(可覆盖)
  onUpdate(point, distance) {
    console.log(`新位置: ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | 移动: ${distance.toFixed(1)}m`);
  },

  // 错误处理
  onError(error) {
    console.error('追踪错误:', error.message);
  },

  // 停止追踪
  stop() {
    if (this.watchId !== null) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
      console.log('追踪已停止');
      console.log(`总距离: ${this.totalDistance.toFixed(0)} 米`);
      console.log(`轨迹点数: ${this.trackPoints.length}`);
      console.log(`总时间: ${((Date.now() - this.startTime) / 1000).toFixed(0)} 秒`);
    }
  },

  // Haversine 距离计算
  calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371000;
    const dLat = (lat2 - lat1) * Math.PI / 180;
    const dLon = (lon2 - lon1) * Math.PI / 180;
    const a = Math.sin(dLat / 2) ** 2 +
              Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
              Math.sin(dLon / 2) ** 2;
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  },

  // 获取统计信息
  getStats() {
    const elapsed = (Date.now() - this.startTime) / 1000;
    const avgSpeed = elapsed > 0 ? this.totalDistance / elapsed : 0;
    return {
      totalDistance: this.totalDistance,
      totalPoints: this.trackPoints.length,
      elapsedTime: elapsed,
      averageSpeed: avgSpeed
    };
  }
};

实战示例

移动轨迹追踪器

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>移动轨迹追踪</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }

    .tracker-card {
      max-width: 600px;
      margin: 0 auto;
      background: white;
      border-radius: 12px;
      padding: 24px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }

    h2 { text-align: center; color: #1a1a2e; }

    .controls {
      display: flex;
      gap: 10px;
      margin: 20px 0;
    }

    .btn {
      flex: 1;
      padding: 12px;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 600;
    }

    .btn-start { background: #4CAF50; color: white; }
    .btn-stop { background: #F44336; color: white; }
    .btn-stop:disabled { opacity: 0.5; cursor: not-allowed; }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 12px;
      margin-top: 16px;
    }

    .stat-item {
      text-align: center;
      padding: 16px 8px;
      background: #f8f9fa;
      border-radius: 8px;
    }

    .stat-value {
      font-size: 24px;
      font-weight: 700;
      color: #1a73e8;
      font-family: monospace;
    }

    .stat-label {
      font-size: 12px;
      color: #888;
      margin-top: 4px;
    }

    .trail-list {
      margin-top: 20px;
      max-height: 300px;
      overflow-y: auto;
      padding: 12px;
      background: #1a1a2e;
      border-radius: 8px;
      font-family: monospace;
      font-size: 12px;
      color: #0f0;
    }

    .trail-list .entry {
      padding: 2px 0;
      border-bottom: 1px solid #333;
    }

    .current-pos {
      margin-top: 16px;
      padding: 16px;
      background: #e3f2fd;
      border-radius: 8px;
      text-align: center;
    }

    .current-pos .coords {
      font-size: 18px;
      font-weight: 600;
      color: #1565c0;
      font-family: monospace;
    }
  </style>
</head>
<body>
  <div class="tracker-card">
    <h2>移动轨迹追踪</h2>

    <div class="controls">
      <button class="btn btn-start" id="startBtn" onclick="startTracking()">开始追踪</button>
      <button class="btn btn-stop" id="stopBtn" onclick="stopTracking()" disabled>停止追踪</button>
    </div>

    <div class="current-pos" id="currentPos">
      <div style="color:#888;">等待开始追踪...</div>
    </div>

    <div class="stats-grid">
      <div class="stat-item">
        <div class="stat-value" id="totalDist">0</div>
        <div class="stat-label">总距离 (米)</div>
      </div>
      <div class="stat-item">
        <div class="stat-value" id="pointCount">0</div>
        <div class="stat-label">轨迹点数</div>
      </div>
      <div class="stat-item">
        <div class="stat-value" id="elapsed">0</div>
        <div class="stat-label">用时 (秒)</div>
      </div>
    </div>

    <div class="trail-list" id="trailLog">
      轨迹记录将在此显示...
    </div>
  </div>

  <script>
    let watchId = null;
    let trackPoints = [];
    let totalDistance = 0;
    let startTime = null;
    let timerInterval = null;

    function startTracking() {
      trackPoints = [];
      totalDistance = 0;
      startTime = Date.now();
      document.getElementById('trailLog').innerHTML = '';

      document.getElementById('startBtn').disabled = true;
      document.getElementById('stopBtn').disabled = false;

      // 更新计时器
      timerInterval = setInterval(updateTimer, 1000);

      watchId = navigator.geolocation.watchPosition(
        onPositionUpdate,
        (error) => {
          addLog(`错误: ${error.message}`, '#f44');
          stopTracking();
        },
        { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
      );

      addLog('开始追踪位置');
    }

    function onPositionUpdate(position) {
      const c = position.coords;
      const point = { lat: c.latitude, lng: c.longitude, time: position.timestamp };

      // 更新当前位置显示
      document.getElementById('currentPos').innerHTML = `
        <div style="color:#1565c0;font-size:12px;">当前位置</div>
        <div class="coords">${c.latitude.toFixed(6)}, ${c.longitude.toFixed(6)}</div>
        <div style="color:#888;font-size:12px;margin-top:4px;">精度: ${c.accuracy.toFixed(0)}m</div>
      `;

      // 计算距离
      if (trackPoints.length > 0) {
        const last = trackPoints[trackPoints.length - 1];
        const dist = haversine(last.lat, last.lng, point.lat, point.lng);

        if (dist > 2 && dist < 100) { // 过滤抖动和异常
          totalDistance += dist;
          trackPoints.push(point);
          addLog(`#${trackPoints.length} | ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | +${dist.toFixed(1)}m | 总计: ${totalDistance.toFixed(0)}m`);
        }
      } else {
        trackPoints.push(point);
        addLog(`#${1} | ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | 起点`);
      }

      // 更新统计
      document.getElementById('totalDist').textContent = totalDistance.toFixed(0);
      document.getElementById('pointCount').textContent = trackPoints.length;
    }

    function stopTracking() {
      if (watchId !== null) {
        navigator.geolocation.clearWatch(watchId);
        watchId = null;
      }
      if (timerInterval) {
        clearInterval(timerInterval);
        timerInterval = null;
      }

      document.getElementById('startBtn').disabled = false;
      document.getElementById('stopBtn').disabled = true;
      addLog(`追踪结束 | 总距离: ${totalDistance.toFixed(0)}m | 点数: ${trackPoints.length}`);
    }

    function updateTimer() {
      const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
      document.getElementById('elapsed').textContent = elapsed;
    }

    function addLog(text, color = '#0f0') {
      const log = document.getElementById('trailLog');
      const time = new Date().toLocaleTimeString();
      log.innerHTML += `<div class="entry" style="color:${color}">[${time}] ${text}</div>`;
      log.scrollTop = log.scrollHeight;
    }

    function haversine(lat1, lon1, lat2, lon2) {
      const R = 6371000;
      const dLat = (lat2 - lat1) * Math.PI / 180;
      const dLon = (lon2 - lon1) * Math.PI / 180;
      const a = Math.sin(dLat / 2) ** 2 +
                Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
                Math.sin(dLon / 2) ** 2;
      return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    }
  </script>
</body>
</html>

注意事项

  1. 电量消耗:持续使用 GPS 会显著增加耗电量,非必要时应及时 clearWatch
  2. 后台限制:当页面不可见时,浏览器可能降低更新频率或暂停追踪
  3. GPS 抖动:静止时 GPS 数据会有轻微波动,需要过滤处理
  4. 内存管理:长时间追踪会积累大量轨迹点,注意控制存储

最佳实践

  1. 使用合理的最小距离阈值(如 2 米)过滤无效的位置更新
  2. 在页面不可见时暂停追踪(visibilitychange 事件)
  3. 为轨迹点设置最大数量限制,超出后移除旧点
  4. 及时调用 clearWatch 释放资源
  5. 持续追踪时提供清晰的 UI 反馈

下一节

继续学习:Worker 基础

参考链接