diff --git a/docs/AMap面试题.md b/docs/AMap面试题.md index 278047d..2701565 100644 --- a/docs/AMap面试题.md +++ b/docs/AMap面试题.md @@ -1,21 +1,22 @@ -# 高德地图 Composable 面试题 +# 高德地图面试题 ---- +--- ## Q1:如何在 Vue 3 中集成高德地图(AMap)? ---- +--- ## 题目 请你设计一个 Vue 3 组合式函数(Composable)来集成高德地图 JSAPI,要求: + 1. 支持在多个页面中独立使用,但 SDK 脚本只下载一次 2. 安全密钥在脚本加载之前正确设置 3. 地图容器在 flex 布局中能正确渲染 请描述关键实现步骤和设计决策。 ---- +--- ## 参考答案 @@ -23,64 +24,58 @@ 集中管理 Key、安全密钥、版本号和插件列表: -| 配置项 | 说明 | -|--------|------| -| `AMAP_JSAPI_KEY` | 前端地图 Key(`VITE_AMAP_JSAPI_KEY` 环境变量) | -| `AMAP_SECURITY_CODE` | 安全密钥,2021/12/02 后申请的 Key 必须配合使用 | -| `AMAP_VERSION` | JSAPI 版本,如 `'2.0'` | -| `AMAP_PLUGINS` | 需要加载的插件数组,如 `Geocoder`、`Driving`、`ToolBar` 等 | +| 配置项 | 说明 | +| -------------------- | ---------------------------------------------------------- | +| `AMAP_JSAPI_KEY` | 前端地图 Key(`VITE_AMAP_JSAPI_KEY` 环境变量) | +| `AMAP_SECURITY_CODE` | 安全密钥,2021/12/02 后申请的 Key 必须配合使用 | +| `AMAP_VERSION` | JSAPI 版本,如 `'2.0'` | +| `AMAP_PLUGINS` | 需要加载的插件数组,如 `Geocoder`、`Driving`、`ToolBar` 等 | ### 2. `loadAMap()` — SDK 全局单例加载 用两个模块级变量实现双重锁: ``` -let amapPromise = null // 正在进行的加载 Promise(防并发重复加载) -let AMapGlobal = null // 已完成的加载结果(后续调用 O(1) 秒返) - -loadAMap(): - ① if (AMapGlobal) → 直接返回缓存 // 最快路径 - ② if (!amapPromise) → 创建加载 Promise // 首次 / 上次失败后 - ├─ 先设置 window._AMapSecurityConfig // ⚠️ 必须在 load 之前 - ├─ 调用 AMapLoader.load({ key, version, plugins }) - ├─ 成功 → 存入 AMapGlobal - └─ 失败 → amapPromise = null(允许重试) - ③ return amapPromise // 并发调用复用同一 Promise -``` - -关键设计: -- **Promise 去重**:多个组件同时调用,只发一次网络请求 -- **失败可重试**:`catch` 中重置 `amapPromise`,不污染 `AMapGlobal` - -### 3. `useAmap()` — 地图实例管理 - -```ts -// 返回值 -containerRef // ref → 模板绑定到
-mapInstance // shallowRef → 地图实例(为什么用 shallowRef 见 Q2) -loading/error // ref → 加载/错误状态 -initMap() // 异步初始化:loadAMap() → new AMap.Map(container, options) -destroyMap() // 销毁实例 + onUnmounted 自动调用 +let amapPromise = null // 正在进行的加载 Promise(防并发重复加载) +let AMapGlobal = null // 已完成的加载结果(后续调用 O(1) 秒返) + +loadAMap(): + ① if (AMapGlobal) → 直接返回缓存 // 最快路径 + ② if (!amapPromise) → 创建加载 Promise // 首次 / 上次失败后 + ├─ 先设置 window._AMapSecurityConfig // ⚠️ 必须在 load 之前 + ├─ 调用 AMapLoader.load({ key, version, plugins }) ├─ 成功 → 存入 AMapGlobal └─ 失败 → amapPromise = null(允许重试) + ③ return amapPromise // 并发调用复用同一 Promise``` + +关键设计: +- **Promise 去重**:多个组件同时调用,只发一次网络请求 +- **失败可重试**:`catch` 中重置 `amapPromise`,不污染 `AMapGlobal` + +### 3. `useAmap()` — 地图实例管理 + +```ts +// 返回值 +containerRef // ref → 模板绑定到
mapInstance // shallowRef → 地图实例(为什么用 shallowRef 见 Q2) +loading/error // ref → 加载/错误状态 +initMap() // 异步初始化:loadAMap() → new AMap.Map(container, options) +destroyMap() // 销毁实例 + onUnmounted 自动调用 ``` ### 4. 页面中使用 ```vue - - - + + + ``` ### 5. 关键细节 @@ -89,27 +84,27 @@ onMounted(async () => { - **flex 布局修正**:地图创建后调用 `map.resize()`,因为 flex 分配的尺寸可能尚未生效 - **`%` 而非 `vh`**:容器用 `height: 100%` 逐级继承,适配微前端宿主不一定是全视口的场景 ---- +--- ## 总结一句话 > **配置文件集中管理 Key/插件 → `loadAMap()` 全局单例确保 SDK 只下载一次 → `useAmap()` 封装实例生命周期,页面只需绑定 `containerRef` 并调用 `initMap()`。** - ---- +--- ## Q2:多个地图页面复用时有哪些性能陷阱?如何解决? ---- +--- ## 题目 你的应用中有 3 个页面各自包含高德地图,用户在不同页面间切换。请分析: + 1. **JSAPI 脚本会重复下载吗?如何保证只下载一次?** 2. **`mapInstance` 为什么用 `shallowRef` 而不是 `ref`?从性能和副作用两个角度说明。** 3. **页面切换时旧地图实例如何清理?画出关键生命周期。** ---- +--- ## 参考答案 @@ -119,61 +114,57 @@ onMounted(async () => { **进阶:`AMap` 与 `mapInstance` 的本质区别** -| 对象 | 本质 | 作用 | 数量 | -|------|------|------|------| -| **`AMap`** | 全局命名空间 (SDK) | 提供构造函数(如 `new AMap.Marker`)和工具方法 | 全局唯一 | -| **`mapInstance`** | 地图实例 (Instance) | 具体的地图对象,负责渲染、缩放、事件监听 | 每个容器一个 | +| 对象 | 本质 | 作用 | 数量 | +| ----------------- | ------------------- | ---------------------------------------------- | ------------ | +| **`AMap`** | 全局命名空间 (SDK) | 提供构造函数(如 `new AMap.Marker`)和工具方法 | 全局唯一 | +| **`mapInstance`** | 地图实例 (Instance) | 具体的地图对象,负责渲染、缩放、事件监听 | 每个容器一个 | > **面试官提问:既然已经有了 `mapInstance`,为什么在点击事件处理函数中还要重新调用 `await loadAMap()`?** -> +> > **回答:** 因为 `mapInstance` 只是一个渲染好的地图“窗口”,它不包含创建新零件(如 Marker 或 Polyline)的“工厂工具”。为了在点击位置创建新的 Marker,必须通过 `loadAMap()` 获取 `AMap` 这个构造函数库。由于 `loadAMap` 做了单例缓存,这种重复调用是 O(1) 级别的,既保证了代码的健壮性(确保 SDK 已加载),又不会产生额外的网络开销。 页面切换时的关键流程: ``` -页面 A 活跃 → 页面 B 挂载 - │ - ├─ A.onUnmounted → destroyMap() - │ └─ mapInstance.value.destroy() // 释放 WebGL 上下文、事件监听 - │ - └─ B.onMounted → initMap() - ├─ loadAMap() → 命中 AMapGlobal 缓存(不下载) - └─ new AMap.Map(containerB, options) // 全新实例挂到新 DOM +页面 A 活跃 → 页面 B 挂载 + │ ├─ A.onUnmounted → destroyMap() + │ └─ mapInstance.value.destroy() // 释放 WebGL 上下文、事件监听 + │ └─ B.onMounted → initMap() ├─ loadAMap() → 命中 AMapGlobal 缓存(不下载) + └─ new AMap.Map(containerB, options) // 全新实例挂到新 DOM ``` **核心结论**:SDK 全局常驻,地图实例按页面创建/销毁,互不干扰。 ### 2. 为什么 `mapInstance` 必须用 `shallowRef`? -| 维度 | `shallowRef` ✅ | `ref` ❌ | -|------|-----------------|----------| -| **性能** | 仅追踪 `.value` 整体替换,初始化为 O(1) | 递归代理整个 AMap.Map 对象树(数百个属性/WebGL 上下文),阻塞主线程 | -| **副作用** | 不劫持属性访问,AMap 内部 `this` 指向不变 | Proxy 改变 `this` 指向,可能破坏地图渲染循环、事件系统 | -| **语义** | 代码中只需要 `mapInstance.value = new Map()` / `.destroy()` / `= null`,不关心内部属性变化 | 完全用不到深度追踪能力 | +| 维度 | `shallowRef` ✅ | `ref` ❌ | +| ---------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | +| **性能** | 仅追踪 `.value` 整体替换,初始化为 O(1) | 递归代理整个 AMap.Map 对象树(数百个属性/WebGL 上下文),阻塞主线程 | +| **副作用** | 不劫持属性访问,AMap 内部 `this` 指向不变 | Proxy 改变 `this` 指向,可能破坏地图渲染循环、事件系统 | +| **语义** | 代码中只需要 `mapInstance.value = new Map()` / `.destroy()` / `= null`,不关心内部属性变化 | 完全用不到深度追踪能力 | **一句话**:`shallowRef` 用于"只关心对象整体是否被替换,不关心内部怎么变"的第三方复杂实例。 ### 3. 页面切换时的完整生命周期 -| 步骤 | 时机 | 动作 | -|------|------|------| -| ① 旧地图销毁 | `onUnmounted` → `destroyMap()` | `map.destroy()` 释放画布、事件、DOM | -| ② 响应式清理 | Vue 组件卸载 | `mapInstance`、`containerRef` 随组件 GC | -| ③ 新地图创建 | `onMounted` → `initMap()` | `containerRef` 绑定新 DOM → `new AMap.Map(新容器)` | -| ④ SDK 缓存命中 | `initMap()` 内部 `loadAMap()` | `AMapGlobal` 非空,O(1) 返回 | +| 步骤 | 时机 | 动作 | +| -------------- | ------------------------------ | -------------------------------------------------- | +| ① 旧地图销毁 | `onUnmounted` → `destroyMap()` | `map.destroy()` 释放画布、事件、DOM | +| ② 响应式清理 | Vue 组件卸载 | `mapInstance`、`containerRef` 随组件 GC | +| ③ 新地图创建 | `onMounted` → `initMap()` | `containerRef` 绑定新 DOM → `new AMap.Map(新容器)` | +| ④ SDK 缓存命中 | `initMap()` 内部 `loadAMap()` | `AMapGlobal` 非空,O(1) 返回 | ---- +--- ## 总结一句话 > **SDK 全局单例只下载一次,地图实例按页面生灭,`shallowRef` 避免深度代理第三方对象 —— 三者配合才能让多页面地图既快又稳。** - ---- +--- ## Q3:如何在高德地图上添加和管理 Marker? ---- +--- ## 题目 @@ -183,21 +174,21 @@ onMounted(async () => { 2. **不同类型的 Marker 在"创建/更新/销毁"策略上有什么不同?为什么?** 3. **为什么 Marker 的 CSS 样式不能写在 Vue 的 ` + ``` 通过 **业务前缀命名**(如 `geofence-`)来隔离,而非依赖 Vue 的 scoped 机制。 ---- +--- ## 总结一句话 > **Marker 通过 `content` + `offset` 实现自定义 UI;静态标记复用 `setPosition`,临时标记批量清理,样式用全局 `