面试总结~~~ 录音复盘 -- 90分钟一轮面试
总结在前面
结果:一面通过
lowlight
- 抢答:表现在面试官问题刚好中埋伏的时候,太着急回答,应该先组织一下回答的顺序(思考几秒钟),这样回答的逻辑更加清晰。
- 思考时间不充分:边想边回答,顺序混乱。
- 提前准备不足:提前思考项目中可能会问哪些问题?思考(背景-痛点、 方案、 技术点亮点【一般写在简历上的都是核心技术点!!!】)
- 算法题复习:差点死掉
- 个人介绍:未准备,因为准备的不好,所以很紧张!!!
hightlight
临场的猜测:根据了解的内容进行合理的猜测回答,CSS 加载部分、缓存部分。
个人介绍
有点紧张~
20年毕业,校招加入xx-地图事业部,主要从事地图数据编辑平台开发。个人擅长技术栈 React / TS / 地图可视化渲染。
问题一:请具体介绍一下在xx-地图主要负责哪些方面的业务?具体一些
地图数据更新维护的可视化编辑平台,目的:快速将现实世界变更同步到地图数据上,保障app端上用户导航/打车使用体验。举例:天安门国庆期间需要封闭多条道路【我们需要快速发现这个变化,并且通过一些资料验证准确定后将数据更新】
情报源:互联网情报、交委给出的信息、网约车异常发现【偏航、绕路、投诉、用户反馈】
参考资料:互联网媒体资料(图文、新闻、视频等)、行车记录仪图像(直观,准确)、轨迹热力(订单等)
编辑数据:路网数据(车道线、点、红绿灯、电子眼等等所有现实道路上的要素数据)、HD数据(精度更高,车道级10cm偏差)、交通事故数据
我们团队的目标就是,提供这样的编辑平台让人专业的作业员能够处理这些数据更新。
问题二:你所说的地图数据编辑用户是谁?B端还是C端用户直接交互
反思:没介绍清楚数据编辑的过程,
回答:平台主要用户是B端用户编辑,数据编辑有一个全流程的流水线,情报发现->数据修改核实->校验检查->推送发版->C端生效| 干预导航规划
问题三: 介绍一下地图数据渲染方面的逻辑和细节 ....第10分钟
利用mapbox相似的一些库,mapbox 有一个坐标体系,地图分层级从 16-21 级。每个级别渲染的数据加载逻辑是,根据当前屏幕中央所在的经纬度点以及层级来确定一个现实世界中的一个经纬度范围(类似于:方圆100m,方圆500m),然后按照数据类型加载不同的数据 | 然后渲染成可视化的图形【点、线、面】geojson 数据。
范围越大,数据越抽象(不详细),视口放大后,范围越小,数据更精细。这里存在一个金字塔结构的转换【按照缩放层级】。数据加载完成后缓存在内存中、缩放时详细数据未一般未加载【加载->缓存】。针对于两种场景策略有些不同:浏览模式(查看数据: 边看边加载、边缓存),作业模式(任务携带了范围大小和编辑数据类型,因此初始化后主动加载精细化的数据,主要用到的作业层级都会加载)。
紧张到这里结束!!适应了节奏
问题4:你在地图数据加载和渲染过程中遇到哪些性能瓶颈吗?
思考:我认为面试官想知道我对,数据编辑过程中大量数据计算以及渲染的性能处理方案。
回答引用了简历上针对车道线编辑的性能优化场景:车道线数据由点构成,当车道线绘制超过1km~2km以后点数据非常多,使用贝塞尔曲线化平滑算法后点更加密集【平行车道线随着鼠标移动动态生成周边数据(涉及到:车道线方向、车道方向、车道宽度、车道面数据、平行车道线数据)】。因此在主线程中计算会出现明显卡顿(每帧 16ms , 流畅情况下js主线程用于计算的时间不多)。
我将数据计算采用 WebWorker 进行分离计算,通过 SharedBuffer 进行数据共享(加锁没说) 。计算时区分预览数据【预览数据抽稀计算得到特征,确定时按照特征重新生成精细数据】(所见即所得: 预览时采用抽稀算法将精细的车道线稀疏,确保特诊不变的情况下(车道线大体形态、车道宽度、车道方向等等采用抽稀后的数据生成降低计算量); 另外限制频率,防抖节流(100毫秒内的连续交互只计算最后一次);限制数据计算范围,范围限制从屏幕大小和缩放层级确定了最大范围为 200m 左右, 按照GB车道宽度【3,3.25,3.5,3.75】限制距离上限)。
问题5: 你在地图编辑器里面讲到采用依赖注入来解决新的架构问题?介绍一下依赖注入解决的问题以及有哪些好处?你怎么使用的 -- 来源于简历
平台历史问题:从ID编辑器迭代过来(类似于JQuery 的写法),经过长期迭代后新的业务进场(我们需要支持HD数据编辑,全新的数据规格),从旧平台架构上继续实现难度太大。
因此我们参考了 vscode 的架构理念【分层、贡献点机制、插件机制、command模式等等】,在这个架构实现过程中,我们需要解决模块之间的依赖关系【举例: xx 模块依赖Person 类的实例或者派生类实例,普通实现(从参数中传递实例;)此时模块之间的耦合就需要采用实例传参的方式来实现 。而在依赖注入 IoC 模式中,只需要在模块上编程修饰参数 符合 Person的抽象类和接口,模块继续开发,按需从依赖注入容器 container 上读取】
JS 执行完成后,装饰器代码会将模块中定义的 class 初始化成实例注册到依赖注入容器上;各个模块根据业务逻辑切换依赖注入容器中的实例(来实现子模块动态切换)而不影响其他模块的代码实现。
依赖注入解决模块之间的耦合问题(依赖引用)。模块依赖只需要关注引用模块提供的接口定义,而不需要关注具体实现。
问题6:你刚刚介绍的TS里面的类型有点类似于python的鸭子类型对吧?
是的,TS 和是一个鸭式辩型(结构上相同即可),动态类型。
问题7: 好的,刚刚又说到贡献点机制方面讲讲吗? --- 第22分钟
思考:面试官想了解,我们定义的贡献点机制到底是什么?怎么工作的
比如:我们在页面中存在一个 Footer 组件,需要在使用A工具时展示支持哪些快进键、使用B工具时展示B工具对应的快捷键。那么此时 Footer 就是一个插槽,A工具就需要将快捷键内容以贡献点的方式 register 到 Footer 中,B 工具同理。 Footer 负责切换工具时切换对应的快捷键渲染。
这是UI类的,那针对于生命周期、通用型模块(数据渲染模块MapEditor)、在调度到某个生命周期时 A、B、C、D、都需要执行一些初始化操作,那么这些模块的初始化函数就以贡献点的方式注册到生命周期插槽中。
结合依赖注入,就实现了模块是模块、流程是流程、流程与模块之间的执行关系由模块贡献点机制来呈现。贡献点的目的就是将职责定义清楚。
问题8:第二个项目中有介绍到动态表单---第25分钟
思考: 回答往低代码和动态表单上靠
业务困境&背景:
我在团队内部搞过低代码平台建设和动态表单建设。另外在地图数据更新场景中有许多(用户上报、申请单、审批单)等等业务需要实现以表单来填写内容(不同的表单),为了一劳永逸,避免重复性的编写表单,我就采用了动态表达的方式来支持业务方动态配置表单schema。
配置表单 schema -> 存储后端 -> 以key作为 path hash 加载对应schema渲染成表单。
面试官:复杂组件是如何实现的?另外,表单中字段需要与其他字段关联进行一些联动和计算如何处理?
采用 dependency 来关联其他字段,另外这个 dependency 支持定义一些处理函数(进行判断)。
问:函数采用字符串存储的?怎么执行的?
答:函数是采用字符串执行的,执行时注入对应的上下文对象(form实例、 ruller、 window等等上下文对象),函数执行时会在ErrorBounds 中运行。
问:表单中的数据是自定义配置的,payload 到远程时后端怎么处理
在配置的时候会有一个渲染测试的页面,数据都转成对应的json发送到对应的接口,后端自己兼容处理数据。
问:表单中支持复杂组件吗?比如表格编辑
我支持antd的通用表单组件这个是一致的,另外基于antd开发了一些复杂的业务组件来作为受控组件【schema配置时可以从里面选择类型,组件支持的数据格式、功能都有个文档格式参考】,最后 schema 在渲染的时候会从 componentMapping中去找到对应的组件来渲染。
问题8:工时统计中的时序模型和反作弊可以展开讲讲吗? --第33分钟
背景:我们提供的平台是给第三方供应商来生产数据,我们需要根据工时统计来计算支付。因此有这么些场景需要考虑:前端交互真值是不知道的、多个平台作业同一个任务数据时分不开、采用一些作弊手段(按着键盘睡觉)、丢失关键点数据时无法补回、网络原因导致统计请求未发送成功导致的丢失问题。都会导致工时不准,公司审计也过不了。
方案:将数据进行结构化设计提供唯一的tabID来标志当前的tab页面,数据包含(事件类型、触发事件源、 可能来自的工具id、),将数据在前端交互过程中触发的存储到 indexDB 中 【发送失败时:触发重试机制、关闭浏览器后下次打开平台会重新发送】。此时前端完成了页面交互过程的真值备份(不是所有的交互事件、而是业务定义的关键点数据和普通心跳【6s、10s内存在交互发送一个dot】)
主线程中出现交互时,通知子线程、子线程负责 indexDB读写/过期数据清除、作弊检测、数据发送和失败重试。
当出现数据丢失时能够从第二次重传数据中补充,基本上大部分因为服务波动、网络异常等问题导致的都会被二次修复。
问题9:简历有提到浏览器插件的开发?怎么开发的?解决什么问题?---第40分钟
背景:三方来源的资源【获取的媒体链接而不是具体的视频图文数据】,因此需要在平台中加载第三方资源,但是大部分第三方都存在防爬的限制。因此我需要用浏览器插件来绕过这个限制的策略。
这些策略大部分是不允许的白名单外的域名加载网站任何内容、不允许内嵌到白名单以外的域名平台当中加载。方案就是采用浏览器插件:网络拦截修改能力,将请求头修改、cookie 修改以跳过策略限制。
问:service-worker 除了做网络请求的拦截还能实现缓存,浏览器缓存这方面能聊聊嘛?【答的不好】
当时有点懵了,缓存这个部分确实了解的不全面,回答的不好~但是我没跳过。回答了:请求头里面配置cache相关的属性,浏览器加载以后如果需要缓存则存在缓存一份在disk中,下一请求时检查如果缓存没有过期则使用这份缓存数据。【面试官提醒强缓存和弱缓存!!!回头仔细看看】
问题10:打开浏览器输入URL到页面渲染的完整链路可以讲一下---第46分钟
思考:核心介绍 chrome 里面发生的事情(多进程架构中创建进程、加载资源、HTML解析、CSSOM 解析、 Layout、 Print、 过程、主线程执行JS过程),
当chrome 打开新的 tab 时 chrome 会为当前的 tab 创建一个render进程。
从URL输入框输入内容时, chrome 首先进行解析,判断如果是一个网址格式,如果只是简单的搜索词而非域名地址,那么调用chrome的搜索引擎进行搜索【如果搜索引擎默认指定为baidu.com,那么就是百度的搜索引擎提供内容】,如果是一个http协议的域名地址,那么通过DNS解析进行ip地址查询,失败则提醒DNS域名查询失败、成功则通过 Network 进程加载 HTML 页面。
HTML 加载完成后 Render 进程对 HTML 文档从上到下开始解析:如果遇到 Script、 Link、Source ,Img 等需要加载资源的则推送到 Network进程进行资源加载。其中 Script 标签内如果存在行内代码,则执行代码。如果标签上存在 src 属性则需要等待js文件加载执行(async 标记异步加载,加载完成后再执行,不阻塞HTML解析)。
HTML 解析完成后开始构建 CSSOM 、 即使没有CSS文件引入也会采用默认样式构建CSSOM 。
CSSOM 构建完成后进入 Layout 阶段,此时 HTML 中元素的位置大概已经确定盒模型生成。
Print 以及后续阶段主要是负责:将需要渲染的内容和z-index[屏幕垂直] 以及栅格系统进行分片(瓦片),然后调用GPU将瓦片转换成位图渲染。
另外的,当JS文件加载完成后 Render 进程会将内容推送给JS main-thread 执行。这里的主线程实际调用的就是V8引擎,主线程额外补充给V8引擎其他的一些执行环境内容【事件循环、WebAPI】。
问: JS 分阻塞不阻塞,那么CSS呢?【不确定】
明确回答这个部分不太确定,因此我猜测,CSS 是异步加载,等CSS加载完成后触发新的 CSSOM 生成。再重新渲染~ 【需要再核实一下这个部分】
问:刚刚说的 js 线程是在chrome哪个进程中?
js 主线程是在每个 tab render进程中,因此js执行和渲染会互斥。
问题11:chrome 中使用的 JS 引擎是V8引擎,你了解吗?稍微介绍一下--第54分钟
V8 是采用C++ 编写的一个JS代码编译执行的引擎(也可以叫做解释执行)。V8 引擎拿到JS代码会第一遍会进行字节码转换,然后解释执行字节码【解释器】,然后在运行过程中会进行 Hot 代码标记【调用频率、函数大小、函数历史执行稳定性等方面评估】,然后将 hot 代码编译成机器码执行(机器码运行快、但是机器码占内存比较大)。
hot 编译优化过程中涉及到多层的模型:记不住机器码编译器的名字【分层编译最后一层为TurboFan】,不同的层的机器码编译器的差异是【机器码编译的完整程度、以及编译速度】。 V8 在里面做了权衡,编译考虑:内存大小 vs 运行效率。
当 hot 代码热度降低、或者当前环境中内存占用较高时,触发去优化。会将已经编译成机器码的热点代码回退到字节码水平。
问:JS是解释性语言,里面又有编译的过程对吧。那么你怎么理解解释性语言和编译性语言的,差异在哪儿?
编译性的语言,在打包构建阶段已经明确了CPU的架构,那么直接将代码编译成机器码。比如C++针对macOs 、 windows 不同的CPU架构需要打包出不同的产物包。好处就是机器码运行快,缺点就是跨平台兼容不好。
解释性语言比如JS、Python 那么就是在解释器中临时编译执行。好处是跨平台,不同的平台解释器适配了对应CPU架构,代码是通用的,跨平台性好,写一份代码到处执行。缺点就是慢~ 编译成机器码这个过程耗时(最后还是CPU来运行,CPU 只认机器码)。
问:解释性语言跨平台性比较好,那么是谁来提供的这个跨平台的能力?
嗯,我认为是解释器提供的,解释器在将字节码转换成机器码的时候,会内置映射对应的CPU架构参数。
问题12: 算法题 全排列 --- 第66分钟
思考:采用递归的方式去实现。
这题在去年刷过,因此有点印象,想全靠回忆写出代码。卡顿了, 面试官提示了新的思路(他的思路,另一种实现的思路),仔细想了以后还是按照之前的方式回忆写代码,运行通过。
function testFunc(nums: number[]): Array<Array<number>> {
const result: number[][] = [];
if (nums.length === 1 ) {
return [nums];
}
for (let i = 0; i<nums.length; i++) {
const curent= nums[i];
const nexts = nums.filter((val, index)=>{
return index !==i;
});
const perms = testFunc(nexts);
perms.forEach((item)=>{
result.push([curent, ...item]);
})
}
return result;
}
testFunc([1,2,3,4])
实在不是知道全排列怎么命名函数。
至此结束,你有什么问我的吗环节跳过?因为问的都是一些目标部门的业务问题,以及团队的情况。
晚上收到消息,一面通过~