2023滴滴一面复盘

141 阅读22分钟

写在前面

由于自己实在是太菜,所以每次面试完需要自我复盘一下。欢迎同行大佬们给些建议,写的不对的地方请不吝赐教。

开始正文

日常工作,做过哪些页面的性能优化

对于页面性能优化,可以从以下几个方向入手:

  1. 减少 HTTP 请求:减少页面中的 HTTP 请求可以显著提高页面加载速度。可以通过合并 CSS/JS 文件、使用 CSS Sprites 或者使用 Data URI 来减少 HTTP 请求次数。
  2. 压缩资源文件:将 CSS、JavaScript 和 HTML 文件进行压缩,可以减小文件大小,从而提高页面加载速度。
  3. 缓存优化:使用浏览器缓存技术可以减少服务器请求,加快页面加载速度。可以通过设置 Expires、Cache-Control 等 HTTP 头来控制缓存策略。
  4. 图片优化:优化图片可以减小图片的大小,从而加快图片的加载速度。可以使用图片压缩工具、将图片转换成 WebP 格式、使用图片懒加载等技术来优化图片。
  5. 使用 CDN:使用 CDN(内容分发网络)可以将静态资源缓存在 CDN 服务器上,从而加快资源加载速度。
  6. 代码优化:通过合理的代码结构和编写方式,可以减少代码的冗余和重复,从而提高页面性能。
  7. DNS 解析优化:DNS 解析是浏览器加载网页的第一步,优化 DNS 解析可以加速页面的加载速度。可以通过减少 DNS 查询次数、使用 DNS 预加载等技术来优化 DNS 解析。
  8. 首屏渲染优化。

首屏渲染优化的思路大致如下:

  1. 减少首屏需要加载的资源:减小首屏需要加载的资源大小可以减少页面的加载时间,提高首屏渲染速度。可以通过使用图片懒加载、延迟加载 JavaScript 等技术来减小首屏需要加载的资源大小。

  2. 提高首屏渲染速度:提高首屏渲染速度可以减少用户等待时间,提高用户体验。可以通过以下几个方面来提高首屏渲染速度:

    • HTML 和 CSS 的优化:将 CSS 放在 head 标签中,减少 JavaScript 和 CSS 的重定向和重绘,减少 DOM 树的深度等。
    • JavaScript 的优化:提高 JavaScript 的执行效率,避免 JavaScript 的阻塞渲染,使用异步加载等。
    • 图片的优化:使用正确的图片格式,压缩图片大小等。
    • CDN 的使用:使用 CDN 可以加速加载速度,减少网络延迟。
  3. 预加载和预渲染:预加载可以在页面加载完成前提前加载资源,从而提高页面打开速度。预渲染可以在页面打开前将页面生成为静态 HTML 文件,从而提高页面打开速度。

  4. 服务端渲染:服务端渲染可以将页面的渲染工作放在服务器端完成,从而减少客户端的渲染时间,提高页面打开速度。

通过以上的优化措施,可以显著提高页面的首屏渲染速度,从而提高用户体验和页面的质量。

通过上述优化措施,可以显著提高页面加载速度,从而提升用户体验和页面的排名。

如何监控页面白屏异常

要监控静态资源加载异常导致的页面白屏异常,可以使用以下几种方式:

  1. 使用Performance API:可以通过Performance API来获取页面的性能指标,如页面加载时间、资源加载时间等,从而识别页面白屏异常。具体来说,可以使用performance.timing API获取页面各个阶段的时间戳,比较DOMContentLoaded事件的时间戳和load事件的时间戳,如果这两个事件的时间戳相差较大,说明页面存在白屏问题。
  2. 监听DOMContentLoaded事件:在页面DOM树构建完成后,会触发DOMContentLoaded事件。可以在该事件的回调函数中对页面的样式和脚本进行检查,以确定是否存在白屏问题。
  3. 监听window的load事件:当页面所有资源都加载完成后,会触发window对象的load事件。可以在该事件的回调函数中检查页面是否正常显示,如果存在白屏问题,则可以进行相应的处理。
  4. 使用可视化检测工具:可以使用一些可视化检测工具,如WebPageTest、Lighthouse等,来评估页面的性能和用户体验,从而找出页面白屏问题的原因。

需要注意的是,页面白屏异常可能由多种原因引起,如网络问题、代码问题等,因此需要综合使用多种监控手段来识别和解决问题。

笨蛋回答:将js监控的代码文件写在html靠前的位置,以此来在页面尚未加载完成时进行静态资源文件加载异常的监控。

arr.sort()方法的时间复杂度

在JavaScript中,arr.sort()方法的时间复杂度取决于实现方式和数组的大小。通常情况下,JavaScript引擎使用快速排序(Quicksort)或归并排序(Mergesort)来实现sort()方法,具体实现方式可能因不同的浏览器和JavaScript引擎而异。

在V8引擎中,对于长度超过10个元素的数组,sort()方法会使用快速排序算法,并且在数组长度小于10时会使用插入排序算法。快速排序算法的时间复杂度为O(n log n),但是最坏情况下可能会退化成O(n^2)。插入排序算法的时间复杂度为O(n^2)。因此,arr.sort()方法的时间复杂度为O(n log n)或O(n^2),具体取决于数组的大小和排序算法的实现方式。

说一下react的合成事件

React 的合成事件是一种封装了原生 DOM 事件的高级事件系统,它具有以下特点:

  1. 跨浏览器兼容性:React 的合成事件可以在不同浏览器下保持一致的行为,避免了浏览器之间的差异性。
  2. 自动绑定this:React 的合成事件会自动绑定this到当前组件实例,避免了手动绑定this的麻烦。
  3. 事件委托:React 的合成事件支持事件委托,可以将事件处理函数绑定在父组件上,避免了在每个子组件上都绑定事件处理函数的重复代码。
  4. 性能优化:React 的合成事件支持事件池,可以重复利用事件对象,减少了创建和销毁事件对象的开销。

React 的合成事件系统是基于原生 DOM 事件系统实现的,但是它并不是直接绑定在 DOM 节点上的。当组件渲染时,React 会将事件处理函数注册到一个事件委托的父节点上。当事件触发时,React 会根据事件类型和目标节点在组件树中查找到合适的组件,并依次触发事件处理函数,最终触发到目标组件的事件处理函数。

需要注意的是,由于 React 的合成事件是基于原生 DOM 事件系统实现的,因此有些事件属性和方法可能与原生事件略有不同。例如,React 中的事件属性是驼峰式命名,而原生事件属性是小写字母命名。另外,React 中的事件处理函数不能通过 return false 来阻止事件冒泡或默认行为,而是需要通过调用事件对象的 stopPropagation() 和 preventDefault() 方法来实现。

react-router动态路由的原理

React Router 可以使用 hash 路由来监听路径变化。实际上,在早期版本中,React Router 主要使用 hash 路由来实现路由功能。

使用 hash 路由的原理是,在 URL 中使用 # 符号来分隔路径和查询参数,例如 http://example.com/#/users。这样做的好处是,即使 URL 发生变化,浏览器也不会向服务器发送新的请求,而是只会在前端进行路由切换,从而实现单页应用的效果。

React Router 使用了浏览器的 hashchange 事件来监听 URL 的变化,并根据变化的 URL 匹配对应的路由组件进行渲染。当用户在应用程序中进行路由跳转时,React Router 会修改 URL 中的 hash 值,使得路由器组件能够检测到 URL 变化并进行相应的路由渲染。

不过,自 React Router v5.1.0 版本起,React Router 开始支持使用 HTML5 History API 来实现路由功能,这使得 React Router 可以使用更加现代的路由实现方式,例如使用 pushState 和 replaceState 方法来修改 URL,而不是使用 hash 符号。使用 HTML5 History API 可以使得 URL 更加美观、语义化,并且可以与服务器端路由更好地配合使用。

react的diff算法

React是一款流行的JavaScript库,它采用虚拟DOM(Virtual DOM)来提高性能。虚拟DOM是一个轻量级的JavaScript对象,它代表着真实DOM的结构和内容,可以在内存中进行操作,然后通过Diff算法比较虚拟DOM树的差异,并且最小化地更新真实DOM中需要更新的部分。

React中的Diff算法是一种高效的算法,它在比较两个虚拟DOM树的差异时,只比较同一层级的节点,而不会跨越层级进行比较。这种算法的时间复杂度是O(n),其中n是虚拟DOM树中节点的数量。

React的Diff算法主要有以下三个步骤:

  1. 比较两个根节点的类型

如果两个根节点的类型不同,React会销毁旧节点,并创建新节点。如果两个根节点的类型相同,则进入下一步比较。

  1. 比较两个节点的属性

如果两个节点的属性不同,React会更新节点的属性。如果两个节点的属性相同,则进入下一步比较。

  1. 比较子节点

在比较子节点时,React会采用以下策略:

  • 如果新节点没有子节点,React会销毁旧节点的子节点。
  • 如果旧节点没有子节点,React会创建新节点的子节点。
  • 如果新节点和旧节点都有子节点,React会使用Diff算法比较它们的差异,并更新只需要更新的部分。

在比较子节点时,React会使用key来标识节点的唯一性,从而避免出现不必要的更新。如果两个节点的key相同,则React会认为它们是同一个节点,不需要销毁和创建新节点。如果两个节点的key不同,则React会销毁旧节点,并创建新节点。如果两个节点的位置发生了改变,则React会将旧节点移动到新位置,而不是销毁和创建新节点。

在实际中,React还会使用一些优化策略来进一步提高Diff算法的性能,例如:

  • 对于文本节点,React会比较它们的内容,而不是像其他节点一样比较属性和子节点。
  • 对于同级的节点,React会尽可能地复用它们的父节点,从而减少对真实DOM的操作次数。
  • 对于列表节点,React会使用Key来标识每个节点,从而避免重新渲染整个列表。

总的来说,React的Diff算法是一种高效的算法,它可以快速比较虚拟DOM树的差异,并最小化地更新真实DOM中需要更新的部分。在实际中,我们可以通过合理地使用Key和避免不必要的更新来进一步提高Diff算法的性能。

react中给列表组件的每一项设置key值的作用

在React中,key是用来标识列表中每个元素的唯一性的。当我们在列表中添加、删除或移动元素时,React会使用key来判断哪些元素是新元素、哪些元素是已存在的元素、哪些元素被移动了位置,从而避免不必要的DOM操作,提高性能。

具体来说,React在进行列表渲染时,会为每个元素分配一个key。当我们对列表进行更新时,React会使用新的key和旧的key进行比较,从而判断哪些元素需要更新,哪些元素需要移动位置,哪些元素需要删除,哪些元素需要添加。

如果我们不使用key,React会默认使用列表中每个元素的索引作为key。但是,这种方式存在一些问题:

当我们在列表中添加或删除元素时,索引也会发生变化。这样会导致React错误地判断哪些元素需要更新、哪些元素需要移动位置,从而引发不必要的DOM操作。

因此,使用key来标识每个元素的唯一性是非常重要的。在使用key时,我们需要确保key是唯一的、稳定的、可预测的。通常,我们可以使用每个元素的id、名称或其他唯一标识作为key。如果没有唯一标识,我们可以使用列表中每个元素的索引作为key,但是需要注意当列表中的元素发生变化时,索引也会发生变化,可能会引发不必要的DOM操作。

npm包如何去兼容cjs跟esmodule

在 npm 包中,为了兼容 CommonJS 和 ES6 Module,我们可以在 package.json 文件中设置 "main" 字段和 "module" 字段。其中,"main" 字段指向 CommonJS 模块入口文件,"module" 字段指向 ES6 Module 入口文件。

例如,我们的 npm 包中有一个入口文件 index.js,它既支持 CommonJS,又支持 ES6 Module。那么,我们可以这样设置 package.json:

{
  "name": "my-package",
  "main": "index.js",
  "module": "index.mjs"
}

在这个例子中,"main" 字段指向 CommonJS 模块入口文件 index.js,"module" 字段指向 ES6 Module 入口文件 index.mjs。这样,当用户使用 CommonJS 引入包时,npm 会自动加载 index.js 文件;当用户使用 ES6 Module 引入包时,npm 会自动加载 index.mjs 文件。

需要注意的是,ES6 Module 目前还没有被所有的浏览器和 Node.js 版本完全支持,所以在编写 ES6 Module 时,需要特别注意兼容性问题。同时,我们还可以使用工具如 Babel 等来将 ES6 Module 转换成 CommonJS 模块,以提高兼容性。

webpack的作用

Webpack是一个现代化的JavaScript应用程序静态模块打包器,它的主要作用是将各种不同类型的文件(如JavaScript、CSS、HTML、图片等)打包成静态资源,以供前端应用程序使用。

具体来说,Webpack可以完成以下任务:

  1. 模块化管理:Webpack可以将前端应用程序拆分成多个模块,每个模块可以单独编写、测试、维护和复用,提高代码的可维护性和可重用性。
  2. 资源打包:Webpack可以将多个模块打包成一个或多个静态资源文件,以减少网络请求,提高页面加载速度。
  3. 加载器:Webpack支持使用各种不同类型的加载器,以便加载各种类型的文件,如CSS、图片等。这样,在应用程序中引入这些文件时,Webpack会自动使用对应的加载器将它们转换为合适的模块。
  4. 插件:Webpack支持使用各种不同类型的插件,以扩展Webpack的功能,如压缩、优化代码、生成HTML文件等。
  5. 开发环境和生产环境的构建:Webpack可以根据不同的环境,采用不同的打包方式和配置,以便在开发环境和生产环境中实现最佳的性能和开发体验。

总的来说,Webpack可以大大提高前端应用程序的开发效率和可维护性,同时还能优化应用程序的性能和用户体验。

如何加速代码构建过程

以下是一些提高代码在编译构建过程的效率的实践方法:

  1. 使用合适的编译工具:选择合适的编译工具可以提高编译过程的效率。例如,对于JavaScript应用程序,可以使用Webpack、Rollup等工具进行打包构建;对于CSS预处理器,可以使用Sass、Less等工具进行编译。
  2. 缩小文件范围:在编译构建过程中,只编译需要更新的文件可以提高编译速度,可以使用工具如Webpack的watch模式来实现。
  3. 使用缓存:通过使用缓存可以减少编译时间,例如,在Webpack中可以使用cache-loader、hard-source-webpack-plugin等插件实现缓存。
  4. 分离开发和生产环境:将开发环境和生产环境分开,可以提高编译速度。在开发环境中可以禁用一些生产环境特有的工具,如代码压缩、混淆等。
  5. 优化编译工具配置:合理的编译工具配置可以提高编译速度。例如,在Webpack中可以通过使用多线程和并行处理等方式来优化编译速度。
  6. 减少依赖:减少不必要的依赖可以减小编译过程中的负担,提高编译速度。
  7. 优化代码:编写高效的代码可以减少编译时间。

需要注意的是,提高代码在编译构建过程的效率并不是一次性的任务,而是需要不断的优化和调整。同时,提高编译速度也不能以牺牲代码质量和可维护性为代价,需要在保证代码质量和可维护性的前提下进行优化。此外,在优化编译速度的同时,还需要确保构建出的代码符合设计要求和业务需求,避免出现不必要的错误和问题。因此,在进行编译构建优化时,需要综合考虑多方面的因素,并根据具体情况进行调整和优化。

说一下 Tree-shaking

Tree-shaking是一种通过静态分析来消除JavaScript未被使用代码的技术。它的名称来自于构建工具将未被使用的代码"摇"掉,就像是一棵树上的枯枝被风吹落一样。

它的实现过程可以分为以下几个步骤:

  1. 分析模块依赖关系。Tree-shaking需要分析代码模块之间的依赖关系,以确定哪些模块是被使用的,哪些是未被使用的。对于ES6的模块系统,可以通过静态分析import和export语句来确定依赖关系。
  2. 标记被使用的代码。根据分析出的依赖关系,Tree-shaking会标记哪些代码是被使用的,哪些是未被使用的。被使用的代码会被保留,未被使用的代码会被剔除。
  3. 剔除未被使用的代码。根据标记的结果,Tree-shaking会将未被使用的代码从构建结果中删除,从而减小代码体积。

在进行Tree-shaking时,需要注意以下几点:

  1. Tree-shaking只能消除可以被静态分析的未使用代码,即那些不会被执行的代码。一些动态生成的代码(比如通过函数调用或者变量引用来执行的代码)、通过eval()函数动态执行的代码、以及一些需要被保留的副作用代码无法被Tree-shaking消除。
  2. Tree-shaking需要依赖于ES6模块系统或者类似的静态依赖关系系统,对于一些不支持ES6模块系统的代码,需要使用babel等工具将其转换为ES6模块系统。
  3. 在进行Tree-shaking时,需要仔细考虑代码的结构和依赖关系,以确保未被使用的代码能够被正确地消除。

Tree-shaking 的静态分析过程主要包括以下几个步骤:

  1. 代码解析:打包工具会通过自己的解析器将源代码转换成抽象语法树(AST)的形式,以方便后续的分析。
  2. 依赖分析:打包工具会遍历所有的模块,分析它们之间的依赖关系,以构建出一个依赖树。这个依赖树表示了代码模块之间的导入和导出关系。
  3. 标记未使用的代码:打包工具会对每个模块进行静态分析,标记出其中未被使用的代码。这些代码可能是模块的导出成员、局部变量或函数。
  4. 删除未使用的代码:打包工具会根据标记的信息,在最终的打包文件中删除未被使用的代码,从而减小打包文件的体积。

在进行静态分析时,打包工具会基于模块的静态结构来确定哪些代码是未被使用的。因此,如果代码的执行路径是动态的,那么 Tree-shaking 就无法准确地判断哪些代码是死代码,因此也就无法消除它们。

如果tree-shaking失败 - 从哪些角度去寻找原因

如果Tree-shaking失败,可能的原因有以下几个角度:

  1. 代码中存在副作用(Side Effect):Tree-shaking依赖于静态代码分析,只有无副作用的代码才可以被安全地删除。如果代码中存在具有副作用的代码(如I/O操作、修改全局变量、修改原型链等),那么Tree-shaking就无法准确地分析代码,可能会导致未使用的代码被保留下来。因此,需要检查代码中是否存在副作用。

  2. 代码中存在动态导入(Dynamic Import):Tree-shaking只能删除静态导入的代码,无法删除动态导入的代码。如果代码中存在动态导入的语句(如import()语句),那么Tree-shaking就无法准确地分析代码,可能会导致未使用的代码被保留下来。因此,需要检查代码中是否存在动态导入的语句。

  3. Webpack配置问题:Tree-shaking依赖于Webpack的配置,如果Webpack的配置不正确,就可能会导致Tree-shaking失败。可能的问题包括:

    (1)mode配置错误;

    (2)未开启optimization.minimize选项;

    (3)未开启optimization.usedExports选项;

    (4)未将babel-loader的cacheDirectory选项设置为true等。因此,需要检查Webpack的配置是否正确。

  4. 模块没有使用ES6模块规范:Tree-shaking只能删除使用ES6模块规范的代码,无法删除使用CommonJS或AMD规范的代码。如果代码中使用了CommonJS或AMD规范,那么Tree-shaking就无法准确地分析代码,可能会导致未使用的代码被保留下来。因此,需要检查代码中是否使用了ES6模块规范。

组件库按需导入与Tree-shaking

组件库按需导入的原理是利用模块化的特性,将组件库中每个组件都拆分为一个个独立的模块,然后根据用户需要动态地加载和渲染。这样可以避免一次性将整个组件库打包到应用程序中,从而减小应用程序的代码体积,提高应用程序的性能。

具体来说,按需导入的原理可以分为以下几个步骤:

  1. 用户在代码中按需导入需要使用的组件,例如 import { Button } from 'antd'
  2. 根据用户导入的组件名称,组件库会将对应的组件模块打包成一个独立的文件,并生成一个对应的导入语句,例如 import Button from 'antd/lib/button'
  3. 当应用程序需要使用这个组件时,会动态地加载这个组件模块,然后进行渲染。
  4. 组件库可以根据用户的需求动态地加载和卸载组件模块,从而实现按需导入。

需要注意的是,按需导入并不是 Tree-shaking,按需导入只是将组件库中的每个组件拆分为独立的模块,并根据用户需要进行动态加载和渲染,而 Tree-shaking 是通过静态分析来消除未被使用的代码,从而减小代码体积。

代码题

1. 将一个无序的包含大小写字母的字符串去重之后,按照字典序进行输出,例如'abfgAASc' -> 'abcfgs',并计算时间复杂度(这个很简单,自己实现)

2. 计算一颗二叉树的最大深度(这个很简单,自己实现)

3. 实现一个方法,控制请求的并发数
// 控制并发数
function controlConcurrency (urls, maxConcurrent, callback) {
    const results = []
    let index = 0
    let running = 0

    function next() {
        // 如果所有URL都已经请求完成,则执行毁掉函数
        if(index === urls.length && running === 0) {
            callback(results)
            return
        }

        // 如果当前正在运行的请求数量小于最大并发数,则发起新的请求
        while(running < maxConcurrent && index < urls.length) {
            const i = index++
            running++
            fetch(urls[i])
                .then(res => res.json())
                .then(data => {
                    results[i] = data
                    running--
                    next()
                })
                .catch(error => {
                    console.error(`Failed to fetch ${urls[i]}: ${error}`)
                    running--
                    next()
                })
        }
    }
    next()
}

4. 给出代码输出结果
let myObj = {
    name: 1,
    test1: function () {
        console.log(this)
        const test2 = () => {
            console.log(this)
            this.name = 2
        }
        test2()
    }
}
myObj.test1()
说一下上面代码的输出,以及myObj.name的值

愿大家在负重前行的路上依然能保持对生活的信心和热爱,共勉!!!