likes
comments
collection

全球顶级交易所前端二面

作者站长头像
站长
· 阅读数 2

背景

今天早上在脉脉上看到一个关于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实现

前端并发控制的库 asyncpolES7实现版本

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软件工程师

如果感觉写得不错,可以点个赞,帮忙关注下公众号:前端巅峰