前端面试题汇总

176 阅读27分钟
  • js 比较两个对象的属性和值是否都相等

在 JavaScript 中比较两个对象的属性和值是否完全相等可以通过编写一个递归函数来实现。由于对象是引用类型,不能简单地用 === 来比较两个对象。我们需要逐个比较对象的每个属性和值。

以下是一个通用的函数,可以用来比较两个对象的属性和值是否完全相等:

function deepEqual(obj1, obj2) {
  // 检查对象引用相等
  if (obj1 === obj2) {
    return true;
  }

  // 判断是否是对象类型和非null
  if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }

  // 获取两对象的键
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  // 判断键的数量是否相等
  if (keys1.length !== keys2.length) {
    return false;
  }

  // 递归比较每个属性和值
  for (let key of keys1) {
    if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
}

// 示例测试
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };

console.log(deepEqual(obj1, obj2)); // true
console.log(deepEqual(obj1, obj3)); // false

功能解析:

  1. 检查对象引用的相等性: 如果 obj1obj2 引用相同的对象,则立即返回 true。这是最基本的一步,因为它可以快速处理引用相同的情况。

  2. 判断是否是对象类型和非null: 此步骤是为了确保我们处理的是对象和非 null 值。两个对象如果一个是 null 或者不是对象类型,则不能是相等的。

  3. 获取对象的键: 使用 Object.keys 获取对象所有可枚举的属性名数组。

  4. 判断键的数量是否相等: 如果两个对象键的数量不同,他们必然不相等。

  5. 递归比较每个属性和值: 逐个比较对象的每个键,递归调用 deepEqual 来比较键对应的值。如果任意一个键对应的值不相等,则返回 false

说明:

  • 此函数可以处理嵌套对象的比较,但不处理循环引用的对象。
  • 此函数仅适用于比较简单的属性和值,不比较对象的方法和不可枚举的属性。
  • 此函数不区分 MapSetFunction 等高级对象类型的差异,仅限于基本的对象和数组。

如果你需要比较更复杂的对象,可以使用一些库,如 lodash 提供的 _.isEqual 方法:

const _ = require('lodash');

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };

console.log(_.isEqual(obj1, obj2)); // true
console.log(_.isEqual(obj1, obj3)); // false

使用库中的方法可以使代码更简洁,并且解决更多复杂场景。

  • 前端应用的渲染层优化有哪些

前端应用的渲染层优化旨在改进浏览器处理和显示网页内容的性能,从而提升用户体验和界面响应速度。渲染层优化可以通过多种手段来实现,包括减少重排和重绘、提高动画和交互的流畅性、优化渲染路径等。以下是常见的渲染层优化方法。

1. 减少重排与重绘

什么是重排(Reflow)和重绘(Repaint)?

  • 重排 (Reflow):当元素的几何属性(如位置、尺寸)发生变化时,需要重新计算布局。
  • 重绘 (Repaint):当元素的外观(如颜色、边框)发生变化时,需要重新绘制这些元素。

尽量减少 DOM 的更改和样式的调整可以减少重排和重绘。

优化措施:

  • 避免频繁操作 DOM:批量操作 DOM 可以减少多次渲染。

    // Example: using DocumentFragment for batch updates
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      const div = document.createElement('div');
      div.textContent = `Item ${i}`;
      fragment.appendChild(div);
    }
    document.body.appendChild(fragment);
    
  • 避免逐个修改样式:将需要改动的样式集中到一次修改当中。

    // Example: batch styles changes
    element.style.cssText += 'width: 100px; height: 100px; background-color: #f00;';
    
  • 使用类进行批量样式修改:通过添加或删除类而不是直接修改样式。

    // Example: toggling classes
    element.classList.add('highlight');
    

2. 优化动画和过渡

使用 CSS3 动画和过渡

CSS3 动画和过渡通常比 JavaScript 动画更高效,因为它们利用了浏览器的硬件加速。

/* Example: using CSS transitions */
.element {
  transition: transform 0.3s ease-in-out;
}

.element:hover {
  transform: translateX(100px);
}

请求动画帧 (requestAnimationFrame)

使用 requestAnimationFrame 执行动画而不是使用 setTimeoutsetInterval,可以保证在下一次重绘时执行,减少性能损失。

// Example: requestAnimationFrame for smoother animation
function animate() {
  // Update animation state
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

3. 减少及优化绘制层

将动画层提到合成层

通过 will-changetranslateZ 等属性将动画元素提升到合成层,减少重排和重绘次数。

/* Example: using will-change */
.element {
  will-change: transform;
}

4. 延迟和分解工作(Defer and Split Work)

将大量计算任务拆分成多个小任务可以提高页面的响应能力。

使用 requestIdleCallback

requestIdleCallback 可以在浏览器处于空闲状态时执行非紧急任务。

// Example: using requestIdleCallback
function doHeavyTask(deadline) {
  while (deadline.timeRemaining() > 0) {
    // Execute tasks
  }
  if (/* there are still tasks to do */) {
    requestIdleCallback(doHeavyTask);
  }
}
requestIdleCallback(doHeavyTask);

5. 图片优化

图片加载和显示是造成页面渲染迟缓的常见原因。使用合适的图片格式和延迟加载技术可以显著改善性能。

使用合适的图片格式

选择合理的图片格式,如 WebP,可以有效压缩图片体积。

<!-- Example: using WebP format -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description">
</picture>

延迟加载图片(Lazy Loading)

仅在需要时加载图片,比如用户滚动到相关位置信息时。

<!-- Example: lazy loading images in HTML -->
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy">

<script>
  document.addEventListener("DOMContentLoaded", function() {
    const lazyImages = [...document.querySelectorAll("img.lazy")];
    const lazyLoad = () => {
      lazyImages.forEach(img => {
        if (img.getBoundingClientRect().top < window.innerHeight) {
          img.src = img.dataset.src;
          img.classList.remove("lazy");
        }
      });
    };
    window.addEventListener("scroll", lazyLoad);
  });
</script>

6. CSS 和 JavaScript 的高效管理

避免阻塞渲染的资源

将 CSS 放在 <head> 中,JavaScript 放在 <body> 底部或使用 async/defer 属性。

<!-- Example: loading CSS in the head -->
<link rel="stylesheet" href="styles.css">

<!-- Example: loading JavaScript with defer -->
<script defer src="script.js"></script>

使用 Webpack 等工具进行代码拆分和懒加载

将代码拆分成多个文件,并在需要时动态加载,减少初始加载时间。

// Example: using dynamic import in JavaScript
import(/* webpackChunkName: "module" */ './module').then(module => {
  module.doSomething();
});

7. 浏览器优化工具

使用 DevTools 进行性能分析

浏览器开发者工具提供了丰富的性能分析功能,可以帮助我们找出性能瓶颈,进行有针对性的优化。

  1. Performance 面板:记录、分析页面的加载和执行流程。
  2. Lighthouse 工具:提供全面的页面性能评估报告。

总结

优化前端应用的渲染层,通过减少重排和重绘、优化动画和交互、提升图片加载效率、管理 CSS 和 JavaScript 资源等方面的策略,能够显著提高页面的性能和用户体验。在实际开发过程中,可以结合具体的应用场景,选取合适的优化方法,逐步提升渲染性能。

  • 结合渲染指标

结合核心 Web Vitals 渲染指标,优化前端渲染层可以显著提升用户体验。核心指标包括:

  • LCP(Largest Contentful Paint,最大内容绘制):表示浏览器绘制出最大的可见内容元素所需的时间。
  • FID(First Input Delay,首次输入延迟):衡量用户第一次与页面交互(如点击按钮或链接)到浏览器开始处理响应之间的延迟时间。
  • CLS(Cumulative Layout Shift,累积布局偏移):衡量页面加载过程中视觉稳定性,即意外布局偏移的频率。

优化方案

1. 优化 LCP(最大内容绘制)

目标:缩短最大内容绘制(LCP)的时间,使其在 2.5 秒以内。

1.1 服务器优化
  • 使用CDN:将静态资源托管在全球的 CDN 节点上,减少网络延迟。
  • 启用缓存:利用服务器端缓存(如 HTTP 缓存、Redis、Memcached)减少生成页面的时间。
  • 优化服务器响应时间:通过降低服务器负载、优化数据库查询、启用 HTTP/2 等方式提高响应速度。
1.2 资源优化
  • 压缩和优化图像:使用现代图像格式(如 WebP),减小文件大小。
  • 预加载关键资源:在头部 preload 关键 CSS 和 JavaScript 资源。
<link rel="preload" href="/path/to/important.css" as="style">
<link rel="preload" href="/path/to/important.js" as="script">
  • 延迟加载非关键资源:使用 loading="lazy" 属性延迟加载图像和视频。
<img src="example.jpg" alt="Example" loading="lazy">
1.3 优化 CSS 和 JavaScript
  • 内联关键 CSS:将首屏关键 CSS 内联在 HTML 中,减少首屏渲染时间。
<style>
  body {
    font-family: Arial, sans-serif;
  }
  .header {
    background-color: #4CAF50;
    color: white;
    padding: 1em;
  }
</style>
  • 代码拆分和懒加载:使用动态导入和代码拆分减少初始 JavaScript 大小。
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </Suspense>
);
  • JavaScript 压缩和最小化:通过 Webpack、Terser 等工具压缩 JavaScript 文件。

2. 优化 FID(首次输入延迟)

目标:将首次输入延迟(FID)控制在 100 毫秒以内。

2.1 减小主线程阻塞时间
  • 异步加载第三方脚本:使用 asyncdefer 属性加载第三方脚本,避免阻塞主线程。
<script src="https://example.com/third-party.js" async></script>
  • 减少 JavaScript 执行时间:优化和精简代码,移除不必要的库和代码。
2.2 使用 Web Workers
  • 移出复杂计算:将复杂计算任务移至 Web Workers,减轻主线程负担。
const worker = new Worker('path/to/worker.js');
worker.postMessage(data);
worker.onmessage = (event) => {
  console.log(event.data);
};
2.3 分解长任务
  • 使用 requestIdleCallback:将低优先级任务放入 requestIdleCallback,在浏览器空闲时执行。
requestIdleCallback(() => {
  // 低优先级任务
});

3. 优化 CLS(累积布局偏移)

目标:将累积布局偏移(CLS)控制在 0.1 以内。

3.1 保留图像和视频的空间
  • 设置宽高属性:为图像和视频元素指定明确的宽度和高度,避免布局的意外偏移。
<img src="example.jpg" alt="Example" width="600" height="400">
3.2 避免动态内容插入
  • 确保空间的预先分配:加载动态内容(广告、图像等)时,预先分配空间。
<style>
  .ad-slot {
    width: 300px;
    height: 250px;
  }
</style>
<div class="ad-slot">
  <!-- 广告加载内容 -->
</div>
3.3 合理使用 CSS 动画和转场
  • 使用 transform 和 opacity:避免使用导致重排的属性(如 top, left, right, bottom),推荐使用 transformopacity 属性。
/* 推荐 */
.element {
  transition: transform 0.3s ease-in-out;
}
/* 不推荐 */
.element {
  transition: top 0.3s ease-in-out;
}

高级优化技术

1. Service Workers

  • 使用 Service Workers:缓存静态资源和接口数据,减少后续页面加载时间。
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my-cache').then(cache => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/script/main.js'
      ]);
    })
  );
});

2. HTTP/2 和 HTTP/3

  • 升级到 HTTP/2 或 HTTP/3:利用多路复用、头部压缩等特性提升资源加载效率。

3. 字体优化

  • 使用 font-display:合理配置字体显示方式,避免无文本(FOIT)和闪烁文本(FOUT)。
@font-face {
  font-family: 'MyFont';
  src: url('myfont.woff2') format('woff2');
  font-display: swap;
}

工具和监控

  1. Google Lighthouse:综合性性能、可访问性、SEO 检查工具。
  2. WebPageTest:详细的页面加载性能分析工具。
  3. Chrome DevTools Performance:网站性能诊断和分析工具。

总结

前端渲染层的优化策略紧密围绕核心 Web Vitals 指标(LCP、FID、CLS)进行展开。通过服务器优化、资源优化、CSS 和 JavaScript 优化,以及合理使用高级技术(如 Service Workers、HTTP/2、字体优化),可以显著提升页面加载速度和用户体验。始终使用专业工具进行监测和优化,是确保应用性能保持优质的重要手段。

  • 前端应用的网络层优化有哪些

前端应用的网络层优化主要涉及减少请求次数、减少传输数据和加快请求响应时间。这可以通过多种技术和策略实现。以下是常见的网络层优化方法:

1. 减少 HTTP 请求数量

合并资源文件

将多个 CSS 或 JavaScript 文件合并为一个文件,减少请求次数。

// Example: combining multiple scripts into one
<script src="combined.js"></script>

图片合并(CSS Sprites)

将多个小图标合并成一张大图,通过 CSS 的 background-position 属性来显示不同部分的图像。

.icon {
  background-image: url('sprite.png');
}
.icon-home {
  background-position: 0 0;
}
.icon-settings {
  background-position: -20px 0;
}

使用图标字体

使用 Font Awesome 或自定义的图标字体替代多次小图标的请求。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<i class="fas fa-home"></i>

Inline 资源

对于小的 CSS 和 JS 可以直接内联到 HTML 中,减少额外的请求。

<style>
  body { font-family: Arial, sans-serif; }
</style>
<script>
  console.log('Hello, World!');
</script>

2. 使用缓存

HTTP 缓存头部

通过设置合适的 HTTP 缓存头部(如 Cache-ControlExpiresETag 等)来利用客户端缓存。

Cache-Control: public, max-age=31536000
Expires: Wed, 21 Oct 2022 07:28:00 GMT
ETag: "686897696a7c876b7e"

Service Workers

使用 Service Workers 进行离线缓存和资源管理。

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/script/main.js'
      ]);
    })
  );
});

3. 减少传输数据

文件压缩

使用 Gzip 或 Brotli 压缩 HTML、CSS 和 JavaScript 文件。

// Example using Node.js and Express
const express = require('express');
const app = express();
const compression = require('compression');

app.use(compression());

图片优化

使用合适的图片格式(如 JPEG、PNG、WebP、SVG),并对图片进行压缩。

<img src="image.webp" alt="Optimized Image">

懒加载

仅在需要的时候加载图片和其他资源。

// Example: lazy loading images
document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Fallback for browsers without intersection observer support
  }
});

4. 优化传输通道

使用 CDN

将静态资源部署到 Content Delivery Network (CDN),利用其遍布全球的节点来加速资源传输。

<link rel="stylesheet" href="https://cdn.example.com/styles.css">

HTTP/2

利用 HTTP/2 的多路复用、头部压缩和服务器推送等特性来加速传输。

# Example: enabling HTTP/2 in Nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/cert.key;

    # Other settings...
}

5. 优化内容交付

使用适当的内容配置

根据用户的地理位置、网络条件等因素动态生成和分配资源。

// Example: serving different image sizes based on screen size
const isRetina = window.devicePixelRatio > 1;
const imageUrl = isRetina ? 'image@2x.jpg' : 'image.jpg';

Content Negotiation

基于 Accept 头部的内容协商,提供不同格式和版本的资源。

// Example: serving WebP images if supported
if (supportsWebP()) {
  serveImage('image.webp');
} else {
  serveImage('image.jpg');
}

6. 杂项

Prefetching, Preloading, 和 Preconnecting

利用 <link> 标签的各种属性来优化资源加载,例如 rel="preload", rel="prefetch", 和 rel="preconnect"

<link rel="preload" href="styles.css" as="style">
<link rel="prefetch" href="next-page.html">
<link rel="preconnect" href="https://example.com">

避免重定向

尽量避免 URL 重定向,以减少不必要的网络延迟。

<!-- 从 http://example.com 重定向到 https://example.com -->
<!-- 尽量直接访问 https://example.com -->
<meta http-equiv="refresh" content="0; url=https://example.com">

总结

通过结合使用上述多个优化策略,可以显著提高前端应用的网络性能。减小请求数量、减小传输数据量、利用缓存、优化传输通道和内容交付等手段,使得网络资源的加载和交互更加高效。这不仅提高用户体验,也可以减少服务器负载和带宽费用。

  • http请求,从发起到页面渲染的全过程是什么

在现代 Web 应用中,HTTP 请求的整个生命周期从发起请求到页面渲染,涉及多个步骤和技术组件。以下是 HTTP 请求从发起到页面渲染的详细过程。

1. 用户操作触发请求

用户的操作,如输入网址、点击链接或者提交表单,会触发浏览器发起一个 HTTP 请求。

2. DNS 解析

浏览器将域名转换为服务器的 IP 地址,这是通过 DNS(域名系统)完成的。

  • DNS 解析过程
    1. 浏览器缓存:首先,浏览器检查其缓存是否有该域名的 IP 地址。
    2. 操作系统缓存:如果浏览器缓存中没有,查询操作系统缓存。
    3. 本地 DNS 服务器:如果操作系统缓存中也没有,本地 DNS 服务器(由 ISP 提供)会进一步解析。
    4. 上级 DNS 服务器:如果本地 DNS 服务器不能解析,会向上级 DNS 服务器查询,直到根 DNS 服务器完成解析。

3. 建立 TCP 连接

通过三次握手建立 TCP 连接,这是基于 TCP/IP 协议的。

  • 三次握手过程
    1. SYN:客户端发送一个 SYN 数据包给服务器,表示客户端希望建立连接。
    2. SYN-ACK:服务器接收到 SYN 包后,回传一个 SYN-ACK 包给客户端。
    3. ACK:客户端收到 SYN-ACK 包后,再发送一个 ACK 包给服务器,连接建立。

4. 发起 HTTP 请求

建立 TCP 连接后,浏览器向服务器发送 HTTP 请求。请求头部包含了许多关键信息,如浏览器类型、接受的内容类型以及 Cookie 等。

HTTP 请求示例:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.5
Cookie: sessionId=abc123

5. 服务器处理请求并返回响应

服务器接收请求后,处理该请求(如读取文件、查询数据库),生成响应并返回给浏览器。

HTTP 响应示例:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 3056
Server: Apache/2.4.1 (Unix)
Last-Modified: Wed, 21 Oct 2021 07:28:00 GMT
Cache-Control: max-age=3600
ETag: "686897696a7c876b7e"
Date: Wed, 21 Oct 2021 07:28:00 GMT

<!DOCTYPE html>
<html>
<head>
  <title>Example</title>
</head>
<body>
  <h1>Hello, World!</h1>
</body>
</html>

6. 浏览器接收响应并处理

浏览器接收服务器的响应,根据响应头部信息进行相应的处理,例如检查缓存、解压缩内容(若使用了 gzip)等。

7. 渲染页面

浏览器解析 HTML 和渲染页面,包括以下几个步骤:

  1. 解析 HTML:将 HTML 转换为 DOM 树。
  2. 解析 CSS:将 CSS 转换为 CSSOM(CSS Object Model)。
  3. 合并 DOM 和 CSSOM:生成渲染树。
  4. 布局(Reflow):计算元素的几何信息(尺寸、位置)。
  5. 绘制(Painting):绘制页面的像素信息。

渲染示例:

<!DOCTYPE html>
<html>
<head>
  <title>Example</title>
  <style>
    body { font-family: Arial, sans-serif; }
    h1 { color: blue; }
  </style>
</head>
<body>
  <h1>Hello, World!</h1>
  <script>
    console.log("Page loaded");
  </script>
</body>
</html>

在上述过程中,浏览器执行了一系列步骤:

  • HTML 解析:将 HTML 内容解析为 DOM 树。
  • CSS 解析:将 CSS 解析为 CSSOM。
  • JavaScript 执行:执行嵌入的或引用的 JavaScript 代码。
  • 构建渲染树:合并 DOM 和 CSSOM,生成渲染树。
  • 布局和绘制:计算布局信息,并将页面内容绘制到屏幕上。

8. 异步请求和资源加载

在页面初步渲染完成后,浏览器会继续处理异步请求和资源加载,如执行 AJAX 请求、加载图片和其他外部资源。现代浏览器通常会在这些过程中进行优化,以提升用户体验和减少页面加载时间。

总结

通过以上详细步骤,我们可以看到一个 HTTP 请求从发起到页面渲染涉及到的关键环节和技术:

  • 用户操作
  • DNS 解析
  • TCP 连接
  • HTTP 请求
  • 服务器处理
  • 浏览器接收
  • 页面渲染

理解这些过程有助于我们优化网页性能、提升用户体验并有效地调试潜在问题。

  • http缓存

HTTP 缓存是一种非常有效的网页性能优化技术,它可以减少带宽消耗、降低服务器压力、并加快页面加载速度。HTTP 缓存的核心概念是通过缓存策略使客户端(浏览器)在一定时间内重用相同的网络资源(如 HTML、CSS、JavaScript、图片等),而不是每次都从服务器请求这些资源。

HTTP 缓存的工作原理

HTTP 缓存主要通过以下 HTTP 头部字段进行控制和管理:

  • 缓存控制头部(Cache-Control)
  • 过期头部(Expires)
  • 实体标签头部(ETag)
  • 最后修改头部(Last-Modified)

缓存控制头部(Cache-Control)

Cache-Control 是一个广泛使用的 HTTP 头字段,用于指定所有缓存机制必须遵循的指令。

常用指令包括:

  • public:表示响应可以被任何缓存区缓存,包括浏览器的缓存和 CDN 缓存。
  • private:表示响应只能被终端用户的浏览器缓存,不允许中间代理服务器缓存。
  • no-cache:强制请求直接发送至服务器,使用缓存前必须经过服务器验证。
  • no-store:完全禁止缓存,所有请求均需从服务器获取最新响应。
  • max-age=<seconds>:设置缓存的最大存储时间,单位为秒。

示例

Cache-Control: public, max-age=3600

表示资源可以被任何缓存缓存,并且缓存时间为 3600 秒(1 小时)。

过期头部(Expires)

Expires 头字段指定资源的到期时间。与 Cache-Control: max-age 类似,但是 Expires 使用绝对时间。

示例

Expires: Wed, 21 Oct 2021 07:28:00 GMT

如果 Cache-Control: max-ageExpires 同时存在,Cache-Control: max-age 优先级更高

实体标签头部(ETag)

ETag 是一种资源的唯一标识符,用于比较资源的版本。浏览器在请求时会发送 If-None-Match 头字段附带先前的 ETag,服务器对比后决定资源是否改变。

示例

ETag: "686897696a7c876b7e"

当浏览器再次请求该资源时,会发送以下头字段:

If-None-Match: "686897696a7c876b7e"

如果 ETag 未改变,服务器返回 304 Not Modified 状态码。

最后修改头部(Last-Modified)

Last-Modified 头字段指示资源的最后修改时间。浏览器可以使用 If-Modified-Since 头字段将最后修改时间发送给服务器,服务器根据该时间决定资源是否更新。

示例

Last-Modified: Wed, 21 Oct 2021 07:28:00 GMT

浏览器再次请求时会发送:

If-Modified-Since: Wed, 21 Oct 2021 07:28:00 GMT

如果资源未修改,服务器返回 304 Not Modified 状态码。

浏览器缓存策略

浏览器通常采用两种缓存策略:强制缓存协商缓存

强制缓存

即缓存未过期,浏览器直接使用本地缓存,不发起请求到服务器。常见字段为 Cache-ControlExpires

协商缓存

即每次请求资源时,浏览器询问服务器资源是否更新。如果未更新则使用本地缓存。常见字段为 ETagLast-Modified

示例代码

以下是一个示例,通过设置 HTTP 头字段来实现缓存控制:

HTML 示例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTTP 缓存示例</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Hello World</h1>
  <script src="app.js"></script>
</body>
</html>

随附的 CSS 文件

/* styles.css */
body {
  background-color: lightblue;
  color: #333;
  font-family: Arial, sans-serif;
}

随附的 JavaScript 文件

// app.js
console.log("Hello World");

配置 HTTP 头的服务器响应示例(例如 Nginx 配置)

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html;
        index index.html;
    }

    location ~* \.(css|js|jpg|png|gif|ico)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
        try_files $uri $uri/ =404;
    }

    location / {
        add_header Cache-Control "no-store";
    }
}

结论

HTTP 缓存是提升网页性能的关键技术,通过合理设置缓存头字段,可以显著减少请求数量和响应时间。了解并恰当使用这些缓存机制,能让你的网页加载速度更快、用户体验更好。

  • react-redux实现原理

react-redux 是用于将 Redux 与 React 结合起来的库,它提供了一些高级 API 来简化在 React 组件中使用 Redux。react-redux 的核心功能包括 <Provider> 组件connect 函数。理解这两个核心功能如何工作有助于掌握 react-redux 的实现原理。

1. <Provider> 组件

<Provider> 组件是一个高阶组件,它的主要作用是将 Redux store 通过 React 的 Context API 传递给整个应用的组件树。这使得 connect 函数可以在任何地方访问到 Redux store。

核心实现原理:

import React from 'react';
import { createContext } from 'react';

const ReduxContext = createContext();

export class Provider extends React.Component {
  render() {
    return (
      <ReduxContext.Provider value={this.props.store}>
        {this.props.children}
      </ReduxContext.Provider>
    );
  }
}

2. connect 函数

connectreact-redux 中最重要的函数之一。它的作用是将 React 组件与 Redux store 连接起来。

核心实现原理:

  1. 访问 Redux storeconnect 使用 Context API 获取 Redux store。
  2. 订阅 store 的变化:一旦 store 发生变化,组件会主动更新。
  3. 传递 state 和 actions:通过 mapStateToProps 将 state 映射到组件的 props,通过 mapDispatchToProps 将 actions 映射到组件的 props。

connect 的实现可以分为以下几步:

  • 创建 HOC(Higher Order Component),获取 Redux store。
  • 使用 mapStateToPropsmapDispatchToProps
  • 订阅 store 的变化,并调用 setState 触发组件更新。
import React from 'react';
import { ReduxContext } from './Provider';

export function connect(mapStateToProps, mapDispatchToProps) {
  return function(WrappedComponent) {
    return class extends React.Component {
      static contextType = ReduxContext;

      constructor(props, context) {
        super(props, context);
        this.state = {
          ...mapStateToProps(context.getState()),
        };
        this.dispatchProps = mapDispatchToProps(context.dispatch);
      }

      componentDidMount() {
        this.unsubscribe = this.context.subscribe(() => {
          this.setState({
            ...mapStateToProps(this.context.getState()),
          });
        });
      }

      componentWillUnmount() {
        this.unsubscribe();
      }

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

3. 高阶 API 解析

mapStateToProps

mapStateToProps 函数用于指定如何将 Redux store 中的 state 映射到组件的 props 中。

const mapStateToProps = (state) => {
  return {
    someProp: state.someReducer.someProp,
  };
};

mapDispatchToProps

mapDispatchToProps 函数用于将 action creators 转换为可以直接调用的 props。

const mapDispatchToProps = (dispatch) => {
  return {
    someAction: () => dispatch(someActionCreator()),
  };
};

4. 使用示例

假设我们有一个简单的 Redux store 和一个 React 应用,我们可以使用 <Provider>connect 将它们结合起来:

// store.js
import { createStore } from 'redux';

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
}

export const store = createStore(reducer);
// App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import ConnectedCounter from './ConnectedCounter';

ReactDOM.render(
  <Provider store={store}>
    <ConnectedCounter />
  </Provider>,
  document.getElementById('root')
);
// Counter.js
import React from 'react';
import { connect } from 'react-redux';

const Counter = ({ count, increment }) => (
  <div>
    <p>{count}</p>
    <button onClick={increment}>Increment</button>
  </div>
);

const mapStateToProps = (state) => ({
  count: state.count,
});

const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
});

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

完整版 connect 函数 (优化细节,代码拆分)

为了避免每次 store 更新都重新计算 mapDispatchToProps,我们可以在 connect 函数内部进行优化,只在 mount 或 store 改变时重新计算。

优化后的 connect 函数如下:

import React from 'react';
import { ReduxContext } from './Provider';

export function connect(mapStateToProps, mapDispatchToProps) {
  return function(WrappedComponent) {
    class Connect extends React.Component {
      static contextType = ReduxContext;

      constructor(props, context) {
        super(props, context);
        this.state = {
          ...mapStateToProps(context.getState()),
        };
        this.dispatchProps = mapDispatchToProps(context.dispatch);
      }

      componentDidMount() {
        this.unsubscribe = this.context.subscribe(() => {
          this.setState({
            ...mapStateToProps(this.context.getState()),
          });
        });
      }

      componentWillUnmount() {
        this.unsubscribe();
      }

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

总结

react-redux 通过 Context API 使 Redux store 可在 React 应用中全局访问,并使用 connect 函数将组件与 Redux store 连接起来,实现了状态管理的便捷性和组件更新的高效性。理解这两个主要功能的原理有助于更好地设计和优化 Redux 和 React 结合的应用。

  • JS数组去重

在 JavaScript 中,数组去重是一项常见的操作。以下列出了几种常见的数组去重方法及其详细解释,包括使用 ES6 的新特性和一些经典的实现方法。

1. 使用 Set

Set 是 ES6 引入的一种 JavaScript 内置对象,它允许你存储任何类型的唯一值。

const array = [1, 2, 2, 3, 3, 4, 5, 6, 6, 7];
const uniqueArray = [...new Set(array)];

console.log(uniqueArray); // [1, 2, 3, 4, 5, 6, 7]

2. 使用 Array.prototype.filterindexOf

这种方法利用 indexOf 查找第一个匹配的元素索引,如果当前索引和查找到的索引相同则保留该元素。

const array = [1, 2, 2, 3, 3, 4, 5, 6, 6, 7];
const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);

console.log(uniqueArray); // [1, 2, 3, 4, 5, 6, 7]

3. 使用 Array.prototype.reduce

通过 reduce 方法迭代数组,并使用一个辅助数组来存储已经出现过的元素。

const array = [1, 2, 2, 3, 3, 4, 5, 6, 6, 7];
const uniqueArray = array.reduce((accumulator, current) => {
  if (!accumulator.includes(current)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);

console.log(uniqueArray); // [1, 2, 3, 4, 5, 6, 7]

4. 使用 Object 键唯一性

通过对象的键唯一性来记录出现过的元素,这种方法对于基于字符串的数组非常有效。

const array = [1, 2, 2, 3, 3, 4, 5, 6, 6, 7];
const uniqueArray = array.filter((item, index, self) => {
  return self.indexOf(item) === index;
});

console.log(uniqueArray); // [1, 2, 3, 4, 5, 6, 7]

5. 使用 Map

Map 也可以用来去重,特别是对于数组对象去重时非常有用。

const array = [1, 2, 2, 3, 3, 4, 5, 6, 6, 7];
const map = new Map();
array.forEach(item => map.set(item, true));
const uniqueArray = Array.from(map.keys());

console.log(uniqueArray); // [1, 2, 3, 4, 5, 6, 7]

6. 处理复杂数据(对象数组去重)

对于对象数组的去重,我们可以根据对象的某个属性值来进行去重。

const array = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice' }
];

// 利用 Set 和 Map 结合去重
const uniqueArray = Array.from(new Map(array.map(item => [item.id, item])).values());

console.log(uniqueArray);
// [
//   { id: 1, name: 'Alice' },
//   { id: 2, name: 'Bob' }
// ]

7. 对比总结

不同的方法在性能和使用场景上各有优缺点:

  • Set:简单、高效,最适用于基本类型的数组。
  • filter + indexOf:适用于小型数组,性能较低。
  • reduce:强调函数式编程,可以更灵活地处理附加逻辑。
  • Object 键唯一性调`:特别适用字符串数组。
  • Map:不仅可以去重,还适用于对象数组去重。
  • 复杂数据去重(对象数组):
    • 使用SetMap结合,通过映射和键值对操作高效去重。

不同的方法可以根据实际需求选择,处理简单数组时可以选择Set, 处理复杂数据时使用Map和其他方法结合处理。

  • 快速排序

快速排序(Quick Sort)是一种高效的排序算法,通过分而治之的策略将一个数组分为两部分,递归地排序子数组。以下是快速排序的详细步骤、代码示例以及其优化方案。

快速排序算法步骤

  1. 选择一个基准元素(pivot)。
  2. 分区操作:将数组分为两部分,一部分元素小于基准元素,另一部分元素大于基准元素。
  3. 递归地对两部分分别进行快速排序。
  4. 合并已排序的部分。

JavaScript 实现

下面是一个典型的用 JavaScript 实现的快速排序算法。

基础版快速排序

function quickSort(arr) {
  // 若数组长度小于等于1,则无需排序,直接返回数组
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(arr.length / 2)]; // 选择中间元素作为基准
  const left = [];
  const right = [];

  for (let i = 0; i < arr.length; i++) {
    // 跳过基准元素
    if (i === Math.floor(arr.length / 2)) continue;

    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }

  // 递归排序左半部分和右半部分,并合并结果
  return [...quickSort(left), pivot, ...quickSort(right)];
}

// 测试
const array = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSort(array)); // [1, 1, 2, 3, 6, 8, 10]

优化版快速排序

为了提高性能,特别是在原地排序时,可以采用双指针交换法,减少内存使用。

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
  }
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[right]; // 选择最右边的元素作为基准
  let pivotIndex = left;

  for (let i = left; i < right; i++) {
    if (arr[i] < pivot) {
      [arr[i], arr[pivotIndex]] = [arr[pivotIndex], arr[i]]; // 交换小于基准的元素
      pivotIndex++;
    }
  }
  [arr[pivotIndex], arr[right]] = [arr[right], arr[pivotIndex]]; // 将基准元素放到正确的位置
  return pivotIndex;
}

// 测试
const array = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSort(array)); // [1, 1, 2, 3, 6, 8, 10]

优化策略

  1. 基准选择优化:选择基准对快速排序的性能影响很大,如果每次都选择首元素或尾元素可能极端情况下导致算法退化到 O(n²)。可以随机选择基准,或采用三数取中法(Median of Three)。

    function medianOfThree(arr, left, right) {
      const mid = Math.floor((left + right) / 2);
      if (arr[left] > arr[mid]) [arr[left], arr[mid]] = [arr[mid], arr[left]];
      if (arr[left] > arr[right]) [arr[left], arr[right]] = [arr[right], arr[left]];
      if (arr[mid] > arr[right]) [arr[mid], arr[right]] = [arr[right], arr[mid]];
      return mid;
    }
    
    function partition(arr, left, right) {
      const mid = medianOfThree(arr, left, right);
      [arr[mid], arr[right]] = [arr[right], arr[mid]];
      const pivot = arr[right];
      let pivotIndex = left;
    
      for (let i = left; i < right; i++) {
        if (arr[i] < pivot) {
          [arr[i], arr[pivotIndex]] = [arr[pivotIndex], arr[i]];
          pivotIndex++;
        }
      }
      [arr[pivotIndex], arr[right]] = [arr[right], arr[pivotIndex]];
      return pivotIndex;
    }
    
  2. 小数组优化:对于小数组,快速排序的优势不明显,可以切换到插入排序来优化。

    function insertionSort(arr, left, right) {
      for (let i = left + 1; i <= right; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= left && arr[j] > key) {
          arr[j + 1] = arr[j];
          j--;
        }
        arr[j + 1] = key;
      }
    }
    
    function quickSort(arr, left = 0, right = arr.length - 1) {
      if (left < right) {
        if (right - left < 10) {
          insertionSort(arr, left, right);
        } else {
          const pivotIndex = partition(arr, left, right);
          quickSort(arr, left, pivotIndex - 1);
          quickSort(arr, pivotIndex + 1, right);
        }
      }
      return arr;
    }
    
  3. 尾递归优化:减少递归深度,优化递归过程中占用的栈空间。

    function quickSort(arr, left = 0, right = arr.length - 1) {
      while (left < right) {
        const pivotIndex = partition(arr, left, right);
    
        // 对较小部分进行递归
        if (pivotIndex - left < right - pivotIndex) {
          quickSort(arr, left, pivotIndex - 1);
          left = pivotIndex + 1;
        } else {
          quickSort(arr, pivotIndex + 1, right);
          right = pivotIndex - 1;
        }
      }
      return arr;
    }
    

结合上述优化策略,可以进一步提高快速排序在实际应用中的效率和性能。

  • 深克隆

在 JavaScript 中,对象的浅拷贝和深拷贝有着显著差异。浅拷贝只复制对象的引用,而深拷贝则会递归地复制对象及其子对象。对于深拷贝,以下是几种常见的方法及其优缺点。

1. JSON 序列化和反序列化

这是最简单的方法之一,但它有显著的局限性(不能复制函数、Date 对象、undefined 等不能被 JSON 表示的数据类型)。

function deepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// 示例
const original = { a: 1, b: { c: 2 } };
const copy = deepCopy(original);
copy.b.c = 42;

console.log(original.b.c); // 2
console.log(copy.b.c);     // 42

2. 使用递归

递归可以处理多种数据类型,但需要小心避免循环引用导致的无限递归。

function deepCopy(object) {
  if (object === null || typeof object !== 'object') {
    return object; // 基本类型直接返回
  }

  if (Array.isArray(object)) {
    const copy = [];
    for (let i = 0; i < object.length; i++) {
      copy[i] = deepCopy(object[i]);
    }
    return copy;
  }

  if (object instanceof Date) {
    return new Date(object.getTime());
  }

  const copy = {};
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      copy[key] = deepCopy(object[key]);
    }
  }

  return copy;
}

// 示例
const original = { a: 1, b: { c: 2 } };
const copy = deepCopy(original);
copy.b.c = 42;

console.log(original.b.c); // 2
console.log(copy.b.c);     // 42

3. 使用 Lodash

Lodash 库提供了一个内置的深拷贝函数 _.cloneDeep,非常强大且兼容性好。

npm install lodash
const _ = require('lodash');

const original = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(original);
copy.b.c = 42;

console.log(original.b.c); // 2
console.log(copy.b.c);     // 42

4. 使用 ES6 支持

通过 ReflectObject 对象的方法结合,可以更灵活地处理原型链和不可枚举属性。

function deepCopy(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) return obj; // 基本类型和函数
  if (hash.has(obj)) return hash.get(obj); // 循环引用处理
  const result = obj instanceof Set ? new Set(obj)
               : obj instanceof Map ? new Map(Array.from(obj, ([key, val]) => [key, deepCopy(val, hash)]))
               : obj instanceof Date ? new Date(obj)
               : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
               : obj.constructor ? new obj.constructor()
               : Object.create(null);
  hash.set(obj, result);
  return Object.assign(result, ...Object.keys(obj).map(
    key => ({ [key]: deepCopy(obj[key], hash) })));
}

// 示例
const original = { a: 1, b: { c: 2 }, d: new Date(), e: /abc/g, f: new Set([1, 2]), g: new Map([[1, 'a']]) };
const copy = deepCopy(original);
copy.b.c = 42;

console.log(original.b.c); // 2
console.log(copy.b.c);     // 42
console.log(original.d === copy.d); // false
console.log(original.e === copy.e); // false
console.log(original.f === copy.f); // false
console.log(original.g === copy.g); // false

5. 结合可遍历对象处理能力的深拷贝

针对不同类型的对象,特别是自定义类实例,处理起来需要更灵活的方法。

function deepCopy(object, hash = new WeakMap()) {
  if (object === null || typeof object !== 'object') {
    return object; 
  }

  if (hash.has(object)) {
    return hash.get(object);
  }

  let copy;
  if (object instanceof Date) {
    copy = new Date(object);
  } else if (object instanceof RegExp) {
    copy = new RegExp(object.source, object.flags);
  } else if (object instanceof Map) {
    copy = new Map();
    object.forEach((value, key) => {
      copy.set(key, deepCopy(value, hash));
    });
  } else if (object instanceof Set) {
    copy = new Set();
    object.forEach(value => {
      copy.add(deepCopy(value, hash));
    });
  } else {
    copy = new object.constructor();
    hash.set(object, copy);

    for (let key in object) {
      if (object.hasOwnProperty(key)) {
        copy[key] = deepCopy(object[key], hash);
      }
    }

    const symbols = Object.getOwnPropertySymbols(object);
    for (let symbol of symbols) {
      copy[symbol] = deepCopy(object[symbol], hash);
    }
  }

  return copy;
}

// 示例
const original = {
  a: 1,
  b: { c: 2 },
  d: new Date(),
  e: /abc/g,
  f: new Set([1, 2]),
  g: new Map([[1, 'a']])
};
const copy = deepCopy(original);
copy.b.c = 42;

console.log(original.b.c); // 2
console.log(copy.b.c);     // 42
console.log(original.d === copy.d); // false
console.log(original.e === copy.e); // false
console.log(original.f === copy.f); // false
console.log(original.g === copy.g); // false

总结

不同的方法在性能和局限性上各有优缺点:

  • JSON序列化:实现简单,但有显著局限性(不能处理函数、Date 对象、undefined 等)。

  • 递归实现:灵活性高,可以处理自定义类型,但需要注意循环引用问题。

  • Lodash:方便实用,适合快速开发,但依赖第三方库。

  • ES6 方法结合 Reflect:处理更复杂的对象类型,但实现较为复杂。

  • 结合可遍历对象处理能力的深拷贝:最灵活,处理各类内置对象和自定义类,适合大项目和复杂场景。

  • Tcp三次握手和四次挥手

在计算机网络领域,HTTP 和 TCP 是两种不同的协议。具体来说,HTTP 是应用层协议,而 TCP 是传输层协议。TCP(传输控制协议)的三次握手和四次挥手是建立和终止 TCP 连接的基本过程。了解这两个过程对于理解网络通信和优化网络应用非常重要。

TCP 三次握手

三次握手 是为了连接客户端和服务器,确保数据可以可靠地传输。它的目的是确认双方的发送和接收能力正常。具体步骤如下:

  1. SYN (Synchronize)——客户端发送连接请求

    • 客户端发送一个带有 SYN 标志位的 TCP 包,表示要请求建立连接。
    • 同时,客户端为这个包指定一个初始序列号 seq
    客户端 -> 服务器:SYN=1, seq=x
    
  2. SYN-ACK——服务器确认连接请求

    • 服务器收到客户端发来的 SYN 包后,回应一个带有 SYN 标志位和 ACK 标志位的 TCP 包,表示同意建立连接。
    • 服务器同时为这个包指定一个初始序列号 seq,并确认收到客户端的序列号 seq + 1
    服务器 -> 客户端:SYN=1, ACK=1, seq=y, ack=x+1
    
  3. ACK——客户端确认响应

    • 客户端收到服务器的 SYN-ACK 包之后,再回应一个带有 ACK 标志位的 TCP 包,表示确认建立连接。
    • 客户端将服务器的序列号加1,作为响应的序列号。
    客户端 -> 服务器:ACK=1, seq=x+1, ack=y+1
    

经过这三个步骤,客户端和服务器都已经确认连接的可靠性,并进入数据传输阶段。

TCP 四次挥手

四次挥手 是为了终止客户端和服务器之间的连接,确保双方都能安全地关闭连接。具体步骤如下:

  1. FIN (Finish)——客户端请求断开连接

    • 客户端发送一个带有 FIN 标志位的 TCP 包,表示要断开连接。
    • 同时,客户端指定一个序列号 seq
    客户端 -> 服务器:FIN=1, seq=u
    
  2. ACK——服务器确认断开请求

    • 服务器收到客户端的 FIN 包后,回应一个带有 ACK 标志位的 TCP 包,表示同意断开连接。
    • 确认序列号是 seq + 1
    服务器 -> 客户端:ACK=1, seq=v, ack=u+1
    
  3. FIN——服务器关闭连接

    • 服务器发送一个带有 FIN 标志位的 TCP 包,表示要关闭连接。
    • 同时,服务器指定一个序列号 seq
    服务器 -> 客户端:FIN=1, seq=w
    
  4. ACK——客户端确认关闭连接

    • 客户端收到服务器的 FIN 包后,再回应一个带有 ACK 标志位的 TCP 包,表示确认。
    • 确认序列号是 seq + 1
    客户端 -> 服务器:ACK=1, seq=u+1, ack=w+1
    

经过这四个步骤,客户端和服务器都已经确认可以安全地断开连接。

时序图

为了更直观地理解三次握手和四次挥手,下图展示了时序图:

三次握手

客户端                  服务器
  |                       |
  |----SYN seq=x--------->|
  |                       |
  |<---SYN seq=y ACK x+1--|
  |                       |
  |----ACK seq=x+1------->|
  |                       |

四次挥手

客户端                  服务器
  |                       |
  |----FIN seq=u--------->|
  |                       |
  |<---ACK seq=v ACK u+1--|
  |                       |
  |                       |
  |<---FIN seq=w----------|
  |                       |
  |----ACK seq=u+1------->|
  |                       |

总结

  • TCP 三次握手 是为了建立可靠的连接,双方确认发送和接收能力。
  • TCP 四次挥手 是为了安全地断开连接,确保双方都可以安全地关闭连接。

了解和掌握这两个过程对于网络编程和优化网络通信至关重要。希望这些解释和代码示例能帮助你更好地理解 TCP 的三次握手和四次挥手过程。

react 父组件怎么调用子组件函数

在 React 中,父组件调用子组件中函数的方法有多种,主要包括以下几种:

  1. 通过 ref 引用
  2. 通过回调 ref
  3. 通过 props 传递回调函数
  4. 利用 Context(上下文)
  5. 通过事件和状态管理(如 Redux、MobX 等)

下面将详细介绍每种方法。

1. 通过 ref 引用

自 React 16.3 引入的新 ref API,可以很方便地在父组件中获取子组件实例,并调用其方法。

示例代码:

import React, { Component, createRef } from 'react';

// 子组件
class Child extends Component {
  doSomething = () => {
    alert('Child component method called!');
  };

  render() {
    return <div>Child Component</div>;
  }
}

// 父组件
class Parent extends Component {
  constructor(props) {
    super(props);
    this.childRef = createRef();
  }

  callChildMethod = () => {
    if (this.childRef.current) {
      this.childRef.current.doSomething();
    }
  };

  render() {
    return (
      <div>
        <Child ref={this.childRef} />
        <button onClick={this.callChildMethod}>Call Child Method</button>
      </div>
    );
  }
}

export default Parent;

2. 通过回调 ref

除了直接使用 createRef,我们还可以使用回调函数形式的 ref 来实现相同的功能。

示例代码:

import React, { Component } from 'react';

// 子组件
class Child extends Component {
  doSomething = () => {
    alert('Child component method called!');
  };

  render() {
    return <div>Child Component</div>;
  }
}

// 父组件
class Parent extends Component {
  callChildMethod = () => {
    if (this.childRef) {
      this.childRef.doSomething();
    }
  };

  render() {
    return (
      <div>
        <Child ref={ref => (this.childRef = ref)} />
        <button onClick={this.callChildMethod}>Call Child Method</button>
      </div>
    );
  }
}

export default Parent;

3. 通过 props 传递回调函数

父组件可以将一个回调函数通过 props 传递给子组件,子组件在适当的时候调用这个回调函数,从而达到父组件调用子组件方法的目的。

示例代码:

import React, { Component } from 'react';

// 子组件
class Child extends Component {
  handleButtonClick = () => {
    this.props.onTrigger();
  };

  render() {
    return (
      <div>
        Child Component
        <button onClick={this.handleButtonClick}>Trigger Parent Method</button>
      </div>
    );
  }
}

// 父组件
class Parent extends Component {
  doSomething = () => {
    alert('Parent component method called!');
  };

  render() {
    return (
      <div>
        <Child onTrigger={this.doSomething} />
      </div>
    );
  }
}

export default Parent;

4. 利用 Context(上下文)

通过 React 的 Context API,可以实现跨层级传递信息和方法。

示例代码:

import React, { createContext, Component } from 'react';

const MyContext = createContext();

class Child extends Component {
  static contextType = MyContext;

  handleButtonClick = () => {
    this.context.doSomething();
  };

  render() {
    return (
      <div>
        Child Component
        <button onClick={this.handleButtonClick}>Call Parent Method</button>
      </div>
    );
  }
}

class Parent extends Component {
  doSomething = () => {
    alert('Parent component method called!');
  };

  render() {
    return (
      <MyContext.Provider value={{ doSomething: this.doSomething }}>
        <Child />
      </MyContext.Provider>
    );
  }
}

export default Parent;

5. 通过事件和状态管理(如 Redux、MobX 等)

通过全局状态管理,可以使父组件和子组件共享同一个状态或方法,达到互相调用的目的。

示例代码(以 Redux 为例):

import React from 'react';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

// action
const CALL_METHOD = 'CALL_METHOD';

const callMethod = () => ({
  type: CALL_METHOD,
});

// reducer
const reducer = (state = {}, action) => {
  switch (action.type) {
    case CALL_METHOD:
      alert('Child method called by parent!');
      return state;
    default:
      return state;
  }
};

// store
const store = createStore(reducer);

// 子组件
const Child = ({ callMethod }) => (
  <div>
    Child Component
    <button onClick={callMethod}>Trigger Method</button>
  </div>
);

const mapDispatchToChildProps = { callMethod };
const ConnectedChild = connect(null, mapDispatchToChildProps)(Child);

// 父组件
const Parent = ({ callMethod }) => (
  <div>
    <ConnectedChild />
    <button onClick={callMethod}>Call Child Method</button>
  </div>
);

const mapDispatchToProps = { callMethod };
const ConnectedParent = connect(null, mapDispatchToProps)(Parent);

// App
const App = () => (
  <Provider store={store}>
    <ConnectedParent />
  </Provider>
);

export default App;

总结

在 React 中,父组件调用子组件函数主要有以下几种方法:

  1. 通过 ref 引用:适用于需要直接访问子组件实例的方法。
  2. 通过回调 ref:适用于相同的场景,并避免了 createRef 的一些限制。
  3. 通过 props 传递回调函数:适用于需要通过子组件事件触发父组件方法的场景。
  4. 利用 Context:适用于跨层级的场景。
  5. 通过事件和状态管理:适用于更加复杂的全局状态管理,如 Redux、MobX 等。

每种方法有其特定的应用场景,开发者可以根据实际需求选择合适的方式进行实现。

  • 闭包的使用

闭包是 JavaScript 中的一种特性,允许函数访问其外部函数作用域甚至在外部函数执行完毕后,这使得它可以保存和操作外部函数中的变量。理解和使用闭包是 JavaScript 高级编程中的一项重要技能。下面列出几种常见的闭包使用场景,并附上相应的示例代码。

1. 数据封装和私有化

闭包可以用于将变量封装在函数内部,使其无法从外部直接访问。这有助于实现数据的私有化和保护。

function createCounter() {
  let count = 0;  // 私有变量

  return {
    increment: function() {
      count += 1;
      return count;
    },
    decrement: function() {
      count -= 1;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.getCount());  // 输出: 2
console.log(counter.decrement()); // 输出: 1
console.log(counter.getCount());  // 输出: 1

2. 实现模块模式

模块模式利用闭包将公共和私有代码分开,是一种创建块级作用域的设计模式。这通常用于组织和封装代码。

const myModule = (function() {
  let privateVariable = 'Hello';

  function privateMethod() {
    console.log(privateVariable);
  }

  return {
    publicMethod: function() {
      privateMethod(); // 能访问私有成员
    },
    setPrivateVariable: function(val) {
      privateVariable = val;
    },
    getPrivateVariable: function() {
      return privateVariable;
    }
  };
})();

myModule.publicMethod(); // 输出: 'Hello'
myModule.setPrivateVariable('World');
myModule.publicMethod(); // 输出: 'World'
console.log(myModule.getPrivateVariable()); // 输出: 'World'

3. 函数工厂

闭包使得我们可以创建可以产生其他函数的工厂。这是代码复用的一种方式。

function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 输出: 10
console.log(triple(5)); // 输出: 15

4. 延迟函数

闭包可以用于创建延迟执行的函数。这在需要延迟执行某些操作的场景中很有用。

function delayFunction(func, delay) {
  return function() {
    setTimeout(func, delay);
  };
}

function sayHello() {
  console.log('Hello!');
}

const delayedHello = delayFunction(sayHello, 2000);
delayedHello(); // 2秒钟后输出: 'Hello!'

5. 柯里化(Currying)

柯里化是一种函数式编程技术,允许将多个参数的函数转换成一系列单个参数的函数。闭包在柯里化中被广泛应用。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出: 6
console.log(curriedAdd(1, 2)(3)); // 输出: 6
console.log(curriedAdd(1, 2, 3)); // 输出: 6

6. 迭代器实现

闭包可以用于实现迭代器,控制和管理状态。

function createIterator(array) {
  let index = 0;

  return {
    next: function() {
      if (index < array.length) {
        return { value: array[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const myIterator = createIterator([1, 2, 3]);

console.log(myIterator.next()); // 输出: { value: 1, done: false }
console.log(myIterator.next()); // 输出: { value: 2, done: false }
console.log(myIterator.next()); // 输出: { value: 3, done: false }
console.log(myIterator.next()); // 输出: { value: undefined, done: true }

7. 回调函数中的闭包

闭包可以在异步操作中保持对当前状态的访问。

function fetchData(url) {
  // 模拟异步操作
  setTimeout(function() {
    console.log('Data from ' + url);
  }, 1000);
}

function processUrls(urls) {
  for (let i = 0; i < urls.length; i++) {
    fetchData(urls[i]);
  }
}

processUrls(['url1', 'url2', 'url3']);
// 1秒钟后输出: 'Data from url1', 'Data from url2', 'Data from url3'

总结

闭包是 JavaScript 中非常强大的特性,它使您可以创建更复杂和灵活的代码结构。常见的闭包使用场景包括数据封装和私有化、模块模式、函数工厂、延迟函数、柯里化、迭代器实现和回调函数中的状态管理等。掌握闭包对于编写高质量的 JavaScript 代码是至关重要的。

  • react的hooks为什么写在顶层

在 React 中,Hooks 必须始终在函数组件或自定义 Hook 的顶层调用。这是一个重要的规则,称为 "只在顶层调用 Hook"。原因如下:

  1. 保证 Hook 的顺序一致: Hooks 的调用顺序必须在每次渲染时保持一致。当你在顶层调用 Hooks 时,无论组件在什么时候渲染,Hooks 的调用顺序总是保持不变,这样 React 才能正确地将 state 和副作用与适当的 useStateuseEffect 等 Hook 关联起来。

  2. 避免条件判断: 如果在条件判断或循环中调用 Hooks,会导致在不同的渲染周期中 Hooks 的调用顺序发生变化。举个例子,如果在 if 语句内调用一个 Hook,那么在某个渲染周期内该 Hook 可能被跳过,这会破坏 React 的内部机制。

// 错误示例
function MyComponent() {
    if (someCondition) {
        const [state, setState] = useState(0);  // 这会在某些渲染周期中被跳过
    }
}
  1. 编写和维护更简单的代码: 当 Hooks 始终在顶层调用时,函数的结构会更加简洁和清晰;逻辑更易于理解和跟踪。这使代码更具可读性和可维护性。

  2. React 的静态分析工具: React 的开发工具和静态分析工具(如 ESLint 插件)可以帮助自动检测 Hook 的问题。这些工具基于 Hooks 在顶层调用的假设来工作,以帮助开发者更早地发现潜在的错误。

示例

正确使用 Hooks 的示例

function MyComponent() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('John');

    useEffect(() => {
        // 副作用逻辑
        document.title = `You clicked ${count} times`;

        // 可选清除函数
        return () => {
            // 清除逻辑
        };
    }, [count]);

    // 其他逻辑...

    return (
        <div>
            <p>Hello, {name}!</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

总之,确保 Hooks 始终在函数组件或自定义 Hook 的顶层调用,是维护 React 内部机制的一致性和正确性的重要实践。

  • 怎样封装plugin和loader

在 Webpack 中,封装自定义的 plugin 和 loader 可以帮助你实现特定需求,并增强 Webpack 的功能。下面是创建和封装自定义 plugin 和 loader 的详细步骤和示例。

封装自定义 Plugin

Plugins 提供了很强的灵活性,可以通过钩子系统在 Webpack 的编译生命周期的不同阶段执行特定任务。

创建一个自定义 Plugin

  1. 创建 Plugin 类

一个 Webpack plugin 通常是一个 JavaScript 类,它具有 apply 方法。apply 方法会在 Webpack compiler 上调用,并注册钩子。

class MyCustomPlugin {
    apply(compiler) {
        compiler.hooks.done.tap('MyCustomPlugin', (stats) => {
            console.log('MyCustomPlugin: 编译完成!');
        });
    }
}

module.exports = MyCustomPlugin;

上面的代码中,apply 方法在 compiler.hooks.done 钩子上注册了一个回调函数,这个回调函数会在 Webpack 编译完成时调用。

  1. 使用自定义 Plugin

在 Webpack 配置文件中,使用你创建的插件:

const MyCustomPlugin = require('./path/to/MyCustomPlugin');

module.exports = {
    // ...其他配置...
    plugins: [
        new MyCustomPlugin(),
    ],
};

封装自定义 Loader

Loaders 用于对模块的源代码进行转换。Loader 是一个导出为函数的模块,这个函数会在 Webpack 处理模块时调用。

创建一个自定义 Loader

  1. 创建 Loader 函数

Loader 是一个 Node.js 模块,它导出一个函数。这个函数接收源代码作为参数,并返回转换后的代码。

module.exports = function (source) {
    // source 是文件的原始内容
    // 你可以在这里对 source 进行处理
    const result = source.replace(/console\.log\(/g, 'console.debug(');
    
    // 返回结果,该结果会被 Webpack 用于替换原内容
    return result;
};

上面的示例 Loader 会将源代码中的所有 console.log 替换为 console.debug

  1. 使用自定义 Loader

在 Webpack 配置文件中,使用你创建的 Loader:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,                  // 匹配需要处理的文件类型
                use: './path/to/my-custom-loader',   // 指定自定义 Loader 的路径
            },
        ],
    },
};

示例

自定义 Plugin 示例

让我们编写一个稍微复杂一点的自定义 Plugin,它会在编译完成时输出编译结果的文件大小统计。

MyStatsPlugin.js:

class MyStatsPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync('MyStatsPlugin', (compilation, callback) => {
            let stats = 'File sizes:\n';
            for (const filename in compilation.assets) {
                const size = compilation.assets[filename].size();
                stats += `${filename}: ${size} bytes\n`;
            }
            compilation.assets['stats.txt'] = {
                source: () => stats,
                size: () => stats.length,
            };
            callback();
        });
    }
}

module.exports = MyStatsPlugin;

上述代码中,emit 钩子会在所有输出资源到 output 目录之前调用。我们统计了每个文件的大小,并将结果写入一个新的 stats.txt 文件中。

在 Webpack 配置文件中使用这个插件:

const MyStatsPlugin = require('./MyStatsPlugin');

module.exports = {
    // ...其他配置...
    plugins: [
        new MyStatsPlugin(),
    ],
};

自定义 Loader 示例

下面的自定义 Loader 会将代码中的 ES6 模块语法转换为 CommonJS 模块语法。

my-es6-to-commonjs-loader.js:

const { transform } = require('@babel/core');

module.exports = function (source) {
    const options = {
        presets: ['@babel/preset-env'],
        plugins: ['@babel/plugin-transform-modules-commonjs']
    };

    const result = transform(source, options);
    return result.code;
};

在你的 Webpack 配置文件中使用这个 Loader:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: './my-es6-to-commonjs-loader.js',
            },
        ],
    },
};

总结

通过创建和封装自定义的 plugin 和 loader,你可以扩展 Webpack 的功能,满足特定的构建需求。Plugin 通过 Webpack 提供的钩子系统在编译生命周期的不同阶段执行任务,而 Loader 则用于对模块的源代码进行转换。了解其工作原理和使用场景,将帮助你更好地发挥 Webpack 的强大功能。

  • 有哪些webpack打包的优化

优化 Webpack 打包过程对于提升构建速度和减小打包结果体积至关重要。以下是一些常见的 Webpack 打包优化策略:

1. 优化打包体积

使用 Tree Shaking

Tree Shaking 是 ES6 模块(importexport)的一种优化技术,它可以去掉没有使用的代码。

配置

确保你的项目使用 ES6 模块语法,并在 mode 中使用 production(生产)模式,因为生产模式默认启用 Tree Shaking。

module.exports = {
    mode: 'production', // 启用生产模式
};

使用 TerserPlugin 压缩代码

Terser 是一种 JavaScript 压缩工具,可以去除未使用的代码,压缩剩余的代码,从而减小打包体积。

配置

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    optimization: {
        minimize: true,
        minimizer: [new TerserPlugin()],
    },
};

使用 SplitChunksPlugin 分割代码

利用代码分割技术可以将代码拆分为多个小块,从而更好地管理缓存和资源加载。

配置

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all',
        },
    },
};

移除无用 CSS

使用 mini-css-extract-pluginpurgecss-webpack-plugin 移除无用的 CSS。

安装

npm install mini-css-extract-plugin purgecss-webpack-plugin glob-all --save-dev

配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const glob = require('glob-all');
const path = require('path');

module.exports = {
    mode: 'production',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
        ],
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css',
        }),
        new PurgecssPlugin({
            paths: glob.sync([
                path.join(__dirname, 'src/**/*.js'),
                path.join(__dirname, 'src/**/*.html'),
            ]),
        }),
    ],
};

2. 提升打包速度

使用缓存

启用持久化缓存来提升构建速度:

配置

module.exports = {
    cache: {
        type: 'filesystem', // 启用文件系统缓存
    },
};

开启多线程打包

使用 thread-loader 开启多线程打包,提升构建速度。

安装

npm install thread-loader --save-dev

配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'thread-loader',
                        options: {
                            workers: 2, // 线程数
                        },
                    },
                    'babel-loader',
                ],
                exclude: /node_modules/,
            },
        ],
    },
};

使用 hard-source-webpack-plugin

此插件可以通过将模块编译输出缓存到硬盘来极大地提升打包速度。

安装

npm install hard-source-webpack-plugin --save-dev

配置

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
    plugins: [
        new HardSourceWebpackPlugin(),
    ],
};

使用 babel-loader 进行缓存

通过 babel-loader 配置缓存,以减少重复转换的时间。

配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            cacheDirectory: true, // 启用缓存
                        },
                    },
                ],
                exclude: /node_modules/,
            },
        ],
    },
};

3. 提升开发体验

使用 webpack-dev-server

webpack-dev-server 可以提供一个简单的开发服务器,带有热更新功能,极大地提升开发效率。

安装

npm install webpack-dev-server --save-dev

配置

module.exports = {
    devServer: {
        contentBase: './dist',
        hot: true, // 启用热更新
    },
};

使用 webpack-bundle-analyzer 分析打包结果

webpack-bundle-analyzer 插件可以直观地展示打包后各个模块的体积,有助于发现并优化体积较大的模块。

安装

npm install webpack-bundle-analyzer --save-dev

配置

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin(),
    ],
};

总结

通过对代码分割、压缩、移除无用代码和使用缓存、多线程等技术手段,可以显著优化 Webpack 的打包性能。优化不仅可以使构建过程更快,还可以减少打包后的文件体积,提高页面加载速度,从而提升用户体验。选择适合自己项目和团队现状的优化方法,将得到更好的效果。

  • react中怎样实现一个keep-alive

在 React 中,实现类似 Vue.js 中的 keep-alive 组件的功能,可以通过利用 React 的 staterefs 来缓存和恢复组件的状态。在实现过程中,我们需要确保组件卸载时保留其内部状态,并在重新挂载时恢复。

下面是如何在 React 中实现一个 keep-alive 组件的步骤和示例:

实现思路

  1. 创建一个容器组件: 创建一个高阶组件 KeepAlive,它可以缓存子组件的状态。
  2. 使用 Map 或其他数据结构缓存子组件: 利用 Map 对象存储子组件实例,以便在重新挂载时恢复其状态。
  3. 使用 React.cloneElement 重新挂载组件: 通过 React.cloneElement 重新渲染子组件,并且通过 ref 保存和恢复状态。

代码示例

首先,需要使用 React 的 hooks 来实现此功能。

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

// KeepAlive 组件
function KeepAlive({ children, activeKey }) {
    // 使用 Map 缓存组件
    const cache = useRef(new Map());

    // 每个 Key 的节点引用
    const activeRef = useRef();

    useEffect(() => {
        return () => {
            // 组件卸载时,清除所有缓存
            cache.current.clear();
        };
    }, []);

    // 通过 activeKey 确定当前激活的组件
    const activeElement = useMemo(() => {
        if (!cache.current.has(activeKey)) {
            cache.current.set(activeKey, React.createRef());
        }
        activeRef.current = cache.current.get(activeKey);
        return React.cloneElement(children, { ref: activeRef.current });
    }, [activeKey, children]);

    return (
        <>
            {[...cache.current.keys()].map(key => (
                <div
                    key={key}
                    style={{ display: key === activeKey ? 'block' : 'none' }}>
                    {key === activeKey ? activeElement : React.cloneElement(children, { ref: cache.current.get(key) })}
                </div>
            ))}
        </>
    );
}

export default KeepAlive;

使用 KeepAlive 组件

import React, { useState } from 'react';
import KeepAlive from './KeepAlive';

// 模拟需要缓存的组件
const ComponentA = React.forwardRef((props, ref) => {
    return <div ref={ref}>Component A</div>;
});

const ComponentB = React.forwardRef((props, ref) => {
    return <div ref={ref}>Component B</div>;
});

const App = () => {
    const [activeKey, setActiveKey] = useState('A');

    return (
        <div>
            <button onClick={() => setActiveKey('A')}>Show Component A</button>
            <button onClick={() => setActiveKey('B')}>Show Component B</button>
            <KeepAlive activeKey={activeKey}>
                {activeKey === 'A' ? <ComponentA /> : <ComponentB />}
            </KeepAlive>
        </div>
    );
};

export default App;

解释

  1. 缓存保持:在 KeepAlive 组件中,cache 使用 Map 数据结构来缓存所有被渲染过的组件。activeKey 用来标识当前需要展示的组件。
  2. 组件重用:每次刷新或切换组件时,通过 React.cloneElement 重新渲染子组件,并通过 ref 属性绑定到缓存中相同的引用上。
  3. 显示隐藏:通过切换 display 属性来控制当前激活组件的显示状态,使得非激活组件隐藏,但保留其 DOM 结构和内部状态。

优化与注意事项

  1. 内存管理:在实际应用中,缓存的组件数量很大时,可能会带来内存占用问题。可以适当控制缓存大小或者在组件不再需要时手动清除缓存。
  2. 性能影响display: 'none' 依然会保留所有组件的 DOM 节点和状态,长时间或频繁切换页面可能会造成性能问题;可以考虑异步加载或其他性能优化手段。
  3. 复杂状态管理:如需缓存复杂的组件状态,可以使用更多的 React 技术,如 React.ContextuseReducer 等结合来完成更加复杂的需求。

通过上述方法,你可以在 React 中实现一个 keep-alive 组件,用于缓存和恢复子组件的状态。这对于构建复杂的、多层次的 UI 应用,尤其是 SPA(单页面应用)非常有用。

  • vue2重写的数组方法怎样写的

在 Vue 2 中,为了能够在数据项发生变化时自动更新视图,Vue 提供了一套机制来监视数组的变化。Vue 2 通过重写数组的某些方法使得这些方法具有响应性。当我们对数组进行修改时,Vue 会捕获这些操作并触发相应的更新。这是通过 Object.definePropertyArray.prototype 中的方法实现的。

Vue 重写的数组方法有:pushpopshiftunshiftsplicesortreverse

下面是 Vue 2 重写 Array 方法的大致原理示例:

具体实现

Vue 2 的实现是通过覆盖方法来实现响应式的:

  1. 首先,我们创建一个新的数组原型对象,并在其中定义新的数组方法。
  2. 然后,我们将新的原型对象应用到需要监视的数组上。
// 保存原生数组方法
const arrayProto = Array.prototype;

// 创建一个新的对象,原型指向原生数组原型
const arrayMethods = Object.create(arrayProto);

// 需要被重写的方法列表
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

// 拓展不能是响应式数据的情况
const observeArray = (items) => {
  if(!Array.isArray(items)) return;
  items.forEach(item => {
    observe(item);
  });
}

// 重写数组方法
methodsToPatch.forEach(method => {
    // 保留原始方法
    const original = arrayProto[method];

    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 调用原始方法
            const result = original.apply(this, args);

            // 获取数组的 observer 对象
            const ob = this.__ob__;

            // 判断哪些操作会插入新项
            let inserted;
            switch (method) {
                case 'push':
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice':
                    inserted = args.slice(2);
                    break;
            }

            // 对插入的项目进行观察
            if (inserted) ob.observeArray(inserted);
            
            // 通知变化
            ob.dep.notify();
            return result;
        },
        enumerable: true,
        writable: true,
        configurable: true
    });
});

// Observer 类
class Observer {
    constructor(value) {
        this.value = value;

        // 为对象新增一个依赖管理对象
        this.dep = new Dep();

        // 在对象上新增__ob__ 属性,指向自己
        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,  // 不可枚举
            writable: true,
            configurable: true
        });

        if (Array.isArray(value)) {
            // 替换数组上的原型方法
            value.__proto__ = arrayMethods;
            // 对数组内部对象进行观测
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    }

    // 遍历对象属性,设置响应式
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]);
        }
    }

    // 遍历数组元素,设置响应式
    observeArray(items) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    }
}

// 观察一个对象,将其设置为响应式
function observe(value) {
    if (!value || typeof value !== 'object') {
        return;
    }
    new Observer(value);
}

// 设置响应式
function defineReactive(obj, key, val) {
    const dep = new Dep();
    const property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return;
    }

    const getter = property && property.get;
    const setter = property && property.set;

    if (!getter || setter) {
        val = obj[key];
    }

    let childOb = observe(val);

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return;
            }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            childOb = observe(newVal);
            dep.notify();
        }
    });
}

// 依赖管理对象
class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub(sub) {
        remove(this.subs, sub);
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Dep.target = null;

export { observe };

解释

  1. 创建 arrayMethods:

    • 创建一个新的对象,其原型指向 Array 原型(Array.prototype),并重写 pushpop 等方法。
  2. 重写数组方法:

    • 我们重写了 methodsToPatch 中的数组方法,并保留了原始的数组方法。
    • 在新的方法中,调用原始方法,并在必要时对新增元素进行观察。
    • 每次数组变化后,通过访问 ob.dep 的依赖管理对象,调用 notify 方法通知所有依赖项进行更新。
  3. Observer:

    • Observer 类负责为对象或数组设置响应性。
    • 添加一个不可枚举的 __ob__ 属性到数组上,保存 Observer 实例。
    • 对数组元素执行递归观察。
  4. 设置响应式对象:

    • 提供 observe 函数,用于为任意对象或数组设置响应性。
    • 使用 defineReactive 函数为对象属性添加 getter 和 setter,以实现依赖收集和变更通知。
  5. 依赖管理:

    • 使用 Dep 类来管理依赖关系。
    • 在 getter 中收集依赖,在 setter 中触发依赖更新。

Vue 2 通过这种方式,确保在修改数组内容时,可以很好地触发响应式更新机制,从而更新视图。

  • Vue响应式原理

Vue 的响应式原理是 Vue 框架的核心之一,它使得数据的变化能自动驱动视图更新,避免繁琐的手动操作数据和视图同步。Vue 实现响应性主要通过以下几个关键步骤:

  1. 数据劫持(数据观察)
  2. 依赖收集
  3. 通知更新

下面详细解释这几个步骤:

1. 数据劫持(数据观察)

Vue 通过使用 Object.defineProperty 劫持对象属性的 getter 和 setter,在对象被访问或修改时,能够自动触发相应的逻辑,实现对数据变化的监听。

简单的实现

function defineReactive(obj, key, val) {
    // 创建一个依赖管理对象
    const dep = new Dep();

    // 对子属性递归调用 defineReactive
    let childOb = observe(val);

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.depend();

                if (childOb) {
                    childOb.dep.depend();
                }
            }

            return val;
        },
        set(newVal) {
            if (newVal === val) return;
            val = newVal;
            childOb = observe(newVal);
            dep.notify();
        }
    });
}

observe 函数用于递归观测所有子属性:

function observe(value) {
    if (!value || typeof value !== 'object') {
        return;
    }

    return new Observer(value);
}

class Observer {
    constructor(value) {
        this.value = value;
        this.dep = new Dep();

        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,
            writable: true,
            configurable: true
        });

        if (Array.isArray(value)) {
            // 重写数组方法(具体见前述内容)
            value.__proto__ = arrayMethods;
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    }

    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    }

    observeArray(items) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    }
}

2. 依赖收集

Vue 使用 Dep 类来管理依赖(订阅者)。每个被劫持的对象属性都有自己的 Dep 实例,当属性被访问(getter)时,会通过 Dep 实例收集当前的观察者(依赖)。

Dep 类的实现

class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub(sub) {
        remove(this.subs, sub);
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Dep.target = null;

function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1);
        }
    }
}

3. 通知更新

当数据被修改(setter)时,会触发 dep.notify() 方法,告知所有依赖该数据的观察者进行更新。

观察者(Watcher)类的实现

Watcher 是 Vue 中用于连接数据和视图的桥梁。每一个属性的变化都会通知相关的观察者,观察者再通知相关的视图更新。

class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.cb = cb;
        this.depIds = {};
        this.getter = parsePath(expOrFn);
        this.value = this.get(); // 读取当前值,触发依赖收集
    }

    get() {
        Dep.target = this;
        const value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }

    addDep(dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }

    update() {
        const value = this.get();
        const oldValue = this.value;
        this.value = value;
        this.cb.call(this.vm, value, oldValue);
    }
}

function parsePath(path) {
    const segments = path.split('.');
    return function(obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]];
        }
        return obj;
    };
}

4. 实现总结

总体流程是:

  1. 数据劫持:通过 Object.defineProperty 劫持对象属性的 getter 和 setter,当数据被访问和修改时进行处理。
  2. 依赖收集:在 getter 中,将依赖收集到 Dep 实例中,记录当前的使用了此属性的 Watcher
  3. 通知更新:在 setter 中,触发 Depnotify 方法,通知所有依赖此属性的 Watcher 进行更新。
  4. 观察者模式:通过 Watcher 类,将数据变化与视图更新关联起来。

这样的响应式系统在 Vue 中很高效,也使得开发者能以声明式的方式开发应用,而不需要手动操作 DOM 同步数据与视图。

  • webpack5的打包优化手段

Webpack 5 引入了一些新的功能和优化手段,可以显著提升打包性能和输出结果的质量。以下是 Webpack 5 中常用的优化手段:

1. 持久化缓存(Persistent Caching)

Webpack 5 原生支持持久化文件缓存,可以大幅度加快二次构建速度。

配置

module.exports = {
    cache: {
        type: 'filesystem', // 使用文件系统缓存
    },
};

2. 更好的 Tree Shaking

Webpack 5 增强了 Tree Shaking 的能力,能够更好地移除未使用的代码。确保使用 ES6 模块语法 (import/export),并在生产模式下启用 Tree Shaking:

module.exports = {
    mode: 'production',
    // 不再需要 `sideEffects: false`,Webpack 5 会自动进行 Tree Shaking
};

3. Code Splitting (代码分割)

Webpack 5 更智能地进行代码分割,不需要太多手动配置。我们可以通过 splitChunks 更细粒度地控制代码分割策略。

基础配置

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all', // 选择对所有类型的代码进行分割
        },
    },
};

高级配置(自定义分割策略):

module.exports = {
    optimization: {
        splitChunks: {
            cacheGroups: {
                defaultVendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    },
};

4. 模块联邦(Module Federation)

模块联邦是 Webpack 5 新引入的一项功能,允许多个独立构建的应用之间共享代码和依赖,实现微前端架构。

配置示例:

// 在两个或多个项目中分别添加以下内容:
module.exports = {
    plugins: [
        new webpack.container.ModuleFederationPlugin({
            name: 'app1', // 当前应用名称
            library: { type: 'var', name: 'app1' },
            filename: 'remoteEntry.js',
            exposes: {
                './Component': './src/Component', // 公开组件
            },
            remotes: {
                app2: 'app2@http://localhost:3002/remoteEntry.js', // 使用远程组件
            },
            shared: ['react', 'react-dom'], // 共享依赖
        }),
    ],
};

5. 更好的资源管理(Asset Management)

Webpack 5 提供了更灵活的资源管理机制,例如 asset modules,简化了对文件资源(如图片、字体等)的处理。

配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset/resource', // 根据文件大小自动选择内联或独立文件
            },
        ],
    },
};

6. 改进的模块解析(Module Resolution)

Webpack 5 对模块解析进行了改进,可以更好地处理包的入口文件和条件导入。

配置

module.exports = {
    resolve: {
        mainFields: ['browser', 'module', 'main'], // 主入口字段优先级
    },
};

7. 压缩与优化选项(Terser、CSS Minimizer)

Webpack 5 内置了更好的压缩插件支持,如 Terser 和 CSS Minimizer,简化了代码压缩配置。

配置

const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({ /* 自定义优化选项 */ }),
            new CssMinimizerPlugin({ /* 自定义优化选项 */ }),
        ],
    },
};

8. 链接(Linking)和提升构建速度(Build Speed Enhancements)

Webpack 5 支持增量构建、持久化缓存和多线程编译,这些特性可以显著提升构建速度。

缓存与多线程配置示例

const os = require('os');

module.exports = {
    cache: {
        type: 'filesystem', // 使用文件系统缓存
    },
    experiments: {
        topLevelAwait: true, // 支持顶级 await,可选
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: 'thread-loader',
                        options: {
                            workers: os.cpus().length - 1, // 启用多线程,利用 CPU 核心
                        },
                    },
                    'babel-loader',
                ],
            },
        ],
    },
};

9. Webpack Dev Server 改进

Webpack 5 提升了 webpack-dev-server 的性能和配置灵活性。你可以使用 webpack-dev-middlewarewebpack-hot-middleware 更加灵活地控制开发服务器。

配置示例

module.exports = {
    devServer: {
        contentBase: './dist',
        hot: true, // 启用热模块替换
        compress: true, // 启用 gzip 压缩
        port: 3000, // 自定义端口
        historyApiFallback: true, // 支持 SPA 路由
    },
};

总结

通过利用 Webpack 5 的新功能和优化手段,可以显著提升构建速度、减小打包体积和增强开发效率。持久化缓存、改进的 Tree Shaking、模块联邦以及灵活的代码分割和资源管理等功能,使得 Webpack 5 成为前端工程化中的强大工具。选择适合自己项目和团队的优化方法,将充分发挥 Webpack 5 的优势。

  • v8引擎原理

V8是一个由Google开发的高性能JavaScript和WebAssembly引擎,被广泛应用于Google Chrome浏览器和Node.js中。V8引擎的高效性能主要得益于其内部的几个重要机制和优化技术。了解V8引擎的原理,有助于更好地理解JavaScript的执行过程,从而编写更高效的代码。以下是对V8引擎原理的详细介绍。

V8 引擎的核心组件

V8引擎主要包括以下几个核心组件:

  1. Parser(解析器):解析JavaScript代码,生成AST(抽象语法树)。
  2. Ignition(解释器):将AST编译为字节码并执行。
  3. TurboFan(编译器):优化字节码,生成高效的机器码。

V8 引擎的执行流程

整个执行流程可以分为以下几个阶段:

  1. JavaScript代码解析
  2. 生成抽象语法树(AST)
  3. 生成字节码
  4. 解释执行字节码
  5. 优化编译生成机器码

1. JavaScript代码解析

当V8引擎接收到JavaScript代码时,首先会进行词法分析和语法分析。

  • 词法分析:将代码分解为一系列的Token,如关键字、标识符、操作符等。
  • 语法分析:根据Token生成AST,表示代码的结构和关系。

示例: 对于下面的简单JavaScript代码:

function add(a, b) {
  return a + b;
}

生成的AST可能如下所示:

FunctionDeclaration
 ├── Identifier: "add"
 ├── Parameters: [Identifier: "a", Identifier: "b"]
 └── BlockStatement
      └── ReturnStatement
           └── BinaryExpression
                ├── Identifier: "a"
                └── Identifier: "b"

2. 生成抽象语法树(AST)

解析生成AST后,V8会根据AST进行进一步的分析和转换。AST包含了代码的结构和语义信息,是下一步生成字节码的基础。

3. 生成字节码

V8引擎的解释器(Ignition)将AST编译为字节码。字节码是一种低级中间表示形式,比AST更接近机器码。

示例: V8将上述add函数编译为类似于以下的字节码指令:

0: Ldar a
1: Add b
2: Return

4. 解释执行字节码

Ignition解释器负责执行这些字节码指令。解释器逐条解释执行字节码,由于字节码是一种低级表示,执行效率高于直接解释AST。

5. 优化编译生成机器码

V8的优化编译器(TurboFan)会在运行过程中,动态分析代码的执行情况,识别出需要优化的热点代码(Hot Code),并将这些热点代码进一步编译为高效的机器码。

  • 内联缓存(Inline Caches, ICs):用于优化属性访问和方法调用。通过缓存对象属性访问的类型信息,减少重复的类型检查和查找操作。
  • 隐式类型转换消除:通过类型推断和优化,消除不必要的隐式类型转换,生成高效的机器码。

示例: 对于多次调用add函数的执行情况,TurboFan可能会针对add函数生成特定的优化机器码:

mov eax, [ebp+8]   // 读取参数a
add eax, [ebp+12]  // 将参数b加到eax
ret                // 返回结果

V8 引擎的优化技术

为了保证高效的执行性能,V8引擎采用了多种优化技术,包括:

1. 内联缓存 (Inline Caches)

内联缓存用于优化属性访问和方法调用,通过缓存对象属性访问的类型信息,减少重复的类型检查和查找操作。

示例:

function foo(obj) {
  return obj.x;
}

let obj = { x: 10 };
for (let i = 0; i < 10000; i++) {
  foo(obj);
}

在上述代码中,内联缓存会存储obj.x访问的类型信息,从而加速后续的属性访问。

2. 隐式类型转换消除

JavaScript是一种动态弱类型语言,V8通过类型推断和优化,消除不必要的隐式类型转换,生成更高效的机器码。

示例:

function add(a, b) {
  return a + b;
}

add(1, 2); // TurboFan可能会将其优化为直接的整数相加

3. 内联函数 (Function Inlining)

内联函数优化通过将频繁调用的小函数直接嵌入到调用位置,减少函数调用的开销。

示例:

function multiply(a, b) {
  return a * b;
}

function foo(x) {
  return multiply(x, 2) + multiply(x, 3);
}

// 优化后,foo函数可能会被内联为:
function foo(x) {
  return x * 2 + x * 3;
}

4. 垃圾回收 (Garbage Collection)

V8引擎采用了多种垃圾回收算法(如标记-清除、分代垃圾回收等)来管理内存,确保及时回收不再使用的对象,避免内存泄漏。

总结

V8引擎通过多个组件和优化技术实现了对JavaScript和WebAssembly的高效执行。其执行流程包括代码解析、生成AST、生成字节码、解释执行字节码以及优化编译生成机器码。在执行过程中,V8引擎使用内联缓存、类型转换消除、内联函数以及垃圾回收等技术,提高了执行效率和内存管理能力。了解V8引擎的原理,有助于我们编写性能更高、效率更好的JavaScript代码。

  • 如何设计一个高可维护性和可扩展性的组件库?请描述你的设计原则,并举例说明如何实现这些原则。

设计一个高可维护性和可扩展性的组件库需要遵循一些设计原则,这些原则可以确保组件库在长时间内易于使用、测试和扩展。以下是一些关键设计原则及其实现方法的详细说明:

1. 模块化设计

原则: 每个组件应尽可能独立,职责单一,让每个组件只做一件事并做好。这种方式有助于确保组件的高可维护性和可扩展性。

实现:

  • 隔离组件: 将每个组件的逻辑、样式和依赖关系尽可能隔离开来。
  • 职责单一: 例如,一个按钮组件只负责渲染按钮的外观和点击事件,不应该承担复杂的业务逻辑。
// Button.jsx
import React from 'react';

const Button = ({ label, onClick }) => {
  return (
    <button onClick={onClick}>
      {label}
    </button>
  );
};

export default Button;

2. 可配置和可定制性

原则: 组件应能够接受不同的配置和样式,以满足各种应用场景的需求。

实现:

  • Props传递: 通过给组件传递不同的props来定制其行为和外观。
  • 样式覆盖: 使用CSS-in-JS或者CSS模块化的方法,允许用户传递定制的样式。
// CustomButton.jsx
import React from 'react';
import PropTypes from 'prop-types';
import './CustomButton.css';

const CustomButton = ({ label, onClick, style }) => {
  return (
    <button className="custom-button" onClick={onClick} style={style}>
      {label}
    </button>
  );
};

CustomButton.propTypes = {
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func.isRequired,
  style: PropTypes.object,
};

export default CustomButton;

3. 组件组合

原则: 通过组合多个简单的组件来构建复杂的组件,从而增强组件库的灵活性。

实现:

  • 组合模式: 通过将多个简单组件组合在一起,创建更复杂的UI元素。
// Form.jsx
import React from 'react';
import Input from './Input';
import Button from './Button';

const Form = ({ onSubmit, onInputChange, buttonText }) => {
  return (
    <form onSubmit={onSubmit}>
      <Input onChange={onInputChange} />
      <Button label={buttonText} onClick={onSubmit} />
    </form>
  );
};

export default Form;

4. 强类型系统

原则: 使用强类型系统如TypeScript,可以在开发过程中捕捉错误,并提供更好的文档和自动补全功能。

实现:

  • TypeScript: 在组件开发中使用TypeScript,可以保证组件的类型安全,并减少运行时错误。
// Button.tsx
import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  return (
    <button onClick={onClick}>
      {label}
    </button>
  );
};

export default Button;

5. 良好的文档和测试

原则: 编写全面的文档和测试,帮助使用者了解组件的功能、使用方法和限制,提高组件库的可维护性。

实现:

  • 自动化测试: 使用工具如Jest和React Testing Library编写单元测试和集成测试。
  • 文档生成工具: 使用工具如Storybook生成组件文档,提供交互式的示例。
// Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('Button renders with correct label', () => {
  render(<Button label="Click Me" onClick={() => {}}/>);
  expect(screen.getByText('Click Me')).toBeInTheDocument();
});

test('Button calls onClick handler when clicked', () => {
  const handleClick = jest.fn();
  render(<Button label="Click Me" onClick={handleClick}/>);
  fireEvent.click(screen.getByText('Click Me'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

6. 遵循开闭原则

原则: 软件实体(模块、类、函数等)应该对扩展开放,对修改关闭。这意味着在增加新功能时,尽可能通过添加新代码,而不是修改现有代码。

实现:

  • 使用组合而非继承: 通过组合数个简单的组件来实现新的功能,而不是通过继承和修改已有组件的代码。
// IconButton.jsx
import React from 'react';
import Button from './Button';
import Icon from './Icon';

const IconButton = ({ icon, label, onClick }) => {
  return (
    <Button onClick={onClick}>
      <Icon name={icon} />
      {label}
    </Button>
  );
};

export default IconButton;

通过遵守这些设计原则,你可以创建一个高可维护性、可扩展性的组件库,使开发者能够轻松地使用和扩展这些组件。

理解并设计一个大型单页应用(SPA)、微前端架构以及服务端渲染(SSR),是现代前端开发的核心知识领域。下面我将详细解释这些概念,并为每个部分提供设计思路和示例。

大型单页应用(SPA)架构设计

设计思路

一个大型单页应用需要考虑可扩展性、性能优化、模块化、路由管理、状态管理以及代码分割等方面。以下是一个典型的大型SPA的设计思路:

  1. 模块化设计

    • 将应用分为多个子模块,每个模块负责一个独立的功能领域(例如用户管理、订单管理、报表分析等)。
    • 使用现代JavaScript模块系统(如ES6模块)来组织代码。
  2. 路由管理

    • 使用React Router或Vue Router等路由库进行客户端路由管理。
    • 路由懒加载:按需加载路由对应的组件,减少初始加载时间。
  3. 状态管理

    • 使用Redux、MobX、Vuex等状态管理工具集中管理应用状态。
    • 模块化的状态管理:将状态划分为多个子模块,每个子模块只管理其相关的状态。
  4. 代码分割

    • 使用Webpack或其他打包工具,通过动态导入(import())实现代码分割。
    • 懒加载非核心功能代码,减小初始加载体积。
  5. 性能优化

    • 使用虚拟列表(如React Virtualized)在长列表中只渲染可视部分。
    • 利用浏览器缓存和Service Worker缓存静态资源。

分层结构

  1. 视图层

    • 负责渲染UI,使用React或Vue等框架。
    • 通过UI组件库(如Ant Design、Material-UI)提高开发效率和一致性。
  2. 业务逻辑层

    • 负责业务逻辑处理,包括表单校验、数据转换等。
    • 使用Hooks或自定义服务层封装重复业务逻辑。
  3. 数据层

    • 负责与后端API通信,进行数据请求和处理。
    • 使用 Axios 或 Fetch API 进行 HTTP 请求,集中化的 API 服务管理。
  4. 状态管理层

    • 集中管理应用状态,分模块管理不同的状态区域。
    • 会使用 Redux 或 MobX 等状态管理工具。

示例:模块化及代码分割

// src/store/index.js
import { createStore, combineReducers } from 'redux';
import userReducer from './userReducer';
import productReducer from './productReducer';

const rootReducer = combineReducers({
  user: userReducer,
  product: productReducer,
});

const store = createStore(rootReducer);
export default store;

// src/routes.js
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const User = lazy(() => import('./pages/User'));
const Product = lazy(() => import('./pages/Product'));

function AppRoutes() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/user" component={User} />
          <Route path="/product" component={Product} />
        </Switch>
      </Suspense>
    </Router>
  );
}

export default AppRoutes;

微前端架构

概念

微前端是指将前端应用拆分为多个小的、独立的微应用,每个微应用都可以独立开发、部署和运行。这种架构类似于后端的微服务架构,具有以下优点:

  • 独立开发与部署:各个微应用独立开发,部署时互不影响。
  • 技术栈无关:不同微应用可以使用不同的前端技术栈。
  • 团队协作提升:不同团队可以负责各自的微应用,提高开发效率。

实现思路

  1. 主应用和子应用

    • 主应用负责微应用的加载和路由管理。
    • 子应用独立开发和运行,可以通过iframe、Web Components或JavaScript API与主应用通信。
  2. 通信机制

    • 通过自定义事件、消息中心或状态管理工具(如Redux)在微应用之间进行通信。
  3. 共享库与资源

    • 可以通过NPM包或CDN共享常用库(如React、Vue)和组件,减少重复加载。

示例:使用 single-spa 框架实现微前端

主应用设置

// src/index.js
import { registerApplication, start } from 'single-spa';

registerApplication(
  'navbar',
  () => import('./micro-apps/navbar/navbar.app.js'),
  (location) => location.pathname.startsWith('/')
);

registerApplication(
  'main-app',
  () => import('./micro-apps/main/main.app.js'),
  (location) => location.pathname.startsWith('/main')
);

start();

子应用配置

// micro-apps/main/main.app.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Main from './Main';

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Main,
});

export const { bootstrap, mount, unmount } = lifecycles;

服务端渲染(SSR)

优缺点

优点

  1. SEO友好:搜索引擎能够抓取和索引服务端生成的HTML,有助于SEO。
  2. 性能优化:首屏渲染速度快,用户体验好。
  3. 预渲染内容一致:适用于内容驱动的应用,如博客和新闻网站。

缺点

  1. 服务器负载高:每次请求都需要渲染页面,增加服务器压力。
  2. 复杂性增加:需要处理客户端和服务端的代码,同时要保证两者的一致性。
  3. 缓存挑战:难以实现复杂的内容缓存策略,特别是涉及用户动态数据时。

实现方法

Next.js 实现 SSR

npx create-next-app my-next-app
cd my-next-app
npm run dev

示例:基本的SSR页面

// pages/index.js
import React from 'react';
import fetch from 'isomorphic-unfetch';

const Home = ({ posts }) => (
  <div>
    <h1>My Blog</h1>
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  </div>
);

export async function getServerSideProps() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await res.json();

  return { props: { posts } };
}

export default Home;

Nuxt.js 实现 SSR

安装和启动

npx create-nuxt-app my-nuxt-app
cd my-nuxt-app
npm run dev

示例:基本的SSR页面

<!-- pages/index.vue -->
<template>
  <div>
    <h1>My Blog</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  async asyncData() {
    const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
    return { posts: data };
  }
};
</script>

这些代码示例展示了如何设计并实现复杂的前端应用架构,涵盖了单页应用、微前端和服务端渲染的不同方面。如果有更详细的需求或其他疑问,请随时告诉我!

  • qiankun的实现原理

微前端框架 qiankun,基于 Single-SPA,允许不同技术栈的前端应用可以独立开发、独立部署,并最终集成到一个主应用中。qiankun 实现了高效、灵活的微前端架构,其核心原理包括应用注册、资源预加载、沙箱机制和通信机制等。下面详细解释 qiankun 的原理以及沙箱的实现。

qiankun 的核心原理

1. 应用注册与生命周期管理

qiankun 通过基于 Single-SPA 的应用注册机制和生命周期钩子,来管理微应用的加载、挂载和卸载。

  • 应用注册:主应用通过 qiankun 的 API 注册微应用,定义其加载入口、加载容器和激活规则。

    import { registerMicroApps, start } from 'qiankun';
    
    registerMicroApps([
      {
        name: 'app1',
        entry: '//localhost:7100',
        container: '#container',
        activeRule: '/app1',
      },
      {
        name: 'app2',
        entry: '//localhost:7101',
        container: '#container',
        activeRule: '/app2',
      },
    ]);
    
    start();
    
  • 生命周期钩子:定义并实现微应用的生命周期函数,如 bootstrapmountunmount

    export async function bootstrap() {
      console.log('app1 bootstraped');
    }
    
    export async function mount() {
      console.log('app1 mounted');
    }
    
    export async function unmount() {
      console.log('app1 unmounted');
    }
    

2. 资源预加载

通过资源预加载技术,qiankun 可以在主应用初始化时,预先加载微应用的静态资源,减少用户访问时的加载延迟。

import { loadMicroApp } from 'qiankun';

const microApp = loadMicroApp(
  {
    name: 'app1',
    entry: '//localhost:7100',
    container: '#container',
    activeRule: '/app1',
  },
  {
    sandbox: true,
    preload: true,
  }
);

3. 沙箱机制

沙箱机制是 qiankun 确保不同微应用之间的隔离性和独立性的关键。它通过 Proxy 沙箱Iframe 沙箱来实现。

沙箱机制的实现

Proxy 沙箱

Proxy 沙箱利用了 JavaScript 的 Proxy 对象,拦截对全局对象(如 window)的访问和修改,将修改限定在当前微应用的上下文中。

// Proxy-based sandbox example
class ProxySandbox {
  constructor() {
    this.proxy = new Proxy(window, {
      get: (target, name) => {
        // 从 `sandbox` 中获取变量
        if (name in this.sandbox) {
          return this.sandbox[name];
        }
        // 否则从 `window` 获取变量
        return target[name];
      },
      set: (target, name, value) => {
        // 设置变量到 `sandbox`
        this.sandbox[name] = value;
        return true;
      }
    });
    this.sandbox = {}; // 存储劫持的变量
  }

  activate() {
    // 激活沙箱
    this.prevState = Object.keys(this.sandbox);
  }

  deactivate() {
    // 取消激活沙箱
    const currentState = Object.keys(this.sandbox);

    // 删除新增的属性
    currentState.forEach(key => {
      if (!this.prevState.includes(key)) {
        delete this.proxy[key];
      }
    });
  }
}

// 创建沙箱
const sandbox = new ProxySandbox();
sandbox.activate();

// 现在 `document.title` 修改只作用于当前沙盒环境
sandbox.proxy.document.title = 'New Title In Sandbox';
console.log(sandbox.proxy.document.title); // 输出: New Title In Sandbox
sandbox.deactivate();
  • 原理:通过 Proxy 拦截对象(如 window)的 getset 操作,将修改记录在沙箱对象中。

Iframe 沙箱

Iframe 沙箱利用 iframe 实现更强的隔离性。每个微应用运行在自己的 iframe 内部,DOM 和 CSS 隔离。

<!-- Iframe-based sandbox example -->
<iframe id="sandbox-iframe" src="micro-app.html" sandbox="allow-scripts allow-same-origin"></iframe>
  • 原理:将微应用的内容加载到 iframe 中,使其运行在与主应用和其他微应用隔离的环境中。

沙箱集成示例

在实际使用中,qiankun 默认采用 Proxy 沙箱,同时提供对 iframe 沙箱的支持。沙箱机制在微应用挂载和卸载时自动激活和解除。

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:7100',
    container: '#container',
    activeRule: '/app1'
  }
], {
  sandbox: {
    strictStyleIsolation: true, // 是否严格样式隔离
  }
});

start();

总结

qiankun 是一个强大且灵活的微前端框架,其核心功能和原理包括基于 Single-SPA 的应用注册与生命周期管理、资源预加载、通信机制以及沙箱机制。沙箱机制在 qiankun 中扮演了关键角色,通过 Proxy 沙箱和 iframe 沙箱实现了微应用之间的隔离和独立性,确保不同微应用可以安全、可靠地共同运行在同一个主应用中。理解这些原理对于实现高效、可靠的微前端架构至关重要。

  • 算法的复杂度

算法复杂度是衡量算法在计算资源(如时间和空间)上的消耗的一种方式,通常用时间复杂度和空间复杂度来表示。复杂度帮助我们理解和比较不同算法的效率和性能。

时间复杂度

时间复杂度表示算法执行所需的时间随输入规模变化的情况。常见的时间复杂度有以下几种,从低到高依次排序:

  1. O(1)O(1):常数时间

    • 算法的执行时间与输入规模无关。例如,访问数组的某个固定元素。
  2. O(logn)O(\log n):对数时间

    • 算法的执行时间随着输入规模的对数增长。例如,二分查找。
  3. O(n)O(n):线性时间

    • 算法的执行时间与输入规模成线性关系。例如,遍历数组。
  4. O(nlogn)O(n \log n):线性对数时间

    • 算法的执行时间与输入规模的乘积成对数关系。例如,快速排序、归并排序。
  5. O(n2)O(n^2):平方时间

    • 算法的执行时间与输入规模的平方成正比。例如,冒泡排序、选择排序、插入排序。
  6. O(nk)O(n^k):多项式时间

    • 算法的执行时间是输入规模的 k 次方。例如,一些图论算法的复杂度。
  7. O(2n)O(2^n):指数时间

    • 算法的执行时间随着输入规模呈指数级增长。例如,解决某些组合问题的暴力搜索算法。
  8. O(n!)O(n!):阶乘时间

    • 算法的执行时间与输入规模的阶乘成正比。例如,解决旅行商问题的暴力方法。

例子

下面列出一些常见算法的时间复杂度:

  1. 常数时间
function getFirstElement(arr) {
    return arr[0]; // O(1)
}
  1. 对数时间
function binarySearch(arr, target) {
    let left = 0, right = arr.length - 1;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1; // O(log n)
}
  1. 线性时间
function linearSearch(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1; // O(n)
}
  1. 线性对数时间
function mergeSort(arr) {
    if (arr.length <= 1) return arr;
    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));
    return merge(left, right); // O(n log n)
}

function merge(left, right) {
    const result = [];
    let leftIndex = 0, rightIndex = 0;
    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            result.push(left[leftIndex++]);
        } else {
            result.push(right[rightIndex++]);
        }
    }
    return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
  1. 平方时间
function bubbleSort(arr) {
    const len = arr.length;
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr; // O(n^2)
}

空间复杂度

空间复杂度表示算法执行过程中所需内存空间随输入规模变化的情况。常见的空间复杂度有:

  1. O(1)O(1):常数空间

    • 算法执行过程中所需的额外空间不随输入规模变化。例如,交换两个数的算法。
  2. O(n)O(n):线性空间

    • 算法执行过程中所需的额外空间与输入规模成线性关系。例如,使用辅助数组存储结果的算法。

例子

  1. 常数空间
function swap(a, b) {
    const temp = a;
    a = b;
    b = temp;
    return [a, b]; // O(1)
}
  1. 线性空间
function copyArray(arr) {
    const copy = [];
    for (let i = 0; i < arr.length; i++) {
        copy.push(arr[i]);
    }
    return copy; // O(n)
}

总结

算法复杂度是衡量算法效率的重要指标,理解常见的时间复杂度和空间复杂度,可以帮助我们选择和设计更高效的算法。在实际应用中,通常需要在时间复杂度和空间复杂度之间进行权衡,以找到一个平衡点。