米哈游秋招一面(详解版)

2,409 阅读7分钟

背景

笔者在最近的秋招中,参加了米忽悠的提前批,自我感觉面的非常不错,然后一面挂(小丑.jpg

下面是笔者的面经

面经

1. 说说 HTML 的语意化

  1. HTML 语意化有啥
  • article
  • section
  • header
  • footer
  • nav
  • sider
  1. 好处
  1. 网页加载慢导致 CSS 文件还未加载时(没有 CSS),页面依然清晰、可读、好看。
  2. 有利于 SEO,和搜索引擎建立良好沟通,有利于爬虫抓取更多的有效信息。
  3. 方便其他设备(如屏幕阅读器、盲人阅读器、移动设备)更好的解析页面。
  4. 使代码更具可读性,便于团队开发和维护。

1. async await 原理

本质上就是使用了 generator,可以看看下面的详细代码,本质是通过 generator 来处理实现循环,实现“卡住”的效果。

在实际面试的时候,我把这部分代码写出来了。

const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000))

// 这样的一个async函数 应该再1秒后打印data
async function test() {
  const data = await getData()
  console.log(data)
  return data
}

// async函数会被编译成generator函数 (babel会编译成更本质的形态,这里我们直接用generator)
function* testG() {
  // await被编译成了yield
  const data = yield getData()
  console.log('data: ', data);
  const data2 = yield getData()
  console.log('data2: ', data2);
  return data + '123'
}

function generatorToAsync(generatorFunc) {
  return function() {
    const gen = generatorFunc.apply(this, arguments)
    return new Promise((resolve, reject) => {
      function step(key, args) {
        let generatorResult;
        try {
          generatorResult = gen[key](args)
        } catch(err) {
          return reject(err)
        }
        const { value, done } = generatorResult
        if(done) {
          return resolve(value)
        } else {
          return Promise.resolve(value).then(val => step("next", val), err => step("throw", err))
        }
      }
      step("next")
    })
  }
}

const testGAsync = generatorToAsync(testG)
testGAsync().then(result => {
  console.log(result)
})

2. 垂直居中怎么做的,回答了五种,就比较详细

  1. relative + absolute
.wp {
  position: relative;
}
.box {
  position: absolute;
	top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}
  1. calc + absolute
.wp {
  position: relative;
}
.box {
  position: absolute;
  top: calc(50% - 50px);  <!--50 是父盒子的高度-->
  left: calc(50% - 50px);
}
  1. absolute + transform
.wp {
  position: relative;
}
.box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%)
}
  1. flex
.wp {
  display: flex;
  justify-content: center;
  align-items: center;
}
  1. grid
.wp {
  display: grid;
}
.box {
  align-self: center;
  justify-self: center;
}

3. http1.1 和 http2

  • 头信息压缩: HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表「HPACK 算法」,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
  • HPACK 算法的优势
  • 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,...)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。
    • 其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。

  • 二进制协议:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码){这个问题被字节一面面试官问过,还答错了,和数据体搞混了。} ,数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。
  • 多路复用: 多路复用产生的原因是「HTTP队头阻塞」,而队头阻塞又由「HTTP请求-应答」模式所造成(在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。),后面我们使用了并发连接域名分片去解决这个问题,但这并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级流量控制等功能

  • 服务器推送: HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。在 HTTP/2 当中,服务器已经不再是完全被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件一起返回给客户端,减少客户端的等待。

4. 说说 Vue2 中 computed 的原理

响应式基础:

vue是采用数据劫持结合观察者模式的方式,通过defineProperty()/Proxy来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

在Vue中,Dep是一个订阅器,它的作用是收集依赖(watcher)并在数据变化时通知它们进行更新。Dep中的subs存储的是所有依赖该Dep的watcher实例。Watcher是一个观察者,它的作用是当数据变化时,执行相应的回调函数。Watcher中的deps存储的是该watcher实例所依赖的所有Dep实例。Dep.target是一个静态属性,它的值为当前正在计算的watcher实例,该值是在watcher实例计算之前被赋值的。

回答:

  1. 依赖追踪:当 computed 属性被访问时,Vue 会记录所有依赖于该 computed 属性的观察者(Watcher)。
  2. 缓存策略computed 属性会缓存其计算结果。只有当依赖的数据发生变化时,才会重新计算。这是通过 Watcher 实例的 dirty 属性来控制的。只有当 dirtytrue 时,才会调用 evaluate 方法进行计算。
  3. 惰性求值computed 属性在首次访问时才会进行计算,之后会根据依赖数据的变化情况决定是否重新计算。

Detail:

  1. 使用 Object.defineProperty 劫持数据,使其变为响应式数据。
  2. 创建 Watcher 实例,用于依赖收集和更新通知。
  3. 通过 Dep 实现依赖收集,当依赖数据变化时,触发 computed 属性的重新计算。

4. 手写debounce

function debounce(fn, delay) {
  let timer
  return function() {
    clearTimeout(timer)
    let args = arguments
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

5. 手写 topK

leetcode.cn/problems/g5… 这个看 LC 官方解答就好