滴滴面试(戏剧性)

384 阅读13分钟

首先面试第一个部门(前面2轮面试),通过评级D6,希望涨薪百分之十,hr说需要特殊申请,在申请的过程中这个部门没有hc了,当然hr跟我聊的时候就说过hc比较紧张,可能没有,没进入这个部门只能说没有缘分。所以把我推到第二个部门,进行了第三轮面试,没通过。
事后一位就职滴滴的朋友告诉,幸好没去,说我面试的那个部门比较卷,当时面试滴滴的考虑就是想去一个轻松的部门。

一面

1. 写一个最优的继承

create静态方法以现有的一个对象作为原型,创建一个新对象

function Parent() {}
function Children() { Person.call(this); }
Chilren.prototype = Object.create(Parent.prototype);
Chidlren.prototype.constructor = Chidlren;

2. EventBus

function EventBus() {
    const eventsMap = new Map();
    this.on = (key, event) => {
        const preEvents = (eventsMapget(key) || []).concat(event)
        eventsMap.set(key, preEvents);
     }
     this.emit = (key) => {
         const events = eventMap.get(key) || []
         events.forEach((event) => { event(); })
     }
     this.off = (key) => {
         eventsMap.deelet(key);
     }
}

3. 写一个失败重试的函数

function retry(fn, maxTime = 3) {
    let retryNum = 1;
    return new Promise((resolve, reject) => {
        const repeat = () => {
            fn().then((res) => { resolve(res); }).catch((error) => {
                retryNum++;
                if(retryNum > maxTime) {
                    reject(error)
                } else {
                    repeat();
                }
            })
        }
        repeat();
    })
}

4. 重复次数最多的字符和次数

function repeatCharNum(s) {
    let preIndex = 0;
    let maxNum = 1; const res = {[s[0]]: 1};
    let len = s.length;
    for(let i = 0; i < len; i++) {
        const nextChar = s[i + 1];
        const preChar = s[preIndex]
        if(nextChar !== preChar) {
            const preNum = res[preChar] || 0;
            const curNum = i - preIndex + 1;
            const curMax = Math.max(preNum, curNum);
            maxNum = Math.max(maxNum, curMax);
            res[preChar] = curMax;
            preIndex = i + 1;
        }
    }
    return Object.keys(res).reduce((pre, key) => {
        if(res[key] === maxNum) {
            pre[key] = maxNum;
        }
        return pre;
    }, {})
}
console.log(repeatCharNum('aaabbbccccaaaaa'))

5. 写一个递归的组件

function List(props) {
    const { list } = props;
    function Item(props) {
        const {name, child} = props;
        return (<>
            <span>{name}</span>
            {child?.length && <List list={child}/>}
        </>)
    }
    return (<>
        {list.map((item) => <Item name={item.name} child={item.children} />}
    </>)
}

二面

1. 自我介绍

1. 寻找子节点的路径

// 类似下面多层嵌套数据结构:children 为空数组时,表明为最后一层
var list = [
    {
        id: 1,
        children: [
            {
                id: 11,
                children: [
                    { id: 111, children: [] },
                    { id: 112, children: [] },
                    { id: 113, children: [] }
                ]
            },
            { id: 12, children: [ { id: 121, children: [] } ] },
            { id: 13, children: [] }
        ]
    },
    {
        id: 2,
        children: [
            {
                id: 21,
                children: [
                    { id: 211, children: [] },
                    { id: 212, children: [] },
                    { id: 213, children: [] }
                ]
            },
            {
                id: 22,
                children: [
                    { id: 221, children: [] } ] },
                    { id: 23, children: [] }
                ]
            }
        ]
// 给出任意一个节点值,找到根节点到这个节点的路径节点。例如给出一个 id = 113, 找到这个 id 所属于的所有父节点的 id,例如: console.log(findPath(list, 113))
// 结果为 [1, 11] console.log(findPath(list, 21))
// 结果为 [2]
function findPath(list, chidlrenId) {
    let res;
    const search = (nodes, path) => {
        // 递归出口
        if (res || !nodes.length) {
            return;
        }
        for (let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            if (node.id === chidlrenId) {
                res = path; break;
            } else {
                search(node.children, [...path, node.id]);
            }
        }
   }
   search(list, []);
   return res;
}

3. promise调度问题(手写)

//JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。完善代码中Scheduler类,使得以下程序能正确输出
class Scheduler {
    constructor() {
        // 保存需要执行的任务
        this.tasks = [];
        // 最大执行的任务数量
        this.maxQueue = 2;
        // 正在执行的任务
        this.runningQueue = 0;
    }
    add(task) {
        const promise = Promise((resolve, reject) => {
            this.tasks.push(() => task().then(resolve, reject));
        });
        this.run();
        return promise;
    }
    run() {
        if (this.runningQueue >= this.maxQueue) {
            return;
        }
        while (this.runningQueue < this.maxQueue && this.tasks.length) {
            _run();
        }
        const _run = () => {
            if (!this.tasks.length) { return; }
            const task = this.tasks.shift();
            this.runningQueue++;
            task.then(() => { 
                this.runningQueue--;
                _run()
            })
       } 
   }
}
const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
})
const scheduler = new Scheduler()
const addTask = (time, order) => {
    scheduler.add(() => timeout(time)) .then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4') 
// output: 2 3 1 4

4. 小程序怎么做优化的

5. 什么是tree-shaking 以及原理是什么

静态分析模块之间的导入导出,确定哪些模块导出值没有被使用并打上标记,并将其删除,从而实现了大包的产物的优化。

实现基础是 ES module(依赖关系高度确定)

原理:

  • Make阶段:收集模块导出变量并记录到模块依赖关系图ModuleGraph变量中
  • Seal阶段:遍历ModuleGraph标记模块导出变量有没有被使用‘
  • 生成产物时,若变量没有被其他模块使用则删除对应的导出语句
  • 由Terseer等DCE工具"摇"掉这部分无效代码

6. 开发小程序过程中遇到什么问题

7. 页面卡死,如何排查问题

首先明确页面可能卡死的原因

  • js代码,循环、递归、大量DOM操作等。可以使用开发者工具的性能分析器(Performmance)帮助找到性能瓶颈
  • 内存泄漏。可以使用开发者工具Memory面板监测内存使用情况,特别是持续云心事是否有内存泄漏的迹象
  • 异步代码的死锁。例如多个异步操作互相依赖或者存在的死循环等。
  • 网络请求。是否存在大量的网络请求,或者某个请求一直未完成导致页面卡死
  • 事件处理器:检查是否窜在大量的事件处理器绑定,或者事件的触发频率过高
  • 动画和渲染性能:如果页面包含大量的动画或者复杂的DOM操作,可能会影响页面性能。可以使用开发者工具的动画面板(Animation)和渲染面板(Rendering)来分析
  • 定时器和周期性任务。检查是否是否窜在大量的定时器和周期任务,以及他们的执行频率

问题解决手段:

  • 重现问题,尝试在不同的环境或者浏览器重现问题,看是否是特定于某个环境或者浏览器
  • 查看浏览器控制台的报错信息,根据报错信息查找问题
  • 如果只是某些电脑出现页面卡死,可以尝试使用性能分析工具如 lighthouse、WebPageTest等来分析页面性能,并找到瓶颈

8. 大量数据如何做优化

9. chrom自带的debug使用多吗?

10. setData后数据是怎么更新的

  • setState产生当前的更新优先级(老expirationTime,新版本用lane)
  • React从fiber root根bugiber向下调和子节点,调和阶段对比发生更新的地方,更新对比expirationTime,合并state,然后触发render函数,得到新的UI视图层,完成render阶段
  • 接下来commit阶段,替换真实DOM,完成此次更新流程
  • 最后执行setState的callback函数

类组件初始化过程中绑定了负责更新的Updater对象,对于如果调用setState方式,实际是react底层调用的Updater对象上的enqueueSetState。

  1. 调用 setState:当调用 setState 方法时,React 会将新的状态添加到更新队列中。
  2. 合并更新:React 会将多个 setState 调用合并成一个更新,以减少重新渲染的次数
  3. 进入调和阶段(Reconciliation Phase):在 React 内部,会开始调和阶段,也就是 Virtual DOM 的比对过程。React 会比较前后两次渲染的 Virtual DOM 树,找出需要更新的部分。
  4. 生成更新队列:根据比对结果,React 会生成一个更新队列,其中包含需要更新的组件及其对应的新状态。
  5. 调用生命周期方法:在更新之前,React 会触发相应组件的生命周期方法,如 getDerivedStateFromProps
  6. 更新组件状态: React 会将新的状态应用到组件中
  7. 生成 DOM 更新队列:根据 Virtual DOM 的比对结果,React 会生成一个 DOM 更新队列,其中包含需要更新的实际 DOM 节点及其对应的新属性。
  8. 批量更新 DOM:React 会将 DOM 更新队列中的所有更新合并成一个批量更新,以减少 DOM 操作的次数。
  9. 更新实际 DOM:React 会根据批量更新的结果,对实际 DOM 进行更新。
  10. 调用生命周期方法(更新后):在 DOM 更新完成后,React 会触发相应组件的生命周期方法,如 componentDidUpdate

11. react的diff算法是如何比较的

12. 保证任务执行顺序(手写)

class Task {
    constructor() {
        this.tasks = [];
        this.isRuning = false;
    }
    add(fn, context, ...args) {
        this.tasks.push({ fn, context, args });
        return this;
    }
    run() {
        this.isRuning = true;
        const _fn = () => {
            if (!this.tasks.lenght || !this.isRuning) { return; }
            const { fn, context, arg } = this.tasks.shift();
            fn.call(context, () => { _fn(); }, ...arg)
        }
        _fn();
    }
    stop() { this.isRuning = false; }
}
function task1(next) {
    setTimeout(() => { console.log('red') next() }, 3000)
}
function task2(next, b) { 
    setTimeout(() => { console.log(b) next() }, 1000)
}
function task3(next, c) {
    setTimeout(() => { console.log('yellow') next() }, 2000)
}
let task = new Task()
task.add(task1).add(task2, null, 3).add(task3)
task.run()
setTimeout(() => { task.stop() }, 3500)

三面(B部门)

1. 怎么做单元测试

2. 怎么做优化

如何获取用户的DNS和TCP耗时

  1. 在服务器端设置。Timing-Allow-Origin字段,将其设置为*,以允许跨域请求获取服务器性能数据。
  2. 在客户端发送HTTP请求时,在请求头中添加Timing-Allow-Origin字段,将其设置为服务器的域名或IP地址,以获取服务器的性能数据。
  3. 在客户端接收到服务器响应后,可以通过浏览器的开发者工具或其他网络分析工具,查看DNS解析和TCP连接的耗时信息。

performance.getEntriesByType("navigation")

performance.getEntriesByType("navigation")是 Performance 接口提供的一个方法,用于获取与页面导航相关的性能条目信息。

当调用 performance.getEntriesByType("navigation")时,它会返回一个包含了与页面导航相关的性能信息的数组。

这个数组中的每个元素都是一个对象,包含了以下属性:

  • name:该条目的名称,通常是当前页面的URL。
  • entryType:条目的类型,对于导航条目,这个值是 "navigation"。
  • startTime:导航开始的时间戳,以毫秒为单位,相对于页面加载开始的时间。
  • duration:导航的持续时间,以毫秒为单位,从导航开始到导航结束的时间间隔。
  • initiatorType:导航的发起者类型,比如 "click",`"reload", "typed", "history" 等。
  • nextHopProtocol:导航所使用的网络协议,例如 "h2"(HTTP/2) 和"h3" (HTTP/3)
  • workerStart:如果导航涉及到了 Service Worker,这是 Service Worker 开始处理导航请求的时间戳。
  • redirectStart`:第一个 HTTP 重定向开始的时间戳。
  • redirectEnd:最后一个 HTTP 重定向完成的时间戳。
  • fetchStart:开始获取文档的时间戳,通常是在发起第一个 HTTP 请求之前。
  • domainLookupStart:开始进行 DNS 查询的时间戳。
  • domainLookupEnd:DNS 查询结束的时间戳。
  • connectStart:开始建立与服务器的连接的时间戳。
  • connectEnd:建立连接完成的时间戳。
  • secureConnectionStart:安全连接开始的时间戳,仅适用于 HTTPS。
  • requestStart:开始请求文档的时间戳,通常是在发起第一个 HTTP 请求之前。
  • responseStart:收到第一个字节的时间戳,也就是服务器开始发送响应的时间。
  • responseEnd:收到最后一个字节的时间戳,也就是接收完整个响应的时间。
  • transferSize:导航过程中所有资源的传输大小的总和,以字节为单位。
  • encodedBodySize:导航过程中所有资源的编码后的总大小,以字节为单位。
  • decodedBodySize:导航过程中所有资源的解码后的总大小,以字节为单位。

prefetch、preload

和 都是用于优化网页性能的HTML标签,但它们有一些关键的区别:

  • 作用:用于告诉浏览器在空闲时预加载指定的资源(例如页面内链接的下一个页面)。
  • 优点:在浏览器空闲时加载资源,不会影响当前页面的加载
  • 适用场景:适用于预加载下一个页面的资源,以提升用户体验。
  • 注意事项:并不是所有浏览器都支持 prefetch。一些旧版本的浏览器可能会忽略 prefetch 指令。
  • 作用:用于提前加载当前页面所需的关键资源,以缩短页面加载时间。
  • 优点:可以明确指定哪些资源是页面加载所必需的,避免了浏览器自己做出判断。
  • 适用场景:适用于加载当前页面所需的关键资源,如CSS、字体、脚本等。
  • 注意事项:可以使用 as属性来指定资源的类型,以帮助浏览器更好地处理资源。

dns-prefretch

域名预解析(DNS Prefetching)是一种浏览器优化技术,它可以在用户点击链接之前提前解析与当前页面相关的域名,从而加速页面的加载速度。

通常,当浏览器解析一个网页时,它会按需解析其中的资源链接,如图像、脚本、样式表等。这个过程可能会导致一些延迟,尤其是当解析和连接远程服务器的过程需要一定的时间时。

域名预解析通过在页面加载时提前解析可能会在未来加载的资源的域名,可以节省一些加载时间。这在以下情况下尤其有效:

  1. 使用了多个域名来分布资源,例如使用 CDN 或者将静态资源放在独立的子域名下。
  2. 在页面中包含了许多外部资源,比如第三方的脚本库或广告。

要在页面中使用域名预解析,可以使用以下方法:

  1. 使用 元素<link rel="dns-prefetch" href="//example.com">

  2. 使用 HTTP 头部,在服务器端配置 HTTP 头部:Link: <//example.com>; rel=dns-prefetch

  3. 使用 元素

<meta http-equiv="x-dns-prefetch-control" content="on"> <meta http-equiv="x-dns-prefetch-control" content="off">

需要注意的是,虽然域名预解析可以加速资源的加载,但也需要谨慎使用,以避免浪费带宽和资源。不要对不需要的域名进行预解析。

另外,一些现代浏览器如Chrome、Firefox等通常会自动进行DNS预解析,因此在某些情况下可能不需要手动设置预解析。

preconnect

是一种HTML标签,用于指示浏览器在加载页面时预先建立与特定资源的连接,以加速页面的加载速度。

具体来说, 可以告诉浏览器在页面加载时预先建立到指定域名的连接,包括DNS解析、TCP握手和TLS建立(如果是HTTPS)。这样,在实际请求资源时,已经建立了相应的连接,从而减少了等待时间。

示例:<link rel="preconnect" href="https://example.com" />

在这个示例中, 元素的 rel 属性被设置为 "preconnect",并且 href 属性指定了要预先连接的域名。

可以用于以下情况:

  1. 预先连接到主机名:可以在页面中预先连接到可能会用到的主机名,例如CDN、第三方服务等。
  2. 预先连接到字体服务器:可以在页面中预先连接到用于提供字体文件的服务器。
  3. 预先连接到API服务:如果网页需要从某个API服务获取数据,可以在页面加载时预先连接到该API的服务器。

注意事项:

  • 预先连接可能会增加网络流量和服务器负担,因此需要谨慎使用,只在确实有必要的情况下使用。
  • 不是所有浏览器都支持 rel="preconnect"

3. 如何找到页面的性能问题

根据页面加载流程进行分析,DNS,TCP、服务端相应,前端收到数据,解析

4. 如何设计一个抽奖SDK,h5和小程序都能用

5. 对下份工作的期望

写在最后

我把面试中一些开放的问题的答案省略了,大家可以做参考。就我自己来说,面试前会预测一些开发性的问题,提前准备好答案,然后写成文档的形式,自己会尽量多读几遍。
面试中的话,三思而后答。 最后,祝大家面试顺利,拿到满意的offer。