背景
今天早上在脉脉上看到一个关于BN
的前端二面分享,作者出于纯粹的目的分享了一下最近的面试题。
我觉得这是一套不错的面试题,于是分享给了大家。
为什么会有这套面试题
前端界,到底什么样子的项目,会用到这类型的面试题背后蕴含的知识?
我有幸从0 - 1
参与过几个项目,例如:
- 桌面端IM项目(Electron、React、Node.js),端到端加密,主打20万人群聊功能
- 几个大的SAAS系统(React)
- 小程序(Taro)
- 混合APP
- 微信公众号
- 一些web3项目(流动池几千万,solidity React TypeScript Node.js)
等等..
里面有些需要一定技术深度背后蕴含的知识有:
- 通信,基于TCP的端到端加密长链接通信,
- 安全,用户隐私,安全,像Telegram一样的方向
- 性能:数据量大的处理与展示,前端任务调度,re-render控制等
- 设计模式的理解与实践和面向对象编程:例如单例模式,控制反转,依赖注入
- 对react和Vue关键节点源码的阅读与理解
- 对ES6异步实现的理解
- 浏览器的渲染原理
- Node.js
- Linux、docker、K8s、nginx等基础运维知识
等等...这里不展开是因为写这篇文章时候中午还没吃饭。很饿,况且大部分人根本用不到其他冷门的知识
假设一个场景
例如每秒同时有两个人给你发消息,你的客户端(前端)是不需要做任务调度。
假如每秒同时有一千个人给你发很多消息,这个时候就要做任务调度了,因为这里面涉及到网络层、DB层、缓存层(前端内存,例如redux等),以及数据流向、更新频次与时机控制。
交易,同理。例如一个币价一秒钟内波动剧烈,由于是IM场景,双工通信,可能一秒你接收到多次推送。这个频次如果根据用户实际场景拆解做精细化,是一个极度复杂的需求。这里就不展开讲了
那么这个时候,你就会用到我在上面提到的大部分知识,在做性能优化的时候,当你的知识足够全面丰富,其实更像是在下棋,子落后不可反悔。有利有弊
随着互联网的推进,我认为前端会越来越像是一个完整的客户端,现在有webContainer技术和webasm等技术,只要谷歌解决native-socket和安全的一些关键节点问题,就是完整的客户端。不再需要Electron之类的
大概讲讲题目
1.React的时间切片思想
可以结合我三年前文章 手写mini-react源码看看
https://github.com/JinJieTan/Peter-/tree/master/mini-React
- 先看看
cpu
调度时间片
时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行
- 那么react的时间切片思想是什么呢?
两年前,我们公司一个项目从react0.14版本升级上来react16,记得当时给公司一些同事科普过一次。react16引入了fiber,其实这个时间切片思想,就是react16的fiber。
当时react0.14版本的项目有一个问题,就是会出现卡顿,因为react16版本之前,是一口气完成更新。如果这个过程很长,就会导致等待(卡顿)的时间很长
react16版本后,react更新,会有一个Reconcilation阶段,这个阶段是会遍历虚拟dom树,找出更新的节点,完成一系列操作。这个阶段计算比较多,就会长时间占用cpu.而这个Reconcilation阶段是可以中断的(暂时挂起),让浏览器先响应高优先级事件,例如用户交互等。这就是所谓的时间切片思想,本质上是任务调度
- 2.为什么不用
requestIdleCallback
在代码里面我有备注过,我测试过requestIdleCallback
,当时我在做1秒钟1000个人频繁发消息的性能优化,就在结合手写react做任务调度。
原因是:requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务
我们可以发现,很多优化思想,来自于对操作系统本身的认知,对事物的本身认知决定了发展的天花板。
useMemo之类的原理和优化原理
背后使用了Object.js方法遍历浅对比了传入的dependencys的prev和current值。
使用简单的比较,省去不必要的render
react的副作用
比较笼统的问题,这个问题我就不回答了
vue的nextTick
vue2有一个优雅降级的过程
- 先是promise.then
- 而后是MutationObserver
- 然后是setImmediate
最后是setTimeout
let timerFunc // nextTick异步实现fn if (typeof Promise !== 'undefined' && isNative(Promise)) { // Promise方案 const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 将flushCallbacks包装进Promise.then中 } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // MutationObserver方案 let counter = 1 const observer = new MutationObserver(flushCallbacks) // 将flushCallbacks作为观测变化的cb const textNode = document.createTextNode(String(counter)) // 创建文本节点 // 观测文本节点变化 observer.observe(textNode, { characterData: true }) // timerFunc改变文本节点的data,以触发观测的回调flushCallbacks timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // setImmediate方案 timerFunc = () => { setImmediate(flushCallbacks) } } else { // 最终降级方案setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } }
出这个问题,是想知道面试者对Vue框架的数据更新 - 渲染异步是否真的理解,并非只是这个nextTick而已。
剩下的宏任务和微任务,可以跟第六题一起回答。
什么是控制反转和依赖注入
出这个题目,说明面试官比较崇尚这种风格模式,不然不会问这个特殊问题,但是要注意的是,既然问了这方面的,肯定会拓展发散,问你实际的使用和其他设计模式等。所以背面试题,对于稍微上点档次的面试,是不靠谱的。
我个人反对背面试题,更看重过往项目经验和基础知识掌握与实践思考
- 控制反转(IoC):
在单一职责原则的设计下,很少有单独一个对象就能完成的任务。大多数任务都需要复数的对象来协作完成,这样对象与对象之间就有了依赖。一开始对象之间的依赖关系是自己解决的,需要什么对象了就New一个出来用,控制权是在对象本身。但是这样耦合度就非常高,可能某个对象的一点小修改就会引起连锁反应,需要把依赖的对象一路修改过去。
经典的控制反转(IoC)原则:
上层模块不应该依赖于下层模块,他们共同依赖于一个抽象,抽象不能够依赖于具体 ,具体必须依赖于抽象。
放在TypeScript中,上面这句话可以理解为,多个class遵循一个interface,这些class的对应数据值不同,但是字段和类型都是一样的。
当需要被单独、组合使用时,直接使用这些class即可
控制反转此时的好处:如果后面要更新进化,只要新的interface兼容现有的interface即可,不需要改动现有class代码去做兼容。这涉及到Ts的协变和逆变,感兴趣的去了解下
- 依赖注入(DI—Dependency Injection):
把对象之间的依赖关系提到外部去管理,可是还如果组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中
例如react的Context,使用Context.Provider注入数据
例如装饰器
@Foo()
智能合约内部也有修饰器,例如access control
里面的
modifier onlyOnwer(){
require(msg.sender == onwer,'msg.sender not onwer');
__;
}
function _mint () public onlyOnwer(){
//dosomething
}
依赖注入,本质上帮助简化组装依赖过程。
asyncpool实现
前端并发控制的库 asyncpol
ES7实现版本
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // 存储所有的异步任务
const executing = []; // 存储正在执行的异步任务
for (const item of array) {
// 调用iteratorFn函数创建异步任务
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p); // 保存新的异步任务
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e); // 保存正在执行的异步任务
if (executing.length >= poolLimit) {
await Promise.race(executing); // 等待较快的任务执行完成
}
}
}
return Promise.all(ret);
}
ES6实现版本:
function asyncPool(poolLimit, array, iteratorFn) {
let i = 0;
const ret = []; // 存储所有的异步任务
const executing = []; // 存储正在执行的异步任务
const enqueue = function () {
if (i === array.length) {
return Promise.resolve();
}
const item = array[i++]; // 获取新的任务项
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);
let r = Promise.resolve();
// 当poolLimit值小于或等于总任务个数时,进行并发控制
if (poolLimit <= array.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
r = Promise.race(executing);
}
}
// 正在执行任务列表 中较快的任务执行完成之后,才会从array数组中获取新的待办任务
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.all(ret));
}
总结
面试题出得比较贴近实际,看中对框架原理和前端异步以及基础的考察,这些知识点跟框架开发中复杂功能的debug息息相关。学习源码是必不可少的进阶过程,有可能当时学了没用,但是真的理解精髓以后你会发现,大部分优秀的框架源码都差不多,包括他们的使用,思路和理念等,源码最重要的是帮助你在未来做复杂场景需求debug时使用。
当然,这些都是基于我很久没有更新的前端知识的认知基础写的,如果有问题,欢迎你指出。
写于2022年5月31日
一个写智能合约的web2.5软件工程师
如果感觉写得不错,可以点个赞,帮忙关注下公众号:前端巅峰
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。