2023.10.02 更新前端面试问题总结(24道题)

350 阅读51分钟

2023.09.16 - 2023.10.02 更新前端面试问题总结(24道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…

目录:

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

    • 562.JavaScript 和 BOM、DOM 、ECMAScript、Nodejs 之间是什么关系【JavaScript】【出题公司: 阿里巴巴】
    • 567.实现一个函数, 计算两个日期之间的天数差【代码实现/算法】【出题公司: 网易】
    • 570.Object 对象有哪些场景 api ?【热度: 509】【JavaScript】【出题公司: 网易】
  • 中级开发者相关问题【共计 11 道题】

    • 563.JavaScript 如何做内存管理?【热度: 603】【JavaScript】【出题公司: 网易】
    • 565.CSS 如何实现文本溢出?【热度: 382】【CSS】【出题公司: Shopee】
    • 566.https 如何保证安全的?【热度: 782】【网络】【出题公司: 网易】
    • 568.实现日期格式化 format 函数【热度: 489】【代码实现/算法】【出题公司: 网易】
    • 569.前端有哪些跨页面通信方式?【热度: 791】【web应用场景】【出题公司: 京东】
    • 573.axios 有哪些特性?【热度: 464】【网络】【出题公司: 阿里巴巴】
    • 574.axios 是如何区分是 nodejs 环境还是 浏览器环境 的?【热度: 113】【网络】【出题公司: 阿里巴巴】
    • 576.[Vue] 为什么 data 属性是一个函数而不是一个对象?【热度: 448】【web框架】【出题公司: Shopee】
    • 579.[Vue] 中为何不要把 v-if 和 v-for 同时用在同一个元素上, 原理是什么?【热度: 546】【web框架】【出题公司: 网易】
    • 581.[React] 构建组件的方式有哪些【热度: 482】【web框架】【出题公司: 腾讯】
    • 582.[React] Class Components 和 Function Components 有区别?【热度: 229】【web框架】【出题公司: 腾讯】
  • 高级开发者相关问题【共计 10 道题】

    • 564.JavaScript 中, 隐藏类是什么概念?【热度: 146】【JavaScript】【出题公司: 网易】
    • 571.[Vue] 动态给 data 添加一个新的属性时会发生什么【热度: 164】【web框架】【出题公司: Shopee】
    • 572.如何拦截 web 应用的请求【热度: 487】【网络】【出题公司: 快手】
    • 575.XHR 和 Fetch 是否支持取消请求【热度: 122】【网络】【出题公司: 阿里巴巴】
    • 577.SPA首屏加载速度慢的怎么解决【热度: 868】【工程化】【出题公司: 网易】
    • 578.将静态资源缓存在本地的方式有哪些?【热度: 584】【工程化】【出题公司: 网易】
    • 580.[React] 如何实现vue 中 keep-alive 的功能?【热度: 255】【web框架】【出题公司: 网易】
    • 583.[React] 高阶组件理解多少?【热度: 655】【web框架】【出题公司: 腾讯】
    • 584.[React] 从 React 层面上, 能做的性能优化有哪些?【热度: 1,005】【web框架】【出题公司: 美团】
    • 585.[React] 是如何进行渲染的?【热度: 623】【web框架】【出题公司: 阿里巴巴】

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

562.JavaScript 和 BOM、DOM 、ECMAScript、Nodejs 之间是什么关系【JavaScript】【出题公司: 阿里巴巴】

ECMAScript

ECMAScriptJavaScript的标准化规范,它定义了JavaScript的语法、数据类型、函数、控制流等。ECMAScript最早在1997年发布,由欧洲计算机制造商协会(ECMA)负责制定和维护。

ECMAScript的目的是为了确保不同厂商的JavaScript实现在语法和行为方面保持一致性,以便开发者能够轻松地编写跨平台、跨浏览器的JavaScript代码。标准化的ECMAScript 规范使得开发者可以在不同的JavaScript环境中编写相同的代码,而不必担心语法差异和行为不一致性。

ECMAScript规范每年进行一次更新,新版本通常包含了新的语法特性、API和改进。在每个ECMAScript 版本发布之前,由各个浏览器厂商先行实现并测试新特性,然后将其添加到浏览器中。这就是为什么不同浏览器可能对同一版本的ECMAScript支持程度不同的原因。

常见的ECMAScript版本包括ES5(2009年发布)、ES6(2015年发布,也被称为ES2015)、ES7(2016年发布,也被称为ES2016)等。每个版本都引入了新的语法和功能,使得JavaScript 变得更加强大和灵活。开发者可以根据目标浏览器的支持情况选择使用不同版本的ECMAScript特性。

JavaScript是一种高级编程语言,用于为网页添加交互和动态功能。它实现了ECMAScript标准,该标准定义了JavaScript的语法、数据类型、函数、控制流等。JavaScript 是一种解释性脚本语言,代码在运行时由浏览器解析和执行。

BOM(Browser Object Model) 是浏览器对象模型,它提供了与浏览器交互的API。BOM并不是ECMAScript的一部分,而是浏览器厂商自行实现的一组对象和方法。通过BOM ,开发者可以操作浏览器窗口、解析URL、发送HTTP请求、控制浏览器历史记录等。其中最常见的BOM对象是window对象,它代表了浏览器的窗口或框架。

DOM(Document Object Model) 是文档对象模型,它定义了用于访问和操作HTML、XML等文档的API。DOM提供了一组对象和方法,用于表示文档的结构和内容。通过DOM ,开发者可以通过JavaScript动态地创建、修改和删除HTML元素,修改样式和属性,处理事件等。DOM也不是ECMAScript的一部分,而是由浏览器厂商实现的标准。

Node.js是一个基于V8引擎的JavaScript运行时环境,使JavaScript可以在服务器端运行。与浏览器中的JavaScript不同,Node.js 提供了一组基于事件驱动的API,用于构建高性能和可伸缩的网络应用程序。Node.js可以执行文件操作、网络通信、数据库访问等服务器端任务,并且可以通过包管理器npm安装和管理第三方模块。

总结来说,JavaScript是一种编程语言,实现了ECMAScript标准。BOMDOM是浏览器提供的API,用于与浏览器交互并操作文档。Node.js是一个独立的运行时环境,使JavaScript 可以在服务器端运行,并提供了一组用于构建网络应用程序的API。

567.实现一个函数, 计算两个日期之间的天数差【代码实现/算法】【出题公司: 网易】

以下是使用JavaScript实现计算两个日期之间的天数差的函数:

function calculateDateDifference(date1, date2) {
  // 将日期字符串转换为 Date 对象
  const d1 = new Date(date1);
  const d2 = new Date(date2);

  // 计算两个日期的时间差(毫秒数)
  const timeDiff = Math.abs(d2.getTime() - d1.getTime());

  // 将时间差转换为天数
  const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));

  return daysDiff;
}

// 示例用法
const date1 = '2022-01-01';
const date2 = '2022-01-10';

const difference = calculateDateDifference(date1, date2);
console.log(difference); // 输出结果为 9

上述函数首先将两个日期字符串转换为Date对象,然后计算两个日期对象之间的时间差(以毫秒表示),最后将时间差转换为天数。通过调用calculateDateDifference函数,可以获取两个日期之间的天数差。

570.Object 对象有哪些场景 api ?【热度: 509】【JavaScript】【出题公司: 网易】

关键词:Object对象api

方法/属性描述
Object.keys(obj)返回一个由给定对象的所有可枚举自身属性的名称组成的数组
Object.values(obj)返回一个给定对象所有可枚举属性值的数组
Object.entries(obj)返回一个给定对象自身可枚举属性的 [key, value] 数组
Object.assign(target, ...sources)将一个或多个源对象的可枚举属性复制到目标对象,并返回目标对象
Object.create(proto, [propertiesObject])使用指定的原型对象和属性创建一个新对象
Object.defineProperty(obj, prop, descriptor)定义对象中的新属性或修改现有属性的配置
Object.getOwnPropertyDescriptor(obj, prop)返回指定对象上一个自有属性对应的属性描述符
Object.freeze(obj)冻结一个对象,使其属性无法修改、添加或删除
Object.is(value1, value2)判断两个值是否相同
Object.seal(obj)封闭一个对象,防止向对象添加新属性,但允许修改或删除现有属性
Object.getPrototypeOf(obj)返回指定对象的原型(__proto__
Object.setPrototypeOf(obj, proto)设置指定对象的原型(__proto__

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

563.JavaScript 如何做内存管理?【热度: 603】【JavaScript】【出题公司: 网易】

关键词:JavaScript内存管理

JavaScript中的内存管理是由垃圾收集器负责的。垃圾收集器会自动追踪不再使用的对象,并在适当的时候释放它们占用的内存。

JavaScript的垃圾收集器使用了一种称为"标记-清除"(mark and sweep)的算法来确定哪些对象是不再需要的。该算法通过标记所有被引用的对象,然后清除未被标记的对象。

以下是JavaScript中的一些内存管理的原则和技巧:

  1. 自动内存管理:JavaScript的垃圾收集器会自动管理内存,不需要手动释放内存。你只需确保不再使用的对象没有被引用,垃圾收集器会在适当的时候自动回收内存。
  2. 避免全局变量:全局变量会一直存在于内存中,直到页面关闭。尽量减少使用全局变量,而是使用函数作用域或模块化的方式来限制变量的作用范围。
  3. 及时释放引用:当你不再需要一个对象时,最好将对它的引用设置为null,这样可以使垃圾收集器更早地释放对象所占用的内存。
  4. 避免循环引用:如果对象之间存在循环引用,即使它们已经不再被使用,垃圾收集器也不会自动释放它们。确保及时断开循环引用,使垃圾收集器能够正确地回收内存。
  5. 避免大量对象的创建和销毁:频繁地创建和销毁大量对象会导致垃圾收集器频繁地执行,影响性能。如果可能的话,尽量重用对象,而不是频繁地创建和销毁它们。

虽然JavaScript的垃圾收集器自动管理内存,但仍然需要开发人员编写高效的代码来避免内存泄漏和浪费,以确保JavaScript应用程序的性能和可靠性。

565.CSS 如何实现文本溢出?【热度: 382】【CSS】【出题公司: Shopee】

关键词:文本溢出样式

单行文本溢出

在CSS中,可以使用text-overflow属性来实现单行文本的溢出省略样式。同时,还需要设置white-space属性为nowrap,使文本不换行,以及overflow属性为hidden,隐藏溢出的文本。

以下是一个示例:

.ellipsis {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

然后,在HTML中,可以将这个类应用到指定的元素上:

<p class="ellipsis">这是一段很长的文本,如果超过指定的宽度,就会显示省略号。</p>

这样,如果文本超过了指定的宽度,就会自动显示省略号。


多行文本溢出

CSS中没有直接的属性可以实现省略样式。但是,可以使用一些技巧来实现多行文本的省略样式。其中一种常用的方法是使用-webkit-line-clamp属性和-webkit-box-orient 属性来限制显示的行数,并且设置display属性为-webkit-box以创建一个块级容器。

以下是一个示例:

.ellipsis-multiline {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; /* 设置显示的行数 */
    overflow: hidden;
    text-overflow: ellipsis;
}

然后,在HTML中,将这个类应用到指定的元素上:

<div class="ellipsis-multiline">
  这是一个多行文本的示例,如果文本内容超过了指定的行数,就会显示省略号。这是一个多行文本的示例,如果文本内容超过了指定的行数,就会显示省略号。这是一个多行文本的示例,如果文本内容超过了指定的行数,就会显示省略号。
</div>

请注意,-webkit-line-clamp属性只在某些WebKit浏览器中(如Chrome和Safari)支持。在其他浏览器中,可能需要使用其他解决方案来实现多行文本的省略样式。

566.https 如何保证安全的?【热度: 782】【网络】【出题公司: 网易】

关键词:https 安全性

HTTPS相比HTTP更安全的原因主要有以下几点:

  1. 数据传输加密:HTTPS使用SSL/TLS协议对数据进行加密传输,通过使用对称密钥加密传输数据,并使用非对称密钥进行身份验证和密钥交换。这意味着即使攻击者截获了数据包,也无法解密其中的内容。
  2. 身份验证和数据完整性:HTTPS使用数字证书对网站进行身份验证,确保用户连接的是正确的网站。同时,数字证书也用于确保数据的完整性,以防止数据在传输过程中被篡改。
  3. 防止中间人攻击:HTTPS通过使用公钥加密和数字签名等技术,可以防止中间人攻击。中间人攻击是指攻击者在用户与服务器之间插入自己的恶意代理,在两者之间进行通信并窃取敏感信息。
  4. HTTP头隐私保护:HTTPS可以提供对HTTP头信息的隐私保护,防止攻击者通过分析HTTP头信息获取用户的敏感信息。

HTTPS通过数据加密、身份验证和数据完整性保护等机制,提供了更高的安全性,能够有效防止数据被窃取、篡改和中间人攻击等风险。相比之下,HTTP是明文传输,不具备这些安全保护机制。因此,对于需要保护用户隐私和防止数据被攻击的网站,使用HTTPS是更安全的选择。

568.实现日期格式化 format 函数【热度: 489】【代码实现/算法】【出题公司: 网易】

关键词:日期format函数、日期format实现

问题

// js 实现日期的  format 函数
//
// YYYY 对应年
// MM 对应月
// DD 对应日
//
// HH 对应 24 小时制度
// hh 对应 12 小时制度
// mm 对应分钟
// ss 对应秒

const date = new Date();
const formattedDate = date.format('YYYY-MM-DD HH:mm:ss');
console.log(formattedDate); // 输出结果为当前日期和时间的格式化字符串

解答 以下是使用JavaScript实现日期格式化的format函数:

Date.prototype.format = function(format) {
  const date = this;

  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const hours = date.getHours();
  const minutes = date.getMinutes();
  const seconds = date.getSeconds();

  format = format.replace('YYYY', year);
  format = format.replace('MM', month.toString().padStart(2, '0'));
  format = format.replace('DD', day.toString().padStart(2, '0'));
  format = format.replace('HH', hours.toString().padStart(2, '0'));
  format = format.replace('hh', (hours % 12).toString().padStart(2, '0'));
  format = format.replace('mm', minutes.toString().padStart(2, '0'));
  format = format.replace('ss', seconds.toString().padStart(2, '0'));

  return format;
};

// 示例用法
const date = new Date();
const formattedDate = date.format('YYYY-MM-DD HH:mm:ss');
console.log(formattedDate); // 输出结果为当前日期和时间的格式化字符串

上述代码中,我们通过在Date对象的原型上定义format函数,使得所有的Date对象都可以调用format函数进行日期格式化。在函数内部,我们使用getFullYeargetMonthgetDate 等方法获取日期的年、月、日、时、分、秒的值,并将其替换到传入的format字符串中对应的占位符。最后返回格式化后的字符串。

569.前端有哪些跨页面通信方式?【热度: 791】【web应用场景】【出题公司: 京东】

在前端中,有多种跨页面通信的方式,下面列举了其中一些常见的方式:

  1. 使用URL参数:可以通过URL参数在不同页面之间传递数据。例如,可以在URL中添加查询字符串参数来传递数据,并通过解析URL参数来获取传递的数据。
  2. 使用localStorage或sessionStorage :可以使用浏览器的本地存储(localStorage或sessionStorage)在不同页面之间共享数据。一个页面可以将数据存储在本地存储中,另一个页面可以读取该数据。
  3. 使用Cookies:可以使用Cookies在不同页面之间共享数据。一个页面可以将数据存储在Cookie中,另一个页面可以读取该Cookie。
  4. 使用postMessage API:postMessage API允许不同窗口或iframe之间进行跨页面通信。可以使用postMessage发送消息,接收方可以通过监听message事件来接收消息。
  5. 使用Broadcast Channel API:Broadcast Channel API允许不同页面或不同浏览器标签之间进行广播式的消息传递。可以使用Broadcast Channel发送消息,其他订阅同一频道的页面都可以接收到消息。
  6. 使用Shared Worker:Shared Worker是一种特殊的Web Worker,可以在多个页面之间共享。可以通过Shared Worker进行通信和共享数据。
  7. 使用WebSocket:WebSocket是一种双向通信协议,可以在不同页面之间建立持久的连接,实现实时的跨页面通信。

以上是一些常见的跨页面通信方式,选择适合自己需求的方式来实现跨页面通信。

573.axios 有哪些特性?【热度: 464】【网络】【出题公司: 阿里巴巴】

关键词:axios特性

直接可以参考官网链接: axios-http.com/docs/intro

特点

  • 从浏览器创建XMLHttpRequest

  • 从node.js生成http请求

  • 支持 Promise API

  • 拦截请求和响应

  • 转换请求和响应数据

  • 取消请求

  • 超时时间

  • 支持嵌套项的查询参数序列化

  • 自动请求体序列化为:

    • JSON (应用程序/ison)
    • 多部分/表格数据 (多部分/表格数据)
    • URL编码形式 (申请书/x-www-form-urlencoded )
  • 以JSON格式发布HTML表单

  • 响应中的自动JSON数据处理

  • 为浏览器和node.js捕获进度,附带额外信息 (速度、剩余时间)

  • 设置node.is的带宽限制

  • 兼容符合规范的FormData和Blob (包括节点) js)

  • 客户端对XSRF的保护支持

574.axios 是如何区分是 nodejs 环境还是 浏览器环境 的?【热度: 113】【网络】【出题公司: 阿里巴巴】

关键词:nodejs与浏览器环境判定

Axios 是一个跨平台的 HTTP 客户端库,可以在浏览器和 Node.js 中使用。Axios 通过判断当前环境来确定是在浏览器还是在 Node.js 环境中运行。

在浏览器环境中,Axios 默认会使用浏览器提供的 XMLHttpRequest 对象来发送 HTTP 请求。

在 Node.js 环境中,Axios 会检查是否存在 process 全局对象,以及 process 对象中是否存在 nextTick 方法。如果存在以上两个条件,Axios 就默认在 Node.js 环境中运行,并使用 Node.js 内置的 http 模块发送 HTTP 请求。

如果需要明确指定运行环境,可以使用 axios.defaults.adapter 属性来设置适配器(adapter),以便在需要时手动选择使用 XMLHttpRequest 或 Node.js 内置的 http 模块。

例如,在 Node.js 环境中可以这样设置适配器:

const axios = require('axios');
const httpAdapter = require('axios/lib/adapters/http');

axios.defaults.adapter = httpAdapter;

通过上述方式,Axios 可以根据环境自动选择适当的底层实现来发送 HTTP 请求,使其在不同的环境中都能正常工作。

576.[Vue] 为什么 data 属性是一个函数而不是一个对象?【热度: 448】【web框架】【出题公司: Shopee】

关键词:vue data 函数

实例和组件定义data的区别

vue 实例的时候定义data属性既可以是一个对象,也可以是一个函数

const app = new Vue({
  el: "#app",
  // 对象格式
  data: {
    foo: "foo"
  },
  // 函数格式
  data() {
    return {
      foo: "foo"
    }
  }
})

组件中定义data属性,只能是一个函数

如果为组件data直接定义为一个对象

Vue.component('component1', {
  template: `<div>组件</div>`,
  data: {
    foo: "foo"
  }
})

则会得到警告信息

警告说明:返回的data应该是一个函数在每一个组件实例中

组件data定义函数与对象的区别

上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?

在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例

这里我们模仿组件构造函数,定义data属性,采用对象的形式

function Component() {

}

Component.prototype.data = {
  count: 0
}

创建两个组件实例

const componentA = new Component()
const componentB = new Component()

产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响

如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)

function Component() {
  this.data = this.data()
}

Component.prototype.data = function() {
  return {
    count: 0
  }
}

修改componentA组件data属性的值,componentB中的值不受影响

console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 0

vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染

原理分析

首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象

源码位置:/vue-dev/src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // ...
}

data既能是object也能是function,那为什么还会出现上文警告呢?

别急,继续看下文

组件在创建的时候,会进行选项的合并

源码位置:/vue-dev/src/core/util/options.js

自定义组件会进入mergeOptions进行选项合并

Vue.prototype._init = function(options?: Object) {
  // ...
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

定义data会进行数据校验

源码位置:/vue-dev/src/core/instance/init.js

这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示

strats.data = function(
  parentVal: any,
  childVal: any,
  vm?: Component
): Function {
  if (!vm) {
    if (childVal && typeof childVal !== "function") {
      process.env.NODE_ENV !== "production" &&
      warn(
        'The "data" option should be a function ' +
        "that returns a per-instance value in component " +
        "definitions.",
        vm
      );

      return parentVal;
    }
    return mergeDataOrFn(parentVal, childVal);
  }
  return mergeDataOrFn(parentVal, childVal, vm);
};

结论

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

579.[Vue] 中为何不要把 v-if 和 v-for 同时用在同一个元素上, 原理是什么?【热度: 546】【web框架】【出题公司: 网易】

关键词:v-if和v-for性能

确实,将v-ifv-for同时用在同一个元素上可能会导致性能问题。原因在于v-for具有比v-if更高的优先级,它会在每次渲染的时候都会运行。这意味着,即使在某些情况下v-if的条件为false ,v-for仍然会对数据进行遍历和渲染。

这样会导致一些不必要的性能消耗,特别是当数据量较大时。Vue在渲染时会尽量复用已经存在的元素,而不是重新创建和销毁它们。但是当v-for遍历的数据项发生变化时,Vue会使用具有相同key的元素,此时v-if 的条件可能会影响到之前的元素,导致一些不符合预期的行为。

让我们来看一个具体的例子来说明这个问题。

假设我们有以下的Vue模板代码:

<ul>
  <li v-for="item in items" v-if="item.isActive">{{ item.name }}</li>
</ul>

这里我们使用v-for来循环渲染items数组,并且使用v-if来判断每个数组项是否是活动状态。现在,让我们看一下Vue的源码,特别是与渲染相关的部分。

在Vue的渲染过程中,它会将模板解析为AST(抽象语法树),然后将AST转换为渲染函数。对于上面的模板,渲染函数大致如下:

function render() {
  return _c(
    'ul',
    null,
    _l(items, function(item) {
      return item.isActive ? _c('li', null, _v(_s(item.name))) : _e();
    })
  );
}

上面的代码中,_l是由v-for指令生成的渲染函数。它接收一个数组和一个回调函数,并在每个数组项上调用回调函数。回调函数根据v-if条件来决定是否渲染li元素。

问题出在这里:由于v-for的优先级比v-if高,所以每次渲染时都会执行v-for循环,无论v-if的条件是否为false。这意味着即使item.isActivefalse,Vue仍然会对它进行遍历和渲染。

此外,Vue在渲染时会尽量复用已经存在的元素,而不是重新创建和销毁它们。但是当v-for遍历的数据项发生变化时,Vue会使用具有相同key的元素。在上面的例子中,如果item.isActivetrue变为false ,Vue会尝试复用之前的li元素,并在其上应用v-if条件。这可能会导致一些不符合预期的行为。

为了避免这种性能问题,Vue官方推荐在同一个元素上不要同时使用v-ifv-for。如果需要根据条件来决定是否渲染循环的元素,可以考虑使用计算属性或者v-for 的过滤器来处理数据。或者,将条件判断放在外层元素上,内层元素使用v-for进行循环渲染,以确保每次渲染时都能正确地应用v-if条件。

581.[React] 构建组件的方式有哪些【热度: 482】【web框架】【出题公司: 腾讯】

关键词:React构建组件方式

  1. Class Components(类组件):使用ES6的类语法来定义组件。类组件继承自React.Component,并通过render方法返回需要渲染的React元素。
class MyComponent extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}
  1. Function Components(函数组件):使用函数来定义组件,函数接收props作为参数,并返回需要渲染的React元素。
function MyComponent(props) {
  return <div>Hello</div>;
}
  1. Higher-Order Components(高阶组件):高阶组件是一个函数,接收一个组件作为参数,并返回一个新的增强组件。它用于在不修改原始组件的情况下,添加额外的功能或逻辑。
function withLogger(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log('Component did mount!');
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

const EnhancedComponent = withLogger(MyComponent);
  1. Function as Children(函数作为子组件):将函数作为子组件传递给父组件,并通过父组件的props传递数据给子组件。
function MyComponent(props) {
  return <div>{props.children('Hello')}</div>;
}

<MyComponent>
  {(message) => <p>{message}</p>}
</MyComponent>

这些是React中常见的构建组件的方式。每种方式都适用于不同的场景,你可以根据自己的需求选择合适的方式来构建组件。

  1. React.cloneElementReact.cloneElement是一个函数,用于克隆并返回一个新的React元素。它可以用于修改现有元素的props,或者在将父组件的props传递给子组件时进行一些额外的操作。
const parentElement = <div>Hello</div>;
const clonedElement = React.cloneElement(parentElement, { className: 'greeting' });

// Result: <div className="greeting">Hello</div>
  1. React.createElementReact.createElement是一个函数,用于创建并返回一个新的React元素。它接收一个类型(组件、HTML标签等)、props和子元素,并返回一个React元素。
const element = React.createElement('div', { className: 'greeting' }, 'Hello');

// Result: <div className="greeting">Hello</div>

React.createElementReact.cloneElement 通常在一些特殊的场景下使用,例如在高阶组件中对组件进行包装或修改。它们不是常规的组件构建方式,但是在某些情况下是非常有用的。非常抱歉之前的遗漏,希望这次能够更全面地回答您的问题。

582.[React] Class Components 和 Function Components 有区别?【热度: 229】【web框架】【出题公司: 腾讯】

关键词:Class Components 和 Function Components、Class Components 和 Function Components 区别、Class Components 和 Function Components 差异

概要对比

Class组件是使用ES6的类语法定义的组件,它是继承自React.Component的一个子类。Class组件有自己的状态和生命周期方法,可以通过this.state来管理状态,并通过this.setState()来更新状态。

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

函数组件是使用函数来定义的组件,在React 16.8版本引入的Hooks之后,函数组件可以拥有自己的状态和副作用,可以使用useState和其他Hooks来管理状态。

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

函数组件通常比Class组件更简洁和易于理解,尤其是在处理简单的逻辑和状态时。然而,Class组件仍然在一些特定情况下有它们的优势,例如需要使用生命周期方法、引入Ref或者需要更多的精确控制和性能优化时。

细节对比

方面Class组件函数组件
语法使用ES6类语法定义组件使用函数语法定义组件
继承继承自React.Component类无需继承任何类
状态管理可通过this.state和this.setState来管理状态可使用useState Hook来管理状态
生命周期方法可使用生命周期方法,如componentDidMount、componentDidUpdate等可使用Effect Hook来处理副作用
Props可通过this.props来访问父组件传递的props可通过函数参数来访问父组件传递的props
状态更新使用this.setState来更新状态使用对应的Hook来更新状态
内部引用可以通过Ref引用组件实例或DOM元素可以使用Ref Hook引用组件实例或DOM元素
性能优化可以使用shouldComponentUpdate来控制组件是否重新渲染可以使用React.memo或useMemo Hook来控制组件是否重新渲染
访问上下文可以使用this.context来访问上下文可以使用useContext Hook来访问上下文

需要注意的是,这只是一些常见的区别,并不是所有的区别。在实际开发中,具体的区别可能还会根据需求和使用的React版本而有所变化。

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

564.JavaScript 中, 隐藏类是什么概念?【热度: 146】【JavaScript】【出题公司: 网易】

关键词:JavaScript隐藏类

隐藏类是JavaScript引擎中的一种优化技术,用于提高对象访问的性能。隐藏类是一种数据结构,用于跟踪对象的属性和方法的布局和类型,以便在代码运行时能够快速访问它们。

当JavaScript引擎在执行代码时,会动态地创建对象的隐藏类。隐藏类会跟踪对象的属性和方法,并为它们分配固定的内存偏移量。每当对象的属性和方法发生变化时,隐藏类会根据变化的情况进行更新。

使用隐藏类可以提高代码的执行速度,因为JavaScript引擎可以根据隐藏类的信息来直接定位和访问对象的属性和方法,而不需要进行动态查找或解析。这种优化技术可以减少对象访问的开销,提高代码的性能。

需要注意的是,隐藏类是在运行时动态创建的,因此代码中创建对象的顺序和属性的添加顺序都会影响隐藏类的生成。如果对象的属性添加顺序不一致,可能会导致隐藏类的生成不一致,从而降低代码的性能。

隐藏类是现代JavaScript引擎(如V8、SpiderMonkey等)中的一项重要优化技术,可以显著提高JavaScript代码的执行速度。

下面是一个使用隐藏类的简单示例:

function MyClass(a, b) {
  this.prop1 = a;
  this.prop2 = b;
}

MyClass.prototype.method1 = function() {
  console.log("Method 1");
};

MyClass.prototype.method2 = function() {
  console.log("Method 2");
};

var obj1 = new MyClass(10, 20);
var obj2 = new MyClass(30, 40);

obj1.method1(); // 输出 "Method 1"
obj2.method2(); // 输出 "Method 2"

在上面的示例中,我们创建了一个名为MyClass的类,它有两个属性prop1prop2,以及两个方法method1method2。我们用new关键字创建了两个实例obj1obj2

当我们使用隐藏类优化的JavaScript引擎运行这段代码时,它会动态地创建隐藏类来跟踪MyClass的属性和方法。每个实例都会有一个关联的隐藏类,它包含了实例的属性和方法的布局和类型信息。

在调用obj1.method1()obj2.method2()时,JavaScript引擎会使用隐藏类的信息来直接定位并执行相应的方法,而不需要进行动态查找和解析,从而提高了代码的执行速度。

需要注意的是,这只是一个简单的示例,实际上隐藏类的优化是更复杂和细致的。不同的引擎可能会有不同的隐藏类实现方式,并且隐藏类的生成和优化过程会受到许多因素的影响,如代码的结构、对象的属性访问模式等。

571.[Vue] 动态给 data 添加一个新的属性时会发生什么【热度: 164】【web框架】【出题公司: Shopee】

关键词:vue更改data属性

直接添加属性的问题

我们从一个例子开始

定义一个p标签,通过v-for指令进行遍历

然后给botton标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行

<template>
  <p v-for="(value,key) in item" :key="key">
    {{ value }}
  </p>
  <button @click="addProperty">动态添加新属性</button>
</template>

实例化一个vue实例,定义data属性和methods方法

const app = new Vue({
  el: "#app",
  data: () => {
    item:{
      oldProperty:"旧属性"
    }
  },
  methods: {
    addProperty() {
      this.items.newProperty = "新属性"  // 为items添加新属性
      console.log(this.items)  // 输出带有newProperty的items
    }
  }
})

点击按钮,发现结果不及预期,数据虽然更新了(console打印出了新属性),但页面并没有更新

原理分析

为什么产生上面的情况呢?

下面来分析一下

vue2是用过Object.defineProperty实现数据响应式

const obj = {}
Object.defineProperty(obj, 'foo', {
  get() {
    console.log(`get foo:${val}`);
    return val
  },
  set(newVal) {
    if (newVal !== val) {
      console.log(`set foo:${newVal}`);
      val = newVal
    }
  }
})

当我们访问foo属性或者设置foo值的时候都能够触发setter与getter

obj.foo
obj.foo = 'new'

但是我们为obj添加新属性的时候,却无法触发事件属性的拦截

obj.bar = '新属性'

原因是一开始objfoo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据

解决方案

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set()

Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property 同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

function set(target: Array<any> | Object, key: any, val: any): any {
...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用 defineReactive 方法,实现新增属性的响应式

关于 defineReactive 方法,内部还是通过 Object.defineProperty 实现属性拦截

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`);
        val = newVal
      }
    }
  })
}

Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({}, this.someObject, { newProperty1: 1, newProperty2: 2 ... })

$forceUpdate

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate 迫使 Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结

如果为对象添加少量的新属性,可以直接采用Vue.set()

如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象

如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

572.如何拦截 web 应用的请求【热度: 487】【网络】【出题公司: 快手】

关键词:web前端监听请求、前端拦截请求

在前端拦截和处理 Web 应用的所有请求,可以使用以下方法:

  1. 使用 Fetch 或 XMLHttpRequest:在前端代码中使用 Fetch API 或 XMLHttpRequest 对象发送请求。通过拦截 Fetch 或 XMLHttpRequest 对象的 open 和 send 方法,可以在请求发出前进行拦截和修改。这样可以捕获请求的相关信息,并进行相应的处理。

示例代码(使用 Fetch API):

const originalFetch = window.fetch;
window.fetch = function(url, options) {
  // 在请求发出前进行拦截和处理
  console.log('拦截到请求:', url);

  // 可以修改请求的相关信息
  // options.headers['Authorization'] = 'Bearer token';

  return originalFetch.apply(this, arguments);
};
  1. 使用 Service Worker:Service Worker 是一种在浏览器背后运行的脚本,可以拦截和处理网络请求。通过注册一个 Service Worker,可以在其中监听和处理请求事件。从而实现拦截和处理 Web 应用的所有请求。

示例代码:

self.addEventListener('fetch', function(event) {
  // 在请求发出前进行拦截和处理
  console.log('拦截到请求:', event.request.url);

  // 可以修改请求的相关信息
  // event.request.headers.set('Authorization', 'Bearer token');

  event.respondWith(fetch(event.request));
});

需要注意的是,前端拦截和处理请求只能在客户端进行,对于服务器端的请求无法拦截。此外,拦截和处理请求可能会对性能产生一定的影响,因此要根据实际情况进行权衡和调优。同时,对于一些敏感信息(如密码、个人信息等),应该谨慎处理,确保安全性。

  1. 如果是使用是三方请求库, 比如 aixos , 可以直接使用三方库提供的能力

是的,使用 axios 也可以拦截请求。axios 提供了拦截器(interceptors)的功能,可以在请求发出前进行拦截和处理。

示例代码:

import axios from 'axios';

// 请求拦截器
axios.interceptors.request.use(function(config) {
  // 在请求发出前进行拦截和处理
  console.log('拦截到请求:', config.url);

  // 可以修改请求的相关信息
  // config.headers['Authorization'] = 'Bearer token';

  return config;
}, function(error) {
  return Promise.reject(error);
});

// 发送请求
axios.get('/api/data')
  .then(function(response) {
    console.log(response.data);
  })
  .catch(function(error) {
    console.error(error);
  });

在上述代码中,通过使用 interceptors.request 方法,可以对请求进行拦截和处理。在拦截器函数中,可以修改请求的相关信息,并返回修改后的配置对象。

使用 axios 拦截请求只能在客户端进行,对服务器端的请求无法拦截。同样需要谨慎处理敏感信息,并确保安全性。

575.XHR 和 Fetch 是否支持取消请求【热度: 122】【网络】【出题公司: 阿里巴巴】

关键词:XHR 取消请求、Fetch 取消请求

XHR 支持取消请求

XHR(XMLHttpRequest)对象支持取消请求。你可以使用 xhr.abort() 方法来取消正在进行的请求。

下面是一个使用 XHR 取消请求的示例代码:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.send();

// 取消请求
xhr.abort();

使用 xhr.abort() 方法会导致 XHR 请求被中止,并触发 abort 事件。

需要注意的是,取消请求后,XHR 对象将不再可用,不能再使用已经取消的 XHR 对象发送新的请求。

Fetch API 原生不提供直接的方法来取消请求。然而,你可以使用 AbortController 来实现取消 fetch 请求的功能。

AbortController 是一个用于控制和取消异步操作的 API,它可以与 fetch 一起使用来取消网络请求。下面是一个使用 AbortController 来取消 fetch 请求的示例代码:

const controller = new AbortController();
const signal = controller.signal;

fetch('https://api.example.com/data', { signal })
  .then(response => {
    // 处理响应
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求被取消');
    } else {
      console.log('发生错误', error);
    }
  });

// 取消请求
controller.abort();

在上面的代码中,我们创建了一个 AbortController 对象,并从中获取一个 signal 信号。然后,将 signal 信号传递给 fetch 请求的 options 参数中。当调用 controller.abort() 方法时,fetch 请求会被取消,并且会触发一个 AbortError 错误。

需要注意的是,AbortController 是一个较新的 API,不是所有浏览器都完全支持。为了确保在不支持 AbortController 的情况下仍能取消 fetch 请求,你可以使用 polyfill 或使用第三方库(如 Axios)来实现取消功能。

577.SPA首屏加载速度慢的怎么解决【热度: 868】【工程化】【出题公司: 网易】

关键词:SPA首屏、加快首屏加载

统计首屏时间

可以参考下面的文档:

影响首屏可能得因素

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

解决方案

  • 减小入口文件积
  • 静态资源本地缓存
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启 GZip 压缩
  • 使用 SSR
  • 启用 CDN 加速

578.将静态资源缓存在本地的方式有哪些?【热度: 584】【工程化】【出题公司: 网易】

关键词:静态资源缓存本地

浏览器可以使用以下几种方式将前端静态资源缓存在本地

HTTP缓存:浏览器通过设置HTTP响应头中的Cache-Control或Expires字段来指定资源的缓存策略。常见的缓存策略有:no-cache(每次都请求服务器进行验证)、no-store(不缓存资源)、max-age(设置资源缓存的最大时间)等。浏览器根据这些缓存策略来决定是否将资源缓存在本地。

ETag/If-None-Match:服务器可以通过在响应头中添加ETag字段,用于标识资源的版本号。当浏览器再次请求资源时,会将上次请求返回的ETag值通过If-None-Match字段发送给服务器,由服务器判断资源是否发生了变化。如果资源未发生变化,服务器会返回304 Not Modified状态码,浏览器则直接使用本地缓存的资源。

Last-Modified/If-Modified-Since:服务器可以通过在响应头中添加Last-Modified字段,用于标识资源的最后修改时间。浏览器再次请求资源时,会将上次请求返回的Last-Modified值通过If-Modified-Since字段发送给服务器。服务器根据资源的最后修改时间判断资源是否发生了变化,如果未发生变化,则返回304 Not Modified状态码,浏览器使用本地缓存的资源。

  1. Service Worker缓存:使用Service Worker可以将前端资源缓存在浏览器的Service Worker缓存中。Service Worker是运行在浏览器后台的脚本,它可以拦截和处理网络请求,因此可以将前端资源缓存起来,并在离线状态下提供缓存的资源。

LocalStorage或IndexedDB:对于一些小的静态资源,可以将其存储在浏览器的LocalStorage或IndexedDB中。这些存储方式是浏览器提供的本地存储机制,可以将数据以键值对的形式存储在浏览器中,从而实现缓存的效果。

如何将静态资源缓存在 LocalStorage或IndexedDB

以下是一个使用LocalStorage将静态资源缓存的示例代码:

// 定义一个数组,包含需要缓存的静态资源的URL
var resources = [
  'https://example.com/css/style.css',
  'https://example.com/js/main.js',
  'https://example.com/images/logo.png'
];

// 遍历资源数组,将资源请求并存储在LocalStorage中
resources.forEach(function(url) {
  // 发起资源请求
  fetch(url)
    .then(function(response) {
      // 检查请求是否成功
      if (!response.ok) {
        throw new Error('Request failed: ' + response.status);
      }
      // 将响应数据存储在LocalStorage中
      return response.text();
    })
    .then(function(data) {
      // 将资源数据存储在LocalStorage中,以URL作为键名
      localStorage.setItem(url, data);
      console.log('Resource cached: ' + url);
    })
    .catch(function(error) {
      console.error(error);
    });
});

以下是一个使用IndexedDB将静态资源缓存的示例代码:

// 打开或创建一个IndexedDB数据库
var request = indexedDB.open('myDatabase', 1);

// 创建或更新数据库的对象存储空间
request.onupgradeneeded = function(event) {
  var db = event.target.result;
  var objectStore = db.createObjectStore('resources', { keyPath: 'url' });
  objectStore.createIndex('url', 'url', { unique: true });
};

// 成功打开数据库后,将资源请求并存储在IndexedDB中
request.onsuccess = function(event) {
  var db = event.target.result;
  var transaction = db.transaction('resources', 'readwrite');
  var objectStore = transaction.objectStore('resources');

  resources.forEach(function(url) {
    // 发起资源请求
    fetch(url)
      .then(function(response) {
        // 检查请求是否成功
        if (!response.ok) {
          throw new Error('Request failed: ' + response.status);
        }
        // 将响应数据存储在IndexedDB中
        return response.blob();
      })
      .then(function(data) {
        // 创建一个资源对象,以URL作为键名
        var resource = { url: url, data: data };
        // 将资源对象存储在IndexedDB中
        objectStore.put(resource);
        console.log('Resource cached: ' + url);
      })
      .catch(function(error) {
        console.error(error);
      });
  });

  // 完成事务
  transaction.oncomplete = function() {
    console.log('All resources cached in IndexedDB.');
  };

  transaction.onerror = function(event) {
    console.error('Transaction error:', event.target.error);
  };
};

以上代码仅为示例,实际应用中需要根据具体的需求进行相应的优化和错误处理。

580.[React] 如何实现vue 中 keep-alive 的功能?【热度: 255】【web框架】【出题公司: 网易】

关键词:keep-alive组件缓存、keep-alive实现、keep-alive原理

keep-alive 原理 可以参考这个文章: #119

实现 当使用函数式组件时,可以使用React的Hooks来实现类似Vue的<keep-alive>功能。下面是一个使用React函数式组件和Hooks实现类似Vue的<keep-alive>功能的示例:

import React, { useEffect, useRef } from 'react';

const withKeepAlive = (WrappedComponent) => {
  const cache = new Map(); // 使用Map来存储缓存的组件实例

  return (props) => {
    const { id } = props;
    const componentRef = useRef(null);

    useEffect(() => {
      if (!cache.has(id)) {
        cache.set(id, componentRef.current); // 缓存组件实例
      }

      return () => {
        cache.delete(id); // 组件销毁时从缓存中移除
      };
    }, [id]);

    const cachedInstance = cache.get(id); // 获取缓存的组件实例

    if (cachedInstance) {
      return React.cloneElement(cachedInstance.props.children, props); // 渲染缓存的组件实例的子组件
    }

    return <WrappedComponent ref={componentRef} {...props} />; // 初次渲染时渲染原始组件
  };
};

使用这个高阶函数组件来包裹需要缓存的函数式组件:

const SomeComponent = (props) => {
  return (
    <div>
      <h1>Some Component</h1>
      <p>{props.message}</p>
    </div>
  );
};

const KeepAliveSomeComponent = withKeepAlive(SomeComponent);

在父组件中使用KeepAliveSomeComponent来实现缓存功能:

const ParentComponent = () => {
  const [showComponent, setShowComponent] = useState(false);

  const toggleComponent = () => {
    setShowComponent(!showComponent);
  };

  return (
    <div>
      <button onClick={toggleComponent}>Toggle Component</button>
      {showComponent && (
        <KeepAliveSomeComponent id="some-component" message="Hello, World!" />
      )}
    </div>
  );
};

在上述示例中,ParentComponent包含一个按钮,点击按钮时切换KeepAliveSomeComponent的显示与隐藏。每次切换时,KeepAliveSomeComponent 的状态将保留,因为它被缓存并在需要时重新渲染。

同样地,这个示例只实现了最基本的缓存功能,并没有处理更复杂的场景。如果需要更复杂的缓存功能,可以考虑使用状态管理库来管理组件的状态和缓存。

583.[React] 高阶组件理解多少?【热度: 655】【web框架】【出题公司: 腾讯】

关键词:什么是React高阶组件、React高阶组件满足的条件、React高阶组件使用场景

什么是高阶组件

React高阶组件(Higher-Order Component,HOC)是一种用于复用组件逻辑的设计模式。它本质上是一个函数,接受一个组件作为参数,并返回一个新的增强过的组件。

通过使用高阶组件,我们可以将一些通用的功能逻辑抽象出来,并将其应用到多个组件中,从而避免代码重复和逻辑分散的问题。

React高阶组件需要满足以下条件

  1. 接受一个组件作为参数:高阶组件函数应该接受一个组件作为参数,并返回一个新的增强过的组件。
  2. 返回一个新的组件:高阶组件函数应该在内部创建一个新的组件,并将其返回作为结果。这个新组件可以是一个类组件或函数组件。
  3. 传递props:高阶组件应该将传递给它的props传递给原始组件,可以通过使用展开运算符或手动传递props进行传递。
  4. 可以修改props:高阶组件可以对传递给原始组件的props进行处理、转换或增加额外的props。
  5. 可以访问组件生命周期方法和状态:高阶组件可以在新组件中访问组件的生命周期方法和状态,并根据需要执行逻辑。

使用场景

React高阶组件有以下几个常见的使用场景:

  1. 代码复用:当多个组件之间有相同的逻辑和功能时,可以将这些逻辑和功能抽象成一个高阶组件,并在多个组件中使用该高阶组件进行代码复用。
const withLogging = (WrappedComponent) => {
  return (props) => {
    useEffect(() => {
      console.log('Component is mounted');
    }, []);

    return <WrappedComponent {...props} />;
  }
}

const MyComponent = withLogging(MyOriginalComponent);
  1. 条件渲染:高阶组件可以根据一些条件来决定是否渲染原始组件或其他组件。这对于实现权限控制、用户认证等场景非常有用。
const withAuthorization = (WrappedComponent) => {
  return (props) => {
    if (props.isAuthenticated) {
      return <WrappedComponent {...props} />;
    } else {
      return <div>Unauthorized</div>;
    }
  }
}

const MyComponent = withAuthorization(MyOriginalComponent);
  1. Props 改变:高阶组件可以监听原始组件的props的变化,并在变化时执行一些逻辑。这对于实现数据的深拷贝、数据的格式化等场景非常有用。
const withDeepCopy = (WrappedComponent) => {
  return (props) => {
    const prevPropsRef = useRef(props);

    useEffect(() => {
      if (prevPropsRef.current.data !== props.data) {
        const copiedData = JSON.parse(JSON.stringify(props.data));
        // Do something with copiedData...
      }

      prevPropsRef.current = props;
    }, [props.data]);

    return <WrappedComponent {...props} />;
  }
}

const MyComponent = withDeepCopy(MyOriginalComponent);
  1. 功能增强:高阶组件可以对原始组件的功能进行增强,例如增加表单校验、日志记录、性能优化等。
const withFormValidation = (WrappedComponent) => {
  return (props) => {
    const [isValid, setValid] = useState(false);

    const validateForm = () => {
      // Perform form validation logic...
      setValid(true);
    }

    return (
      <div>
        <WrappedComponent {...props} />
        {isValid ? <div>Form is valid</div> : <div>Form is invalid</div>}
      </div>
    );
  }
}

const MyComponent = withFormValidation(MyOriginalComponent);
  1. 渲染劫持:高阶组件可以在原始组件渲染之前或之后执行一些逻辑,例如在渲染之前进行数据加载,或在渲染之后进行动画效果的添加等。
const withDataFetching = (WrappedComponent) => {
  return (props) => {
    const [data, setData] = useState(null);

    useEffect(() => {
      // Fetch data...
      axios.get('/api/data')
        .then(response => {
          setData(response.data);
        })
        .catch(error => {
          console.error('Error fetching data:', error);
        });
    }, []);

    if (data === null) {
      return <div>Loading...</div>;
    } else {
      return <WrappedComponent data={data} {...props} />;
    }
  }
}

const MyComponent = withDataFetching(MyOriginalComponent);

总的来说,React高阶组件提供了一种灵活的方式来对组件进行组合和功能增强,可以在不修改原始组件的情况下对其进行扩展和定制。

584.[React] 从 React 层面上, 能做的性能优化有哪些?【热度: 1,005】【web框架】【出题公司: 美团】

关键词:React性能优化

从 React 层面上,可以进行以下性能优化:

  1. 使用 memoization(记忆化):通过使用 React.memo() 或 useMemo() 来避免不必要的重新渲染。这对于纯函数组件和大型组件特别有用。
  2. 使用 shouldComponentUpdate 或 PureComponent:在类组件中,可以通过重写 shouldComponentUpdate 方法或使用 PureComponent 来避免不必要的重新渲染。
  3. 使用 React.lazy 和 Suspense:通过使用 React.lazy 和 Suspense 来按需加载组件,从而减少初始加载时间。
  4. 使用虚拟化:对于大型列表或表格等组件,可以使用虚拟化技术(如 react-window 或 react-virtualized)来仅渲染可见区域内的元素,从而提高性能。
  5. 避免不必要的渲染:在函数组件中,可以使用 useCallback 和 useMemo 来避免不必要的函数创建和计算, 使用 useRef 保持函数应用的唯一性。
  6. 使用 key 属性:在使用列表或动态元素时,确保为每个元素提供唯一的 key 属性,这有助于 React 有效地识别和更新元素。
  7. 使用 React DevTools Profiler:使用 React DevTools Profiler 来分析组件的渲染性能,并找出性能瓶颈。
  8. 使用 React.StrictMode:在开发环境中,可以使用 React.StrictMode 组件来检测潜在的问题和不安全的使用。
  9. 避免深层嵌套:尽量避免过多的组件嵌套,这可能会导致性能下降。
  10. 使用组件分割:将大型组件拆分成多个小组件,可以提高组件的可维护性和性能。

这些是一些常见的 React 层面上的性能优化技巧,根据具体的应用场景和需求,可能还有其他优化方式。

585.[React] 是如何进行渲染的?【热度: 623】【web框架】【出题公司: 阿里巴巴】

关键词:jsx渲染、react渲染过程

在 React 中,JSX 最终被转换为真实的 DOM 经历了以下步骤:

1. 解析 JSX:在编译阶段,React 会使用 Babel 等工具将 JSX 转换为 JavaScript 对象。

在编译阶段,React 使用 Babel 等工具将 JSX 转换为 JavaScript 对象的过程可以使用以下代码示例来说明:

原始的 JSX 代码:

const element = <h1>Hello, world!</h1>;

经过编译后,会被转换为类似的 JavaScript 对象:

const element = React.createElement("h1", null, "Hello, world!");

上述代码中,React.createElement 是一个由 React 提供的方法,它接收三个参数:元素的类型、元素的属性(可以是一个对象或 null)、元素的子元素。这样,通过调用 React.createElement,JSX 元素就被转换成了一个 JavaScript 对象。

在 React 项目中,Babel 是一个常用的工具,用于将 JSX 代码转换为 JavaScript 代码。Babel 实际上是一个 JavaScript 编译器,可以根据配置和插件,将代码从一种语法转换为另一种语法。

当 Babel 遇到 JSX 代码时,它会使用一个名为 @babel/preset-react 的 preset(预设)来进行转换。这个 preset 包含了一系列的插件,用于处理 JSX 语法。

具体的工作流程如下

  1. Babel 解析代码:Babel 会将代码解析成抽象语法树(AST),以便于之后的处理。

  2. JSX 转换:Babel 使用 @babel/preset-react 预设来处理 JSX 代码。这个预设包含了一个插件 @babel/plugin-transform-react-jsx,用于将 JSX 转换为函数调用。

    例如,将 <h1>Hello, world!</h1> 转换成 React.createElement("h1", null, "Hello, world!")

  3. 生成 JavaScript 代码:Babel 使用转换后的 AST,将其重新生成为 JavaScript 代码。

    例如,将 React.createElement("h1", null, "Hello, world!") 转换成实际的 JavaScript 代码。

总结起来,Babel 的作用就是将 JSX 代码转换为 JavaScript 代码,使其能够在浏览器中执行。这样,React 就可以理解和处理 JSX 语法,并通过转换后的 JavaScript 代码来创建虚拟 DOM 和进行后续的更新操作。

2. 创建虚拟 DOM:React 使用解析后的 JSX 对象来创建虚拟 DOM(Virtual DOM)。虚拟 DOM 是一个轻量级的、以 JavaScript 对象表示的 DOM 树的副本。

createElement 创建虚拟dom

在 React 中,React.createElement 函数用于创建虚拟 DOM 元素。它接受三个参数:元素类型、属性对象以及子元素。

const element = React.createElement(type, props, children);

React.createElement 函数会返回一个描述虚拟 DOM 元素的 JavaScript 对象。这个对象包含了元素的类型、属性和子元素等信息。例如,对于 <div className="container">Hello, React!</div> 这个 JSX 语法,它被转换为以下形式:

React.createElement("div", { className: "container" }, "Hello, React!");

这样就创建了一个描述 <div> 元素的虚拟 DOM 对象。虚拟 DOM 对象可以通过 ReactDOM.render 方法渲染到实际的 DOM 中。当虚拟 DOM 发生变化时,React 会通过比较新旧虚拟 DOM,找出差异并进行局部更新,从而最小化对实际 DOM 的操作。

createElement 原理

以下是 React 源码中 React.createElement 函数的简化版本:

function createElement(type, props, ...children) {
  const element = {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object' ? child : createTextElement(child)
      )
    }
  };

  return element;
}

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  };
}

在上面的源码中,createElement 函数接收一个 type 参数(元素类型)、一个 props 参数(元素的属性对象)以及可选的 children 参数(子元素)。

首先,通过创建一个名为 element 的对象,我们存储了虚拟 DOM 元素的信息。element 对象中的 type 属性保存了元素的类型,而 props 属性则是一个对象,用来存储元素的属性和子元素。我们使用了 ES6 中的扩展运算符将 props 参数中的属性分配给 element.props,同时也将 children 参数中的子元素映射为虚拟 DOM 对象。

对于 children 参数的处理,通过 children.map 方法遍历 children 数组,并对每个子元素执行以下操作:

  • 如果子元素是对象类型,即已经是一个虚拟 DOM 对象,直接将其添加到 element.props.children 中。
  • 如果子元素是字符串或数字类型,即文本节点,那么我们调用 createTextElement 函数来创建一个描述该文本节点的虚拟 DOM 对象,并将其添加到 element.props.children 中。

createTextElement 函数用于创建文本节点的虚拟 DOM 对象。它返回一个包含 type 为 'TEXT_ELEMENT' 的对象,且 props 对象中的 nodeValue 属性保存了文本节点的内容,children 属性为空数组。

最后,我们将 element 对象作为结果返回,这样就创建了一个描述虚拟 DOM 元素的 JavaScript 对象。

总结起来,createElement 函数通过创建一个对象来描述虚拟 DOM 元素,其中包含了元素的类型、属性和子元素等信息。对于子元素,会根据其类型进行判断,如果是对象类型,则直接添加到 props.children 中;如果是文本类型,则通过 createTextElement 函数创建对应的虚拟 DOM 对象。这样就生成了一个虚拟 DOM 元素,可以用于进行后续的渲染和更新操作。

3. Diff 算法比较变化:在每次组件更新时,React 使用 Diff 算法对比前后两个虚拟 DOM 树的差异。Diff 算法能够高效地找出需要进行更新的部分。

React中通过diff算法来比较两个虚拟DOM树的差异,以确定需要更新的最小操作集合。

首先,React会比较两个根节点的类型,如果不同,它们代表不同的组件,React会将原来的组件树完全替换为新的组件树。

如果类型相同,React会比较两个根节点的属性,检查它们是否有任何更改。如果有更改,React会更新已有的DOM元素的属性。

接下来,React会递归地比较和更新子节点。React会通过遍历子节点的方式找到相同位置上的子节点,并进行递归比较。

对于子节点,React使用一种称为"key"的特殊属性来判断它们是否是相同的元素。如果两个子节点的key相同,React会认为它们是相同的元素,并只更新它们的属性和子节点。如果key不同,React会将旧的子节点完全替换为新的子节点。

最后,React会将所有需要更新的操作记录下来,并将其发送到浏览器的渲染引擎中执行。这些操作可能包括添加、移动或删除DOM节点。

通过使用diff算法,React可以最小化对真实DOM的操作,提高性能和效率。同时,React还会使用一些启发式策略和优化算法,如批处理和异步更新,来进一步提升性能。

4. 生成 DOM 更新操作:根据 Diff 算法的比较结果,React 会生成一系列的 DOM 更新操作,包括添加、移除和修改节点等。这些操作被存储在更新队列中。

在React中,生成DOM更新操作的过程可以概括为以下几个步骤:

  • 通过diff算法比较新旧虚拟DOM树的差异,得到需要更新的最小操作集合。
  • 对于每个需要更新的操作,React会将其转化为一个待执行的DOM更新任务。
  • React将这些待执行的DOM更新任务放入一个队列中,等待执行。
  • 当React准备执行DOM更新时,会将队列中的任务按照特定的顺序进行执行。这个顺序通常是根据DOM节点的层级和位置来确定的,以保证DOM更新的正确性。
  • 执行DOM更新时,React会根据操作的类型,比如添加、移动或删除DOM节点,调用浏览器提供的DOM API来执行相应的操作。
  • 在执行DOM更新的过程中,React会尽量优化操作,避免一些不必要的DOM操作。例如,将多个连续的DOM插入操作合并为一次操作,或者将多个DOM删除操作合并为一次操作。
  • 执行完所有的DOM更新任务后,React会通知浏览器进行重新渲染,将更新后的DOM树呈现给用户。

总的来说,React通过将虚拟DOM树转化为真实DOM树,并通过diff算法生成DOM更新操作,然后按照特定顺序执行这些操作,最终完成DOM的更新和渲染。这样的设计可以提高性能,减少不必要的DOM操作,并保证DOM的一致性。

5. 批量进行 DOM 更新:React 会将更新队列中的 DOM 更新操作批量进行,以减少浏览器的重绘和回流操作。React 会通过批量更新来优化性能。

React通过批量更新的方式来优化DOM操作,以减少不必要的性能开销。

在React中,当需要更新组件状态或属性时,不会立即执行DOM更新操作,而是将更新请求加入到一个待处理的队列中。React会在适当的时机,比如在事件处理函数执行完毕或在生命周期方法结束时,对队列中的更新请求进行批量处理。

具体的批量更新过程如下:

  • 在React中,每个组件都有一个内部的pending state队列,用于存储待处理的更新请求。
  • 当需要更新组件的状态或属性时,React会将更新请求添加到该组件的pending state队列中。
  • 在React的更新过程中,会遍历组件的pending state队列,将其中的所有更新请求合并为一个批量更新。
  • React会根据合并后的批量更新,生成最小化的DOM操作集合。
  • 最后,React会通过执行这个批量更新的DOM操作集合,将更新应用到真实的DOM树中。

通过批量更新的方式,React可以减少不必要的DOM操作次数,提高性能。同时,React也提供了一些API,让开发者可以手动控制更新的时机,比如使用setState 的回调函数、使用ReactDom.unstable_batchedUpdates方法等。

需要注意的是,React并不保证所有的更新都会批量处理。在一些特殊情况下,比如在事件处理函数中手动调用setState,或者使用ReactDOM.unstable_batchedUpdates 方法,可以强制进行批量更新。但在某些情况下,React可能会选择立即更新,以保证更新的时机和结果的一致性。

6. 应用 DOM 更新:最后,React 将批量的 DOM 更新操作应用到实际的浏览器 DOM 中,从而更新用户界面。这个过程中,React 会尽量最小化对真实 DOM 的操作,以提高性能。

原理同上, 只是进行了重复操作;

总结

一图带千言

image