2024.04.20 更新前端面试问题总结(23 道题)

发布于:2024-05-10 ⋅ 阅读:(243) ⋅ 点赞:(0)

2024.04.15 - 2024.04.20 更新前端面试问题总结(23 道题)
获取更多面试相关问题可以访问
github 地址:
gitee 地址:

目录:

  • 初级开发者相关问题【共计 2 道题】

    • 712.html 的行内元素和块级元素的区别【热度: 796】【web 应用场景】【出题公司: TOP100 互联网】
    • 713.[css] 如何设置渐变色?【热度: 746】【CSS】【出题公司: TOP100 互联网】
  • 中级开发者相关问题【共计 12 道题】

    • 707.在表单校验场景中, 如何实现页面视口滚动到报错的位置【热度: 248】【web 应用场景】【出题公司: 小红书】
    • 708.介绍一下 scrollIntoView api【热度: 286】【web 应用场景】【出题公司: TOP100 互联网】
    • 709.Husky 和 lint-staged 有什么区别?【热度: 387】【工程化】【出题公司: 百度】
    • 710.[React] 在 react 项目开发过程中, 是否可以不用 react-router, 使用浏览器原生 history 路由来组织页面路由?【热度: 632】【web 框架、web 应用场景】【出题公司: TOP100 互联网】
    • 711.[React] react-router 和 原生路由区别【热度: 434】【web 框架、web 应用场景】【出题公司: TOP100 互联网】
    • 715.普通数据类型存储在哪里?堆还是栈【JavaScript】【出题公司: TOP100 互联网】
    • 716.对象深拷贝的原理和代码实现【热度: 892】【JavaScript】【出题公司: TOP100 互联网】
    • 720.express 里面的 中间件 和 插件, 是一个意思吗?【热度: 469】【Nodejs】【出题公司: 阿里巴巴】
    • 721.前端渲染和后端渲染各有啥优缺点, 为何现在技术大方向又逐渐往【后端渲染】方向靠了呢?【热度: 470】【Nodejs】【出题公司: 阿里巴巴】
    • 722.documentFragment api 是什么, 有哪些使用场景?【热度: 115】【web 应用场景】【出题公司: 腾讯】
    • 723.树结构查找, 实现一个函数, 通过 id 来查找 tree 数据结构对应的节点【热度: 867】【代码实现/算法】【出题公司: 百度】
    • 724.扁平数据通过 parent 关联, 实现扁平结构转嵌套 tree 结构【热度: 218】【代码实现/算法】【出题公司: 百度】
  • 高级开发者相关问题【共计 7 道题】

    • 704.如何一次性渲染十万条数据还能保证页面不卡顿【热度: 426】【web 应用场景】【出题公司: TOP100 互联网】
    • 705.虚拟混动加载原理是什么, 用 JS 代码简单实现一个虚拟滚动加加载。【热度: 354】【web 应用场景】【出题公司: 小红书】
    • 706.交叉观察器 API IntersectionObserver 详解【热度: 665】【web 应用场景】【出题公司: 小红书】
    • 714.不同标签页或窗口间的 【主动推送消息机制】 的方式有哪些?(不借助服务端)【热度: 401】【web 应用场景】【出题公司: Shopee】
    • 717.[React] 为何要自己实现调度器, 而不是直接使用 requestIdleCallback ?【热度: 236】【web 框架】【出题公司: 阿里巴巴】
    • 718.介绍一下 requestIdleCallback api【热度: 290】【web 应用场景】【出题公司: 阿里巴巴】
    • 719.[Vue] Vue2 中双向绑定是通过 Object.defineProperty() 实现的, 那么它是如何监控数组的?【热度: 447】【web 框架】【出题公司: 阿里巴巴】
  • 资深开发者相关问题【共计 2 道题】

    • 702.如何从 0 到 1 搭建前端基建【热度: 404】【工程化】【出题公司: TOP100 互联网】
    • 703.[webpack] 打包时 hash 码是如何生成的【热度: 167】【工程化】【出题公司: TOP100 互联网】

初级开发者相关问题【共计 2 道题】

712.html 的行内元素和块级元素的区别【热度: 796】【web 应用场景】【出题公司: TOP100 互联网】

关键词:行内元素、块级元素

HTML 中的行内元素(Inline elements)和块级元素(Block-level elements)在布局行为、外观以及如何参与文档流方面有所不同。以下是它们的主要区别:

特性 块级元素(Block-level elements) 行内元素(Inline elements)
布局 通常开始于新的一行 在同一行内水平排列
宽度 默认填满父容器宽度 宽度由内容决定
高度 可以设置高度 高度通常由内容决定
外边距(margin) 可以设置上下左右的外边距 只能设置左右外边距
内边距(padding) 可以设置上下左右的内边距 只能设置左右内边距
内容 可以包含其他块级或行内元素 通常包含文本或数据
堆叠方式 垂直堆叠 水平方堆放齐
盒模型 表现为完整的盒子模型 只表现部分盒子模型
换行 前后有换行空间 默认没有前后换行空间
常见标签 <div>, <p>, <section>, <h1> <span>, <a>, <strong>, <em>, <img>

即使块级元素和行内元素默认特征不同,你还是可以通过 CSS 的display属性来更改它们的行为。例如,display: inline;会让块级元素表现得像行内元素,并且它们将在其父容器的同一行内显示。另一方面,display: block;会让行内元素表现得像块级元素。

713.[css] 如何设置渐变色?【热度: 746】【CSS】【出题公司: TOP100 互联网】

关键词:设置渐变色

在 CSS 中,设置渐变色可以使用background属性和相应的渐变函数。CSS 提供两种类型的渐变:线性渐变(linear-gradient)和径向渐变(radial-gradient)。以下是如何分别设置这两种渐变色的示例。

线性渐变(Linear Gradient)

线性渐变是从一个点到另一个点的颜色过渡。它可以通过以下方式设置:

.element {
  background: linear-gradient(direction, color-stop1, color-stop2, ...);
}
  • direction:定义渐变的方向,可以是角度(如45deg)或预定义的关键词(如to bottom, to top, to right, to left)。
  • color-stop1color-stop2,...:渐变中颜色停止点,至少需要两个。

示例

.box {
  width: 200px;
  height: 200px;
  background: linear-gradient(to right, blue, red);
}

这个例子创建了一个从蓝色到红色的水平渐变。

径向渐变(Radial Gradient)

径向渐变是从一个中心点向外的颜色过渡。它可以通过以下方式设置:

.element {
  background: radial-gradient(shape size, color-stop1, color-stop2, ...);
}
  • shape:定义渐变的形状,可以是circleellipse
  • size:定义渐变的大小,可以是closest-corner, farthest-corner, closest-side, farthest-side,或者具体的长度值。
  • color-stop1color-stop2,...:同样表示渐变中的颜色停止点。

示例

.circle {
  width: 200px;
  height: 200px;
  background: radial-gradient(circle, white, yellow, red);
}

这个例子创建了一个圆形的径向渐变,从白色到黄色再到红色。

重复渐变(Repeating Gradients)

另外,CSS 中的渐变还可以设置为重复渐变,只需在渐变函数后面添加关键词repeating

.gradient {
  background: repeating-linear-gradient(to bottom, blue, white 20px, white 40px);
}

这个例子创建了一个向下的线性渐变,颜色从蓝色开始,在 20px 处变化为白色,并在 40px 处结束,然后重复该模式。

注意:各种渐变效果在不同的浏览器中可能需要添加特定的浏览器前缀

中级开发者相关问题【共计 12 道题】

707.在表单校验场景中, 如何实现页面视口滚动到报错的位置【热度: 248】【web 应用场景】【出题公司: 小红书】

关键词:滚动到页面视口

基本原理

页面是用户与程序进行交互的界面,在对应表单校验场景中,通常会因为有填写错误需要用户进行修改。为了提高用户体验,可以将页面滚动至对应表单报错的位置,使得用户立即可见错误并进行修改。这通常可以通过 JavaScript 编程实现。

要注意的是,实现滚动至错误表单,一般需要几个步骤:

  1. 记录表单元素的位置:在表单提交前的适当时间里记录所有表单元素的错误位置。
  2. 滚动到特定错误:错误发生时,滚动到第一个错误的表单元素位置。
  3. 优化:可为同一元素多次错误滚动优化,避免不必要的用户干扰。

以下是这些步骤的代码示例

HTML:

<form id="myForm" onsubmit="return false;">
  <input type="text" id="name" name="name" />
  <input type="text" id="age" name="age" />
  <!-- ... 其他表单元素 ... -->
  <button type="submit" onclick="handleValidation()">Submit</button>
</form>

JavaScript:

// 一个假设的表单验证函数
function validateInput(inputId) {
  // 调用此处的校验逻辑,返回是否存在错误
  // 这里以ID "inputId"来获取对应的DOM对象
  var el = document.getElementById(inputId);
  // 此处只是示例, 实际上应根据具体的校验逻辑返回一个布尔类型
  return el.value === "预期值";
}

function handleValidation() {
  var valid = true;

  ["name", "age"].forEach((key) => {
    // 进行校验判断
    if (!validateInput(key)) {
      console.error(`Validation failed for: ${key}`);

      // 标记校验失败
      valid = false;

      // 滚动到出现问题的元素位置
      var element = document.getElementById(key);
      element.scrollIntoView({ block: "center", behavior: "smooth" });

      // 增加一些提示效果, 比如错误边框, 可按需实现
      // element.classList.add('error-highlight');
    }
  });

  // 检查是否验证失败,如果失败则不提交表单
  return valid;
}

// 处理表单提交事件,与HTML中的onclick绑定
document.getElementById("myForm").addEventListener("submit", (e) => {
  e.preventDefault(); // 阻止表单默认提交行为
  handleValidation();
});

补充知识点 scrollIntoView

参考文档:

708.介绍一下 scrollIntoView api【热度: 286】【web 应用场景】【出题公司: TOP100 互联网】

关键词:scrollIntoView api

scrollIntoView 是一个 Web API,允许网页开发者控制元素如何被滚动到浏览器的可视区域。这个方法可以对任何 Element 使用,以改变窗口的滚动位置,以便最终元素位于屏幕可见范围内。它对于某些需要用户立即看到的表单错误、警告,或者在执行完某些操作后需要用户注意的元素比如通知提示尤为有用。

基本用法

element.scrollIntoView(smoothScrollingOptions);

参数说明

smoothScrollingOptions 是一个可选对象。当设定为 falseundefined 或一个计算值为 false 的值(比如 0)的时候,滚动操作将以最简单的方式立即执行,而不会平滑过渡。

当为 true 或者一个与滚动行为不冲突的对象时,浏览器会执行一个平滑的滚动动作,逐渐将元素滚动到视野内。

选项

该方法接受一个可选的 ScrollIntoViewOptions 对象,它包含以下属性:

  1. block: 描述元素应当在其块级方向上的对齐方式。可以是 "start""center""end""nearest" 中的一个。

    • start — 元素顶部与包含块的顶部对齐,只有块级元素会被滚动到这个位置。
    • center — 元素将尽可能被居中对齐地显示。
    • end — 元素底部将与包含块的底部对齐。
    • nearest — 元素将滚动到最近的边缘。
  2. inline: 描述在元素行进方向的对齐方式。同样可以是 "start""center""end""nearest" 中的一个。

  3. behavior: 描述滚动行为。设置为 "auto" 时将使用默认滚动,设置为 "smooth" 时将平滑滚动。

例子

document.getElementById("myElement").scrollIntoView({
  behavior: "smooth", // 平滑滚动
  block: "start", // 元素顶部与包含块顶部对齐
});

请注意,scrollIntoView 只能够使元素完全可见,但仍需留意元素灵感中其他内容可能超出视口之外。此外,滚动的方式也可能会受到 Web 浏览器和平台的不同而有所不同,例如一些浏览器可能有最高滚动速度的限制。

参考文档

709.Husky 和 lint-staged 有什么区别?【热度: 387】【工程化】【出题公司: 百度】

关键词:Husky 和 lint-staged、git hooks

Husky 和 lint-staged 都是与 Git 钩子 (hooks) 配合使用的 Node.js 库,但它们的用途和工作方式有所不同:

  1. Husky

    • Husky 是一个 Git 钩子管理器,它允许你触发自定义脚本在 git 事件发生时运行,如 pre-commit, pre-push, post-merge 等。
    • 它的主要目的是自动化你的版本控制工作流程,例如在提交 (commit) 前运行代码检查、格式化代码或执行测试,以确保代码库的质量和一致性。
  2. lint-staged

    • lint-staged 是一个运行在 Husky 钩子之上的工具,它专门用于对暂存区 (staged) 文件的检查。
    • 当你运行 git commit 并且 Husky 触发 pre-commit 钩子时,lint-staged 会检查你即将提交的代码(即 git add 后的文件列表),并运行你配置好的检查脚本,如代码格式化程序、linter 或其他工具。
    • 它的目的是确保在提交之前,只有没有检查错误的代码会被提交。

简而言之,Husky 是一个可以触发多种钩子事件的工具,而 lint-staged 是一种专门用于检查 Git 暂存区文件的工具。它们通常是配合使用的,因为 lint-staged 需要通过 Husky 来触发钩子。在你初始化项目并配置 CI/CD 流程时,通常会同时用到它们。

710.[React] 在 react 项目开发过程中, 是否可以不用 react-router, 使用浏览器原生 history 路由来组织页面路由?【热度: 632】【web 框架、web 应用场景】【出题公司: TOP100 互联网】

关键词:react router 路由、浏览器原生路由能力

作者备注
此问题实际上是在问:原生 路由 history api 和 react-router 的差距是啥, 或者说 react-router 做了啥

在 React 项目中,你完全可以不使用 react-router 而是使用浏览器原生的 history API 来手动管理路由。这通常会涉及以下几个步骤:

  1. 使用 history.pushState()history.replaceState() 方法来添加和修改浏览器历史条目。
  2. 侦听 popstate 事件来响应浏览器历史的变化。
  3. 根据当前的 URL 状态,手动渲染对应的 React 组件。

例如,下面是一个简单的例子,演示了如何在没有 react-router 的情况下使用原生 history API 来管理路由。

class App extends React.Component {
  componentDidMount() {
    // 当用户点击后退/前进按钮时触发路由变化
    window.onpopstate = this.handlePopState;
    // 初始页面加载时处理路由
    this.route();
  }

  handlePopState = () => {
    // 处理路由变化
    this.route();
  };

  route() {
    const path = window.location.pathname;
    // 根据 path 渲染不同的组件
    switch (path) {
      case "/page1":
        // 渲染 Page1 组件
        break;
      case "/page2":
        // 渲染 Page2 组件
        break;
      // 其他路由分支...
      default:
        // 渲染默认组件或404页面
        break;
    }
  }

  navigate = (path) => {
    // 更新历史记录并触发路由变化
    window.history.pushState(null, "", path);
    this.route();
  };

  render() {
    return (
      <div>
        <button onClick={() => this.navigate("/page1")}>Go to Page 1</button>
        <button onClick={() => this.navigate("/page2")}>Go to Page 2</button>
        {/* 这里根据路由渲染对应的组件 */}
      </div>
    );
  }
}

// 实际的页面组件
const Page1 = () => <div>Page 1</div>;
const Page2 = () => <div>Page 2</div>;

尽管手动管理路由是可能的,但使用 react-router 这类专门设计的库通常会大大简化路由管理的工作。它为路径匹配、路由嵌套、重定向等提供了便利的抽象,并且和 React 的声明式方式很好地集成在一起。如果不是为了特别的原因,通常推荐使用现成的路由库来管理 React 应用的路由,以避免重新发明轮子。

711.[React] react-router 和 原生路由区别【热度: 434】【web 框架、web 应用场景】【出题公司: TOP100 互联网】

关键词:react router 路由、浏览器原生路由能力

React Router 和浏览器原生 history API 在路由管理上主要有以下几个区别:

  1. 抽象级别:

    • React Router 提供了更高层次的抽象,如 <Router><Route>、和 <Link> 等组件,这些都是专门为了在 React 中更方便地管理路由而设计的。它处理了底层 history API 的很多细节,把操作抽象成了 React 组件和 hooks。
    • 原生 history API 更底层,直接作用于浏览器的历史记录栈。使用原生 history API 需要开发者自己编写更多的代码来管理 history 栈和渲染相应的组件。
  2. 便利性:

    • React Router 提供了声明式导航和编程式导航的选项,并且有大量的社区支持和文档,易于使用和学习。
    • 原生 history API 需要开发者自己处理 URL 与组件之间的关系映射,以及页面渲染的逻辑。
  3. 功能:

    • React Router 除了包含对原生 history API 的基本封装外,还提供了如路由守卫、路由懒加载、嵌套路由、重定向等高级功能。
    • 原生 history API 提供基本的历史记录管理功能,但是不包含上述 React Router 提供的高级应用路由需求。
  4. 集成:

    • React Router 是专为 React 设计的,与 React 的生命周期、状态管理等密切集成。
    • 原生 history API 与 React 没有直接关联,需要用户手动实现整合。
  5. 状态管理:

    • React Router 可以将路由状态管理与应用的状态管理(如使用 Redux)结合起来,使路由状态可预测和可管理。
    • 原生 history API 通常需要额外的状态管理逻辑来同步 UI 和 URL。
  6. 服务器渲染:

    • React Router 可以与服务器渲染一起使用,支持同构应用程序,即客户端和服务器都可以进行路由渲染。
    • 原生 history API 主要是针对客户端的,因此在服务器端渲染中需要额外的处理来模拟 routing 行为。

在考虑是否使用 React Router 或者原生 history API 时,通常需要考虑项目的复杂性、团队的熟悉度以及项目对路由的特定需求。对于大多数 React 项目而言,React Router 的便利性和其附加的高级特性通常使得它成为首选的路由解决方案。

表格对比

特性 React Router 原生 History API
抽象级别 高层次抽象,提供了组件和 hooks 底层 API,直接操作历史记录栈
便利性 声明式和编程式导航,社区支持和文档齐全 手动处理 URL 和组件映射,以及渲染逻辑
功能 路由守卫、懒加载、嵌套路由、重定向等 基本的历史记录管理
集成 与 React 生命周期和状态管理紧密集成 需要手动整合到 React 中
状态管理 与应用的状态管理系统(如 Redux)可集成,路由状态可预测和可管理 需要额外实现状态管理逻辑
服务器渲染 支持同构应用程序,客户端和服务器都能渲染 主要用于客户端,服务器端需要模拟
开发者工作量 由库处理大部分的路由逻辑,简化开发者工作 需要开发者手动编写代码管理路由
社区和资源 广泛的社区和资源,易于获取帮助和解决方案 相对较少的社区资源,通常需求独立解决
用户体验 通常能提供更顺畅的用户体验 可能因为实现不当导致的复杂性和用户体验问题

715.普通数据类型存储在哪里?堆还是栈【JavaScript】【出题公司: TOP100 互联网】

作者备注
这个问题没有任何价值, 不做热度评分, 当做科普吧。

在 JavaScript 中,普通数据类型的存储位置通常取决于它们的使用方式和上下文。以下是一些具体情况:

  1. 栈(Stack):当涉及到基本数据类型时(如数字、字符串、布尔值、null、undefined、和符号(Symbol)),它们通常存储在栈上。栈用于存储简单的数据结构和小数据量,因为它访问速度更快。

  2. 堆(Heap):对于复杂的数据结构,如对象、数组(尽管数组在某些情况下可能被视为特殊的对象)和函数的闭包,它们通常存储在堆上。堆用于存储可以动态分配和释放的复杂数据结构,并且比栈具有更大的容量和灵活性。

在 JavaScript 中,变量(无论是基本数据类型还是复杂数据类型)的存储位置是由 JavaScript 引擎来决定的,这个过程对开发者来说是透明的。对于基本数据类型的值,如果他们被用作较小的数据块,它们常常存储在栈上的;但是,如果基本数据类型被视为复杂结构的一部分(例如,多个字符串或数字组合成的一个复杂结构),那部分可能会存储在堆上。

还有值得注意的一点是,JavaScript 中的字符串优化。现代 JavaScript 引擎(如 V8,用于 Google Chrome 和 Node.js)对字符串的处理进行了优化,可能会在特定情况下将字符串存储在堆上,以更有效地处理长字符串或者频繁被修改的字符串。

总的来说,JavaScript 引擎会自动管理内存分配和回收,开发者一般不需要直接关注变量是存储在栈上还是堆上。相反,开发者更应该关注如何编写高效、可读和可维护的代码。

716.对象深拷贝的原理和代码实现【热度: 892】【JavaScript】【出题公司: TOP100 互联网】

关键词:对象什拷贝原理、避免循环引用和栈溢出

在 JavaScript 中,对象深拷贝指的是创建一个对象的副本,使得这个副本与原始对象不共享任何一个引用。这意味着,如果你修改了副本的属性,原始对象不会受到任何影响,反之亦然。

原理

在实现深拷贝时,有几个关键的概念需要理解:

  1. 值类型与引用类型:值类型(如数字、字符串和布尔值)直接存储数据的值,而引用类型(如对象、数组等)存储的是对一个内存地址的引用。

  2. 复制引用:如果你将一个对象赋值给一个新的变量,那么这个变量仅复制了对象的引用,而不是对象本身。因此,两个变量都指向同一个对象。

  3. 深度克隆:深拷贝则需要递归地复制对象中的每个属性,确保每个属性都是独立的副本,而不共享引用。

实现

实现对象的深拷贝有多种方式,以下是几种常见的实现方法:

1. JSON 方法

最简单的深拷贝方法之一是使用 JSON.stringify()JSON.parse()

function deepClone(value) {
  return JSON.parse(JSON.stringify(value));
}

但是,这种方法有局限性:

  • 它无法复制函数。
  • 它无法复制循环引用。
  • 它不会拷贝 undefined
  • 它无法处理特定属性(如 Symbol 属性、属性名为 Symbol 类型的属性等)。

2. 递归方法

你可以编写一个递归函数来复制每个属性:

function deepClone(value) {
  if (typeof value !== "object" || value === null) {
    return value; // 返回原始值类型
  }

  let result = Array.isArray(value) ? [] : {};
  for (let key in value) {
    // 使用 hasOwnProperty 检查以避免原型链中的键
    if (value.hasOwnProperty(key)) {
      // 递归复制每个属性值
      result[key] = deepClone(value[key]);
    }
  }
  return result;
}

这种方法的优点是它可以处理循环引用的拷贝,并且能够处理函数以外的所有类型的值。但它仍然有局限性,比如它不会拷贝对象的原型链。

补充进阶:避免循环引用和栈溢出的问题且支持拷贝原型链上的属性

为了避免循环引用和栈溢出的问题,我们可以在递归函数中加入一个缓存(通常是对象或 Map),来存储已经被拷贝过的引用类型对象。这样,当遇到一个已经被拷贝的引用类型时,我们可以使用缓存中的数据而不是再次进行拷贝。

下面是实现该思想的深拷贝函数示例:

function deepClone(value, map = new WeakMap()) {
  if (typeof value !== "object" || value === null) {
    return value; // 返回基本数据类型的值
  }

  // 检查是否为 Date、RegExp、Function 或循环引用
  if (value instanceof Date || value instanceof RegExp) {
    return value; // Created with built-in constructors, directly returned
  }

  // 如果 map 中已存在,则返回之前拷贝的对象,避免循环引用
  if (map.has(value)) {
    return map.get(value);
  }

  let result;
  if (Array.isArray(value)) {
    result = [];
    map.set(value, result);
    for (let i = 0; i < value.length; i++) {
      result[i] = deepClone(value[i], map); // 处理数组循环引用
    }
  } else {
    result = {};
    map.set(value, result);
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        result[key] = deepClone(value[key], map); // 递归复制每个属性
      }
    }
  }

  // 拷贝原型链上的属性
  // 根据需要可以取消以下注释
  // result.__proto__ = Object.getPrototypeOf(value);

  return result;
}

3. 使用第三方库

另一个选择是使用第三方库,如 Lodash,它提供了 _.cloneDeep 方法来实现深拷贝:

const _ = require("lodash");
const clone = _.cloneDeep(yourObject);

使用第三方库通常是最简单且最健壮的解决方案,因为它们已经考虑到了各种边缘情况,并包含了更高级的拷贝功能。

注意

无论选择哪种方法,都需要注意的是,深拷贝可能无法复制具有特定属性的对象,如:

  • Function 对象
  • Map 和 Set 对象
  • React 组件
  • 日期对象
  • 正则表达式对象
  • 以及一些其他通过构造函数创建的对象,可能会丢失它们的框架或库特定的属性或方法。

在实现深拷贝时,需要根据实际情况调整和选择使用的方法。

720.express 里面的 中间件 和 插件, 是一个意思吗?【热度: 469】【Nodejs】【出题公司: 阿里巴巴】

关键词:express 中间件、express 插件

在 Express.js 中,"中间件" 和 "插件" 这两个术语有时被交替使用,但实际上它们可能指向不同类型的组件,其差异取决于上下文。

中间件 (Middleware)

中间件是 Express 架构的核心部分,它是具有访问请求对象(req),响应对象(res),以及应用请求-响应循环中的下一个中间件的函数。中间件可以执行以下任务:

  1. 执行任何代码
  2. 对请求和响应对象做出更改
  3. 结束请求-响应循环
  4. 调用堆栈中的下一个中间件
  5. 如果当前是一个错误处理中间件,也可以调用 next 函数来跳过执行后续的请求处理中间件

中间件可以用来处理日志记录、用户认证、HTTP 方法限定、跨域资源共享(CORS)、请求体解析等。

示例代码:

app.use((req, res, next) => {
  // 这里是中间件逻辑
  next();
});

插件 (Plugins)

在 Node.js 和 Express 生态系统中,"插件" 通常指的是:

  1. 第三方库:它们不是 Express 的原生部分,但可以被集成到 Express 应用中来提供额外的功能。例如,morgan(日志记录中间件)、cors(处理 CORS 请求)等。

  2. Express 框架的扩展:某些特定的功能或一整套中间件,它们封装了一组特定的行为或应用结构,使之更容易复用于不同的项目中。

  3. 框架本身的一部分:在某些情况下,插件也可以是 Express 框架自身的功能模块或特性。

插件通常是由社区成员创建并维护的,它们可能遵从不同的 API 约定并且提供了比 Express 内置功能更特定的高级功能。

主要区别

  • 集成方式:中间件通常是独立功能的函数,可以在应用的任何地方被 usemiddlewareFunction 调用。插件则可能是更复杂的库,提供一系列中间件、错误处理或者服务级别的功能。

  • 功能范畴:中间件更侧重于 HTTP 请求的处理,通常与单个请求相关。插件则可能提供包括但不限于 HTTP 请求处理的更广泛的功能集。

  • 源码结构:中间件通常是单一功能的模块,而插件则可能是一个完整的包,包含了一个或多个中间件以及附加功能。

在实践使用中,一般不会严格区分中间件和插件,关键是理解它们提供的功能,以及如何将其集成到你的 Express 应用中。开发者通常根据自己的项目需求选择相应的中间件或插件来扩展 Express 应用的功能。

721.前端渲染和后端渲染各有啥优缺点, 为何现在技术大方向又逐渐往【后端渲染】方向靠了呢?【热度: 470】【Nodejs】【出题公司: 阿里巴巴】

关键词:前端渲染优缺点、后端渲染优缺点

前端渲染(Client-Side Rendering,CSR)和后端渲染(Server-Side Rendering,SSR)是两种不同的网页渲染策略,每种方法都有其固有的优势和劣势。近几年来,后端渲染之所以又开始受到重视,主要是由于它在某些方面更加适应了新的技术需求和趋势。

前端渲染 (CSR) 的优缺点:

优点

  1. 快速交互:应用首屏加载后,用户操作通常快速响应,因为交互主要发生在客户端。
  2. 利用缓存:前端渲染可以更有效地利用浏览器缓存,减轻服务器的负担。
  3. 性能优化:通过懒加载和代码分割,可以进行更精细的性能优化。
  4. SEO 优势:随着 JavaScript 框架对搜索引擎优化的重视,前端渲染页面也能够获得良好的搜索引擎排名。

缺点

  1. SEO 不友好:对于搜索引擎爬虫来说,JavaScript 生成的内容不容易被抓取,可能影响 SEO。
  2. 首屏加载时间:页面首次加载时,服务器仅发送 HTML 和 JavaScript,需等待所有脚本下载并执行后才能显示页面。
  3. 服务器端压力:对于交互式应用,每一种状态都需要请求新的页面或数据,增加服务器端的请求压力。

后端渲染 (SSR) 的优缺点:

优点

  1. SEO 友好:SSR 生成的 HTML 在初始请求时就存在,有利于爬虫索引。
  2. 首屏加载快:用户可以更快地看到完全渲染的页面,尤其对于移动和低宽带用户有明显优势。
  3. 减轻前端压力:不需要客户端强大的计算能力,减少了前端资源的限制。

缺点

  1. 服务器负载:每一次页面请求都要通过后端渲染,增加了服务器的负载。
  2. 交互延迟:交互通常需要进行额外的服务器请求,可能增加等待时间。
  3. 开发复杂性:SSR 应用需要在服务器和客户端上运行相同的代码库,增加了开发和维护的复杂性。

为何技术方向逐渐往后端渲染倾斜:

  1. SEO 重要性:由于 SEO 对于现代网站至关重要,而 SSR 提供了一种简单直接的方法来改善网页的搜索引擎友好性。

  2. 更好的用户体验:更快的首屏加载时间可以显著提升用户体验,尤其是在网络环境较差的地区。

  3. 渐进式增强:后端渲染的页面即使在 JavaScript 被禁用或浏览器不支持的情况下也能提供有用内容。

  4. 同构应用趋势:随着开发实践的进步,前端框架开始支持同构或通用 JavaScript 应用,开发者可以重用代码同时进行客户端和服务器渲染。

  5. 新兴框架的支持:诸如 Next.js、Nuxt.js、React Server Components 等现代框架和库开始提供内置的 SSR 支持,降低开发复杂性并提高渲染性能。

  6. 对抗加载时间问题:随着网络使用量的增长,尤其是在移动设备上,加载时间对于用户体验的影响变得更加显著。

  7. 企业级应用需求:面对内容量大、结构复杂的企业级应用,SSR 能够更有效地管理和展示数据。

总结来说,虽然前端渲染在交互性能和灵活性方面具有优势,但是后端渲染在 SEO、首屏加载时间和用户体验等方面显示出了其独特价值。随着现代框架的支持和最佳实践的普及,开发人员可以更加容易地实现后端渲染,这使它成为了许多项目的良好选择。

722.documentFragment api 是什么, 有哪些使用场景?【热度: 115】【web 应用场景】【出题公司: 腾讯】

关键词:documentFragment 概念、documentFragment 使用场景

DocumentFragment 是 Web API 中的一部分,它是 DOM (文档对象模型)的一个非常轻量级的节点,代表一组 DOM 节点的集合。它不是一个真实存在于 DOM 中的实体,因此被认为是“没有名字”的节点,或者说它不在文档的主体中渲染,通常用来作为临时的 DOM 节点仓库。

对于 DocumentFragment 的一部分内容,当它们在 DocumentFragment 之外操作时,并不会引起主 DOM 树的直接重排或重绘。然而,一旦你将整个 DocumentFragment 插入到 DOM 的一个永久节点上,那么在 DocumentFragment 内进行的更改将会触发 DOM 的重新渲染。

DocumentFragment API 有几个关键的特点和用途:

  1. 轻量级DocumentFragment 不会引起布局重排,因为其不是真实渲染的一部分。

  2. 节点集合:可以在 DocumentFragment 中节点集合进行分组,这个集合可以一次性插入到 DOM 的某一部分中。

  3. 性能优化:通过在一个 DocumentFragment 中构建好一大块 DOM 树,然后将它整体插入到主 DOM 中,从而减少重排次数,提高效率。

  4. 事件不冒泡:因为 DocumentFragment 不是真实渲染的一部分,所以它的事件不会冒泡到上层的 DOM 元素,除非它被插入到了 DOM 中。

使用场景

以下是一些使用 DocumentFragment 的常见场景:

  • 批量操作:当你想要一次性添加多个节点到 DOM 树中时,使用 DocumentFragment 可以将这些节点预先堆放在一个轻量级对象中,然后一次性添加。

  • 离屏操作:如果你需要创建复杂的 DOM 结构,可以通过 DocumentFragment 在不触发页面重排和重绘的情况下进行。

  • 内容填充:在填充 DOM 元素内容之前,可以先创建一个 DocumentFragment 完成所有节点的添加和排序,然后把它添加到 DOM 树中。

  • 避免内存泄漏:在某些情况下,它可以作为防止因移动节点而造成的内存泄漏的一个办法。

示例代码

// 创建 DocumentFragment
var fragment = document.createDocumentFragment();

// 创建多个节点或元素
var div = document.createElement("div");
var p = document.createElement("p");

// 将节点添加到 DocumentFragment 上
fragment.appendChild(div);
fragment.appendChild(p);

// 一次性将 DocumentFragment 添加到 DOM 的某个部分
var body = document.querySelector("body");
body.appendChild(fragment);

// 这时 div 和 p 被添加至 body 元素,而不会触发额外的布局重排。

DocumentFragment 提供了一个高效的方式去操作 DOM 而不影响页面的渲染性能,在很多需要进行批量 DOM 操作的场合非常有用。

723.树结构查找, 实现一个函数, 通过 id 来查找 tree 数据结构对应的节点【热度: 867】【代码实现/算法】【出题公司: 百度】

关键词:树结构查找

树结构查找, 实现一个函数, 通过 id 来查找 tree 数据结构对应的节点

题目如下

// 数据如下:
const tree = [
  {
    name: "数据1",
    id: 1,
    children: [
      {
        name: "数据2",
        id: 2,
        children: [
          {
            name: "数据3",
            id: 3,
            children:
              {
                name: "数据4",
                id: 4,
                children: [],
              },
            ],
          },
        ],
      },
    ],
  },
];

function findNodeById(tree, id) {
  // ....
}

const res = findNodeById(tree, 3);
// res 的结果为
//           {
//             name: "数据3",
//             id: 3,
//             children: [
//               {
//                 name: "数据4",
//                 id: 4,
//                 children: [],
//               },
//             ],
//           }

实现

function findNodeById(tree, id) {
  if (!tree.length) return null; // 如果树是空的,则返回 null

  const search = (node) => {
    if (node.id === id) {
      // 如果找到一个匹配的节点,返回它
      return node;
    } else if (node.children) {
      // 否则,如果它有子节点,递归地搜索子节点
      for (const child of node.children) {
        const result = search(child);
        if (result) {
          return result; // 如果递归找到了一个匹配的节点,返回它
        }
      }
    }
    return null; // 如果什么都没找到,返回 null
  };

  for (const root of tree) {
    const result = search(root);
    if (result) {
      return result; // 如果在根节点中找到了一个匹配的节点,返回它
    }
  }

  // 如果循环遍历整个树完成后没有找到,返回 null
  return null;
}

// 使用
const foundNode = findNodeById(tree, 3);
console.log(foundNode); // 将打印出 id 为 3 的节点

724.扁平数据通过 parent 关联, 实现扁平结构转嵌套 tree 结构【热度: 218】【代码实现/算法】【出题公司: 百度】

关键词:扁平结构转嵌套结构

题目

数据输入:
[
  { "name": "数据1", "parent": null, "id": 1 },
  { "name": "数据2", "id": 2, "parent": 1 },
  { "name": "数据3", "parent": 2, "id": 3 },
  { "name": "数据4", "parent": 3, "id": 4 },
  { "name": "数据5", "parent": 4, "id": 5 },
  { "name": "数据6", "parent": 2, "id": 6 }
]

数据输出:
[
  {
    "name": "数据1",
    "parent": null,
    "id": 1,
    "children": [
      {
        "name": "数据2",
        "id": 2,
        "parent": 1,
        "children": [
          {
            "name": "数据3",
            "parent": 2,
            "id": 3,
            "children": [
              {
                "name": "数据4",
                "parent": 3,
                "id": 4,
                "children": [
                  {
                    "name": "数据5",
                    "parent": 4,
                    "id": 5,
                    "children": []
                  }
                ]
              }
            ]
          },
          {
            "name": "数据6",
            "parent": 2,
            "id": 6,
            "children": []
          }
        ]
      }
    ]
  }
]

解法非常有意思, 自己好好体会

function listToTree(list) {
  const map = {},
    roots = [];

  // 首先将每个节点按照 id 存入 map
  for (const item of list) {
    map[item.id] = { ...item, children: [] };
  }

  for (const item of list) {
    if (item.parent === null) {
      // 顶级节点
      roots.push(map[item.id]);
    } else if (map[item.parent]) {
      // 非顶级节点,找到父节点并添加到其 children 数组中
      map[item.parent].children.push(map[item.id]);
    }
  }

  return roots;
}

const tree = listToTree(list);

高级开发者相关问题【共计 7 道题】

704.如何一次性渲染十万条数据还能保证页面不卡顿【热度: 426】【web 应用场景】【出题公司: TOP100 互联网】

原理其实就是 通过 requestAnimationFrame 实现分块儿加载。

requestAnimationFrame + fragment(时间分片)

既然定时器的执行时间和浏览器的刷新率不一致,那么我就可以用requestAnimationFrame来解决

requestAnimationFrame也是个定时器,不同于setTimeout,它的时间不需要我们人为指定,这个时间取决于当前电脑的刷新率,如果是 60Hz ,那么就是 16.7ms 执行一次,如果是 120Hz 那就是 8.3ms 执行一次

因此requestAnimationFrame也是个宏任务,前阵子面试就被问到过这个

这么一来,每次电脑屏幕 16.7ms 后刷新一下,定时器就会产生 20 个lidom结构的出现和屏幕的刷新保持了一致

const total = 100000;
let ul = document.getElementById("container");
let once = 20;
let page = total / once;

function loop(curTotal) {
  if (curTotal <= 0) return;

  let pageCount = Math.min(curTotal, once);

  window.requestAnimationFrame(() => {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerHTML = ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount);
  });
}

loop(total);

其实目前这个代码还可以优化一下,每一次appendChild都是新增一个新的li,也就意味着需要回流一次,总共十万条数据就需要回流十万次

此前讲回流的时候提出过虚拟片段fragment来解决这个问题

fragment是虚拟文档碎片,我们一次for循环产生 20 个li的过程中可以全部把真实dom挂载到fragment上,然后再把fragment挂载到真实dom上,这样原来需要回流十万次,现在只需要回流100000 / 20

const total = 100000;
let ul = document.getElementById("container");
let once = 20;
let page = total / once;

function loop(curTotal) {
  if (curTotal <= 0) return;

  let pageCount = Math.min(curTotal, once);

  window.requestAnimationFrame(() => {
    let fragment = document.createDocumentFragment(); // 创建一个虚拟文档碎片
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerHTML = ~~(Math.random() * total);
      fragment.appendChild(li); // 挂到fragment上
    }
    ul.appendChild(fragment); // 现在才回流
    loop(curTotal - pageCount);
  });
}

loop(total);

进阶: 如果做到极致的话, 可以考虑通过动态计算渲染的量, 一次性渲染多少。 会涉及到一些 长任务 等相关知识。 这部分可以参考:

参考文档

705.虚拟混动加载原理是什么, 用 JS 代码简单实现一个虚拟滚动加加载。【热度: 354】【web 应用场景】【出题公司: 小红书】

关键词:虚拟滚动、虚拟加载

原理

虚拟滚动(Virtual Scrolling)是一种性能优化的手段,通常用于处理长列表的显示问题。在传统的滚动加载中,当面对成千上万项的长列表时,直接在 DOM 中创建并展示所有项会导致严重的性能问题,因为浏览器需要渲染所有的列表项。而虚拟滚动的核心原理是仅渲染用户可视范围内的列表项,以此减少 DOM 操作的数量和提高性能。

实现虚拟滚动,我们需要:

  1. 监听滚动事件,了解当前滚动位置。
  2. 根据滚动位置计算当前应该渲染哪些列表项目(即在视口内的项目)。
  3. 只渲染那些项目,并用占位符(比如一个空的 div)占据其它项目应有的位置,保持滚动条大小不变。
  4. 当用户滚动时,重新计算并渲染新的项目。

基础版本实现

以下是一个简单的虚拟滚动实现的 JavaScript 代码示例:

class VirtualScroll {
  constructor(container, itemHeight, totalItems, renderCallback) {
    this.container = container; // 容器元素
    this.itemHeight = itemHeight; // 每个项的高度
    this.totalItems = totalItems; // 总列表项数
    this.renderCallback = renderCallback; // 渲染每一项的回调函数

    this.viewportHeight = container.clientHeight; // 视口高度
    this.bufferSize = Math.ceil(this.viewportHeight / itemHeight) * 3; // 缓冲大小
    this.renderedItems = []; // 已渲染项的数组

    this.startIndex = 0; // 当前渲染的开始索引
    this.endIndex = this.bufferSize; // 当前渲染的结束索引

    container.addEventListener("scroll", () => this.onScroll());
    this.update();
  }

  onScroll() {
    const scrollTop = this.container.scrollTop;
    const newStartIndex = Math.floor(scrollTop / this.itemHeight) - this.bufferSize / 2;
    const newEndIndex = newStartIndex + this.bufferSize;

    if (newStartIndex !== this.startIndex || newEndIndex !== this.endIndex) {
      this.startIndex = Math.max(0, newStartIndex);
      this.endIndex = Math.min(this.totalItems, newEndIndex);
      this.update();
    }
  }

  update() {
    // 清空已有内容
    this.container.innerHTML = "";

    // 计算并设置容器的总高度
    const totalHeight = this.totalItems * this.itemHeight;
    this.container.style.height = `${totalHeight}px`;

    // 渲染视口内的项
    const fragment = document.createDocumentFragment();
    for (let i = this.startIndex; i < this.endIndex; i++) {
      const item = this.renderCallback(i);
      item.style.top = `${i * this.itemHeight}px`;
      fragment.appendChild(item);
    }
    this.container.appendChild(fragment);
  }
}

// 创建一个列表项的函数
function createItem(index) {
  const item = document.createElement("div");
  item.className = "list-item";
  item.innerText = `Item ${index}`;
  item.style.position = "absolute";
  item.style.width = "100%";
  return item;
}

// 初始化虚拟滚动
const container = document.querySelector(".scroll-container"); // 容器元素需要预先在HTML中定义
const virtualScroll = new VirtualScroll(container, 30, 10000, createItem);

这个例子中,我们创建了一个VirtualScroll类,通过传入容器、项高度、总项数和渲染回调函数来进行初始化。该类的update方法用于渲染出当前可视范围内部分的项目,并将它们放到文档碎片中,然后一次性添加到容器中。这样可以避免多次直接操作 DOM,减少性能消耗。当滚动时,onScroll方法将计算新的startIndexendIndex,然后调用update方法进行更新。请注意,实际应用可能需要根据具体情况调整缓冲区大小等参数。

进阶版本:使用 IntersectionObserver 来实现

使用 IntersectionObserver 实现虚拟滚动就意味着我们会依赖于浏览器的 API 来观察哪些元素进入或离开视口(viewport),而非直接监听滚动事件。这样我们只需在需要时渲染或回收元素。

以下是一个简化版使用 IntersectionObserver 来实现虚拟滚动的例子:

class VirtualScroll {
  constructor(container, itemHeight, totalItems, renderItem) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.totalItems = totalItems;
    this.renderItem = renderItem;

    this.observer = new IntersectionObserver(this.onIntersection.bind(this), {
      root: this.container,
      threshold: 1.0,
    });

    this.items = new Map();

    this.init();
  }

  init() {
    // 填充初始屏幕的元素
    for (let i = 0; i < this.totalItems; i++) {
      const placeholder = this.createPlaceholder(i);
      this.container.appendChild(placeholder);
      this.observer.observe(placeholder);
    }
  }

  createPlaceholder(index) {
    const placeholder = document.createElement("div");
    placeholder.style.height = `${this.itemHeight}px`;
    placeholder.style.width = "100%";
    placeholder.dataset.index = index; // store index
    return placeholder;
  }

  onIntersection(entries) {
    entries.forEach((entry) => {
      const index = entry.target.dataset.index;
      if (entry.isIntersecting) {
        const rendered = this.renderItem(index);
        this.container.replaceChild(rendered, entry.target);
        this.items.set(index, rendered);
      } else if (this.items.has(index)) {
        const placeholder = this.createPlaceholder(index);
        this.container.replaceChild(placeholder, this.items.get(index));
        this.observer.observe(placeholder);
        this.items.delete(index);
      }
    });
  }
}

// Render item function
function renderItem(index) {
  const item = document.createElement("div");
  item.classList.add("item");
  item.textContent = `Item ${index}`;
  item.dataset.index = index;
  item.style.height = "30px"; // Same as your itemHeight in VirtualScroll
  return item;
}

// Example usage:
const container = document.getElementById("scroll-container"); // This should be a predefined element in your HTML
const itemHeight = 30; // Height of each item
const itemCount = 1000; // Total number of items you have

const virtualScroll = new VirtualScroll(container, itemHeight, itemCount, renderItem);

在这里我们创建了一个 VirtualScroll 类,构造函数接收容器元素、每个项的高度、总项目数和用于渲染每个项目的函数。我们在初始化方法中,为每个项目创建了一个占位符元素,并且向 IntersectionObserver 注册了这些占位元素。

当一个占位元素进入到视口中时,我们就会渲染对应的项,并且将它替换这个占位符。当一个项离开视口,我们又会将它替换回原来的占位符并取消它的注册。

这种方法的优势包括:

  • 不需要绑定滚动事件,防止滚动性能问题。
  • 浏览器会自动优化观察者的回调。
  • 不需要手动计算当前应该渲染的项目,当用户快速滚动时也不会遇到空白内容。

706.交叉观察器 API IntersectionObserver 详解【热度: 665】【web 应用场景】【出题公司: 小红书】

关键词:交叉观察器 API、IntersectionObserver 详解

IntersectionObserver API 是现代浏览器提供的一个强大的 API,用于性能友好地跟踪元素是否进入、离开或穿过另一个元素(通常是视口)的边界。这个 API 特别适用于执行懒加载、实现无限滚动、检测广告展示等功能,因为它避免了使用传统的滚动事件监听,后者可能会因频繁的计算和 DOM 操作导致性能问题。

如何使用 IntersectionObserver

  1. 创建一个IntersectionObserver实例: 创建一个IntersectionObserver的新实例,你需要提供一个回调函数,该函数会在目标元素与其祖先或视口交叉状态变化时被调用。此外,你还可以提供一个选项对象来定义观察的具体条件。

  2. 观察元素: 使用observe方法来指定一直观察的目标 DOM 元素。代表当这个 DOM 元素的显示与否达到某个条件时,你的回调函数将会被执行。

  3. 处理交叉事件: 当观察的元素进入或离开另一个元素时,为创建IntersectionObserver实例时指定的回调函数将会被调用。

  4. 停止观察: 使用unobserve方法可以停止观察特定元素。如果你已完成观察任务,使用disconnect方法将停止所有观察,释放资源。

示例代码:

以下是如何使用IntersectionObserver的示例:

// 创建一个回调函数,当观察的元素交叉进入或离开另一个元素时,该函数会被触发
const callback = (entries, observer) => {
  entries.forEach((entry) => {
    // 检查entry.isIntersecting属性
    if (entry.isIntersecting) {
      // 元素已进入视口
      console.log("Element is in the viewport!");
    } else {
      // 元素已离开视口
      console.log("Element is out of the viewport!");
    }
  });
};

// 创建IntersectionObserver实例
const options = {
  root: null, // 使用浏览器视口作为根
  rootMargin: "0px", // 根的外边距,类似于CSS的margin
  threshold: 1.0, // 目标完全可见时触发回调
};

const observer = new IntersectionObserver(callback, options);

// 开始观察目标元素
const target = document.getElementById("yourTargetElementId");
observer.observe(target);

// 停止观察目标元素
// observer.unobserve(target);

在这个示例中,当目标元素(idyourTargetElementId的元素)完全进入视口时,回调函数将被触发。root设为null意味着默认使用视口作为参照根元素。rootMargin设为0px表示根和目标的边界框触碰时回调就会被触发。threshold1.0,表示目标完全可见时回调会被触发。

注意事项

  • IntersectionObserver在性能上比传统的滚动事件检测方式有显著优势,因为它不依赖于JavaScript在主线程上的事件循环。
  • 使用时应当注意浏览器兼容性问题,对于不支持该 API 的旧浏览器,可能需要添加 polyfill 以保证功能的实现。

参考文档

714.不同标签页或窗口间的 【主动推送消息机制】 的方式有哪些?(不借助服务端)【热度: 401】【web 应用场景】【出题公司: Shopee】

关键词:不同页签信息主动推送

在不借助服务器端的帮助下,实现不同标签页或窗口间的主动推送消息机制,可以使用以下客户端技术:

作者备注:
这里要注意一下, 这里讨论的不是跨页签通信,而是跨页签主动推送信息 。如果仅仅是跨页签通信, 那么浏览器的本地存储都可以都可以使用了。 所以排除了本地存储类 API 的介绍

BroadcastChannel API:

作者备注
这个很有意思, 有一个文章, 国内某大佬复刻了《跨窗口量子纠缠粒子效果》就是用的 这个 API

BroadcastChannel API 是一种在相同源的不同浏览器上下文之间实现简单高效通信的方法。这意味着它可以在同一网站的多个标签页或窗口之间发送消息。这是由 HTML5 规范引入的,用于改进 Web Workers 中的通信方法。

下面是如何使用 BroadcastChannel API 的基本指南及几个示例。

创建与发送消息

// 在任何一个 tab 或 iframe 中创建一个广播频道
const channel = new BroadcastChannel("my-channel-name");

// 发送一个消息到频道
channel.postMessage("Hello from a tab!");

监听消息

// 监听这个频道的消息
channel.addEventListener("message", function (event) {
  if (event.data === "Hello from a tab!") {
    console.log("Message received: ", event.data);
  }
});

实现频道消息通信

假设你有两个标签页,并且你想更新每个标签页来显示另一个标签页中发生的事情,比如用户数量计数器:

// 在第一个标签页中
self.addEventListener("load", () => {
  const channel = new BroadcastChannel("visitor-channel");
  let visitorCount = 0;

  // 定时发送随机的用户活动消息
  setInterval(function () {
    visitorCount++;
    channel.postMessage(`Visitor count increased to: ${visitorCount}`);
  }, 5000);
});

// 在另一个标签页中
self.addEventListener("load", () => {
  const channel = new BroadcastChannel("visitor-channel");

  // 监听消息来更新用户数量
  channel.addEventListener("message", function (event) {
    if (event.data.startsWith("Visitor count")) {
      // 用接收到的用户数量更新显示
      updateVisitorCountDisplay(event.data);
    }
  });

  // 这个方法将设置标签页上的用户计数显示
  function updateVisitorCountDisplay(message) {
    // 这里写用于更新显示的代码
    console.log(message);
  }
});

在这个例子中,一个标签页通过定期发送新的消息来模拟用户活动的增加,这个消息在所有监听该频道的上下文中传递。另一个或多个标签页将监听这个频道来接收和响应这些更新。

注意事项:

  • 频道内的通信 仅在同源浏览器上下文(具有相同的协议、域名和端口号)之间有效,也就是说,不同的网站之间的通信是不被允许的,以保护每个网站的安全性。
  • 频道中的通信是 单向的,你可以通过频道向所有连接

Service Workers:

利用 Service Workers,各个标签页可以通过 clients.matchAll() 方法找到所有其他客户端(如打开的标签页),然后使用 postMessage 发送消息。

这个方法相比 BroadcastChannel 更加灵活,因为 Service Workers 可以通过 FocusNavigate 事件来控制页面的焦点和导航等。

ServiceWorkers 提供了在后台运行脚本的能力,这些脚本可以在网络受限或没有网络的情况下运行。当你用 ServiceWorkers 进行页面间的通信,你可以利用它们来推送消息到打开的 Clients(如浏览器标签页)。

要使用 ServiceWorkers 实现从不同 Tab 中主动推送信息,可以通过以下几个步骤:

1. 编写 ServiceWorker 文件

首先,创建名为 sw.js 的 ServiceWorker 文件。这个文件在你的网站目录下,会在用户访问网站时注册并激活。

// sw.js

self.addEventListener("message", (event) => {
  if (event.data === "New message from another tab") {
    self.clients
      .matchAll({
        type: "window",
        includeUncontrolled: true,
      })
      .then((windowClients) => {
        windowClients.forEach((client) => {
          client.postMessage("New message for " + client.id);
        });
      });
  }
});

2. 在主页面注册 ServiceWorker

在主页面(index.html)通过 JavaScript 注册这个 ServiceWorker 文件。

// index.html

if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("/sw.js")
    .then((registration) => {
      console.log("Service Worker registered with scope:", registration.scope);
    })
    .catch((error) => {
      console.log("Service Worker registration failed:", error);
    });
}

3. 监听 message 事件

在主页面使用 navigator.serviceWorker.controller 来检查是否已经有 ServiceWorker 主动控制。

if (navigator.serviceWorker.controller) {
  // Post a message to the ServiceWorker
  navigator.serviceWorker.controller.postMessage("This is from main page");
}

4. 从其他 Tab 推送消息

在其他 Tab 上,一旦 ServiceWorker 被该页面控制后,可以通过同样的 postMessage 方法发送消息。

SharedWorker:

SharedWorker 提供了一种更传统的跨文档通信机制,在不同文档间共享状态和数据。你需要创建一个 SharedWorker 对象,并在所有的文档里监听来自该 worker 的消息。

简单场景的 SharedWorker 的使用步骤:

  1. 创建和连接:
// 创建一个 SharedWorker,并指定要加载的脚本
var myWorker = new SharedWorker("worker.js");
// 开启端口通信
myWorker.port.start();
  1. 端口通信: 使用端口接收和发送消息
// 发送数据给worker
myWorker.port.postMessage({ command: "start", data: [1, 2, 3] });

// 监听来自worker的消息
myWorker.port.onmessage = function (event) {
  if (event.data) {
    console.log("Result from worker:", event.data);
  }
};
  1. 实现 worker 逻辑:

worker.js 内,通过 onconnect 事件监听端口连接,并在使用 postMessage 发送数据的页面之间转发消息。

// worker.js

// 自身的事件监听器
self.onconnect = function (event) {
  var port = event.ports[0];

  // 监听端口的消息
  port.onmessage = function (e) {
    if (e.data.command === "start") {
      var result = someHeavyComputation(e.data.data);
      port.postMessage({ result: result });
    }
  };
};

// 在这里执行一些开销较大的计算逻辑
function someHeavyComputation(data) {
  // 在这里进行计算...
  return data.reduce(function (previousValue, currentValue) {
    return previousValue + currentValue;
  }, 0);
}
  1. 通知其他页面更新:

当你希望基于上文提到的 SharedWorker 执行的计算结果通知其他所有的页面更新时,可以利用 SharedWorkerGlobalScope 中的 clients 对象。

// 在 worker.js 中

self.addEventListener("message", (e) => {
  if (e.data === "Update all clients") {
    // 遍历所有客户端
    self.clients.matchAll().then((clients) => {
      clients.forEach((client) => {
        // 发送消息更新它们
        client.postMessage("Please update your state");
      });
    });
  }
});

使用 localStorage 的变更监听

虽然 localStorage 没有直接提供跨标签页推送机制,但是可以使用 window.addEventListener('storage', listener) 监听 storage 事件,实现不同标签页间的通信。

// 标签页1修改了 localStorage
localStorage.setItem("someKey", "someValue");

// 其他标签页监听 storage 事件
window.addEventListener("storage", function (event) {
  if (event.storageArea === localStorage && event.key === "someKey") {
    console.log(event.newValue);
  }
});

使用 iframe 的 message 事件

如果排他性不是问题(所有标签页都属于同一客户端),可以使用 iframe 来传递消息,父窗口和 iframe 可以使用 DOM 中的 message 事件系统相互通信。

要使用 iframemessage 事件实现不同页签之间的通信,你需要两个关键项的配合:父页面和 iframe 页面之间的协调工作。这种通信非常灵活,因为你可以根据自己需要进行信息的发送和监听。

示例步骤:

1. 创建一个父页面

在父页面中,我们创建一个 iframe 并监听 message 事件。

<!-- parent.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Parent Page</title>
  </head>
  <body>
    <iframe src="iframe.html" style="display:none;"></iframe>

    <script>
      // 监听 iframe 发送的 message 事件
      window.addEventListener("message", function (event) {
        if (event.origin !== "http://example.com") {
          // 确保消息源是可信的
          return;
        }
        if (event.data && event.data.greeting) {
          console.log("Message received from iframe:", event.data);
          // 如果iframe向父页面问好(向父页面发送了一条消息)
          // 假设我们还想再向iframe发送一些信息
          document.querySelector("iframe").contentWindow.postMessage(
            {
              response: "Hello iframe! This is the parent window speaking.",
            },
            "http://example.com"
          );
        }
      });
    </script>
  </body>
</html>

2. 创建一个 iframe 页面

iframe.html 页面中,我们需要发送消息到父页面并监听父页面的消息。

<!-- iframe.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Iframe Page</title>
  </head>
  <body>
    <script>
      // 假设我们有一些需要发送到父页面的信息
      function sendMessageToParent() {
        parent.postMessage({ greeting: "Hello, I am the iframe!" }, "http://example.com");
      }

      // 当页面加载完成后,发送消息
      window.onload = function () {
        sendMessageToParent();
      };

      // 监听来自父页面的消息
      window.addEventListener("message", function (event) {
        if (event.origin !== "http://example.com") {
          // 反向验证消息源的可信度
          return;
        }
        if (event.data && event.data.response) {
          console.log("Message received from parent:", event.data);
          // 可根据消息实现特定的逻辑
        }
      });
    </script>
  </body>
</html>

717.[React] 为何要自己实现调度器, 而不是直接使用 requestIdleCallback ?【热度: 236】【web 框架】【出题公司: 阿里巴巴】

关键词:react 调度器 Scheduler、requestIdleCallback 使用场景

React 在性能优化方面的一个关键组件是调度器(Scheduler),它负责在渲染的过程中合理安排工作,以减少用户的等待时间以及避免单个任务占用过多的主线程时间,从而提高渲染性能。React 在 18.0 版本后引入了新的调度器机制,提供了更好的性能体验。

那么,为什么 React 不直接使用 requestIdleCallback 而要自己实现调度器呢?

  1. 控制精细度: React 需要比 requestIdleCallback 更高的控制精细度。requestIdleCallback 是基于浏览器的空闲时间进行调度的,而 React 调度器可以根据组件优先级、更新的紧急程度等信息,更精确地安排渲染的工作。

  2. 跨浏览器兼容性: requestIdleCallback 直到 2018 年才是浏览器中较普遍支持的 API。React 需要一个能够跨各个版本或框架的解决方案,以实现一致的性能体验。

  3. 时间切片: React 使用一种称为“时间切片”(time slicing)的技术,允许组件分布在多个帧中渲染以维持流畅的 UI。这依赖于 React 自己对任务和帧的精确控制,而不是依赖浏览器的 requestIdleCallback

  4. 更丰富的特性: React 调度器提供了比 requestIdleCallback 更丰富的特性和更加详细的调度策略,这包括:

    • Immediate 模式,用于同步渲染,当它是必需的时候。
    • User-blocking 模式,用于任务需要尽快完成,但能够容忍一定延迟,比如交互动画。
    • NormalLow 模式,用于不同优先级的更新。
  5. 复杂功能的实现: React 使用调度器实现某些特定的特性,比如:

    • Fiber 架构,允许 React 在类组件上实现 Concurrent 特性。
    • 在客户端渲染和服务器端渲染之间实现一致性。
  6. 优化生态工具: 对于 React 生态中的其他工具和实现(如 react-native、fast-refresh 等),它们可能需要特定或不同的调度策略。

  7. 未来兼容性: React 团队可以更好地在自己控制的调度器中实现未来的优化和特性,而不受浏览器 API 变更的影响。

最后,调度器是 React 架构中的一个重要部分,它让 React 能够实现更丰富和灵活的用户界面渲染逻辑。尽管 requestIdleCallback 可以被用来实现一些调度器的特性,但是完全使用它将限制 React 进一步优化的可能性,并迫使 React 依赖于浏览器的调度行为,这可能不符合 React 的长期发展和技术策略。

718.介绍一下 requestIdleCallback api【热度: 290】【web 应用场景】【出题公司: 阿里巴巴】

关键词:requestIdleCallback api、requestIdleCallback 使用场景

requestIdleCallback 是一个 Web API,它允许开发者请求浏览器在主线程空闲时执行一些低优先级的后台任务,这对于执行如分析、整理状态和数据等不紧急的任务是理想的。这种方法可以提高用户的响应性和页面的整体性能。

以下是 requestIdleCallback API 的一些关键特点:

何时使用 requestIdleCallback

requestIdleCallback 特别适合那些不直接关联用户交互及响应的任务,这些任务可以延后执行而不会明显影响用户体验。例如:

  • 清理工作:如标记的 DOM 节点删除、数据的本地存储同步等。
  • 非关键的解析:如解析大量数据。
  • 状态更新:如发送不紧急的状态变更。

如何使用 requestIdleCallback

使用 requestIdleCallback,你需要传递一个回调函数给它,此函数会在浏览器的空闲时间调用。你可以指定一个超时参数,它定义了浏览器在“空闲期”最多可以花费的时间来执行你的回调。

requestIdleCallback(myNonCriticalFunction, { timeout: 5000 });
  • myNonCriticalFunction: 这是你想要浏览器在空闲时间执行的函数。
  • timeout: 一个可选的参数,表示回调执行时间的上限(以毫秒为单位)。如果超时,浏览器可能在下次空闲机会进行执行。

回调函数参数

你的回调函数会接收到一个 IdleDeadline 对象作为参数,通常命名为 deadline。这个对象包含两个属性:

  • didTimeout - 一个布尔值,如果超时已经被触发为 true
  • timeRemaining - 返回当前空闲阶段剩余时间的函数,单位是毫秒。
function myNonCriticalFunction(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && someCondition()) {
    // 执行工作直到时间用完或下次更新不是必要的
  }

  // 如果还有未完成的工作,可以请求下一次空闲周期
  if (someCondition()) {
    requestIdleCallback(myNonCriticalFunction);
  }
}

注意事项

  • requestIdleCallback 不保证你的回调会在一个特定的时刻被调用,它只在浏览器需要的时候调用。
  • 执行低优先级任务时,不应该太过频繁或执行时间太长,以免影响页面性能。
  • 这个 API 为了最大化性能优化,会强制性地结束你的任务,在不迟于指定的超时时长执行结束。

Cross-Browser Compatibility (跨浏览器兼容性)

你可能需要 polyfills(垫片库)来确保 requestIdleCallback 的兼容性,因为它并不是在所有浏览器中都有原生支持。

使用 requestIdleCallback,开发者可以更好地利用浏览器的空闲序列来执行不紧急的任务,同时保持用户交互的流畅度。

参考文档

719.[Vue] Vue2 中双向绑定是通过 Object.defineProperty() 实现的, 那么它是如何监控数组的?【热度: 447】【web 框架】【出题公司: 阿里巴巴】

关键词:Vue2 中双向绑、Vue2 中双向绑监控数组

在 Vue 2 中,双向数据绑定的核心是 Object.defineProperty(),它允许 Vue 对每个属性进行 getter 和 setter 的拦截,从而实现响应式系统。对于普通的响应式属性来说,这一切都很简单,因为属性的 getter 和 setter 可以很容易地更改并通知 Vue 更新视图。

但是,由于 JavaScript 的限制,使用 Object.defineProperty() 注册响应式属性时并不能完美地跟踪数组索引的变化。而 Vue 需要能够捕捉对数组元素的修改,因此它采用了一种特殊的策略来实现对数组的响应式处理。

Vue 是如何监控数组的?

  1. 拦截数组的变异方法:Vue 使用一个数组的代理隔着来拦截七个变异数组方法(pushpopshiftunshiftsplice)和 sort 方法以及 reverse 方法。对这些方法的调用会被重新定义,以保证当它们被调用时,视图会重新渲染。

  2. Vue.set 和 Vue.delete:Vue 提供了两个全局函数 Vue.setVue.delete,这些函数确保任何对数组进行的直接设置或删除操作都能够触发视图更新。

  3. 不直接使用索引赋值:直接进行索引赋值操作(如 vm.items[indexOfItem] = newValue)不会触发视图更新,因为这是一种不能被 Object.defineProperty 拦截的操作。为了避免这个陷阱,你应该使用 Vue.set 代替索引赋值。

  4. 附加属性:Vue 会为每个项目的数组添加一些附加属性,这些属性可以触发一些视图渲染。

使用例子

以下是两个响应式数组操作的示例:

// 展示Vue.set的使用
Vue.set(vm.items, indexOfItem, newValue);

// 展示Vue.delete的使用
Vue.delete(vm.items, indexOfItem);

使用这些方法可以确保 Vue 的观察者模式能够检测到数组的变化,这点对于在循环中使用的内联数组是非常有用的。

注意事项

尽管 Vue 2 通过重新定义数组的变异方法和提供 Vue.setVue.delete 方法来实现对数组的响应式更新,但刀片开发人员在操作数组时还是需要谨慎以避免那些一开始就不会被 Vue 捕获的数组操作。

总结来说,Vue 对数组的响应式更新比对象要复杂,因为数组需要通过一种特殊的机制来捕获变异操作而不是普通的属性赋值。这是 Vue 响应式系统比较高级的部分,也是为什么在 Vue 2 中推荐使用 Vue 提供的方法来操作数组的核心原因。

资深开发者相关问题【共计 2 道题】

702.如何从 0 到 1 搭建前端基建【热度: 404】【工程化】【出题公司: TOP100 互联网】

关键词:前端基建

如何从 0 到 1 搭建前端基建

有一个非常经典的文章, 直接参考即可:

这里简单总结一下文章里面的要点

1.什么是基建?

01

2.为什么要做前端基建?

业务复用; 提升研发效率; 规范研发流程; 团队技术提升; 团队的技术影响力; 开源建设;

3.前端基建如何推动落地?

  • 要合适的同学(资源)
  • 要解决的问题(问题)
  • 要解决问题方案计划书(方案)
  • 要具体执行的步骤(执行)

技术基建四大特性(切记)

  • 技术的健全性
  • 基建的稳定性
  • 研发的效率性
  • 业务的体验性

4.前端基建都有什么?

  • 前端规范(Standard)
  • 前端文档(Document)
  • 前端项目模板管理(Templates)
  • 前端脚手架(CLI)
  • 前端组件库(UI Design)
  • 前端响应式设计 or 自适应设计
  • 前端工具库(类 Hooks / Utils)
  • 前端工具自动化(Tools)
  • 接口数据聚合(BFF)
  • 前端 SSR 推进
  • 前端自动化构建部署(CI/CD)
  • 全链路前端监控/数据埋点系统
  • 前端可视化平台
  • 前端性能优化
  • 前端低代码平台搭建
  • 微前端(Micro App)

703.[webpack] 打包时 hash 码是如何生成的【热度: 167】【工程化】【出题公司: TOP100 互联网】

关键词:webpack hash 码的生成

Webpack 在打包过程中生成 hash 码主要用于缓存和版本管理。主要有三种类型的 hash 码:

  1. hash:是和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改。这意味着任何一个文件的改动都会影响到整体的 hash 值。

  2. chunkhash:与 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值。例如,如果你的配置生成了多个 chunk(例如使用了 code splitting),每个 chunk 的更新只会影响到它自身的 chunkhash。

  3. contenthash:根据文件内容来定义 hash,内容不变,则 contenthash 不变。这在使用诸如 CSS 提取到单独文件的插件时特别有用,因此只有当文件的内容实际改变时,浏览器才会重新下载文件。

生成方式:

  • hash 和 chunkhash 主要是通过某种 hash 算法(默认 MD5)来对文件名或者 chunk 数据进行编码。
  • contenthash 是通过构建时的 webpack 插件(如 mini-css-extract-plugin)来处理的,它会对文件内容进行 hash。

Hash 码的生成可以被 webpack 配置的 hashFunction,hashDigest,hashDigestLength 等选项影响。例如,你可以选择不同的算法如 SHA256 或者 MD5,以及可以决定 hash 值的长度。

在 webpack 的配置文件中,可以通过如下方式设定 hash:

output: {
  filename: '[name].[chunkhash].js',
  path: __dirname + '/dist'
}

这会将输出的文件名设置为入口名称加上基于每个 chunk 内容的 hash。在使用 webpack-dev-server 或者 webpack --watch 时,不会生成实际的文件,所以这些 hash 值是在内存中计算并关联的。


网站公告

今日签到

点亮在社区的每一天
去签到