本轮要点: 本次面试主要考察小程序、前端基础及算法等方面的知识。
本轮共 17 道题。答案默认折叠,便于先自行作答。
1. 讲一下你的小程序项目
题目要点
- 这是一个典型的主观型问题,没有唯一的标准答案。面试官主要考察候选人的项目经验、技术选型能力、问题解决能力以及对小程序生态的整体理解。
- 答题时建议围绕项目背景、个人职责、技术挑战与解决方案、项目成果与思考等方面展开,体现出系统性和条理性。
参考答案
在我的某某项目中,我主要负责开发一个基于微信小程序的某某功能模块,比如一个校园服务小程序,其中我负责了课程表、成绩查询以及校园活动发布的功能。
项目启动之初,我们对需求进行了详细的分析。我选择了小程序作为技术栈,主要是考虑到其轻量级、无需下载安装的特性,能够快速触达用户,并且微信生态提供了丰富的API支持,例如微信支付、地理位置等,非常适合我们校园服务的场景。在技术选型上,我们使用了原生小程序开发模式,结合了小程序自定义组件的能力,将课程表、活动列表等通用UI和逻辑封装成可复用组件,大大提高了开发效率和代码的可维护性。
在开发过程中,我遇到了一些挑战。例如,课程表数据的展示,由于涉及到大量的时间和格子计算,以及复杂的状态管理,我采用了组件化的方式来解耦视图和逻辑。为了避免频繁调用setData导致性能问题,我仔细规划了数据结构,并通过局部更新和事件代理的方式,将数据更新的粒度控制到最小,确保了界面的流畅性。此外,在处理校园活动图片上传和展示时,也遇到了图片内存飙升的问题,我通过启用lazy-load属性,并对图片进行后端压缩和CDN分发,有效解决了内存占用过高的问题,提升了用户体验。在保证安全性的前提下,对于用户敏感信息,我们严格遵循小程序的加密存储和传输规范。
项目上线后,用户反馈良好,尤其是加载速度快、操作流畅得到了肯定。通过这个项目,我不仅深化了对小程序双线程架构、生命周期、组件化等核心概念的理解,也锻炼了我在实际项目中解决复杂性能问题的能力。同时,也让我认识到在项目开发中,前期的技术选型、架构设计以及对潜在性能瓶颈的预判都是至关重要的。
2. 小程序的架构组成
题目要点
- 宿主环境: 了解小程序运行的载体,如微信、支付宝等App提供的运行环境。
- 双线程模型: 掌握小程序的逻辑层和视图层分离,通过异步通信机制进行数据交换。
- 组件系统: 熟悉小程序内置组件和自定义组件的构成。
- 数据层: 理解小程序数据管理和更新机制。
- 小程序生命周期: 熟悉小程序应用和页面的生命周期函数。
参考答案
1.1 原理说明
- 宿主环境: 小程序运行在一个特定的宿主环境中,这个环境由微信、支付宝等App提供。它为小程序提供了运行所需的API、渲染能力、文件系统等。
- 双线程模型: 小程序采用双线程模型,即逻辑层和视图层分别运行在不同的线程中。
- 逻辑层: 运行在JSCore(或V8引擎)中,负责处理业务逻辑、数据请求、数据处理等,使用JavaScript编写。它不直接操作DOM。
- 视图层: 负责渲染页面,使用WebView(或独立的渲染引擎)渲染WXML和WXSS。
- 通信机制: 逻辑层和视图层之间通过Native层进行异步通信。视图层将用户操作(如点击、输入)传递给逻辑层,逻辑层处理后将数据变化通知视图层进行更新。
- 组件系统: 小程序提供了一套丰富的基础组件,如
view、text、image等,开发者也可以基于这些基础组件创建自定义组件,实现模块化开发。 - 数据层: 小程序的数据管理主要通过
Page.prototype.data和this.setData进行。数据变化后,通过双线程通信机制同步到视图层,触发视图更新。 1.2 核心用法 + 示例代码 - 在小程序中,开发者主要通过
JavaScript编写逻辑层代码,WXML构建页面结构,WXSS定义页面样式。 app.js定义小程序的全局逻辑,app.json进行全局配置,app.wxss定义全局样式。- 每个页面由
.js(逻辑)、.json(配置)、.wxml(结构)、.wxss(样式)组成。 - 示例:页面数据更新
// page.js
Page({
data: {
message: 'Hello Mini Program!'
},
onLoad: function() {
setTimeout(() => {
this.setData({
message: 'Data Updated!'
});
}, 2000);
}
});
- 这种架构分离了业务逻辑和页面渲染,提高了性能和安全性,也方便平台对小程序进行管控。 1.3 常见误区或面试陷阱
- 误区: 认为小程序是基于浏览器内核直接运行的H5页面。实际上,小程序运行在独立的宿主环境,有其特定的渲染机制和API。
- 误区: 将双线程通信理解为同步通信。实际上是异步通信,需要注意数据同步的时序问题。
- 陷阱: 不清楚小程序无法直接操作DOM的原因,本质上是为了安全和性能考量,避免开发者直接对渲染层进行复杂操作,影响性能和用户体验。
3. 小程序和 H5 在开发和使用上有哪些主要的区别和优势?
题目要点
- 运行环境: 区分小程序和H5的运行载体和能力限制。
- 开发模式: 掌握两种技术栈的开发流程和工具链。
- 性能: 对比加载速度、运行流畅度等方面的差异。
- 功能和权限: 了解各自能调用的系统能力和接口。
- 用户体验: 分析用户感知到的流畅性、便捷性。
- 分发和推广: 比较获取用户、传播的难易程度。
参考答案
1.1 原理说明
- 小程序: 运行在微信、支付宝等App提供的特定宿主环境中,通过其内置的渲染引擎和JSBridge与Native层通信,提供接近原生应用的体验。
- H5 (Web页面): 运行在浏览器中,通过浏览器内核渲染页面,使用Web标准技术(HTML、CSS、JavaScript)开发。 1.2 核心用法 + 示例代码
- 主要区别:
- 运行环境: 小程序运行在宿主App中,H5运行在浏览器中。
- 开发语言与规范: 小程序使用WXML、WXSS、JS,有自己的一套规范和组件;H5使用HTML、CSS、JS,遵循Web标准。
- 系统权限: 小程序能调用更多的系统原生能力,如微信支付、扫一扫、地理位置、蓝牙等;H5受限于浏览器沙箱模型,权限较少。
- 性能体验: 小程序有预加载、离线缓存机制,渲染性能和流畅度通常优于H5,接近原生应用;H5每次加载都需要重新请求资源,受网络影响大。
- 发布与审核: 小程序需要经过平台审核才能发布;H5可以直接上线部署。
- 包体大小限制: 小程序有严格的包体大小限制(如微信小程序主包2M,总包20M),H5则没有。
- 入口与分发: 小程序可以通过扫码、分享、搜索等多种方式快速进入;H5主要通过URL链接访问。
- DOM操作: 小程序无法直接操作DOM,而H5可以。
- 优势:
- 小程序优势:
- 更接近原生体验: 加载速度快,运行流畅,动画效果好。
- 更强的系统能力: 能调用更多Native API,实现H5难以实现的功能。
- 更便捷的获取用户: 依托于宿主App的巨大用户基础,易于分享和推广。
- 更安全的沙箱环境: 平台统一管理,安全性更高。
- H5优势:
- 开发成本低: 兼容性好,开发调试方便,一次开发多端运行。
- 发布灵活: 无需审核,修改后可即时上线。
- 技术栈通用: 前端开发者学习成本低,资源丰富。
- 无包体限制: 可以承载更复杂、更大的应用。 1.3 常见误区或面试陷阱
- 小程序优势:
- 误区: 认为小程序就是特殊的H5。虽然两者都基于Web技术,但运行环境和能力差异巨大。
- 陷阱: 只强调小程序性能优势,忽略其开发规范、包体限制等劣势。在实际选择时需要综合考虑。
- 陷阱: 将小程序无法直接操作DOM误认为是其缺点,而不是一种设计选择(为了安全和性能)。
4. 如果一个功能既可以使用小程序实现,也可以使用 H5 实现,你会如何选择?
题目要点
- 这是一个主观判断与权衡的问题,没有绝对的对错。面试官希望考察候选人对小程序和H5各自特点的深入理解、业务分析能力、技术选型时的综合考量以及权衡利弊的能力。
- 答题时建议从用户体验、开发成本、功能需求、推广分发、性能和维护等多个维度进行对比和权衡,给出清晰的决策逻辑。
参考答案
在面对一个功能既能用小程序也能用H5实现时,我通常会从以下几个关键维度进行综合考量和选择:
首先,用户体验是首要考虑的因素。如果功能对加载速度、动画流畅度、系统权限调用(如微信支付、扫一扫、蓝牙、地理位置等)有较高要求,且需要提供接近原生应用的体验,那么小程序会是更优的选择。小程序的预加载、离线缓存机制以及更接近原生渲染的能力,通常能带来更流畅的用户体验。例如,如果是一个需要频繁使用相机或地图的服务,小程序的原生能力接入会更加便捷和高效。
其次,我会评估开发成本与周期。H5的开发技术栈更通用,前端开发人员学习成本较低,可以实现一次开发多端运行,发布流程也更灵活,无需平台审核,修改后可即时上线。如果功能需求相对简单,对性能和原生能力要求不高,且追求快速迭代和低成本,H5会更有优势。例如,一个简单的营销活动页或者信息展示页面,H5就能很好地满足需求。
再者,推广与分发能力也是重要考量。小程序依托于微信、支付宝等超级App的巨大用户基础,可以通过扫码、分享、搜索等多种方式快速触达用户,获客成本相对较低,传播效率高。而H5主要通过URL链接传播,在某些场景下推广效率可能不如小程序。如果功能需要借助社交裂变或快速分享来获取用户,小程序会更具优势。
最后,还会考虑功能和权限限制以及维护性。小程序有严格的包体大小限制和平台审核机制,同时不能直接操作DOM,这在某些复杂的前端场景下可能会带来限制。而H5则没有这些限制,可以承载更复杂、更大的应用。但另一方面,小程序的统一规范和组件化开发模式,在一定程度上也降低了长期维护的复杂度。
综上所述,如果功能偏向于工具性、服务性,需要深度整合系统能力、追求极致用户体验和便捷的分享传播,我会倾向于选择小程序。如果功能更偏向于内容展示、营销活动,对性能要求不高,且更看重开发灵活性、低成本和快速迭代,那么H5会是更合适的选择。最终的决策是基于业务需求、目标用户、资源投入和长期发展策略的综合权衡。
5. 小程序架构原理讲一下
题目要点
- 宿主环境: 了解小程序运行的载体,如微信、支付宝等App提供的运行环境。
- 双线程模型: 掌握小程序的逻辑层和视图层分离,通过异步通信机制进行数据交换。
- 组件系统: 熟悉小程序内置组件和自定义组件的构成。
- 数据层: 理解小程序数据管理和更新机制。
- 小程序生命周期: 熟悉小程序应用和页面的生命周期函数。
参考答案
1.1 原理说明
- 宿主环境与运行机制: 小程序并非运行在浏览器中,而是运行在微信、支付宝等宿主App的内置环境中。这个环境提供了一套沙箱机制,隔离了小程序与原生系统,确保安全和稳定。宿主App会内置一个JS引擎(如微信小程序的JSCore)来运行小程序的逻辑层,以及一个渲染层(如WebView)来渲染视图。
- 双线程架构: 这是小程序的核心架构特点。
- 逻辑层(App Service): 运行在独立的JS线程中,负责处理业务逻辑、数据请求、数据处理。开发者编写的JavaScript代码在这里运行。逻辑层不具备渲染能力,不能直接操作DOM。
- 视图层(View): 运行在独立的渲染线程中,负责UI的渲染。它根据WXML描述文件和WXSS样式文件来绘制页面。视图层可以是一个WebView,也可以是更轻量的原生渲染组件。
- 数据通信与渲染: 逻辑层和视图层之间无法直接通信,它们通过一套统一的JSBridge进行消息传递。
- 数据下发: 当逻辑层数据发生变化(如
setData),会将数据通过JSBridge序列化后发送给视图层。视图层接收到数据后,进行Diff算法比较,只更新需要变化的部分,然后渲染到屏幕。 - 事件上报: 视图层的用户交互事件(如点击、滑动)会通过JSBridge传递给逻辑层,由逻辑层进行处理。
- 数据下发: 当逻辑层数据发生变化(如
- 组件化: 小程序提供了丰富的内置组件(
<view>,<text>,<image>等),这些组件是原生组件的封装,性能更好。开发者也可以基于这些基础组件进行自定义组件开发,实现代码复用和模块化。 - 生命周期管理: 小程序提供了应用生命周期(
onLaunch,onShow,onHide)和页面生命周期(onLoad,onShow,onReady,onHide,onUnload),开发者可以利用这些生命周期钩子进行业务逻辑的控制。 - 虚拟DOM (非严格意义): 虽然小程序没有浏览器的完整DOM树,但在数据更新时,宿主环境会进行类似Virtual DOM的Diff算法,对比新旧数据,最小化地更新视图,以提高渲染效率。 1.2 核心用法 + 示例代码
- 数据更新流程示例: 当调用
this.setData时,逻辑层会收集待更新的数据,将其序列化并通过JSBridge发送到视图层。视图层收到数据后,会与当前数据进行比较,找出差异,然后只对有差异的视图节点进行更新。 // 逻辑层 (page.js) Page({ data: { count: 0 }, addCount: function() { this.setData({ count: this.data.count + 1 }); // 此时数据已更新到视图层,但这个过程是异步的 } });<!-- 视图层 (page.wxml) --> <view bindtap="addCount">当前计数:{{count}}</view>- 这种双线程架构和通信机制保证了小程序的运行效率和安全性,避免了前端H5页面常见的性能瓶颈和安全风险。 1.3 常见误区或面试陷阱
- 误区: 将小程序与Vue/React等框架的虚拟DOM混淆。虽然都有Diff更新的概念,但小程序是平台层面的原生渲染,而前端框架是基于浏览器DOM的模拟。
- 陷阱: 不清楚JSBridge的具体作用和通信方式。JSBridge是逻辑层和视图层之间的桥梁,负责序列化和反序列化数据,实现跨线程通信。
- 陷阱: 忽略了小程序安全性考量。双线程隔离和平台审核机制,都旨在提高小程序应用的安全性。
6. 为何小程序无法直接操作DOM?
题目要点
- 安全性: 理解小程序沙箱机制对DOM操作的限制。
- 性能优化: 掌握双线程模型下,通过限制DOM操作来提升渲染效率的原理。
- 统一体验: 了解平台对开发者能力的约束,以保证用户体验一致性。
- 开发模式: 掌握小程序数据驱动视图的开发模式。
参考答案
1.1 原理说明
- 小程序无法直接操作DOM,是其核心架构设计决定的,主要基于以下几点原因:
- 双线程模型: 小程序的逻辑层(JavaScript运行环境)和视图层(渲染环境,如WebView)是分离的,运行在不同的线程中。JavaScript在逻辑层中执行,而DOM存在于视图层。如果允许逻辑层直接操作DOM,会涉及到跨线程操作,这会带来复杂的同步问题和巨大的性能开销。
- 性能考量: 直接操作DOM是前端性能瓶颈之一。小程序通过数据驱动的方式,将数据变化发送给视图层,视图层再进行高效的局部更新(通常会进行Diff计算),从而避免了频繁的、低效的DOM操作,提升了渲染性能和用户体验。
- 安全与管控: 宿主环境(如微信)对小程序有严格的沙箱限制和安全管理。如果开发者可以随意操作DOM,可能引入XSS攻击等安全漏洞,或者进行一些不符合平台规范的操作。限制DOM操作有助于平台对小程序进行统一的管控和优化。
- 统一的用户体验: 为了保证小程序在不同设备和环境下都能提供一致且流畅的用户体验,平台对UI渲染和交互进行了封装和优化。直接操作DOM会打破这种统一性,增加适配成本,也可能导致用户体验的差异。 1.2 核心用法 + 示例代码
- 在小程序中,视图的更新是通过数据驱动的方式进行的。开发者通过修改
data对象的数据,然后调用this.setData()方法,将最新的数据传递给视图层。视图层接收到数据后,会根据数据的变化来自动更新对应的WXML结构。 - 示例:通过数据改变元素样式或内容
// page.js
Page({
data: {
isActive: false,
message: 'Original Message'
},
toggleActive: function() {
this.setData({
isActive: !this.data.isActive,
message: this.data.isActive ? 'Original Message' : 'New Message'
});
}
});
<!-- page.wxml -->
<view class="{{isActive ? 'active-class' : ''}}" bindtap="toggleActive">{{message}}</view>
- 这种数据驱动的模式,简化了开发者对UI状态的管理,减少了直接操作DOM的复杂性和潜在问题,也使得小程序能够更好地在不同的宿主环境和渲染引擎中运行。 1.3 常见误区或面试陷阱
- 误区: 认为小程序不能操作DOM是因为技术实现不足。实际上,这是平台为了实现高性能、高安全和统一体验而做出的设计权衡。
- 陷阱: 不清楚
setData的异步性。setData是一个异步操作,修改data后,视图的更新并不会立即完成。 - 陷阱: 将小程序与React/Vue等框架的"虚拟DOM"混为一谈。虽然都有数据驱动和Diff更新,但底层机制和运行环境不同。小程序是在Native层进行视图更新,而Web框架是在浏览器DOM上进行模拟更新。
7. 如何通过WXSS适配多端屏幕?
题目要点
- 单位适配: 掌握WXSS中特有的rpx单位及其与px的转换关系。
- 响应式设计: 了解如何利用WXSS特性实现不同屏幕尺寸下的布局和样式调整。
- 媒体查询: 熟悉WXSS中媒体查询的使用方法。
- flex/grid布局: 掌握弹性盒和网格布局在多端适配中的应用。
参考答案
1.1 原理说明
- WXSS(WeiXin Style Sheets)是小程序特有的样式语言,它在CSS的基础上进行了一些扩展和限制,以更好地支持小程序的开发和多端适配。
- rpx(responsive pixel): WXSS提供了一种新的长度单位
rpx,它是微信小程序独有的尺寸单位,可以根据屏幕宽度进行自适应。小程序规定所有设备的屏幕宽度都为750rpx。这意味着,无论设备的物理像素是多少,1rpx都等于设备屏幕宽度的1/750。这样,开发者在设计界面时,只需按照750rpx的基准进行设计,即可在不同屏幕宽度的设备上实现等比例缩放,从而达到适配效果。 - 媒体查询: WXSS支持CSS的媒体查询(Media Queries),允许开发者根据设备的特性(如屏幕宽度、高度、DPR等)应用不同的样式。这使得在特定条件下调整布局和样式成为可能。
- Flex布局和Grid布局: WXSS支持CSS3中的Flexbox(弹性盒布局)和Grid(网格布局),这两种布局方式提供了强大的自适应能力,可以更灵活地控制页面元素的排列和分布,从而更好地适应不同屏幕尺寸和方向。 1.2 核心用法 + 示例代码
- 使用rpx进行尺寸适配:
- 设计稿如果以iPhone 6/7/8为基准(物理宽度375px,DPR=2,屏幕逻辑像素宽度为750rpx),那么在设计稿中量取的1px,在小程序中可以直接写成2rpx。例如,如果设计稿中有一个元素宽度是100px,那么在WXSS中就可以写成
width: 200rpx;。
/* styles.wxss */ .container { width: 750rpx; /* 占据屏幕全宽 */ height: 300rpx; font-size: 32rpx; /* 字体大小随屏幕宽度等比缩放 */ } .button { width: 200rpx; height: 80rpx; margin: 20rpx; } - 设计稿如果以iPhone 6/7/8为基准(物理宽度375px,DPR=2,屏幕逻辑像素宽度为750rpx),那么在设计稿中量取的1px,在小程序中可以直接写成2rpx。例如,如果设计稿中有一个元素宽度是100px,那么在WXSS中就可以写成
- 使用媒体查询进行响应式布局:
- 当需要针对不同屏幕尺寸或方向应用不同的样式时,可以使用
@media。
/* styles.wxss */ @media (min-width: 400px) { /* 当屏幕宽度大于400px时应用的样式 */ .container { background-color: lightblue; } } @media (orientation: landscape) { /* 横屏模式下应用的样式 */ .text { color: green; } } - 当需要针对不同屏幕尺寸或方向应用不同的样式时,可以使用
- 使用Flexbox进行流式布局:
- Flexbox可以方便地实现元素的水平或垂直排列、对齐、空间分配等,对于自适应布局非常有效。
/* styles.wxss */ .flex-container { display: flex; flex-wrap: wrap; /* 允许元素换行 */ justify-content: space-around; /* 元素之间和两端平均分布空间 */ } .flex-item { flex: 1 1 30%; /* 每个元素占据容器宽度的30%,可伸缩 */ min-width: 150rpx; /* 最小宽度限制 */ height: 100rpx; background-color: #eee; margin: 10rpx; } - 结合使用
rpx、媒体查询和Flex/Grid布局,可以实现小程序在不同设备上的良好适配效果。 1.3 常见误区或面试陷阱 - 误区: 认为rpx就是px的简单替换。rpx是基于屏幕宽度等比例缩放的,而px是固定像素单位。
- 陷阱: 滥用px而非rpx,导致在不同设备上出现布局混乱或缩放异常。
- 陷阱: 不了解rpx的基准宽度是750rpx,导致计算尺寸时出现偏差。
- 误区: 忽略了在特定场景下,如图片、边框等,可能还需要结合实际情况考虑使用px或vw/vh(如果支持)以达到最佳效果。
8. 为何WXSS禁用部分CSS选择器?
题目要点
- 安全性: 理解禁用选择器是为了防止恶意代码注入或样式污染。
- 性能优化: 掌握限制选择器复杂度,以提高样式解析和渲染效率。
- 组件化封装: 了解如何通过限制选择器来更好地支持组件的独立性和复用性。
- 开发规范: 熟悉小程序对样式的限制,以规范开发行为。
参考答案
1.1 原理说明
- WXSS为了小程序的运行效率、安全性和更好的组件化支持,对CSS选择器进行了一定的限制,禁用了一些复杂的或可能带来风险的选择器。
- 提高性能: 复杂的CSS选择器(如后代选择器、属性选择器、伪类选择器等组合)在解析和匹配时会消耗更多的性能资源,尤其是在视图层进行大量的DOM遍历时。禁用这些选择器可以简化样式计算,提高渲染效率。
- 增强安全性: 某些CSS选择器可能会与JavaScript结合,实现一些恶意注入或样式劫持。例如,通过复杂的选择器结合动态内容,可能绕过安全审查,对页面进行不当篡改。禁用这些选择器有助于构建更安全的沙箱环境。
- 支持组件化和样式隔离: 小程序强调组件化开发,每个组件都应该尽可能地独立和可复用。如果允许使用过于宽泛或具有穿透性的选择器(如全局选择器
*、子选择器>等),可能会导致组件内部样式被外部样式意外污染,或者组件的样式影响到外部。禁用这些选择器有助于实现组件的样式隔离,保证组件的独立性。 - 简化开发模型: 通过限制选择器,可以引导开发者采用更简洁、更明确的样式编写方式,降低学习成本,减少潜在的兼容性问题。 1.2 核心用法 + 示例代码
- WXSS支持大部分常用的CSS选择器,如:
- 类选择器:
.my-class - ID选择器:
#my-id(不推荐多用,因为ID通常是唯一的) - 元素选择器:
view,text - 组合选择器:
.parent .child(后代选择器) - 伪类:
:hover,:active,:first-child等(部分支持,但与浏览器CSS有所不同,例如小程序没有:before,:after伪元素)
- 类选择器:
- 禁用或受限的选择器示例:
- 全局选择器
*: WXSS不支持,防止全局污染。 - 属性选择器
[attr]: 一般不支持,或支持有限。 - 父子选择器
>: 支持,但使用时要注意作用域。 - 兄弟选择器
+、~: 通常不支持。 - 部分伪类和伪元素: 例如,
:before,:after等伪元素是不支持的,因为小程序没有完全的DOM概念。
- 全局选择器
- 实际开发中的替代方案:
- 针对通用样式,可以定义在
app.wxss中,或通过引入公共样式文件。 - 对于组件内部样式,通常通过类名进行控制,并结合组件的样式隔离特性。
- 需要复杂逻辑的样式,可以通过数据绑定动态添加或移除类名。 1.3 常见误区或面试陷阱
- 针对通用样式,可以定义在
- 误区: 认为禁用选择器是小程序"功能不足"的表现。实际上,这是为了性能、安全和组件化而做的策略性限制。
- 陷阱: 尝试使用被禁用的CSS选择器,导致样式不生效或报错。
- 陷阱: 不了解小程序组件的样式隔离特性(默认隔离),导致样式互相影响。
9. WXML与HTML的核心差异
题目要点
- 运行环境: 理解两者运行环境的不同,导致其功能和限制的差异。
- 组件系统: 掌握WXML是基于组件的声明式语言,而HTML是基于标签的标记语言。
- 数据绑定: 了解WXML的数据驱动视图机制,与HTML操作DOM的区别。
- 能力限制: 区分小程序和浏览器在API和权限上的差异。
参考答案
1.1 原理说明
- WXML (WeiXin Markup Language) 是小程序框架设计的一套标签语言,用于描述页面结构,它与HTML有诸多相似之处,但本质上存在核心差异,这些差异源于它们所处的运行环境和设计目标的不同。
- 运行环境: HTML运行在Web浏览器中,基于浏览器提供的完整DOM模型,开发者可以自由操作DOM;而WXML运行在小程序特定的宿主环境(如微信App),其渲染层并非完整的浏览器内核,而是更轻量级、更受控的渲染引擎,且逻辑层与视图层分离,无法直接操作DOM。
- 组件化: WXML更强调组件化。它提供了一套内置组件,这些组件是经过优化封装的原生组件,性能更优。开发者也可以创建自定义组件,实现模块化开发。HTML的标签是浏览器原生支持的,虽然有Web Components等组件化规范,但并非强制或默认。
- 数据绑定: WXML采用的是数据驱动的模式。视图的渲染和更新主要通过绑定逻辑层的数据来实现,通过
setData更新数据,视图层会自动进行局部更新。HTML则通常需要通过JavaScript手动操作DOM来更新视图。 - 能力限制与扩展: WXML作为小程序的一部分,可以调用小程序框架提供的特定API和组件,这些API通常能访问更底层的系统能力(如微信支付、扫一扫等),这是传统HTML和浏览器API难以实现的。同时,WXML也对一些复杂的HTML特性和DOM操作进行了限制,以保证性能和安全。 1.2 核心用法 + 示例代码
- 标签差异: WXML的标签是小程序内置组件,例如
<view>替代<div>、<text>替代<span>、<image>替代<img>等。这些组件的功能更聚焦,并能调用小程序特有的能力。- HTML:
<div class="container"> <p>Hello HTML</p> <img src="example.jpg"> </div>- WXML:
<view class="container"> <text>Hello WXML</text> <image src="example.png"></image> </view> - 数据绑定: WXML支持使用
{{}}进行数据绑定,直接将逻辑层的数据渲染到视图层。同时支持条件渲染(wx:if)、列表渲染(wx:for)等指令。- WXML数据绑定示例:
<view>{{message}}</view> <view wx:if="{{isShow}}">Conditional Render</view> <view wx:for="{{items}}" wx:key="id">{{item.name}}</view>- 对应的JS逻辑:
// page.js Page({ data: { message: 'Hello Mini Program', isShow: true, items: [ {id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'} ] } }); - 事件绑定: WXML使用
bind或catch前缀来绑定事件,例如bindtap替代onclick。- WXML事件绑定示例:
<button bindtap="handleClick">Click Me</button>- 对应的JS逻辑:
// page.js Page({ handleClick: function() { console.log('Button clicked!'); } }); - WXML的设计目标是让开发者以更声明式、更符合小程序生态的方式来构建界面,同时保证性能和安全性。 1.3 常见误区或面试陷阱
- 误区: 将WXML简单等同于HTML。WXML不是HTML的超集,它有自己的语法和限制,不能直接运行在浏览器中。
- 陷阱: 尝试在WXML中进行DOM操作或使用HTML特有的全局属性(如
id、class之外的自定义属性),导致报错或不生效。 - 陷阱: 不清楚WXML的组件化思维,仍然停留在HTML的标签思维,没有充分利用小程序组件的特性。
10. 频繁调用setData为何导致卡顿?
题目要点
- 双线程通信开销: 了解
setData触发的逻辑层与视图层之间的数据传输和处理过程。 - Diff算法效率: 掌握
setData内部的数据比较和局部更新机制。 - 渲染层重绘/重排: 理解数据更新如何导致视图层的渲染操作。
- 数据量大小: 认识到传输数据量对性能的影响。
- JSBridge通信瓶颈: 了解数据序列化和反序列化过程的开销。
参考答案
1.1 原理说明
- 频繁调用
setData是小程序性能卡顿的常见原因,这主要与小程序的双线程架构和数据通信机制紧密相关。- 双线程通信开销: 小程序的逻辑层(JavaScript线程)和视图层(渲染线程,可能是WebView或原生渲染)是分离的。当逻辑层通过
setData更新数据时,这些数据需要经过序列化,然后通过JSBridge跨线程传输到视图层。视图层接收到数据后,需要进行反序列化,并与旧数据进行Diff比较,找出变化的部分,最后再通知渲染引擎进行更新。这个过程涉及数据的序列化/反序列化、跨线程通信以及Diff计算,每一步都有一定的性能开销。如果频繁调用setData,就会频繁触发这些操作,导致性能损耗累积。 - 渲染层重绘/重排: 视图层在接收到数据变化后,会根据Diff结果进行视图更新。如果数据变化涉及大量的DOM节点(或小程序组件树节点),即使是局部更新,也可能导致视图层的大范围重绘(Repaint)甚至重排(Reflow),这些操作都是非常耗费性能的。
- 数据量过大: 每次
setData传输的数据量如果过大,会增加序列化、反序列化和传输的时间,进一步加剧卡顿。即使数据量不大,但更新频率极高,也会导致性能问题。 - 不必要的Diff计算: 即使数据没有实际变化,只要调用了
setData,小程序内部依然会触发数据比较和潜在的渲染更新流程。无效的setData调用会白白消耗性能。 1.2 核心用法 + 示例代码
- 双线程通信开销: 小程序的逻辑层(JavaScript线程)和视图层(渲染线程,可能是WebView或原生渲染)是分离的。当逻辑层通过
- 优化策略:
- 合并
setData调用: 将多次对不同数据或同一数据不同部分的setData调用合并为一次。例如,不要在循环中频繁调用setData,而是在循环结束后一次性更新。
// 不推荐:频繁调用setData for (let i = 0; i < list.length; i++) { this.setData({ [`list[${i}].selected`]: true }); } // 推荐:合并setData const newList = this.data.list.map(item => ({ ...item, selected: true })); this.setData({ list: newList });- 减少
setData的数据量: 只更新需要变化的数据,避免传输整个大的data对象。如果只是修改data对象中某个深层属性,只传递该属性的路径和新值。
// 假设data结构为 { user: { name: '张三', age: 18 } } // 只更新user.age this.setData({ 'user.age': 19 });- 节流防抖: 对于用户频繁触发的事件(如滚动、输入),使用节流(throttle)或防抖(debounce)来减少
setData的调用频率。 - 使用数据绑定优化: 避免在
wxml中进行复杂计算,将计算结果提前在js中处理好并绑定到data。 - 利用自定义组件的数据监听器: 对于组件内部的数据更新,可以考虑使用自定义组件的
observers来监听数据变化,只在必要时进行组件内部的渲染更新。 1.3 常见误区或面试陷阱
- 合并
- 误区: 认为
setData只更新视图,不了解其背后的跨线程通信和Diff算法开销。 - 陷阱: 在循环或高频事件回调中无脑调用
setData,导致页面卡顿。 - 陷阱: 不清楚
setData更新深层数据的语法,误以为只能更新顶层属性,导致传输了大量不需要更新的数据。 - 误区: 忽略了
setData是异步操作,在紧随其后的代码中立即访问data可能获取不到最新值。
11. 如何设计分包降低主包体积?
题目要点
- 分包概念: 了解小程序分包的原理和目的。
- 分包策略: 掌握常见的按功能、按页面、按业务模块等分包方法。
- 分包加载机制: 理解分包的下载、加载和使用流程。
- 主包与分包关系: 区分主包和分包的职责和内容限制。
- 分包优化: 掌握分包在实际项目中的应用和优化技巧。
参考答案
1.1 原理说明
- 小程序为了优化用户体验,限制了主包的体积(微信小程序主包最大2MB,总包最大20MB)。当小程序体积过大时,用户首次启动加载时间会变长,影响体验。**分包(Subpackages)**机制是解决这一问题的重要方案。
- 分包概念: 分包允许开发者将小程序的部分页面和资源独立打包。用户在首次启动小程序时,只下载主包,当用户进入分包页面时,才异步下载对应分包的代码和资源。这样可以显著降低首次启动的加载时间,提高用户体验。
- 主包与分包的关系:
- 主包(Main Package): 包含小程序的启动页面、tabBar页面、以及所有分包都需要引用的公共资源(如公共组件、公共工具函数等)。主包是小程序启动时必须下载的部分。
- 分包(Subpackage): 包含特定功能模块的页面和资源,它们不包含在主包中。分包之间也可以互相引用。 1.2 核心用法 + 示例代码
- 分包配置: 在
app.json中通过subpackages字段配置分包。// app.json { "pages": [ "pages/index/index", "pages/logs/logs" ], "subpackages": [ { "root": "packageA", "pages": [ "pages/detail/detail", "pages/list/list" ] }, { "root": "packageB", "pages": [ "pages/order/order", "pages/address/address" ] } ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "packageB/pages/order/order", "text": "订单" // tabbar页面可以在分包内 } ] }, "preloadRule": { "pages/index/index": { "network": "wifi", "maxAge": 60, "packages": ["packageA"] } } }root: 分包的根目录。pages: 分包包含的页面路径。- 分包预下载(
preloadRule): 可以配置在主包页面或分包页面中,提前下载其他分包。这可以进一步优化用户体验,在用户还未进入分包页面时就完成下载。
- 分包设计策略:
- 按功能模块分包: 将不同功能的页面和组件放到不同的分包中。例如,商品详情页、购物车页、个人中心页等可以作为独立分包。
- 按业务线分包: 对于大型应用,可以按照不同的业务线进行分包,每个业务线有自己的独立分包。
- 公共资源提取到主包: 将多个分包或主包都需要的公共组件、工具函数、图片等资源提取到主包中,避免重复打包。
- 懒加载: 对于非核心功能或不常用的页面,可以考虑将其放入分包,并避免预加载。
- 引用分包资源: 在主包或不同分包之间引用组件或JS文件时,需要使用绝对路径或相对路径,并注意分包加载顺序。 1.3 常见误区或面试陷阱
- 误区: 认为分包就是简单地将代码拆分成多个文件。分包是框架层面的优化,有明确的配置和加载机制。
- 陷阱: 将不常用的或体积较大的公共资源放在每个分包中,导致重复打包,反而增加了总体积。
- 陷阱: 不理解
preloadRule的作用,没有充分利用预下载来优化用户体验。 - 误区: 将tabBar页面放在分包中,但没有在
app.json的tabBar配置中正确指定路径,导致无法访问或显示异常。
12. 小程序图片加载导致内存飙升,如何解决?
题目要点
- 图片优化基础: 掌握图片格式、尺寸、压缩等基本优化手段。
- 图片懒加载: 理解图片懒加载的原理和实现方式。
- 图片管理: 了解如何管理图片资源,避免内存泄漏。
- 内存分析: 熟悉小程序开发工具的性能分析工具,定位内存问题。
- 组件复用与回收: 掌握列表渲染中,组件复用对内存的影响。
参考答案
1.1 原理说明
- 小程序中图片加载导致内存飙升是常见性能问题,尤其是在图片数量多、尺寸大或存在长列表时。这通常是由于图片资源未优化、加载策略不当、或者内存管理不善引起的。
- 图片未优化: 未经压缩或过大尺寸的图片直接加载,会占用大量内存。特别是对于Retina屏等高DPR设备,一张图片可能需要更大的内存来存储。
- 加载策略不当: 所有图片一次性加载,或者不在可视区域内的图片也提前加载,都会迅速消耗内存。
- 内存管理与回收: 在页面切换、组件销毁时,如果图片资源未被及时释放,可能导致内存泄漏,累积的内存占用最终导致飙升。
- 图片缓存: 虽然缓存可以提高加载速度,但如果缓存策略不当,或者缓存的图片过多过大,也会占用大量内存。 1.2 核心用法 + 示例代码
- 优化策略:
- 图片压缩与格式优化: 在图片上传前,使用图片压缩工具(如TinyPNG、ImageOptim)进行压缩。选择合适的图片格式(如WebP、JPG、PNG),WebP通常在保证质量的前提下提供更好的压缩比。对于纯色或简单图标,优先使用SVG或字体图标。
- 按需加载/懒加载: 对于长列表或页面中非首屏的图片,使用懒加载(Lazy Load)技术,只在图片进入可视区域时才加载。小程序
image组件自带lazy-load属性。
<!-- 仅当图片进入可视区域时才开始加载 --> <image src="path/to/your/image.jpg" lazy-load="true"></image>- 合适尺寸图片: 根据图片在页面中显示的实际大小,请求或使用对应尺寸的图片。避免下载过大图片再进行缩放显示。
- CDN加速与图片服务: 使用CDN(内容分发网络)加速图片加载。如果条件允许,使用专业的图片处理服务,这些服务通常提供图片剪裁、缩放、格式转换、智能压缩等功能。
- 避免在WXML中直接写大量图片路径: 尤其是动态生成的图片列表,避免在WXML中直接渲染,可以通过JS逻辑进行控制。
- 列表组件优化: 对于长列表,使用虚拟列表(Virtual List)或滚动加载(Scroll Loading)技术,只渲染可视区域内的列表项,未显示的列表项不渲染或延迟渲染,从而减少DOM节点和图片加载的压力。
- 内存分析与调试: 使用小程序开发者工具的性能分析面板,观察内存使用情况,定位内存飙升的具体原因和代码位置。
- 清理不必要的图片缓存: 在某些场景下,例如退出页面时,如果图片不再需要,可以考虑手动清理图片缓存(如果小程序提供了相关API)。 1.3 常见误区或面试陷阱
- 误区: 认为只要使用
lazy-load就能解决所有图片内存问题。lazy-load只是延迟加载,如果图片本身尺寸过大或数量过多,仍然会消耗大量内存。 - 陷阱: 忽略图片压缩和格式优化的重要性,直接使用原始大图,导致加载慢、内存高。
- 陷阱: 在列表渲染中不合理地使用
wx:if或wx:for,导致重复渲染或销毁大量组件,加剧内存波动。 - 误区: 认为所有图片都应该使用CDN。对于小图片或本地资源,可能不需要CDN。
- 陷阱: 没有定期检查和优化小程序包体中的图片资源,导致初始包体过大,影响首次加载速度。
13. BFC 的作用和触发条件?
题目要点
- 布局控制: 面试官想确认面试者是否理解BFC在页面布局中的核心作用,特别是如何解决浮动元素带来的布局问题、外边距合并等。
- BFC触发条件: 考察面试者是否熟悉哪些CSS属性或值可以创建BFC,这是实际应用BFC的基础。
- 性能和渲染: 理解BFC作为一个独立的渲染区域,对浏览器渲染性能的影响。
参考答案
1.1 原理说明
- BFC (Block Formatting Context):块级格式化上下文。它是Web页面中一块独立的渲染区域,规定了内部块级盒子的布局方式,并且这块区域的布局不会影响外部,同时外部也不会影响BFC内部。可以理解为一个独立的、隔离的容器,其内部元素与外部元素互不影响。
- 作用: BFC主要有以下几个作用:
- 清除浮动: 当BFC内部包含浮动元素时,BFC会计算其内部所有浮动元素的高度,从而包裹住浮动元素,解决了父元素高度塌陷的问题。
- 防止外边距合并: 在同一个BFC内,垂直方向上的相邻块级元素的上下外边距会发生合并。如果将其中一个元素放入新的BFC,则可以阻止外边距合并。
- 自适应两栏布局: 通过一个元素浮动,另一个元素触发BFC,可以实现两栏布局,且BFC元素会环绕浮动元素。 1.2 核心用法 + 示例代码
- 触发BFC的常见条件:
html根元素float的值不为none(例如float: left;或float: right;)position的值为absolute或fixeddisplay的值为flow-root(推荐,语义化更清晰)display的值为inline-block,table-cell,table-caption,flex,gridoverflow的值不为visible(例如overflow: hidden;,overflow: auto;,overflow: scroll;)
- 示例:清除浮动
<div class="container"> <div class="float-box">Float Box</div> <p>This is some content.</p> </div>.container { /* 触发BFC */ overflow: hidden; /* 或 display: flow-root; */ border: 1px solid blue; } .float-box { float: left; width: 100px; height: 100px; background-color: lightcoral; } p { background-color: lightgreen; }- 解决了什么问题: 当
.container内部的.float-box浮动时,如果不触发BFC,.container的高度会塌陷。通过设置overflow: hidden;(触发BFC),.container会包裹住浮动元素,高度正常显示。display: flow-root;是更现代且推荐的清除浮动方式,因为它只影响BFC的创建,没有overflow: hidden;可能带来的副作用(如内容裁剪或滚动条)。
- 解决了什么问题: 当
- 示例:防止外边距合并
<div class="box1">Box 1</div> <div class="wrapper"> <div class="box2">Box 2</div> </div>.box1 { margin-bottom: 20px; background-color: lightblue; height: 50px; } .box2 { margin-top: 30px; background-color: lightsalmon; height: 50px; } .wrapper { /* 触发BFC */ overflow: hidden; /* 或 display: flow-root; */ }- 解决了什么问题: 在没有
.wrapper包裹且不触发BFC的情况下,.box1的margin-bottom和.box2的margin-top会合并,最终两者之间只有30px的间距。通过将.box2放入一个触发了BFC的.wrapper中,外边距合并被阻止,box1和box2之间的实际间距将是20px + 30px = 50px。 1.3 常见误区或面试陷阱
- 解决了什么问题: 在没有
- 误区: 认为BFC仅仅是用来清除浮动的一种"技巧"。实际上,清除浮动只是BFC的一个应用场景,BFC的本质是创建独立的渲染区域。
- 陷阱: 不清楚所有触发BFC的条件。例如,除了
overflow: hidden;,display: flex;、position: absolute;等也能创建BFC。 - 陷阱: 混淆BFC与CSS盒模型或定位上下文。BFC是关于布局的独立渲染区域,而盒模型定义了元素的尺寸和间距,定位上下文与
z-index等层叠顺序相关。 - 误区:
overflow: hidden;可以完美清除浮动。虽然它能清除浮动,但可能会导致内容被裁剪或出现非预期的滚动条,display: flow-root;是更纯粹、无副作用的BFC触发方式。
14. 事件循环(Event Loop)机制。
题目要点
- JavaScript单线程特性: 面试官想确认面试者是否理解JavaScript作为单线程语言如何处理异步操作。
- 任务队列: 考察面试者对宏任务(macrotask)和微任务(microtask)的理解及其执行顺序。
- Event Loop流程: 了解Event Loop在浏览器或Node.js环境下的具体工作机制,包括任务的调度和执行。
- 异步编程: 理解Event Loop是JavaScript异步编程的基础,以及它如何解决阻塞问题。
参考答案
1.1 原理说明
- JavaScript单线程: JavaScript语言的执行是单线程的,这意味着它一次只能执行一个任务。如果遇到耗时操作(如网络请求、定时器、DOM事件),就会导致主线程阻塞,页面卡顿,用户体验下降。
- Event Loop(事件循环): 为了解决单线程的阻塞问题,JavaScript引入了Event Loop机制。它是一种运行时模型,负责协调和管理同步任务和异步任务的执行顺序。
- 任务队列: Event Loop的核心是维护两个主要任务队列:
- 宏任务队列 (Macrotask Queue):包含脚本整体代码、
setTimeout、setInterval、setImmediate(Node.js)、I/O、UI渲染等。每次事件循环只取出一个宏任务执行。 - 微任务队列 (Microtask Queue):包含
Promise的then/catch/finally回调、MutationObserver、process.nextTick(Node.js)等。在一个宏任务执行完毕后,会清空所有微任务。
- 宏任务队列 (Macrotask Queue):包含脚本整体代码、
- Event Loop的工作机制:\
- 执行栈 (Call Stack):所有同步任务都在执行栈中按顺序执行。\
- 异步任务: 当同步任务中遇到异步任务(如
setTimeout、Promise),这些异步任务会被交给宿主环境(浏览器或Node.js)处理。\ - 任务入队: 异步任务执行完毕后,其回调函数会被放入对应的任务队列(宏任务队列或微任务队列)。\
- 循环过程:\
- 首先,执行栈中的所有同步任务执行完毕。\
- 然后,检查微任务队列。如果微任务队列不为空,则清空所有微任务,执行它们的回调。\
- 微任务清空后,从宏任务队列中取出一个宏任务执行。\
- 宏任务执行完毕后,再次检查微任务队列并清空。\
- 重复以上步骤,形成循环,直到两个任务队列都清空。 1.2 核心用法 + 示例代码
- 示例:宏任务与微任务的执行顺序
console.log('start'); // 同步任务 setTimeout(function() { console.log('setTimeout'); // 宏任务 }, 0); Promise.resolve().then(function() { console.log('promise1'); // 微任务 }).then(function() { console.log('promise2'); // 微任务 }); console.log('end'); // 同步任务- 执行结果:
start end promise1 promise2 setTimeout - 解释:
start和end是同步任务,首先执行。setTimeout是宏任务,其回调被放入宏任务队列。Promise.resolve().then是微任务,其回调被放入微任务队列。- 同步任务执行完毕后,执行栈清空。Event Loop首先检查微任务队列。
promise1执行,其返回的Promise的then回调 (promise2) 被再次放入微任务队列。- 微任务队列非空,继续执行
promise2。 - 微任务队列清空后,Event Loop从宏任务队列中取出
setTimeout执行。
- 执行结果:
- 应用场景: Event Loop是理解所有异步操作(如Ajax请求、事件处理、定时器、Promise、async/await)的关键。它使得JavaScript能够以非阻塞的方式处理耗时操作,从而保持页面的响应性。 1.3 常见误区或面试陷阱
- 误区: 认为
setTimeout(fn, 0)会立即执行。实际上,即使延迟为0,它仍然是一个宏任务,需要等待当前同步任务执行完毕、微任务队列清空后才会被执行。 - 陷阱: 混淆宏任务和微任务的优先级。微任务总是在当前宏任务执行完毕后立即执行,且在下一个宏任务开始之前清空所有微任务。
- 陷阱: 不清楚不同宿主环境(浏览器 vs Node.js)下Event Loop的差异。虽然基本原理相同,但在任务源和调度细节上存在差异(例如Node.js的
process.nextTick优先级高于Promise微任务,以及setImmediate宏任务等)。 - 误区: 认为JavaScript是多线程的。JavaScript主线程永远是单线程的,多线程能力通常由宿主环境提供(如Web Workers),但与Event Loop处理异步操作是不同的概念。
15. DNS 的工作原理
题目要点
- DNS概念: 面试官想确认面试者是否理解DNS是域名解析系统的核心概念及其作用。
- 解析过程: 考察面试者对DNS查询过程的了解,包括递归查询和迭代查询。
- DNS服务器类型: 了解不同类型的DNS服务器(根、顶级域、权威)在解析中的角色。
- 缓存机制: 理解DNS缓存对解析速度和系统压力的影响。
- 前端与DNS关系: 了解前端在访问资源时,DNS解析是第一步,以及可能涉及的优化点。
参考答案
1.1 原理说明
- DNS (Domain Name System):域名系统。它是一个分布式数据库,用于将人类可读的域名(如
www.example.com)转换为机器可读的IP地址(如192.0.2.1)。可以理解为互联网上的"电话簿"服务。 - 作用: 计算机通过IP地址来识别和相互通信,而用户习惯使用域名。DNS的作用就是将用户输入的域名解析成对应的IP地址,从而使浏览器能够找到目标服务器。
- DNS服务器层级: DNS解析是一个分层的过程,涉及到不同类型的DNS服务器:
- 根域名服务器 (Root DNS Server):位于DNS层级结构的顶端,存储了所有顶级域名服务器的地址。全球有13组根服务器。
- 顶级域名服务器 (TLD DNS Server):负责管理顶级域名(如
.com,.org,.cn等)下的所有域名,存储了下一级权威域名服务器的地址。 - 权威域名服务器 (Authoritative DNS Server):存储了特定域名(如
example.com)下的所有主机名(如www.example.com)到IP地址的映射关系,是域名最终解析的"权威"来源。 - 本地DNS服务器 (Local DNS Server/DNS Resolver):通常由ISP(互联网服务提供商)提供,或由用户自己配置(如8.8.8.8)。它是用户设备DNS查询的起点,负责向其他DNS服务器发起查询并缓存结果。 1.2 核心用法 + 示例代码
- DNS解析过程(以访问
www.example.com为例):- 浏览器缓存: 浏览器首先检查自身缓存中是否有
www.example.com对应的IP地址。 - 操作系统缓存: 如果浏览器缓存没有,则检查操作系统的DNS缓存(hosts文件)。
- 本地DNS服务器查询: 如果本地缓存都没有,操作系统会将查询请求发送给本地DNS服务器(通常是ISP提供的DNS服务器)。
- 递归查询与迭代查询:
- 本地DNS服务器收到请求后,会进行递归查询(向上级DNS服务器发起查询,直到找到最终结果)。本地DNS服务器首先向根域名服务器发起查询。
- 根域名服务器返回
.com顶级域名服务器的地址。 - 本地DNS服务器向
.com顶级域名服务器发起查询。 .com顶级域名服务器返回example.com权威域名服务器的地址。- 本地DNS服务器向
example.com权威域名服务器发起查询。 example.com权威域名服务器返回www.example.com对应的IP地址。- 本地DNS服务器将IP地址返回给用户设备,并进行缓存。
- 浏览器发起HTTP请求: 浏览器获得IP地址后,就可以向目标服务器发起HTTP请求。
- 浏览器缓存: 浏览器首先检查自身缓存中是否有
- 示意图(伪代码或流程描述):
graph TD A[用户浏览器] --> B{请求 www.example.com}; B --> C{浏览器缓存/Hosts文件?}; C -- 无 --> D[本地DNS服务器]; D -- 递归查询 www.example.com --> E[根域名服务器]; E -- 返回 .com TLD服务器地址 --> D; D -- 递归查询 www.example.com --> F[.com TLD服务器]; F -- 返回 example.com 权威服务器地址 --> D; D -- 递归查询 www.example.com --> G[example.com 权威DNS服务器]; G -- 返回 www.example.com IP地址 --> D; D -- 缓存并返回IP地址 --> A; A --> H[向IP地址发起HTTP请求]; - 解决了什么问题: DNS解决了IP地址难以记忆的问题,使得用户可以通过域名方便地访问网站。同时,分布式和缓存机制保证了DNS解析的高效性和健壮性。
- 优势: 分布式架构保证了DNS的扩展性和容错性;缓存机制大大提升了解析速度,减少了对上层服务器的压力。 1.3 常见误区或面试陷阱
- 误区: 认为DNS解析只涉及一次查询。实际上,DNS解析是一个分层递进的查询过程,可能涉及多次查询。
- 陷阱: 混淆递归查询和迭代查询。用户设备到本地DNS服务器之间通常是递归查询,本地DNS服务器到其他层级DNS服务器之间是迭代查询。
- 陷阱: 忽略DNS缓存的重要性。浏览器、操作系统和本地DNS服务器都会进行缓存,这极大地提高了DNS解析的速度。
- 误区: 认为DNS只用于网页访问。DNS也广泛用于邮件服务、FTP、CDN等各种互联网服务。
- 陷阱: 不清楚DNS解析失败(如DNS污染、DNS劫持)可能导致的问题。
16. TCP 如何保证可靠性?UDP 适用场景?
题目要点
- TCP可靠性机制: 面试官想确认面试者是否理解TCP为了保证数据传输的可靠性所采取的关键机制,如序列号、确认应答、重传、流量控制、拥塞控制等。
- UDP特性: 考察面试者对UDP"不可靠"但"高效"特性的理解。
- 协议选择: 掌握根据应用场景的需求选择合适的传输层协议(TCP vs UDP)的判断依据。
参考答案
1.1 原理说明
- TCP (Transmission Control Protocol):传输控制协议。它是一种面向连接、可靠的、基于字节流的传输层协议。TCP旨在提供可靠的数据传输服务,确保数据在发送方和接收方之间无差错、按顺序、无重复地到达。
- UDP (User Datagram Protocol):用户数据报协议。它是一种无连接、不可靠的传输层协议。UDP不保证数据报的顺序、不保证数据报的完整性、不进行重传,但具有较低的开销和更高的传输效率。 1.2 核心用法 + 示例代码
- TCP保证可靠性的机制:\
- 序列号 (Sequence Number) 和确认应答 (Acknowledgment Number):\
- TCP对每个发送的字节都进行编号(序列号)。接收方在收到数据后,会发送一个确认应答(ACK),其中包含期望收到的下一个字节的序列号(确认号)。这确保了数据按顺序到达,并能检测到丢失的数据包。\
- 超时重传 (Retransmission):\
- 发送方在发送数据后,会启动一个定时器。如果在定时器超时之前没有收到接收方的确认应答,发送方就会认为数据包丢失,并重新发送该数据包。\
- 流量控制 (Flow Control):\
- TCP使用滑动窗口协议(Sliding Window)来实现流量控制。接收方会告知发送方其当前的接收窗口大小(即还能接收多少数据),发送方根据这个窗口大小来调整发送速率,防止发送方发送速度过快导致接收方缓冲区溢出。\
- 拥塞控制 (Congestion Control):\
- TCP通过慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快速重传(Fast Retransmit)、快速恢复(Fast Recovery)等算法来监测网络拥塞情况,并动态调整发送方的发送窗口大小,以避免过多的数据注入到网络中,减轻网络拥塞。\
- 校验和 (Checksum):\
- TCP在发送数据时会计算数据包的校验和,接收方收到数据后也会计算校验和。如果两者不一致,说明数据在传输过程中损坏,数据包将被丢弃。\
- 连接管理 (Connection Management):\
- 通过三次握手(建立连接)和四次挥手(释放连接)机制,确保连接的可靠建立和终止,防止旧的数据包在新的连接中混淆。
- 序列号 (Sequence Number) 和确认应答 (Acknowledgment Number):\
- UDP适用场景:\
- 实时性要求高,允许少量丢包的应用:\
- 流媒体传输(直播、视频会议): 相比于数据的完整性,更看重实时性和流畅性。少量丢包不会对整体观看体验造成太大影响。\
- 在线游戏: 帧率和响应速度是关键,对偶尔的数据包丢失有容忍度。如果使用TCP重传,反而可能导致延迟增加,影响游戏体验。\
- DNS查询: 快速响应是主要目标,DNS查询通常是小数据量且多次查询,如果一次查询失败,可以快速重试或查询其他服务器。\
- 物联网 (IoT) 数据传输: 传感器数据通常是小而频繁的,且单个数据点丢失影响不大,UDP的低开销更适合资源受限的设备。\
- 组播/广播: UDP支持一对多、多对多的通信模式,例如在局域网内进行服务发现。\
- 需要自定义可靠性机制的应用: 某些应用层协议(如QUIC)会在UDP的基础上自行实现可靠性、流量控制等机制,以获得更灵活的控制权和更好的性能。
- 实时性要求高,允许少量丢包的应用:\
- 示例(概念性):\
- TCP连接建立(三次握手):
客户端 -> SYN (seq=x) -> 服务器
服务器 -> SYN+ACK (seq=y, ack=x+1) -> 客户端
客户端 -> ACK (ack=y+1) -> 服务器\ - UDP数据发送:
客户端 -> 数据报 -> 服务器 (不保证到达,不保证顺序) 1.3 常见误区或面试陷阱
- TCP连接建立(三次握手):
- 误区: 认为TCP的可靠性意味着"永远不会出错"。可靠性是指尽最大努力保证数据正确、有序、完整地到达,但仍可能受限于网络物理中断等因素。
- 陷阱: 不清楚TCP流量控制和拥塞控制的区别。流量控制是点对点(发送方和接收方)的控制,防止发送方淹没接收方;拥塞控制是全局性的,防止发送方淹没整个网络。
- 陷阱: 误以为UDP完全没有用处。UDP因其简单、低开销而适用于对实时性要求高、对数据完整性有容忍度的场景。
- 误区: 认为"一次握手"或"两次握手"也可以建立TCP连接。三次握手是建立可靠连接的最小步骤,防止历史连接请求导致资源浪费和数据混淆。
- 陷阱: 无法结合具体应用场景解释为什么选择TCP或UDP。例如,HTTP基于TCP,因为它需要可靠地传输网页内容;而实时音视频通常基于UDP,因为它可以通过少量丢包以保证流畅性。
17. 手写代码题
给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。示例 :输入:s = “Hello World” 输出:5 解释:最后一个单词是"World",长度为 5。
题目要点
- 字符串处理: 面试官想确认面试者对字符串基本操作(如查找、截取、遍历)的熟练程度。
- 边界条件处理: 考察面试者是否能考虑到字符串为空、只有空格、单词之间有多个空格等特殊情况。
- 算法思维: 考察面试者如何设计一个高效的算法来解决问题,以及对时间复杂度和空间复杂度的考量。
参考答案
1.1 原理说明
- 问题分析: 题目要求找到一个字符串中最后一个单词的长度。单词的定义是"仅由字母组成、不包含任何空格字符的最大子字符串"。这意味着我们需要忽略字符串前后的空格,以及单词之间可能存在的多个空格。核心在于从字符串末尾开始查找,定位到最后一个非空格字符的起始位置,并计算其长度。 1.2 核心用法 + 示例代码
- 方法一:从后向前遍历(推荐)
- 原理: 从字符串末尾开始向前遍历。首先跳过所有末尾的空格字符,找到最后一个单词的末尾。然后继续向前遍历,直到遇到空格字符或字符串开头,以此确定最后一个单词的起始位置。两个位置之差即为单词的长度。
- 优势: 只需要一次从后向前的遍历,时间复杂度为O(N),空间复杂度为O(1),效率较高。
/** * @param {string} s * @return {number} */ var lengthOfLastWord = function(s) { let length = 0; let i = s.length - 1; // 1. 跳过末尾的空格 while (i >= 0 && s[i] === ' ') { i--; } // 2. 从最后一个非空格字符开始向前计数,直到遇到空格或字符串开头 while (i >= 0 && s[i] !== ' ') { length++; i--; } return length; }; // 示例 console.log(lengthOfLastWord("Hello World")); // 输出: 5 console.log(lengthOfLastWord(" fly me to the moon ")); // 输出: 4 (moon) console.log(lengthOfLastWord("luffy is still joyboy")); // 输出: 6 (joyboy) console.log(lengthOfLastWord("a")); // 输出: 1 console.log(lengthOfLastWord(" ")); // 输出: 0 console.log(lengthOfLastWord("")); // 输出: 0 - 方法二:使用字符串分割(JavaScript内置方法)
- 原理: 首先去除字符串两端的空格(
trim()),然后使用空格作为分隔符将字符串分割成单词数组(split(' '))。最后取数组的最后一个元素,返回其长度。 - 优势: 代码简洁易懂,依赖内置方法。
- 缺点:
split和trim可能会创建新的字符串和数组,存在一定的空间开销,对于特别长的字符串可能效率不如直接遍历。
/** * @param {string} s * @return {number} */ var lengthOfLastWordWithSplit = function(s) { // 1. 去除字符串两端的空格 const trimmedStr = s.trim(); // 2. 如果去除空格后字符串为空,则返回0 if (trimmedStr.length === 0) { return 0; } // 3. 按空格分割成单词数组 const words = trimmedStr.split(' '); // 4. 返回最后一个单词的长度 return words[words.length - 1].length; }; // 示例 console.log(lengthOfLastWordWithSplit("Hello World")); // 输出: 5 console.log(lengthOfLastWordWithSplit(" fly me to the moon ")); // 输出: 4 console.log(lengthOfLastWordWithSplit("luffy is still joyboy")); // 输出: 6 console.log(lengthOfLastWordWithSplit("a")); // 输出: 1 console.log(lengthOfLastWordWithSplit(" ")); // 输出: 0 console.log(lengthOfLastWordWithSplit("")); // 输出: 0 - 原理: 首先去除字符串两端的空格(
1.3 常见误区或面试陷阱
- 误区: 直接使用
split(' ')而没有先trim()。这会导致如果字符串末尾有空格,split可能会创建一个空字符串作为最后一个元素,从而导致结果错误。 - 陷阱: 忽略字符串可能为空或只包含空格的情况。这种情况下,应该返回0。
- 陷阱: 在遍历时没有正确处理多个连续空格的情况。例如,
\"hello world\",从后向前遍历时需要正确跳过中间的两个空格。 - 性能考量: 对于非常长的字符串,
trim()和split()会创建新的字符串和数组,可能带来额外的内存和时间开销。从后向前遍历的O(1)空间复杂度通常更优。