快手一面

7 阅读10分钟

自我介绍

...

挑两个项目来讲

  • BI平台
    • 大数据量的计算处理
      • 自实现透视算法和优化
      • webwoker多线程处理计算,比如透视和四则运算的自定义列(类似excel)
      • 复杂计算结果进行LRU缓存,下次直接使用
    • 多查询引擎设计如何解决并发查询和中断恢复
      • plugin可插拔架构实现不同数据源查询接入,区分异步(需要不断的轮询和解析日志得到查询进度)和同步(调用一次接口直接返回数据)
      • 内核实现通用的最终可用数据onSuccess, onError, 对日志进行进度解析调用onProgress, onLogger,onDownload
      • 并发优先级调度,用户当前点击的查询tab优先级最高
      • 中断恢复,用户退出页面再进入和其他页面切换,恢复到当前查询
    • 报表如何秒开渲染
      • 接口优先级调度
      • 数据缓存 LRU 策略
      • 进入视口区懒加载
      • 仪表盘之间的切换
    • 数据大盘由网格布局向数据故事如何设计
      • 图层设计
      • 组设计
      • 图表联动设计
      • 可插拔设计,自定义生命周期hook,比如组件加载时,组件更新时,方便自定义plugin接入,类似webpack
      • 屏幕适配设计
  • 广告流量投放和指纹浏览器
    • 大量文件合并zip下载怎么保证并发和断点续传
      • 使用caches apis缓存已经完成的response实现断点续传
      • 并发控制
    • 大量联动表单如何去设计提升开发效率
      • schema设计生成联动表单
    • 国际化如何自动翻译提升开发效率
      • 自定义t函数,自动调取翻译接口
    • 指纹浏览器如何实现
      • 指纹生成固化
      • 无证书自动更新

react router 有几种模式,都是啥场景使用

  • HashRouter
    • 浏览器不会把 hash 内容发送给服务器,所以 hash 变化不会导致浏览器向服务器发送请求,所以 hash 模式无需后端配合,也不会刷新页面
    • 对 SEO 不友好
    • 兼容性更好,一些老版本的浏览器都支持
    • 不会包含在 http 请求中
    • location.hash
    • 通过hashchange事件来监听 hash 的变化
{
    location = / {
        rewrite (.*) /index.html last;
    }
}
  • BrowserRouter/HistoryRouter
    • 利用 HTML5 的 History API(如 pushState 和 replaceState)实现,需要服务器支持,通常需要配置所有路由请求返回同一 HTML 文件
    • 对 SEO 友好
    • 需要 HTML5 支持,在 IE9 及以下版本无法使用 ‌
    • 会包含在 http 请求中
    • 使用 history api 来进行路由跳转无须重新加载页面
    • location.pathname
    • 前进后退通过popstate事件来监听路由的变化,pushState,replaceState 则需要手动实现 eventBus 来监听
{
    location / {
		index index.html;
		root  /Users/xxx/youproject/dist;
	}
}
  • MemoryRouter 不存储 history,所有路由过程保存在内存里,不能进行前进后退,因为地址栏没有发生任何变化
    • 在单元测试中模拟路由行为
    • 在 React Native、Electron 或服务器端渲染中使用
    • 在大型应用中作为子路由系统
    • 需要完全控制路由状态的特殊场景
    • URL 不可见,开发不需要 URL 变化但需要路由逻辑的应用
    • MemoryRouter 适用于那些不需要与服务器交互或不需要浏览器历史记录支持的单页应用(SPA)‌
  • StaticRouter 设置静态路由,需要和后台服务器配合设置,比如设置服务端渲染时使用
    • 运行环境在 node 中,在服务端渲染中使用
    • SEO 优化:生成可被搜索引擎索引的完整 HTML
    • 初始化渲染,加速首屏加载时间
    • 确保服务器和客户端渲染结果一致
    • 根据路由返回正确的状态码(如 404)
  • NativeRouter 经常配合 ReactNative 使用

当时回答:hash 模式下可以单域名多项目使用,通过 pathname 区分,hash 作为路由,比如 H5 活动页

你说的这个场景为啥不用子域名,这不是用 hash 的理由,而且会有文件访问风险

不能经验主义去回答问题

那是运维的成本而不是前端安全考虑的问题

无话可说,项目确实这么用了,这个域名确实可以访问很多不同 entry 的文件,但是仔细想想,你访问到了又如何呢,都是静态文件,而且只有 html,都是一个模版生成,有啥安全风险,css,js 都在另外一个域名 cdn 上了,不过你是面试官,你说的对

闭包的概念

函数嵌套函数,返回一个函数,并且这个函数可以访问外部函数的变量,闭包可以访问函数外部的变量,即使函数执行完毕了

  • 延长变量的生命周期
  • 封装和隐藏:可以封装一些私有变量,这些变量只能通过特定的函数进行访问,增加代码的模块性和安全性
  • 回调函数中的数据持久化

你项目中有啥应用场景么

比如请求参数柯里化,把请求域名,路径,请求的数据分开出来

  • 通用的请求方法是第一级别,const { get, post } = getRequestFnByBoot({ root: 'www.baidu.com' })
  • 第二级别,class Apis { getList: get('/list'), submit: post('/submit') } return new Apis()
  • 第三级别,Apis.getList({ page: 1, pageSize: 10 })

面试官老和我扯这是私有变量了不是闭包使用场景,我最后只能说,你说的对。

跨域的定义是啥

  • 跨域是指浏览器出于安全考虑,限制网页脚本访问不同源(协议、域名、端口)的资源 ‌,这是由浏览器的同源策略(Same-Origin Policy)造成的安全限制。
  • 浏览器的同源策略(Same-Origin Policy):是浏览器核心安全机制,通过限制不同源(协议、域名、端口)之间的资源交互,防止恶意网站窃取用户数据或发起攻击。‌
  • ‌同源策略的定义与判定标准‌:同源策略要求两个 URL 的 ‌ 协议(Scheme)、域名(Hostname)、端口(Port)‌ 完全一致才视为同源。
    • 防止跨站脚本攻击:阻止恶意脚本读取其他源的 DOM、Cookie 或本地存储数据(如 LocalStorage),避免 CSRF/XSS 攻击。‌‌‌‌
    • 限制跨域请求:默认禁止 XMLHttpRequest 或 Fetch 向不同源服务器发送请求,除非服务器显式允许(如 CORS)。‌‌‌‌
    • 隔离数据存储: Web Storage、IndexedDB 等数据仅限同源访问,Cookie 则通过 Domain 和 Path 属性限制作用域

跨域怎么解决

  • JSONP
    • 通过 script 标签引入一个 js 文件,这个 js 文件载入成功后会执行我们在 url 参数中指定的函数,并且会把我们需要的 json 数据作为参数传入
    • 优点:兼容性好,简单易用,支持浏览器与服务器双向通信
    • 缺点:只支持 get 请求,不支持 post 请求,不安全,容易遭受 XSS 攻击
  • CORS
    • CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了浏览器与服务器如何通信,允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制
    • 优点:支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案
    • 缺点:兼容性差,需要服务器配合设置 Access-Control-Allow-Origin

从输入一个 url 到页面渲染完成,中间发生了什么

  • 缓存读取
  • DNS 解析,解析出ip地址
  • TCP 三次握手连接
  • 如果是https, TLS握手, TLS 解密
  • http请求html
  • 流式解析html,边解析边下载
  • 解析html,生成dom树, 遇到外部链接的css,字体图片等异步下载,继续解析,如果遇到了js, 等待js脚本加载并完成执行(这会阻塞后续的资源下载,因为还没解析道),继续解析
  • 解析css,生成cssom树,解析期间会阻塞dom树的构建,但是不会阻塞js脚本的下载和解析,如果js中有dom操作则会被阻塞
  • 合并dom树和cssom树,生成render树
  • 布局计算,回流
  • 绘制
  • display

解析下浏览器缓存

  • 强缓存,不依赖服务器,直接从缓存中读取,使用cache-control(新-相对时间),expires(旧-绝对时间), 命中返回from cache,静态资源cdn用
  • 协商缓存:依赖服务器,发送请求,命中返回304(not modified),html
    • Last-Modified|If-Modified-Since,根据If-Modified-Since 和服务器对比修改时间
    • Etag|If-None-Match,根据If-None-Match 和服务器对比hash值

来做做题吧,第一题,实现一个防抖函数

function debounce(fn: Function, delay: number) {
  let timer: NodeJS.Timeout | null = null
  return function (this: any, ...args: any[]) {
    // 当时这里没用this,因为题目中有这种场景,后面面试官diss说 如果是obj.fn,fn中的this就会有问题
    const context = this
    // 后面不写timer = 0 被面试官diss这里多执行一次,比如sleep(1000)后再调函数,这时候timer不为0就会clearTimeout一次,无效执行
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(context, args)
      timer = null
    }, delay)
  }
}
const testFn = debounce((name: string) => console.log('test =》', name), 1000)
testFn('a1')
testFn('a2')
testFn('a3')
const obj = {
  type: 'fn',
  testFn: function (name: string) {
    console.log(`[obj ${this.type}]: ${name}`)
  }
}
obj.testFn = debounce(obj.testFn, 1000)
obj.testFn('a1')
obj.testFn('a2')
obj.testFn('a3')

题二:求最长不重复子串,比如 aabcdde => abcd

对方说了滑动窗口更简单,快慢指针移动解决,开始我没想明白,下来查了letcode, 明白了思路,

  • 左边记录开始位置 l = 0 从0开始
  • 右边记录重复的结束位置 r = 0 从0开始
  • 移动右边,r++ 直到重复,r-l之间的字符串就是不重复的子串
  • 如果当前子串长度大于res,更新res
// 自己实现了下,letcode跑分(1-4)ms,indexOf代替includes(8ms)性能提升一倍
function lengthOfLongestSubstring(str: string): string {
  let res = ''
  let cacheStr = ''
  for (let l = 0, r = 0; r < str.length; r++) {
    while (l <= r && cacheStr.indexOf(str[r]) > -1) {
      l++
      cacheStr = cacheStr.slice(1)
    }
    cacheStr += str[r]
    res = cacheStr.length > res.length ? cacheStr : res
  }

  return res
};

题三:实现一个并发请求函数,要求并发数为 3

我熟练的双数组操作,让面试官给我快速停止了,然后说我这可以更简单,嗯我上网查一下 promise-limit, 自己实现了下

function runTasksByLimit(tasks: any[], limit: number) {
  return new Promise((resolve, reject) => {
    let runCount = 0
    const taskList: Record<string, any>[] = tasks.map((p, index) => {
      return {
        p,
        key: index
      }
    })

    const remove = (task: any) => {
      runCount--
      taskList.splice(taskList.findIndex(item => item.key === task.key), 1)
      if (runCount < limit) {
        const nextTask = taskList[limit - 1]
        if (nextTask) {
          run(nextTask)
        } else {
          resolve(1)
        }
      }
    }

    const run = (task: any) => {
      runCount++
      return task.p().then((res: any) => {
        remove(task)
        return res
      }).catch((err: any) => {
        remove(task)
        return Promise.reject(err)
      })
    }

    while(runCount < limit) {
      run(taskList[runCount])
    }
  })
}

const p1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p1')
      resolve(1)
    }, 1000)
  })
}

const p2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p2')
      resolve(1)
    }, 3000)
  })
}
const p3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p3')
      resolve(1)
    }, 800)
  })
}
const p4 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('p4')
      resolve(1)
    }, 600)
  })
}

runTasksByLimit([p1, p2, p3, p4], 2)

题四:实现一个 eventBus

class EventBus {
  private events: Record<string, Function[]> = {}

  on(type: string, fn: Function) {
    if (!this.events[type]) {
      this.events[type] = []
    }
    this.events[type].push(fn)
  }
  emit(type: string, ...args: any[]) {
    if (this.events[type]) {
      this.events[type].forEach((fn) => fn(...args))
    }
  }
  off(type: string, fn: Function) {
    if (this.events[type]) {
      this.events[type] = this.events[type].filter((item) => item !== fn)
    }
  }
  once(type: string, fn: Function) {
    const _fn = (...args: any[]) => {
      fn(...args)
      // 这里也被diss了,当时直接this.events[type].filter((item) => item !== fn)
      this.off(type, _fn)
    }
    this.on(type, _fn)
  }
}

面试官评价:时间特意给你足够了,代码应该写细一些,边界一定要考虑到,而不是后来全靠你说,不能相信用户的输入

我也没想那么多,边界问题我是知道的,进入手写代码时候,时间已经过去50分钟了,要是后面手写慢了,其他家的面试直接就给我停了,所以我的思路肯定是先完成再完美,不过面试场景中没时间给我完美。