第 1 轮 · 返回本次面经 · 已是最后一轮 →

本轮要点: 深入框架原理与工程化:小程序加载流程(缓存/渲染机制)、Vue路由动态加载(Code Splitting)、全局状态管理(Vuex模块化)、生命周期资源管理(mounted/beforeDestroy)。难点在于手势识别算法(滑动速度计算)和可控动画引擎(开始/暂停/取消)。

本轮共 12 道题。答案默认折叠,便于先自行作答。

1. 小程序的页面加载流程是怎样的?请简要描述从用户点击小程序图标到页面显示的整个过程。

题库原题:小程序的页面加载流程是怎样的?请简要描述从用户点击小程序图标到页面显示的整个过程。

题目要点

  • 双线程并行:启动过程是渲染层(UI)和逻辑层(JS)同步初始化的过程。
  • 离线包机制:通过本地缓存和分包加载策略,极大减少了网络传输对首屏的影响。
  • 预加载优化:微信客户端通过预热环境和提前下载包来压榨启动耗时。
  • 数据驱动渲染:首屏显示依赖于逻辑层到渲染层的初始数据传输(setData 机制的前身)。
参考答案

小程序的加载是一个多线程协作的过程,主要可以分为环境准备代码下载注入运行以及首屏渲染四个阶段。

1. 运行环境准备(准备阶段)

当用户点击小程序图标时,微信客户端会立即启动一个原生容器。

  • 进程启动:微信会为小程序分配独立的进程或线程。
  • 双线程环境初始化:微信会同时初始化两个运行环境,即逻辑层(AppService)渲染层(WebView)。为了提速,微信通常会预先启动一个通用的空白 WebView(预加载机制),从而缩短环境搭建的时间。

2. 代码包下载与加载(资源阶段)

在环境准备的同时,微信会根据小程序的 AppID 去服务端请求最新的代码包。

  • 增量更新与缓存:如果本地已有缓存,则直接校验版本;若无或版本过旧,则下载加密的代码包。
  • 分包加载优化:如果开发者配置了分包,此时只会下载“主包”内容,从而减少首次下载的数据量,提升启动速度。

3. 代码注入与逻辑初始化(初始化阶段)

代码包下载完成后,微信会开始解析并执行代码。

  • 逻辑层注入:执行 app.js 和各页面的 JS 逻辑。此时,小程序会触发 App.onLaunchApp.onShow 生命周期回调。
  • 配置解析:解析 app.json 和页面配置,确定全局样式、路由关系及初始页面。

4. 渲染层视图构建与首屏渲染(渲染阶段)

这是用户肉眼感知页面出现的关键时刻。

  • 视图层初始化:渲染层载入 WXML 转换后的模板和 WXSS 样式。
  • 初始数据传输(First Paint):逻辑层在实例化页面后,会将初始数据(Initial Data)通过微信底层提供的 evaluateJavascript 方式,跨线程传递给渲染层。
  • 动态渲染:渲染层接收到数据后,结合模板生成最终的 DOM 树,并交由浏览器引擎进行绘制。

5. 交互就绪(Finish 阶段)

  • 生命周期触发:页面初次渲染完成后,逻辑层会收到反馈,并触发页面的 onReady 回调。此时,逻辑层与渲染层已经建立了稳定的双向数据绑定,用户可以开始进行交互操作。

2. 小程序页面间有哪些传递数据的方法?

题库原题:小程序页面间有哪些传递数据的方法?

题目要点

  • URL 参数:最基础,适合少量非敏感数据的单向传递。
  • globalData:全局单例,适合存储应用级的通用配置。
  • Storage:持久化,适合大数据量或需要跨会话保留的数据。
  • EventChannel:官方推荐的双向通信方案,适合具有强关联的页面跳转逻辑。
  • 状态管理/EventBus:高度解耦,适合大型项目中复杂的多对多通信。
参考答案

1. 路由 URL 参数传递

这是最简单、最常用的方式,适用于传递少量、简单的数据。

  • 实现方式:在调用 wx.navigateTowx.redirectTo 时,将数据以查询字符串的形式拼接到 URL 后。在目标页面的 onLoad 生命周期的参数中即可获取。
  • 局限性:URL 长度有限制,且不支持传递复杂的对象或包含特殊字符(需经过 encodeURIComponent 处理)的数据。

2. 全局状态管理(App.globalData)

适用于需要在多个页面间共享的持久性配置或状态。

  • 实现方式:在 app.js 定义 globalData 对象。页面通过 getApp() 访问并修改其中的属性。
  • 特点:简单直观,但由于缺乏响应式监听机制,当 globalData 改变时,已打开的页面无法自动更新 UI,通常需要配合页面生命周期手动同步。

3. 数据缓存(Storage)

适用于持久化存储大数据量的传递。

  • 实现方式:利用 wx.setStorage / wx.getStorage 及其同步版本。
  • 特点:数据会存储在本地磁盘。适合传递如用户信息、配置缓存等不经常变动的数据。需要注意及时清理,避免占用过多本地空间。

4. 事件通道(EventChannel)

这是 wx.navigateTo 提供的一个高级特性,专门用于打开页面与被打开页面之间的双向通信。

  • 实现方式:在跳转时定义 events 监听回调,目标页面通过 this.getOpenerEventChannel() 获取通道。
  • 优势:支持双向实时通信,不仅可以从 A 传到 B,B 也可以在处理完逻辑后通过通道向 A 回传数据,非常适合表单选择、弹窗反馈等场景。

5. 全局事件总线(EventBus)或 状态管理库

对于大型项目,通常会引入像 MobXPinia 或自实现的 Pub/Sub(发布订阅)模式。

  • 实现方式:创建一个全局单例的事件中心。页面 A 订阅某个事件,页面 B 触发该事件并携带数据。
  • 特点:解耦了页面间的直接依赖,适合处理跨层级、多页面的复杂通信需求。

3. 简述微信小程序原理?

题库原题:简述微信小程序原理?

题目要点

  • 逻辑与渲染分离保障稳定性但带来通信成本
  • 混合渲染技术平衡了灵活性与性能
  • 沙箱机制确保安全但限制部分 JS 能力
  • 资源加载策略针对移动端深度优化
  • 整个体系依赖客户端原生能力深度集成
参考答案

微信小程序的运行原理可以拆解为以下几个核心层面:

1. 双线程架构设计

采用逻辑层(App Service)与渲染层(View)分离的模型:

  • 逻辑线程:运行在独立的 JavaScript 引擎中(iOS:JavaScriptCore,Android:V8),处理业务逻辑、数据请求及状态管理
  • 渲染线程:基于 WebView 渲染界面,但禁用了常规浏览器 API(如 DOM 操作)
  • 通信机制:通过 Native 层的中转站进行数据交换(序列化为字符串传输),采用事件驱动更新

2. 预编译与沙箱环境

  • WXML/WXSS 编译:模板语言被编译为虚拟 DOM 结构的 JS 对象,样式被转换为特定 CSS 规则
  • JavaScript 限制:运行在沙箱环境中,无 window/document 对象,禁止动态执行代码(如 eval)
  • 模块系统:基于 CommonJS 规范实现模块隔离,每个页面/组件有独立作用域

3. 原生组件混合渲染

  • 内置组件:如 / 等实际由原生控件实现,通过层级分离技术覆盖在 WebView 之上
  • 性能优化:滚动容器(scroll-view)等关键组件使用原生实现避免 WebView 性能瓶颈

4. 更新与热加载机制

  • 分包加载:主包不超过 2MB,支持按需加载子包(总上限 20MB)
  • 增量更新:基于版本对比的差量更新策略,减少下载体积
  • 热启动:后台保持常驻内存,二次启动速度优化

5. 安全控制体系

  • 域名白名单:网络请求受限为配置的合法域名
  • 代码签名:所有发布包需腾讯云签名验证
  • 数据隔离:不同小程序之间完全隔离存储空间

4. Vue路由的原理是什么?请简述Vue Router如何实现页面跳转和组件的动态加载。

题库原题:Vue路由的原理是什么?请简述Vue Router如何实现页面跳转和组件的动态加载。

题目要点

  • 路由监听:Hash 模式靠 hashchange 事件,History 模式靠 pushState API 和 popstate 事件。
  • 视图同步:利用 Vue 的响应式原理,通过修改全局路由对象触发 <router-view> 组件的重绘。
  • 懒加载机制:利用 ES6 动态导入语法,配合打包工具实现代码分包,按需从服务器拉取组件代码。
  • 流程控制:通过一套复杂的导航守卫体系,控制跳转过程中的权限验证和逻辑处理。
参考答案

在 Vue 的单页面应用(SPA)架构中,Vue Router 的核心目标是:改变 URL 但不刷新页面,并根据 URL 映射对应的组件进行渲染

这主要依赖于浏览器提供的原生能力以及 Vue 的响应式系统。

1. 核心原理:监听 URL 的变化

Vue Router 主要支持两种模式来拦截 URL 的变动:

  • Hash 模式: 利用 URL 中 # 及其后面的部分。当 # 后的路径发生变化时,浏览器不会向服务器发送请求,但会触发 hashchange 事件。Vue Router 通过监听该事件来感知路由变化。
  • History 模式: 利用 HTML5 提供的 history.pushStatehistory.replaceState API。这两个 API 允许开发者在不刷新页面的前提下修改浏览器的历史记录栈和当前的 URL。同时,通过监听 popstate 事件,可以捕获用户点击浏览器前进/后退按钮的行为。

2. 实现页面跳转的逻辑

当用户触发跳转(如点击 <router-link> 或调用 router.push)时,Vue Router 执行以下流程:

  1. 路径匹配:根据目标 URL,在预先定义的路由映射表(Routes Map)中查找对应的路由记录。
  2. 执行导航守卫:依次触发全局守卫、路由独享守卫和组件内守卫,确认是否允许本次跳转。
  3. 更新响应式状态:一旦确认跳转,Vue Router 会更新内部的一个响应式变量(通常是 _route)。
  4. 触发视图渲染:由于 <router-view> 组件强依赖于这个响应式的路由对象,当该对象变化时,Vue 的响应式系统会触发 <router-view> 的重新渲染,从而替换展示的组件。

3. 组件的动态加载(路由懒加载)

为了优化首屏加载速度,Vue Router 支持路由懒加载,其核心结合了 ES 模块的动态导入(Dynamic Import)Webpack/Vite 的代码分包(Code Splitting) 功能。

  • 定义方式:在路由配置中使用 component: () => import('./Home.vue')
  • 实现原理
  • 构建阶段:打包工具识别出 import() 语法,会将对应的组件及其依赖单独打包成一个独立的 JS 文件(Chunk)。
  • 运行阶段:当路由匹配到该路径时,Vue Router 会执行这个函数。此时浏览器会通过网络请求去下载对应的 Chunk 文件。
  • 异步渲染:下载完成后,Vue Router 会将加载到的组件内容缓存起来,并渲染到 <router-view> 中。

5. 在Vue项目中,如何实现全局状态管理?

题库原题:在Vue项目中,如何实现全局状态管理?

题目要点

  • 首选方案:Vue 3 优先使用 Pinia,因其轻量化、模块化和完美的 TS 支持。
  • 版本适配:Vue 2 依然推荐 Vuex 以保持单向数据流的严谨性。
  • 按需选择:中小型项目可利用 Composition API 导出响应式对象实现自研状态管理,降低复杂度。
  • 依赖注入:利用 Provide/Inject 解决特定组件树内的状态共享问题,避免属性钻取(Props Drilling)
参考答案

在 Vue 的工程化实践中,全局状态管理经历了从简单的全局对象到复杂框架的演进。

实现方式的选择通常取决于项目的规模、团队的习惯以及所使用的 Vue 版本。

以下是几种主流的实现方案及其底层逻辑。

1. Pinia:官方推荐的现代方案

在 Vue 3 生态中,Pinia 已经取代 Vuex 成为官方首选。它抛弃了冗长的 Mutation,采用了更扁平、更符合直觉的设计。

  • 实现机制:Pinia 本质上是利用了 Vue 3 的 reactiveref 来创建跨组件共享的响应式存储。
  • 工程优势
  • 极佳的 TypeScript 支持:无需复杂的类型定义即可获得完善的类型推断。
  • 模块化架构:每个 Store 都是独立的,可以按需引入,避免了大型项目中状态树过于臃肿的问题。
  • 符合组合式 API:其 setup 风格的 Store 定义方式与 Vue 3 的开发习惯高度一致。

2. Vuex:经典的集中式存储

对于现存的 Vue 2 项目或特定架构的大型应用,Vuex 依然是核心。

  • 设计模式:采用严格的单向数据流。通过 State 定义数据,Getter 处理派生状态,Mutation 同步修改数据,Action 处理异步逻辑。
  • 核心价值:这种严格的约束虽然增加了代码量,但在多人协作的大型项目中,能够提供清晰的调试轨迹。配合 Vue Devtools,可以实现“时光旅行”式的状态回溯。

3. Composition API (Reactive/Ref):轻量级原生实现

如果项目逻辑简单,不需要调试工具支持或复杂的模块拆分,完全可以利用 Vue 3 的响应式原生能力实现一个微型状态管理。

  • 实现方式:创建一个独立的 JS 文件,使用 reactiveref 定义状态并导出。
  • 逻辑
// store.js
import { reactive } from 'vue';
export const globalState = reactive({
    count: 0,
    increment() { this.count++ }
});
  • 优点:无需引入额外库,打包体积小,性能极高。

4. Provide / Inject:跨层级依赖注入

适用于父组件向深层嵌套的子组件传递状态,而非真正意义上的“全局”状态。

  • 场景:常用于开发组件库或局部复杂的插件架构。
  • 响应式配合:通过 provide 传递一个响应式的 refreactive 对象,子组件通过 inject 接收,从而实现跨组件的同步。

5. 外部状态总线 (EventBus)

在 Vue 3 中,由于官方移除了 $on$emit 实例方法,传统的 EventBus 已不再推荐使用。如果必须使用,通常需要引入第三方库如 mitt。这更多用于非父子组件间的瞬时消息通信,而非状态持久化。

6. 假设你正在开发一个 Vue 项目,需要在组件加载完成后执行一个数据初始化操作,并在组件销毁前清理一些资源(如定时器或事件监听)。你会使用哪些生命周期钩子来实现这些功能,并简述每个钩子的作用。

题库原题:假设你正在开发一个 Vue 项目,需要在组件加载完成后执行一个数据初始化操作,并在组件销毁前清理一些资源(如定时器或事件监听)。你会使用哪些生命周期钩子来实现这些功能,并简述每个钩子的作用。

题目要点

  • onMounted:组件挂载到真实 DOM 后触发,用于异步请求、DOM 操作及第三方库初始化。
  • onBeforeUnmount:组件销毁前触发,用于移除定时器、注销全局事件监听及断开长连接。
  • 内存管理:在销毁阶段手动清理外部资源是预防前端内存溢出(Memory Leak)的行业标准实践。
  • 执行顺序:确保“谁开启,谁关闭”的逻辑闭环,维护组件的纯净性。
参考答案

以下将基于 Vue 3 组合式 API (Composition API) 这一现代主流范式进行说明。

1. 数据初始化:使用 onMounted

为了在组件加载完成后执行数据初始化(如发起 API 请求、访问 DOM 节点),会选择 onMounted 钩子。

  • 作用:该钩子在组件完成初始渲染并创建了真实的 DOM 节点后触发。此时,模板中的所有元素已挂载到页面上,是进行异步数据抓取、初始化第三方插件(如 ECharts)的最佳时机。
  • 为何不在 setupcreated 中做?:虽然在 setup 阶段可以发起网络请求,但如果初始化逻辑涉及到对 DOM 的操作(例如获取元素的宽高),则必须等到 onMounted 阶段,否则会因为 DOM 尚未生成而报错。

2. 资源清理:使用 onBeforeUnmount

为了在组件销毁前清理资源(如 setInterval 定时器、window 全局事件监听、WebSocket 连接),会选择 onBeforeUnmount 钩子。

  • 作用:该钩子在组件卸载(Unmount)发生之前触发。此时组件实例依然完全可用,所有的响应式数据、计算属性和方法都处于可访问状态。
  • 工程价值:这是防止内存泄漏的最后一道防线。如果在组件内开启了定时器而不在销毁前 clearInterval,该定时器会在后台持续运行,占用内存并可能导致逻辑冲突。

3. 代码实现示例

在 Composition API 风格下,通常会将逻辑封装在一起,提高代码的可维护性:

import { onMounted, onBeforeUnmount, ref } from 'vue';

export default {
  setup() {
    const timer = ref(null);

    onMounted(() => {
      // 1. 执行数据初始化
      fetchData();
      // 2. 开启资源
      timer.value = setInterval(() => {
        console.log('执行轮询任务');
      }, 1000);
      window.addEventListener('resize', handleResize);
    });

    onBeforeUnmount(() => {
      // 3. 清理资源,避免内存泄漏
      clearInterval(timer.value);
      window.removeEventListener('resize', handleResize);
      console.log('资源已释放');
    });

    return { /* ... */ };
  }
};

4. Vue 2 风格的对应关系

如果是维护旧项目,对应的钩子分别是:

  • mounted:对应 Vue 3 的 onMounted
  • beforeDestroy:对应 Vue 3 的 onBeforeUnmount

7. 请手写一个简单的Vue组件,实现一个计数器功能,要求包含加、减按钮以及显示当前计数。

题目要点

  • 响应式声明:利用 ref 创建响应式引用,确保数据变化能驱动视图更新。
  • 指令应用:使用 @click 处理交互,使用 :disabled 实现声明式逻辑控制。
  • 代码组织:采用 &lt;script setup&gt; 语法糖,减少模板代码,提升开发效率。
  • 健壮性设计:在递减逻辑中增加了边界检查,确保计数不会出现非预期的负数。
参考答案

在 Vue 3 的标准实践中,通常推荐使用单文件组件(SFC)配合组合式 API(Composition API)来实现这类功能。这种方式不仅代码结构清晰,而且逻辑复用性更强。

以下是一个符合生产环境规范的计数器组件实现:

<template>
  <div class="counter-container">
    <h3>当前计数<span class="count-value">{{ count }}</span></h3>
    <div class="button-group">
      <button @click="decrement" :disabled="count <= 0">减少</button>
      <button @click="increment">增加</button>
      <button @click="reset" class="reset-btn">重置</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 使用 ref 定义响应式状态
const count = ref(0);

// 逻辑处理函数
const increment = () => {
  count.value++;
};

const decrement = () => {
  if (count.value > 0) {
    count.value--;
  }
};

const reset = () => {
  count.value = 0;
};
</script>

<style scoped>
.counter-container {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  text-align: center;
}

.count-value {
  color: #42b983;
  font-weight: bold;
}

.button-group button {
  margin: 0 5px;
  padding: 8px 16px;
  cursor: pointer;
}

.reset-btn {
  background-color: #f5f5f5;
  border: 1px solid #ccc;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
</style>

代码深度分析

1. 响应式核心 (ref)

setup 语法糖中,使用 ref 是声明基础类型响应式数据的标准做法。它的底层原理是利用了 Vue 3 的 Proxy(对于对象)或对 .value 属性的拦截。当 count.value 发生变化时,Vue 的响应式系统会自动追踪依赖并触发模板的重新渲染。

2. 指令与事件绑定 (@click)

通过 @clickv-on:click 的缩写)直接绑定方法。在 Vue 3 中,由于不需要使用 this,逻辑层的代码更加纯粹,同时也对 TypeScript 更加友好。

3. 声明式 UI 的边界控制

在模板中,利用 :disabled 指令(v-bind:disabled)实现了基于状态的逻辑控制。这种声明式的方式避免了直接操作 DOM 来修改按钮状态,体现了数据驱动视图的核心思想。

8. 如何判断用户在屏幕上是快速滑动还是慢速滑动?

题目要点

  • 核心公式:通过 计算速率。
  • 维度记录:需在 touchstarttouchend 记录坐标与时间戳。
  • 阈值设定:定义合理的像素/毫秒比例作为快慢分界线。
  • 体验优化:引入采样队列处理变速滑动,并结合时间窗口过滤无效操作。
参考答案

在移动端开发中,区分滑动速度(Velocity)是实现诸如“惯性滚动”、“侧滑抽屉”或“卡片快速切换”等交互的基础。判断滑动快慢的核心逻辑在于:计算单位时间内位移的变化量。

1. 原生逻辑计算:位移 / 时间

这是最底层的实现方案,不依赖任何库。我们需要记录滑动开始(touchstart)和滑动结束(touchend)时的两个关键维度:坐标和时间戳。

  • 计算步骤
  1. touchstart 触发时,记录起始点坐标和起始时间 。
  2. touchend 触发时,记录终点坐标和结束时间 。
  3. 计算直线位移。
  4. 计算时间差。
  5. 得出平均速度。
  • 阈值判断: 通常我们会设定一个经验阈值(如 0.5px/ms)。如果 ,判定为快速滑动(Flick/Swipe);否则判定为慢速滑动。

2. 高阶频率采样(实时判断)

如果交互要求更细腻(如在滑动过程中实时改变 UI 状态),仅靠首尾两点是不够的,因为用户可能先慢后快。此时需要建立一个滑动点缓冲区

  • 逻辑: 在 touchmove 事件中,维护一个包含最后 2-3 个采样点的队列(包含坐标和时间)。每次触发事件时,计算当前点与前一个采样点的瞬时速度。这种方式可以过滤掉用户长按后的突发滑动,识别精度更高。

3. 工程化方案:利用现有库

在实际项目中,为了处理多端兼容性、手势竞争以及复杂的动效,通常会直接使用成熟的方案:

  • 小程序环境:微信小程序在某些手势事件中会直接返回 velocity 字段。
  • Web 环境:使用 Hammer.jsBetterScroll。这些库内部通过高性能的算法封装了速度判断,开发者直接监听 swipe(快扫)或 pan(慢移)事件即可。

4. 关键交互细节:时间窗口

在工程实践中,判断快滑通常有一个时间窗口限制(比如必须在 300ms 内完成)。如果用户滑行了很长距离但耗时 2 秒,即便总位移很大,在用户体感上这依然属于受控的慢速拖拽,而非快速扫动。

9. 实现一个 animate 方法,能够驱动元素从一个位置移动到另一个位置,要求支持动画的开始、暂停和取消功能。

题库原题:实现一个 animate 方法,能够驱动元素从一个位置移动到另一个位置,要求支持动画的开始、暂停和取消功能。

题目要点

  • 技术选型:使用 requestAnimationFrame 保证动画与屏幕刷新率同步。
  • 状态转换:通过记录 pausedTime 和动态重置 startTime 实现暂停与恢复的平滑衔接。
  • 渲染优化:使用 transform 代替定位属性以减少重排开销。
  • 健壮性:在 movecancel 时及时清理 requestId,防止内存泄漏和动画重叠。
参考答案

实现这类高性能动画的核心在于 requestAnimationFrame (rAF)。相比 setTimeout,rAF 能确保回调函数在浏览器每一次重绘之前执行,从而提供 60fps 左右的平滑体验。

以下是一个采用 ES6 类封装的 Animate 方案,它通过计算时间偏移量(Offset)来支持精准的暂停与恢复。

1. 代码实现

class Animator {
    constructor(element) {
        this.el = element;
        this.requestId = null;
        this.startTime = null;
        this.pausedTime = 0; // 记录已消耗的动画时间
        this.duration = 1000;
        this.targetPos = { x: 0, y: 0 };
        this.startPos = { x: 0, y: 0 };
        this.isPaused = false;
    }

    move(x, y, duration = 1000) {
        this.cancel(); // 启动新动画前重置
        this.targetPos = { x, y };
        this.duration = duration;

        // 获取当前位置作为起始点
        const rect = this.el.getBoundingClientRect();
        this.startPos = { x: rect.left, y: rect.top };

        this.startTime = performance.now();
        this.isPaused = false;
        this.pausedTime = 0;

        this._run();
    }

    _run() {
        const step = (currentTime) => {
            if (this.isPaused) return;

            // 核心公式:进度 = (当前时间 - 开始时间 + 已跳过的时间) / 总时长
            const elapsed = currentTime - this.startTime + this.pausedTime;
            const progress = Math.min(elapsed / this.duration, 1);

            // 线性插值计算当前坐标
            const curX = this.startPos.x + (this.targetPos.x - this.startPos.x) * progress;
            const curY = this.startPos.y + (this.targetPos.y - this.startPos.y) * progress;

            this.el.style.transform = `translate(${curX}px, ${curY}px)`;

            if (progress < 1) {
                this.requestId = requestAnimationFrame(step);
            }
        };
        this.requestId = requestAnimationFrame(step);
    }

    pause() {
        if (this.isPaused || !this.requestId) return;
        this.isPaused = true;
        // 记录暂停时的进度时间
        this.pausedTime += performance.now() - this.startTime;
        cancelAnimationFrame(this.requestId);
    }

    resume() {
        if (!this.isPaused) return;
        this.isPaused = false;
        this.startTime = performance.now(); // 重设参考时间
        this._run();
    }

    cancel() {
        if (this.requestId) {
            cancelAnimationFrame(this.requestId);
            this.requestId = null;
        }
        this.isPaused = false;
        this.pausedTime = 0;
    }
}

2. 核心逻辑解析

  • 状态保存与偏移量计算: 暂停功能的难点在于重新启动时如何衔接。本方案引入了 pausedTime。当暂停时,计算出已经跑了多久;当恢复(Resume)时,重新获取当前时间戳作为 startTime,并叠加 pausedTime。这样能确保动画进度百分比的计算是连续的。
  • 性能优化 (GPU 加速): 代码中通过修改 style.transform 而非 left/top 来移动元素。这可以避开浏览器的重排(Layout)阶段,直接进入复合(Composite)阶段,利用 GPU 渲染提高流畅度。
  • 高精度时间戳: 使用了 performance.now() 而非 Date.now()。前者提供微秒级精度,且不受系统时间漂移影响,是处理动画逻辑的标准做法。

3. 扩展建议

在工程实践中,线性移动(Linear)通常显得僵硬。可以通过引入 Easing Functions(缓动函数,如 Ease-in-out)来修改 progress 的计算方式:

从而使动画更符合物理直觉。

10. requestAnimationFrame如何取消?请给出代码示例,并说明其在动画实现中的优势。

题目要点

动画优化技术。

参考答案

使用cancelAnimationFrame取消:

const id = requestAnimationFrame(callback);
cancelAnimationFrame(id);

优势:自动与屏幕刷新率同步(通常60Hz),减少不必要的重绘,提升性能。

11. 实现一个简单的模板渲染函数,输入一个模板字符串和数据对象,输出渲染后的HTML字符串。

题目要点

模板引擎实现。

参考答案
function renderTemplate(template, data) {
  return template.replace(/{{(\w+)}}/g, (match, key) => {
    return data[key] || '';
  });
}

const template = "<div>{{name}} is {{age}} years old</div>";
const data = { name: "John", age: 30 };
console.log(renderTemplate(template, data)); // 输出:"<div>John is 30 years old</div>"

12. 使用 css 实现一个表格

题目要点

前端布局技巧。

参考答案

使用CSS实现冻结列:

<table>
  <thead>
    <tr>
      <th style="position: sticky; left: 0; background: white;">固定列</th>
      <th>普通列1</th>
      <th>普通列2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="position: sticky; left: 0; background: white;">固定内容</td>
      <td>内容1</td>
      <td>内容2</td>
    </tr>
  </tbody>
</table>

原理:通过position: stickyleft: 0使列固定在左侧,同时设置背景色防止滚动时内容重叠。


第 1 轮 · 返回本次面经 · 已是最后一轮 →