<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>翁先森的博客</title><description>乌拉</description><link>https://nollieleo.github.io/</link><language>zh_CN</language><item><title>NestJS 学习记录 Part 8：拦截器 (Interceptor) 与 RxJS 流处理</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part8/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part8/</guid><description>解析 NestJS 项目中封装的两个核心拦截器：统一响应格式的 TransformInterceptor 和自动化记录审计日志的 OperationLogInterceptor。探讨 AOP 思想、ExecutionContext 的作用以及 RxJS 在请求生命周期中的应用。</description><pubDate>Thu, 26 Mar 2026 12:35:00 GMT</pubDate><content:encoded>&lt;p&gt;在 NestJS 的请求生命周期中，拦截器（Interceptor）是核心特性之一。拦截器底层依赖于 RxJS 的 Observable 数据流，能够在路由函数执行前后绑定额外的逻辑，修改返回结果或处理抛出的异常。&lt;/p&gt;
&lt;p&gt;本篇结合项目中实际封装的两个拦截器——&lt;strong&gt;全局响应拦截器&lt;/strong&gt;与&lt;strong&gt;操作日志拦截器&lt;/strong&gt;，探讨面向切面编程（AOP）在 NestJS 中的工程实践及设计原理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 统一数据结构：TransformInterceptor&lt;/h2&gt;
&lt;p&gt;在前后端分离开发中，前端通常期望后端返回固定的 JSON 结构（如 &lt;code&gt;{ code: 0, message: &apos;success&apos;, data: ... }&lt;/code&gt;）。如果在每个 Controller 中手动组装这个结构，会导致代码冗余。&lt;/p&gt;
&lt;p&gt;利用拦截器，可以拦截控制器返回的原始数据，并在发送给客户端之前统一封装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Client as 客户端
    participant Interceptor as TransformInterceptor
    participant Controller as 控制器

    Client-&amp;gt;&amp;gt;Interceptor: 1. 发起 HTTP 请求
    Interceptor-&amp;gt;&amp;gt;Controller: 2. next.handle() 转发请求
    Controller--&amp;gt;&amp;gt;Interceptor: 3. 返回业务数据 (如 User 对象)
    Note over Interceptor: 4. RxJS map 拦截数据流并重组
    Interceptor--&amp;gt;&amp;gt;Client: 5. 返回统一结构 { code: 0, data: User }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/interceptors/transform.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from &quot;@nestjs/common&quot;;
import { Observable } from &quot;rxjs&quot;;
import { map } from &quot;rxjs/operators&quot;;

export interface Response&amp;lt;T&amp;gt; {
  code: number;
  message: string;
  data: T | null;
}

@Injectable()
export class TransformInterceptor&amp;lt;T&amp;gt; implements NestInterceptor&amp;lt;
  T,
  Response&amp;lt;T&amp;gt;
&amp;gt; {
  intercept(
    _context: ExecutionContext,
    next: CallHandler,
  ): Observable&amp;lt;Response&amp;lt;T&amp;gt;&amp;gt; {
    // next.handle() 触发路由处理函数的执行，并返回一个 Observable
    return next.handle().pipe(
      // 使用 RxJS 的 map 操作符对 Controller 返回的结果进行二次映射
      map((data: unknown) =&amp;gt; ({
        code: 0,
        message: &quot;success&quot;,
        data: data === undefined ? null : (data as T),
      })),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理解析与思考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拦截器的底层逻辑 &lt;code&gt;next.handle()&lt;/code&gt;：&lt;/strong&gt;
&lt;code&gt;CallHandler&lt;/code&gt; 接口包裹了目标 Controller 方法。调用 &lt;code&gt;next.handle()&lt;/code&gt; 会执行业务逻辑并返回一个 RxJS &lt;code&gt;Observable&lt;/code&gt;。这意味着数据的返回是延迟计算（Lazy Evaluation）的。在数据被发送给客户端前，可以通过 RxJS 操作符（如 &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;tap&lt;/code&gt;, &lt;code&gt;catchError&lt;/code&gt;）对数据流进行处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不使用 Express/Fastify 的中间件（Middleware）？&lt;/strong&gt;
中间件作用于底层的 HTTP 协议级别，其上下文中只有 &lt;code&gt;req&lt;/code&gt; 和 &lt;code&gt;res&lt;/code&gt; 对象。中间件无法直接获取 NestJS 序列化前的原生 JavaScript 对象，修改响应体较为困难（通常需要重写 &lt;code&gt;res.send&lt;/code&gt; 方法）。
而拦截器作用于框架的执行上下文中，&lt;code&gt;map&lt;/code&gt; 接收到的 &lt;code&gt;data&lt;/code&gt; 即为 Controller 返回的原生对象。直接对该对象进行结构重组，再由框架底层统一完成 JSON 序列化，实现方式更加清晰解耦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关于 &lt;code&gt;undefined&lt;/code&gt; 的边界处理：&lt;/strong&gt;
如果控制器方法没有 &lt;code&gt;return&lt;/code&gt; 任何值，&lt;code&gt;data&lt;/code&gt; 将是 &lt;code&gt;undefined&lt;/code&gt;。在标准的 JSON 序列化（&lt;code&gt;JSON.stringify&lt;/code&gt;）中，值为 &lt;code&gt;undefined&lt;/code&gt; 的属性会被直接丢弃，导致前端拿到的响应体中缺少 &lt;code&gt;data&lt;/code&gt; 字段。在拦截器中将其兜底为 &lt;code&gt;null&lt;/code&gt;，可确保接口数据结构的稳定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 非侵入式审计日志：OperationLogInterceptor&lt;/h2&gt;
&lt;p&gt;在后台系统中，记录用户的操作日志（如操作人、时间、接口、结果）是常见需求。如果直接在每个 Controller 中调用 &lt;code&gt;LogsService&lt;/code&gt; 写入数据库，业务逻辑将与审计逻辑深度耦合。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OperationLogInterceptor&lt;/code&gt; 结合自定义装饰器，实现了非侵入式的日志采集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Client as 客户端
    participant Interceptor as OperationLogInterceptor
    participant Controller as 控制器
    participant Database as 日志数据库

    Client-&amp;gt;&amp;gt;Interceptor: 1. 发起 HTTP 请求
    Note over Interceptor: 2. Reflector 提取 @OperationLog 元数据
    Interceptor-&amp;gt;&amp;gt;Controller: 3. next.handle()

    alt 业务执行成功
        Controller--&amp;gt;&amp;gt;Interceptor: 4a. 正常返回数据
        Note over Interceptor: 5a. tap 旁路执行
        Interceptor-&amp;gt;&amp;gt;Database: 异步保存操作成功日志
        Interceptor--&amp;gt;&amp;gt;Client: 正常响应
    else 业务抛出异常
        Controller--xInterceptor: 4b. 抛出 HttpException
        Note over Interceptor: 5b. catchError 捕获异常
        Interceptor-&amp;gt;&amp;gt;Database: 异步保存操作失败日志
        Note over Interceptor: 6. throwError 重新抛出异常
        Interceptor--xClient: 交由 ExceptionFilter 统一处理
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，在 Controller 上声明元数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/user.controller.ts
@Post()
@OperationLog(&apos;创建新用户&apos;)
addUser(@Body() user: CreateUserDto) {
  return this.userService.create(user); // 业务代码不包含日志逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，拦截器会在请求处理期间提取元数据并记录日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/interceptors/operation-log.interceptor.ts
@Injectable()
export class OperationLogInterceptor implements NestInterceptor {
  constructor(
    private readonly reflector: Reflector,
    private readonly logsService: LogsService,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;unknown&amp;gt; {
    // 1. 通过反射获取当前路由是否包含 @OperationLog 装饰器
    const metadata = this.reflector.get&amp;lt;OperationLogMetadata&amp;gt;(
      OPERATION_LOG_KEY,
      context.getHandler(),
    );
    if (!metadata) return next.handle(); // 无装饰器则直接放行

    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();

    // 2. 拷贝并脱敏请求体（防止明文密码入库）
    const bodyCopy = { ...request.body };
    if (&quot;password&quot; in bodyCopy) bodyCopy.password = &quot;***&quot;;
    const data = JSON.stringify(bodyCopy);

    // 3. 监听请求结果流
    return next.handle().pipe(
      tap(() =&amp;gt; {
        // 请求成功分支：记录 HTTP 状态码
        const statusCode = response.statusCode || HttpStatus.OK;
        this.saveLog({
          path: request.path,
          method: request.method,
          data,
          result: statusCode,
          userId: request.user?.id,
          description: metadata.description,
        });
      }),
      catchError((error: unknown) =&amp;gt; {
        // 请求失败分支：从 HttpException 中提取错误码
        const statusCode =
          error instanceof HttpException
            ? error.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
        this.saveLog({
          path: request.path,
          method: request.method,
          data,
          result: statusCode,
          userId: request.user?.id,
          description: metadata.description,
        });

        // 必须重新抛出异常
        return throwError(() =&amp;gt; error);
      }),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理解析与思考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ExecutionContext&lt;/code&gt; 的作用：&lt;/strong&gt;
拦截器的入参是 &lt;code&gt;ExecutionContext&lt;/code&gt;（继承自 &lt;code&gt;ArgumentsHost&lt;/code&gt;），而不是直接传入 HTTP Request 对象。这是因为 NestJS 的设计是协议无关的。如果该拦截器应用于 GraphQL 或微服务环境，仍可复用这段核心 AOP 逻辑，只需调用对应的方法（如 &lt;code&gt;GqlExecutionContext.create(context)&lt;/code&gt;）切换上下文即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么使用 RxJS 的 &lt;code&gt;tap&lt;/code&gt; 操作符？&lt;/strong&gt;
&lt;code&gt;tap&lt;/code&gt; 是 RxJS 中用于处理副作用（Side Effects）的操作符。它的特点是旁路执行：可以在数据流经过时触发异步日志记录操作，但不会修改原数据流，也不会阻断数据返回给客户端的过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么必须在 &lt;code&gt;catchError&lt;/code&gt; 中执行 &lt;code&gt;throwError(() =&amp;gt; error)&lt;/code&gt;？&lt;/strong&gt;
当内部业务代码抛出异常（如 &lt;code&gt;ForbiddenException&lt;/code&gt;）时，控制流会进入 &lt;code&gt;catchError&lt;/code&gt;。如果在 &lt;code&gt;catchError&lt;/code&gt; 中仅调用 &lt;code&gt;this.saveLog()&lt;/code&gt; 而不抛出错误，异常会被拦截器隐式捕获并吞没。这会导致外层的异常过滤器无法接收到错误，前端最终会收到一个状态码为 HTTP 200 的空响应。
因此，记录完失败日志后，必须使用 &lt;code&gt;throwError&lt;/code&gt; 重新抛出异常，让流维持错误状态，交由全局 &lt;code&gt;ExceptionFilter&lt;/code&gt; 接管处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;无论是统一响应格式的 &lt;code&gt;map&lt;/code&gt;，还是执行旁路日志记录的 &lt;code&gt;tap&lt;/code&gt; 与异常拦截的 &lt;code&gt;catchError&lt;/code&gt;，NestJS 的拦截器机制体现了 AOP（面向切面编程）的设计思想。&lt;/p&gt;
&lt;p&gt;通过拦截器，可以将响应格式化、审计记录等通用逻辑从核心 Service 业务代码中解耦，从而保证 Controller 与 Service 的职责单一性。&lt;/p&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 7：DTO 嵌套验证、全局 404 与大数据批量插入</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part7/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part7/</guid><description>盘点在 NestJS 与 TypeORM 深度应用中的三个实战技巧：为什么 DTO 嵌套验证必须配合 @Type？如何用 findOneOrFail 配合全局过滤器优雅处理 404？以及在 Node.js 中如何安全地批量插入海量数据。</description><pubDate>Thu, 26 Mar 2026 12:30:00 GMT</pubDate><content:encoded>&lt;p&gt;本文记录在 NestJS 进阶开发中遇到的三个具体场景：复杂的 DTO 嵌套验证陷阱、基于抛出异常的优雅 404 处理，以及针对海量数据的 TypeORM 批量插入策略。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 为什么 DTO 的嵌套验证必须加 @Type 装饰器？&lt;/h2&gt;
&lt;p&gt;在处理用户注册时，前端往往会传过来一个多层嵌套的复杂 JSON 结构（例如包含 &lt;code&gt;profile&lt;/code&gt; 和 &lt;code&gt;addressInfo&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;在 DTO 中，我们通常会写下这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/dto/create-user.dto.ts
export class CreateUserDto {
  // ...
  @IsOptional()
  @ValidateNested()
  @Type(() =&amp;gt; ProfileDto) // ⚠️ 这一行绝对不能漏掉！
  profile?: ProfileDto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果只写 &lt;code&gt;@ValidateNested()&lt;/code&gt; 会怎样？&lt;/strong&gt;
如果漏写了 &lt;code&gt;@Type&lt;/code&gt;，即使你在 &lt;code&gt;ProfileDto&lt;/code&gt; 内部写满了 &lt;code&gt;@IsString()&lt;/code&gt;、&lt;code&gt;@IsNotEmpty()&lt;/code&gt;，这些针对 &lt;code&gt;profile&lt;/code&gt; 内部字段的校验也&lt;strong&gt;完全不会生效&lt;/strong&gt;，脏数据会长驱直入保存到数据库中。这是一个极度危险的安全隐患。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么要配合 &lt;code&gt;@Type&lt;/code&gt;？&lt;/strong&gt;
网络传输过来的 JSON 只是一个纯粹的、无原型的 &lt;code&gt;Object&lt;/code&gt;。&lt;code&gt;class-validator&lt;/code&gt; 必须要在&lt;strong&gt;类的实例 (Instance)&lt;/strong&gt; 上才能读取到那些装饰器元数据并执行校验。
&lt;code&gt;@Type(() =&amp;gt; ProfileDto)&lt;/code&gt; 是 &lt;code&gt;class-transformer&lt;/code&gt; 提供的方法，它的作用是在校验开始前，先把普通的 JSON Object 真正实例化为 &lt;code&gt;ProfileDto&lt;/code&gt; 类的对象。有了实例，&lt;code&gt;@ValidateNested()&lt;/code&gt; 才能顺藤摸瓜，触发内部属性的校验规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 优雅处理 404：findOneOrFail 与全局异常拦截&lt;/h2&gt;
&lt;p&gt;在业务代码中，我们最常写的逻辑就是“先查询，如果不存在就报错”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统写法：代码臃肿
const user = await this.userRepository.findOneBy({ id });
if (!user) {
  throw new NotFoundException(&quot;请求的资源不存在&quot;);
}
return this.userRepository.remove(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而在我们的项目中，删除逻辑被简化为了纯粹的 Happy Path：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/user.service.ts
async remove(id: number) {
  const user = await this.userRepository.findOneByOrFail({ id });
  return this.userRepository.remove(user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配套的，我们增加了一个专门针对 TypeORM 的异常过滤器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/filters/entity-not-found-exception.filter.ts
@Catch(EntityNotFoundError)
export class EntityNotFoundExceptionFilter implements ExceptionFilter {
  // ...
  catch(exception: EntityNotFoundError, host: ArgumentsHost) {
    // 捕获 TypeORM 的 EntityNotFoundError，转换为 HTTP 404 给前端
    sendFormattedExceptionResponse(response, request, this.logger, {
      statusCode: HttpStatus.NOT_FOUND,
      message: &quot;请求的资源不存在&quot;,
      exceptionName: exception.name,
      errorMessage: exception.message,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不手动 &lt;code&gt;if (!user) throw Error&lt;/code&gt;？&lt;/strong&gt;
在复杂的业务线中，各种查询散落在不同的 Service 里。每次都要手写这三行判断代码，极大地增加了代码的噪音。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OrFail 机制的优势：&lt;/strong&gt;
TypeORM 提供了 &lt;code&gt;findOneOrFail&lt;/code&gt; 或 &lt;code&gt;findOneByOrFail&lt;/code&gt;，当查询不到数据时，它会在底层直接抛出 &lt;code&gt;EntityNotFoundError&lt;/code&gt;。
我们利用全局过滤器在框架顶层捕获这个异常，并统一转换为 HTTP 404。这使得我们的 Service 层代码异常干净，完全不需要关注异常的分发，只需关注正确的业务流。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 防止内存溢出：TypeORM 海量数据批量插入&lt;/h2&gt;
&lt;p&gt;在做初始化脚本（如导入全国省市区行政区划数据）时，我们需要将解析出的海量数据一次性写入数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/scripts/seed-region.ts
console.log(
  `总共提取出 ${regionsToInsert.length} 条行政区划数据，准备执行插入...`,
);

// 经典的大数据分块插入 (Chunking)
const chunkSize = 1000;
for (let i = 0; i &amp;lt; regionsToInsert.length; i += chunkSize) {
  const chunk = regionsToInsert.slice(i, i + chunkSize);
  await regionRepo.save(chunk);
  console.log(
    `已插入 ${Math.min(i + chunkSize, regionsToInsert.length)} / ${regionsToInsert.length} 条...`,
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果直接 &lt;code&gt;await regionRepo.save(regionsToInsert)&lt;/code&gt; 会发生什么？&lt;/strong&gt;
这往往会导致两个灾难性的后果：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node.js OOM (内存溢出)&lt;/strong&gt;：TypeORM 在执行 &lt;code&gt;save&lt;/code&gt; 时，会在内存中为每一个实体对象生成极其复杂的依赖图和查询构建树。几万条数据瞬间就会把 V8 引擎的堆内存撑爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库拒收&lt;/strong&gt;：哪怕 Node.js 撑住了，它拼接出来的那条超级巨大的 &lt;code&gt;INSERT INTO&lt;/code&gt; SQL 语句，极有可能超出数据库配置的包大小限制（例如 MySQL 的 &lt;code&gt;max_allowed_packet&lt;/code&gt;，默认通常只有几 MB），导致写入直接失败。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunking (分块) 的工程化思维：&lt;/strong&gt;
通过切割数组，每次只处理 1000 条数据。这不仅将内存占用控制在了一个极低的安全水位，还巧妙避开了数据库的包体积限制，是后端处理批量导入的标准化最佳实践。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>高性能 JSON 工具 (JsonTools) 的架构设计与实现</title><link>https://nollieleo.github.io/posts/json-tools-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/json-tools-architecture/</guid><description>深度剖析 JsonTools项目。探讨在处理前端海量的抓包数据与大型接口返回时，如何利用 Web Worker 离线计算、虚拟列表 (Virtual List) 高效渲染、以及自定义 JSON Diff 乱序比对引擎，解决主线程卡死问题。</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与痛点：充满挑战的“JSON”自由&lt;/h2&gt;
&lt;p&gt;在前端开发、接口联调或生产环境排查的日常中，我每天都在和无数的 JSON 数据打交道。
市面上并不缺 JSON 格式化工具（比如普通的浏览器插件、或者在线的 JSON Editor），但当系统越来越庞大、微服务拆分越来越细时，我们抓包拿到的&lt;strong&gt;接口返回值动辄大几兆、甚至包含几十万个节点的大型 JSON 树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统的 JSON 工具在面对这类“巨物”时，常常暴露出主要的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;渲染主线程卡死 (UI Freezing)&lt;/strong&gt;：直接将几十万行的 JSON 塞进页面渲染，浏览器会瞬间假死，滚动条甚至无法拖动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非常糟糕的搜索体验&lt;/strong&gt;：在浏览器原生 &lt;code&gt;Ctrl+F&lt;/code&gt; 搜索百万级数据时，每按一个字母都会导致长达数秒的页面无响应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构比对 (Diff) 的伪智能&lt;/strong&gt;：普通的 Diff 工具只会做非常死板的“字符串”比对。如果接口返回的数据只是更换了对象 Key 的排列顺序，或是末尾多了一个逗号，传统工具就会标红报错，这让排查排错变得非常痛苦。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决这一系列开发痛点，我独立主导并孵化了 &lt;strong&gt;JsonTools&lt;/strong&gt; 这个专为底层开发者打造的 Chrome 扩展。它融合了极高的性能优化和深度的前端计算逻辑，本文将对它的架构实现进行详细拆解。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心架构设计：多线程离线计算与虚拟化渲染的结合&lt;/h2&gt;
&lt;p&gt;处理超大型 JSON 渲染的唯一解，就是&lt;strong&gt;不在主线程里做繁重的运算&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 JsonTools 的架构中，我严格遵守了“展示与计算彻底分离”的原则。UI 层 (React) 只负责展现当前屏幕能看到的几十行代码，而将 JSON 的反序列化、节点的折叠/展开运算、以及搜索的高亮计算全部移至了后端的 Web Worker 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph UI [&quot;主线程 (React + virtua)&quot;]
        Input[&quot;用户输入/粘贴大型 JSON&quot;] --&amp;gt;|postMessage| WorkerProxy
        Scroll[&quot;用户滚动视窗 (Viewport)&quot;] --&amp;gt; Virtua[&quot;virtua 虚拟列表&quot;]
        Virtua --&amp;gt; Render[&quot;仅渲染可见的 50 个 DOM 节点&quot;]
        
        WorkerProxy --&amp;gt;|接收 1D VirtualLine 数组| Virtua
    end

    subgraph WORKER [&quot;子线程计算核心 (Web Worker)&quot;]
        Parse[&quot;JSON.parse / 安全 Eval 兜底&quot;] --&amp;gt; Flattener{&quot;Transformer (架构优势)&quot;}
        
        Flattener --&amp;gt;|检查展开/折叠状态| Filter[&quot;Regex / Path 搜索匹配&quot;]
        Filter --&amp;gt;|扁平化为一维数组| Result[&quot;VirtualLine[]&quot;]
        Result --&amp;gt;|postMessage 回传| UI
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种架构彻底释放了浏览器的渲染压力。无论用户输入的 JSON 是一百行还是一百万行，主线程始终只维护当前视窗内的轻量级 DOM 结构，做到了极致的毫秒级无延迟反馈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;架构思考：跨线程通信的“结构化克隆”损耗&lt;/strong&gt;
把一个几十 MB 的庞大 JSON 字符串通过 &lt;code&gt;postMessage&lt;/code&gt; 传递给 Web Worker，底层其实会触发浏览器的 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm&quot;&gt;Structured Clone Algorithm (结构化克隆算法)&lt;/a&gt;。这种序列化与反序列化确实会带来几十到上百毫秒的开销。
&lt;strong&gt;但这种取舍是完全值得的&lt;/strong&gt;。这笔微小的算力开销发生在异步的子线程中，主线程的 Event Loop 完全不受影响。在这几十毫秒里，用户的页面滚动、按钮点击甚至 CSS 动画依然保持着 60FPS 的完美顺滑，这正是现代前端性能优化的核心理念——&lt;strong&gt;不要阻塞主线程&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 攻克难题一：百万级 JSON 树的极致展平 (Transformer)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;业务痛点&lt;/strong&gt;：
JSON 本质上是一棵非常庞大、无限嵌套的多叉树（Tree）。而基于虚拟列表 (&lt;code&gt;virtua&lt;/code&gt;) 的渲染器，只能接受一维的数组。如何把“树”扁平化为“线”，并且要完美支持用户随时随地的“展开”与“折叠”交互？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案&lt;/strong&gt;：
我在 Worker 内部编写了一个非常核心的 &lt;code&gt;transformer.ts&lt;/code&gt; 引擎。它不是简单地调用 &lt;code&gt;JSON.stringify&lt;/code&gt;，而是结合了 DFS（深度优先遍历）算法，配合一个维护用户交互状态的 &lt;code&gt;expandedPaths&lt;/code&gt; / &lt;code&gt;collapsedPaths&lt;/code&gt; 集合（Set）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Start([&quot;输入: 当前 JSON 节点 (value, path, depth)&quot;]) --&amp;gt; CheckState{&quot;查询 expandedPaths: 当前节点是否已展开?&quot;}
    CheckState --&amp;gt;|&quot;否 (已折叠)&quot;| Collapsed[&quot;生成单行 VirtualLine (如 &apos;{...}&apos;)&quot;]
    Collapsed --&amp;gt; End([&quot;回溯 (剪枝，不再遍历子节点)&quot;])
    
    CheckState --&amp;gt;|&quot;是 (已展开)&quot;| GenerateStart[&quot;生成起始符 VirtualLine (如 &apos;{&apos;)&quot;]
    GenerateStart --&amp;gt; IsObject{&quot;节点类型?&quot;}
    IsObject --&amp;gt;|&quot;Object/Array&quot;| Loop[&quot;遍历 Object.keys() 或 Array 元素&quot;]
    Loop --&amp;gt; Recursive[&quot;递归调用 jsonToVirtualLines (depth + 1)&quot;]
    Recursive --&amp;gt; Loop
    Loop --&amp;gt; GenerateEnd[&quot;生成闭合符 VirtualLine (如 &apos;}&apos;)&quot;]
    GenerateEnd --&amp;gt; End
    
    IsObject --&amp;gt;|&quot;Primitive (string/number)&quot;| Primitive[&quot;生成基础类型 VirtualLine&quot;]
    Primitive --&amp;gt; End
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心降维代码 (&lt;code&gt;src/utils/transformer.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const jsonToVirtualLines = (
  key: string | undefined,
  value: JsonValue,
  depth: number,
  path: string,
  idPath: (string | number)[],
  onLine: (line: VirtualLine) =&amp;gt; void,
  isExpanded: (path: string, depth: number) =&amp;gt; boolean
) =&amp;gt; {
  // 判断当前节点的折叠状态
  const expanded = isExpanded(path, depth)
  const isCollapsed = !expanded

  const isObj = isPlainObject(value)
  const isArr = Array.isArray(value)

  // 1. 如果当前节点是折叠状态，直接放弃对其子节点的递归遍历，节约海量算力
  if (isCollapsed &amp;amp;&amp;amp; (isObj || isArr)) {
    onLine({
      id: `${path}__start`,
      path, depth,
      type: &quot;collapsed&quot;,
      content: key ? `&quot;${key}&quot;: ${isArr ? &quot;[...]&quot; : &quot;{...}&quot;}` : (isArr ? &quot;[...]&quot; : &quot;{...}&quot;),
    })
    return
  }

  // 2. 否则，生成起始行 ({ 或 [)，并递归下钻子节点
  if (isObj || isArr) {
    onLine({
      id: `${path}__start`,
      path, depth,
      type: isArr ? &quot;array_start&quot; : &quot;object_start&quot;,
      content: key ? `&quot;${key}&quot;: ${isArr ? &quot;[&quot; : &quot;{&quot;}` : (isArr ? &quot;[&quot; : &quot;{&quot;),
    })
    
    // ... 遍历 Object.keys 或 Array 递归调用 jsonToVirtualLines
    
    // 生成闭合行 (} 或 ])
    onLine({ /* ... */ type: &quot;object_end&quot;, content: &quot;}&quot; })
  } else {
    // 3. 压平基本数据类型节点 (string, number, boolean)
    onLine({ /* ... */ type: &quot;primitive&quot;, content: `&quot;${key}&quot;: ${JSON.stringify(value)}` })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用这套在 Web Worker 中的高速递归算法，&lt;strong&gt;一百万行的 JSON 树只需几十毫秒即可被高效映射成一维数组 (&lt;code&gt;VirtualLine[]&lt;/code&gt;)&lt;/strong&gt;。当用户点击“折叠”某个对象时，只需更新状态，再进行一次轻量级的离线重算，主线程 UI 会瞬间完成无缝更替。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;极致渲染的隐秘基石：等宽字体与 O(1) 物理模型&lt;/strong&gt;
虚拟列表 (&lt;code&gt;virtua&lt;/code&gt; 或 &lt;code&gt;react-window&lt;/code&gt;) 最怕的是“动态高度”。如果每一行 JSON 的高度不固定，系统在滚动时就需要实时计算布局重排 (Reflow)，这在百万级数据下依然是灾难。
为了彻底榨干性能，我在 JsonTools 的渲染层强制使用了 &lt;strong&gt;等宽字体 (Monospace)&lt;/strong&gt; 和 &lt;strong&gt;绝对固定的行高 (Fixed Item Height)&lt;/strong&gt;。这个深度的物理设定，把原本非常复杂的 DOM 高度测量全部变成了极速的 &lt;code&gt;O(1)&lt;/code&gt; 数学乘法（&lt;code&gt;scrollTop = index * 20px&lt;/code&gt;），构筑了大量数据滚动流畅的最终防线。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 攻克难题二：基于语义的真·结构化 JSON Diff 引擎&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;业务痛点&lt;/strong&gt;：
平时排查接口问题时，最常见的场景就是把两段 JSON 数据扔进 Diff 工具。
但普通的 Diff 工具（包括 Github 的比对）通常只会简单地对比&lt;strong&gt;纯字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果接口 A 返回的是 &lt;code&gt;{&quot;id&quot;: 1, &quot;name&quot;: &quot;foo&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;接口 B 返回的是 &lt;code&gt;{&quot;name&quot;: &quot;foo&quot;, &quot;id&quot;: 1}&lt;/code&gt;
这两个 JSON 在业务语义上是&lt;strong&gt;完全等价&lt;/strong&gt;的，但纯文本 Diff 会将它们整块标红。甚至在某些换行处多了一个无足轻重的尾部逗号（&lt;code&gt;,&lt;/code&gt;），也会被强行标红，让排查者眼花缭乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;攻克方案&lt;/strong&gt;：
我在 &lt;code&gt;jsonDiff.worker.ts&lt;/code&gt; 中，借助底层的 &lt;code&gt;diff&lt;/code&gt; 库，重新实现了一个&lt;strong&gt;高定版、懂 JSON 语义的乱序比对引擎 (&lt;code&gt;DiffJsonNoSort&lt;/code&gt;)&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    subgraph Input [&quot;输入层&quot;]
        Left[&quot;Left JSON (旧数据)&quot;]
        Right[&quot;Right JSON (新数据)&quot;]
    end
    
    subgraph Preprocess [&quot;预处理层 (Worker 内)&quot;]
        Parse[&quot;JSON.parse 解析为 AST 对象&quot;]
        Sort[&quot;递归执行 sortObjectKeys() (字母序重排)&quot;]
        Stringify[&quot;JSON.stringify 重新序列化&quot;]
    end
    
    subgraph DiffEngine [&quot;自定义语义化 Diff 引擎&quot;]
        Tokenize[&quot;按行切片 (tokenize)&quot;]
        Equals{&quot;重写 equals() 判断逻辑&quot;}
        Regex[&quot;正则忽略尾部逗号和换行: replace(/,([\\r\\n])/g, &apos;$1&apos;)&quot;]
    end
    
    Input --&amp;gt; Parse
    Parse --&amp;gt; Sort
    Sort --&amp;gt; Stringify
    Stringify --&amp;gt; Tokenize
    Tokenize --&amp;gt; Equals
    Equals --&amp;gt; Regex
    Regex --&amp;gt; Output([&quot;输出精准的增删改差异块&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;乱序对比洗牌&lt;/strong&gt;：在传给 Diff 引擎之前，利用深拷贝递归方法 &lt;code&gt;sortObjectKeys&lt;/code&gt;，强行将所有 Object 内部的 Keys 按照字母顺序重新排列，抹平服务端因序列化产生的无序性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重写底层容错规则&lt;/strong&gt;：我继承了原始的 &lt;code&gt;Diff&lt;/code&gt; 类，修改了它的核心 &lt;code&gt;equals&lt;/code&gt; 方法，巧妙地利用正则表达式剥离了碍眼的尾部换行逗号，还开发者一个最纯净的比对视图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心定制代码 (&lt;code&gt;src/workers/jsonDiff.worker.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as Diff from &quot;diff&quot;

// 递归洗牌函数：将所有对象的 key 强行排序，解决乱序 Diff 痛点
function sortObjectKeys(obj: unknown): unknown {
  if (obj === null || typeof obj !== &quot;object&quot;) return obj;
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
  
  const sortedKeys = Object.keys(obj).sort();
  const result: Record&amp;lt;string, unknown&amp;gt; = {};
  for (const key of sortedKeys) {
    result[key] = sortObjectKeys((obj as Record&amp;lt;string, unknown&amp;gt;)[key]);
  }
  return result;
}

// 继承底层引擎，重写高阶匹配法则
class DiffJsonNoSort extends Diff.Diff {
  tokenize(value: string) {
    return value.split(/^/m) // 按行切片
  }
  castInput(value: unknown) {
    return typeof value === &quot;string&quot; ? value : JSON.stringify(value, null, 2)
  }
  // 核心魔法：判定两行是否一致时，无视掉末尾的逗号 (trailing commas)
  equals(left: string, right: string) {
    return (
      left.replace(/,([\r\n])/g, &quot;$1&quot;) === right.replace(/,([\r\n])/g, &quot;$1&quot;)
    )
  }
}
export const diffJsonNoSort = new DiffJsonNoSort()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套降维和重组，前端同学在排查 Diff 时，看到的不再是复杂的红绿代码块，而是非常精准、真正发生变动的核心字段。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 交互层点睛：不仅要快，还要极具扩展性&lt;/h2&gt;
&lt;p&gt;作为一个独立的开发者工具，JsonTools 还内置了一套极高水准的查询引擎（Query Engine）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全方位搜索&lt;/strong&gt;：支持正则表达式 (Regex)、全词匹配 (Whole Word) 以及精准的&lt;strong&gt;对象路径搜索 (Path Search)&lt;/strong&gt;。所有查询都在子线程离线执行完毕并生成包含高亮边界的 &lt;code&gt;VirtualLine&lt;/code&gt; 节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立窗口体验&lt;/strong&gt;：摒弃了小气的弹窗 &lt;code&gt;Popup&lt;/code&gt;，JsonTools 被设计成了利用 &lt;code&gt;chrome.tabs.create&lt;/code&gt; 开启在独立专属的 Tab 页 (&lt;code&gt;tabs/json-preview.html&lt;/code&gt; / &lt;code&gt;json-diff.html&lt;/code&gt;) 中运行，最大化利用开发者的超宽屏幕和排查体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发者体验拉满&lt;/strong&gt;：配合 &lt;code&gt;CopiableText&lt;/code&gt; 提供了一键拷贝任意深层路径的值的能力；结合 &lt;code&gt;HighlightText&lt;/code&gt; 让匹配字符如同自带发光特效。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 业务踩坑：Chrome Extension 的内存沙箱与大文件读取&lt;/h2&gt;
&lt;p&gt;除了渲染，在处理大文件时，Chrome 插件本身也是一个雷区。&lt;/p&gt;
&lt;p&gt;如果你试图在插件的 Popup 页面里，通过 &lt;code&gt;&amp;lt;input type=&quot;file&quot;&amp;gt;&lt;/code&gt; 让用户上传一个 500MB 的 JSON 日志文件，然后用 &lt;code&gt;FileReader.readAsText()&lt;/code&gt; 去读取它，&lt;strong&gt;浏览器会直接崩溃并报出 &lt;code&gt;Out of Memory&lt;/code&gt; (OOM)&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;6.1 突破 V8 字符串长度限制&lt;/h3&gt;
&lt;p&gt;V8 引擎对单个字符串的最大长度是有硬性限制的（通常在 512MB 到 1GB 之间，具体取决于系统架构）。如果你硬生生地把 500MB 的文件读成一个完整的长字符串，再扔进 &lt;code&gt;JSON.parse()&lt;/code&gt;，不仅内存占用会瞬间飙升到几 GB（因为 V8 内部用 UTF-16 编码字符串，体积翻倍），而且一定会触发 V8 引擎底层的分配失败。&lt;/p&gt;
&lt;h3&gt;6.2 工业级解法：Stream API 与增量解析 (Streaming Parser)&lt;/h3&gt;
&lt;p&gt;针对这种极端的本地日志排查场景，JsonTools 舍弃了传统的 &lt;code&gt;FileReader&lt;/code&gt;，转而使用浏览器原生的 &lt;strong&gt;&lt;code&gt;ReadableStream&lt;/code&gt;&lt;/strong&gt; 配合支持流式解析的 JSON 库（如 &lt;code&gt;Oboe.js&lt;/code&gt; 或基于 WASM 的流式解析器）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 极度精简的流式读取伪代码
const file = fileInput.files[0];
const stream = file.stream(); // 获取 ReadableStream
const reader = stream.getReader();
const decoder = new TextDecoder(&apos;utf-8&apos;);

let partialChunk = &apos;&apos;;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  // 每次只处理内存中极小的一块 Buffer (Uint8Array)
  const chunkString = decoder.decode(value, { stream: true });
  
  // 将 chunk 送入流式 JSON 解析器
  // 只要解析出一个完整的子对象（比如数组里的一个 Item），就立刻抛出事件让 Worker 去处理成 VirtualLine
  streamingParser.write(chunkString); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过流式分块（Chunking），我们实现了内存的极低恒定占用（O(1) 空间复杂度），使得 JsonTools 成为了一个能真正处理 GB 级别生产日志的工业级工具。&lt;/p&gt;
&lt;h2&gt;7. 总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JsonTools&lt;/code&gt; 并非只是一个把别人的库集成起来的小玩具，它是一次关于浏览器渲染极限和并发计算能力深挖的工程实践。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;面对海量数据渲染瓶颈&lt;/strong&gt;，果断引入 Web Worker 多线程离线递归与 Virtua 虚拟列表切片渲染，把主线程从沉重的 DOM 负担中解放出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面对僵化的纯文本比对&lt;/strong&gt;，深入到底层 Diff AST 引擎重写匹配规则并增加递归乱序洗牌。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>高性能 JSON 终极处理工具 JsonTools 使用指南</title><link>https://nollieleo.github.io/posts/json-tools-manual/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/json-tools-manual/</guid><description>还在为超大型接口返回值卡死浏览器而苦恼吗？JsonTools 是一款专为硬核开发者打造的 Chrome 扩展，基于 Web Worker 和虚拟列表技术，支持百万级数据秒开、乱序 Diff 比对以及深度路径检索。本文将带你全面了解它的强大功能。</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 简介与安装&lt;/h2&gt;
&lt;p&gt;欢迎使用 &lt;strong&gt;JsonTools&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;这并不是市面上随处可见的普通 JSON 格式化工具。JsonTools 是专为解决&lt;strong&gt;超大型 JSON 数据处理痛点&lt;/strong&gt;而生的高阶扩展程序。&lt;/p&gt;
&lt;p&gt;当你面对数以十兆计的抓包数据，或是包含几十万个节点的巨型接口返回值时，普通工具往往会让浏览器瞬间假死。而 JsonTools 凭借底层的 Web Worker 离线计算与极致的 Virtua 虚拟列表渲染，不仅能做到&lt;strong&gt;百万级数据秒开&lt;/strong&gt;，还内置了&lt;strong&gt;防乱序的高级 Diff 比对引擎&lt;/strong&gt;和强大的&lt;strong&gt;全方位检索系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;&lt;a href=&quot;#&quot;&gt;点击前往 Chrome 网上应用店安装 JsonTools&lt;/a&gt;&lt;/strong&gt; &lt;em&gt;(后续补充商店链接)&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 基础使用与极速预览 (JSON Preview)&lt;/h2&gt;
&lt;h3&gt;2.1 开启独立预览窗口&lt;/h3&gt;
&lt;p&gt;点击 Chrome 浏览器右上角的 JsonTools 插件图标，在弹出的面板中选择 &lt;strong&gt;JSON 预览 (Preview)&lt;/strong&gt;。
为了提供最沉浸的开发者排查体验，JsonTools 摒弃了局促的弹窗，会为你开启一个专属的宽屏独立标签页。&lt;/p&gt;
&lt;h3&gt;2.2 秒开巨型 JSON 数据&lt;/h3&gt;
&lt;p&gt;无论你手头的 JSON 数据有多庞大，只需将其粘贴到左侧的输入框中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;零卡顿解析&lt;/strong&gt;：基于多线程架构，JsonTools 会在后台静默完成解析，绝不阻塞你的任何操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无限流畅滚动&lt;/strong&gt;：右侧的格式化视图采用了虚拟滚动（Virtual Scroll）技术，即使是百万行级别的 JSON 树，你也可以随意拖拽滚动条，丝滑流畅。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./json-format-preview.png&quot; alt=&quot;格式化结果的全屏截图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.3 沉浸式树状交互与一键拷贝&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点折叠/展开&lt;/strong&gt;：点击左侧的箭头，可以快速收起或展开庞大的对象和数组节点。底层引擎会智能过滤不必要的渲染，瞬间完成布局更替。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深层路径一键提取&lt;/strong&gt;：排查接口时，往往需要提取极深层级字段的取值路径（例如 &lt;code&gt;data.users[0].profile.avatar&lt;/code&gt;）。在 JsonTools 中，你只需将鼠标悬浮在任意 Key 上，点击旁边出现的&lt;strong&gt;复制图标&lt;/strong&gt;，即可一键将该节点的完整数据或访问路径提取到剪贴板。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 高阶功能：真·语义化比对 (JSON Diff)&lt;/h2&gt;
&lt;p&gt;如果你经常需要排查“为什么昨天正常的接口今天挂了”，那么 JsonTools 的 &lt;strong&gt;Diff&lt;/strong&gt; 功能将是你的最强辅助。&lt;/p&gt;
&lt;h3&gt;3.1 告别乱序干扰，专注核心变更&lt;/h3&gt;
&lt;p&gt;传统的文本比对工具非常死板：只要对象内部的字段顺序发生了变化（比如 A 接口先返回 &lt;code&gt;id&lt;/code&gt; 后返回 &lt;code&gt;name&lt;/code&gt;，B 接口相反），它们就会标红报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JsonTools 的 Diff 引擎是“懂 JSON 的”&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在面板中点击 &lt;strong&gt;JSON 比对 (Diff)&lt;/strong&gt; 进入专属比对模式。&lt;/li&gt;
&lt;li&gt;将两段数据分别粘贴到 Left 和 Right 窗口。&lt;/li&gt;
&lt;li&gt;引擎会自动对所有对象进行&lt;strong&gt;深层递归洗牌（排序）&lt;/strong&gt;，并智能忽略掉末尾多余的逗号差异。&lt;/li&gt;
&lt;li&gt;你看到的，将是&lt;strong&gt;真正发生变动的业务字段&lt;/strong&gt;，而不是被无意义的乱序干扰的红绿代码块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./json-diff-preview.png&quot; alt=&quot;差异字段精准标红&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 全方位检索引擎 (Query Engine)&lt;/h2&gt;
&lt;p&gt;在几十万行的 JSON 中找一个特定的值无异于大海捞针，且使用浏览器原生的 &lt;code&gt;Ctrl+F&lt;/code&gt; 会导致严重的页面卡顿。JsonTools 为此重写了整套检索机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极速离线搜索&lt;/strong&gt;：输入关键字后，庞大的计算工作全都在 Web Worker 中瞬间完成，匹配结果会高亮显示并带上发光特效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高级过滤选项&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正则搜索 (Regex)&lt;/strong&gt;：支持使用正则表达式进行高级模式匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分大小写 (Match Case)&lt;/strong&gt;：精准锁定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全词匹配 (Whole Word)&lt;/strong&gt;：排除相似词根的干扰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径穿透搜索 (Path Search)&lt;/strong&gt;：你可以直接搜索像 &lt;code&gt;user.*.id&lt;/code&gt; 这样的深层路径键名，快速定位嵌套极深的特定结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./json-search-preview.png&quot; alt=&quot;右上角搜索&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 个性化设置与语言支持&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多语言切换&lt;/strong&gt;：点击插件主面板右上角的翻译图标，可以一键在中文（简体）和英文之间自由切换界面语言。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主题与视效&lt;/strong&gt;：JsonTools 默认采用对开发者极其友好的高对比度暗黑（Dark）主题，配合精致的代码语法高亮，长时间排查问题也不会感到刺眼。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;(如果觉得这款工具对你有帮助，欢迎在扩展面板底部点击支持作者的 Afdian 或 Ko-fi 链接，你的支持是我持续优化的最大动力 ❤)&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>Zion 定制化 ESLint 规则设计与实现</title><link>https://nollieleo.github.io/posts/zed-custom-eslint-rules/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zed-custom-eslint-rules/</guid><description>在数百万行代码的大型前端单体仓库中，如何通过 ESLint 自定义规则实现架构分层、模块黑盒化解耦，以及彻底消除 MobX 响应式状态遗漏的神出鬼没 Bug？本文详细解读了为 Zion Editor 量身定制的三大底层 AST 分析规则。</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与初衷&lt;/h2&gt;
&lt;p&gt;随着 Zion Editor (&lt;code&gt;zed&lt;/code&gt;) 前端工程体量的不断溢出，传统的业界开源 ESLint 规则集（如 &lt;code&gt;eslint-config-airbnb&lt;/code&gt;、&lt;code&gt;plugin:react/recommended&lt;/code&gt;）已经远远无法满足我们对大型项目&lt;strong&gt;架构分层、模块解耦以及状态响应安全性&lt;/strong&gt;的非常苛刻的要求。&lt;/p&gt;
&lt;p&gt;在团队数十人的日常协作中，经常会遇到以下令人头疼的架构腐化问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;跨模块深度引用&lt;/strong&gt;：A 模块图方便，绕过了 B 模块暴露的公共 &lt;code&gt;index.ts&lt;/code&gt;，直接 &lt;code&gt;import&lt;/code&gt; 了 B 模块内部极深层级的一个私有组件或函数。导致 B 模块重构时牵一发而动全身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构反向依赖（反向依赖）&lt;/strong&gt;：底层的 &lt;code&gt;utils&lt;/code&gt; 或 &lt;code&gt;hooks&lt;/code&gt; 模块，为了方便，竟然 import 了最顶层的 &lt;code&gt;views&lt;/code&gt; 模块里的常量，导致底层代码彻底失去独立性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式状态遗漏的难以排查的 Bug&lt;/strong&gt;：开发者在 React 组件里通过 Hook 取出了 MobX 的 Store，却忘记用 &lt;code&gt;observer&lt;/code&gt; 包裹该组件。导致数据发生变化时，UI 死活不更新，非常难排查。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;借着构建系统（Webpack 到 Rsbuild）及 ESLint 引擎（升级至 v9 Flat Config）大升级的契机，我在 &lt;code&gt;config/eslint-rules/&lt;/code&gt; 目录下&lt;strong&gt;从零自研了一套专属于 zed 的定制化 ESLint AST 规则集&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文将详细剖析这三大底层规则的设计思想与实现细节。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 规则一：跨模块深度引用限制 (no-cross-module-deep-import)&lt;/h2&gt;
&lt;h3&gt;痛点与设计目标&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;zed&lt;/code&gt; 各个业务模块（如 &lt;code&gt;views/AF&lt;/code&gt;、&lt;code&gt;components/LegendaryTable&lt;/code&gt;、&lt;code&gt;mobx/stores/FileStore&lt;/code&gt;）在设计上应该像黑盒一样，外部只能通过其根目录的 &lt;code&gt;index.ts&lt;/code&gt; 访问公共 API。
如果放任跨模块深度引用，会产生极强的、不可控的网状耦合，并极易引发循环依赖。&lt;/p&gt;
&lt;p&gt;我的目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;防范私有 API 泄露&lt;/strong&gt;：确保模块内部重构（修改私有组件名、移动目录）安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白名单豁免机制&lt;/strong&gt;：允许跨模块引用纯类型（&lt;code&gt;import type&lt;/code&gt;），因为类型在编译后会被抹除，不造成实际运行时耦合；允许特定公共出口（如 &lt;code&gt;constants&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt;）的深度引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;核心实现思路与 AST 拦截&lt;/h3&gt;
&lt;p&gt;该规则的核心是拦截所有的模块导入途径，包括普通 &lt;code&gt;import&lt;/code&gt;、重导出 (&lt;code&gt;export {x} from&lt;/code&gt;) 甚至动态按需加载 (&lt;code&gt;await import()&lt;/code&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Start([&quot;拦截 Import 语句&quot;]) --&amp;gt; Parse[&quot;提取并格式化目标路径&quot;]
    Parse --&amp;gt; CheckRoot{&quot;目标是否属于受保护的目录?&quot;}
    
    CheckRoot --&amp;gt;|&quot;是 (如 zed/views)&quot;| CheckDepth{&quot;引用深度 &amp;gt; 根目录深度?&quot;}
    CheckRoot --&amp;gt;|&quot;否&quot;| End((&quot;放行&quot;))
    
    CheckDepth --&amp;gt;|&quot;是 (跨界了)&quot;| CheckWhiteList{&quot;是否在白名单出口?&quot;}
    CheckDepth --&amp;gt;|&quot;否 (合法的根级引入)&quot;| End
    
    CheckWhiteList --&amp;gt;|&quot;是 (如 /index, /constants)&quot;| End
    CheckWhiteList --&amp;gt;|&quot;否&quot;| CheckSameModule{&quot;当前文件与目标文件属于同模块?&quot;}
    
    CheckSameModule --&amp;gt;|&quot;是 (模块内互相引用)&quot;| End
    CheckSameModule --&amp;gt;|&quot;否&quot;| Report([&quot;抛出 Lint 报错&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// config/eslint-rules/no-cross-module-deep-import.mjs (节选片段)
create(context) {
  const checkImport = (node, importPath, isTypeOnly = false) =&amp;gt; {
    // 1. 纯类型导入直接豁免，不造成运行时耦合
    if (isTypeOnly) return;
    
    // 2. 解析与格式化路径
    const normalizedImportPath = targetImportPath.replace(/^@src\//, &apos;&apos;);
    const importParts = normalizedImportPath.split(&apos;/&apos;).filter(Boolean);

    // 3. 动态配置各核心目录的合法深度阈值
    const protectedRoots = {
      views: { depth: 3 },       // e.g. zed/views/AF (允许的根深度为3)
      components: { depth: 3 },  // e.g. zed/components/Button
      mobx: { depth: 4, subName: &apos;stores&apos; }, // e.g. zed/mobx/stores/FileStore
    };
    const config = protectedRoots[importParts[1]];
    if (!config) return;

    // 4. 判定引入是否越界（深度大于根目录深度）
    if (importParts.length &amp;lt;= config.depth) return;

    // 5. 白名单放行：允许深入引入暴露的公共出口
    const extraPath = importParts.slice(config.depth).join(&apos;/&apos;);
    const allowedDeepPaths = /^((index(\.(tsx|ts|js|jsx))?)|constants(\.(ts|js))?|types(\/.*)?)$/;
    if (allowedDeepPaths.test(extraPath)) return;

    // 6. 模块内同源放行：属于同一个模块内部的代码可以随意互相引入
    if (currentModuleName === importedModuleName) return;

    // 7. 越界且非同源，触发红牌警告！
    context.report({
      node,
      messageId: &apos;deepImportForbidden&apos;,
      data: { sourceModule: currentModuleName, targetModule: importedModuleName, internalPath: extraPath }
    });
  };

  return {
    ImportDeclaration(node) { checkImport(node.source, node.source.value, node.importKind === &apos;type&apos;); },
    ExportNamedDeclaration(node) { /* ... */ },
    ImportExpression(node) { /* ... */ }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 规则二：架构反向依赖限制 (no-reverse-dependency)&lt;/h2&gt;
&lt;h3&gt;痛点与设计目标&lt;/h3&gt;
&lt;p&gt;标准的分层架构数据流应当是单向的：上层（视图）可以依赖下层（组件/工具），下层绝不能依赖上层。但在历史长河中，常出现底层 &lt;code&gt;utils&lt;/code&gt; 贪图方便引入了顶层 &lt;code&gt;views&lt;/code&gt; 的情况。
这会导致底层逻辑被顶层业务“污染”，破坏了 &lt;code&gt;hooks&lt;/code&gt;、&lt;code&gt;utils&lt;/code&gt; 等通用代码被抽离为独立 NPM 包的可能性（严重影响我们后续引入 Turborepo 的架构演进）。&lt;/p&gt;
&lt;h3&gt;核心实现思路与架构防腐&lt;/h3&gt;
&lt;p&gt;我在 AST 规则中硬编码了整个项目的&lt;strong&gt;架构层级秩序表（数字越小层级越高，权限越高）&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph Layer1 [&quot;Layer 1: 业务顶层&quot;]
        V[&quot;views / App&quot;]
    end
    subgraph Layer2 [&quot;Layer 2: 视图组件层&quot;]
        C[&quot;components&quot;]
    end
    subgraph Layer3 [&quot;Layer 3: 状态模型层&quot;]
        M[&quot;mobx / models&quot;]
    end
    subgraph Layer4 [&quot;Layer 4: 工具与逻辑层&quot;]
        H[&quot;hooks / utils&quot;]
    end
    subgraph Layer5 [&quot;Layer 5: 基础定义层&quot;]
        T[&quot;types / constants&quot;]
    end

    V --&amp;gt; C
    C --&amp;gt; M
    M --&amp;gt; H
    H --&amp;gt; T
    
    H -.-&amp;gt;|&quot;禁止反向依赖&quot;| V
    M -.-&amp;gt;|&quot;禁止反向依赖&quot;| C
    C -.-&amp;gt;|&quot;禁止越级向上&quot;| V
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// config/eslint-rules/no-reverse-dependency.mjs (节选片段)
create(context) {
  // 维护了一份硬编码的绝对秩序架构层级表
  const layerConfig = {
    views: 1,      App: 1,
    components: 2,
    mobx: 3,       models: 3,
    hooks: 4,      utils: 4,
    types: 5,      constants: 5,
  };

  return {
    ImportDeclaration(node) {
      // 1. 获取当前文件所在的层级
      const sourceLayer = layerConfig[sourceModule];
      
      // 2. 获取试图 import 的目标文件层级
      const targetLayer = layerConfig[targetModule];

      // 3. 防腐判定：一旦当前层级数字大于目标层级（意味着底层的低权限模块引入了高层级模块）
      if (sourceLayer &amp;gt; 0 &amp;amp;&amp;amp; targetLayer &amp;gt; 0 &amp;amp;&amp;amp; sourceLayer &amp;gt; targetLayer) {
        context.report({
          node,
          message: `💩 架构规范：禁止反向依赖。 [${sourceModule}] 属于底层模块，不能直接依赖上层的 [${targetModule}]。`,
        });
      }
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;这套规则就像一道单向阀，从根本上杜绝了“反向依赖”的代码腐化。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 规则三：MobX 响应式遗漏检测 (mobx-require-observer)&lt;/h2&gt;
&lt;h3&gt;痛点与设计目标&lt;/h3&gt;
&lt;p&gt;Zion 高度依赖 MobX 进行响应式状态管理。最愚蠢但也最折磨人的 Bug 是：开发者在组件里使用了 &lt;code&gt;useStores()&lt;/code&gt; 提取了数据，却忘记用 &lt;code&gt;observer()&lt;/code&gt; 包裹组件。这导致底层数据变了，但 React 组件根本不更新！&lt;/p&gt;
&lt;p&gt;以前这种问题只能靠肉眼 Code Review，现在我将其写成了一套非常复杂的&lt;strong&gt;多重 AST 分析器&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;核心实现思路与 AST 攀爬探测&lt;/h3&gt;
&lt;p&gt;这个规则的难点在于：Store 是怎么取出来的？组件是怎么被包裹的？包裹的方式可能是在导出时，也可能是在定义时。&lt;/p&gt;
&lt;h4&gt;第一步：多维度 Store 提取捕获&lt;/h4&gt;
&lt;p&gt;我们通过监听 AST 的 &lt;code&gt;VariableDeclarator&lt;/code&gt; (变量声明)，利用正则智能推导是否在提取局部/全局 Store：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监听特定 Hook：&lt;code&gt;useStores()&lt;/code&gt;, &lt;code&gt;useLocalStore()&lt;/code&gt;, &lt;code&gt;useXxxStore()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;监听变量名暗示：&lt;code&gt;const canvasStore = useContext()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;监听解构行为：&lt;code&gt;const { afStore } = useAFContext()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;真实的 AST 语法树解剖视角（以 &lt;code&gt;useStores()&lt;/code&gt; 为例）&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;VariableDeclarator&quot;,
  &quot;id&quot;: { &quot;type&quot;: &quot;ObjectPattern&quot;, &quot;properties&quot;: [{ &quot;key&quot;: { &quot;name&quot;: &quot;canvasStore&quot; } }] },
  &quot;init&quot;: {
    &quot;type&quot;: &quot;CallExpression&quot;,
    &quot;callee&quot;: { &quot;type&quot;: &quot;Identifier&quot;, &quot;name&quot;: &quot;useStores&quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过比对 AST 节点特征：&lt;code&gt;node.init.type === &apos;CallExpression&apos;&lt;/code&gt; 且 &lt;code&gt;callee.name.includes(&apos;Store&apos;)&lt;/code&gt;，我们就能像精准定位一样，在编译阶段精准定位到响应式数据的挂载点。&lt;/p&gt;
&lt;h3&gt;4.1 业务踩坑：AST 向上作用域攀爬 (Scope Climbing) 的精准误判排雷&lt;/h3&gt;
&lt;p&gt;在检测“是否漏写了 &lt;code&gt;observer&lt;/code&gt;”这个规则中，最初级的写法往往是：找到 &lt;code&gt;useStore&lt;/code&gt;，然后就立刻抛出警告。
但这种写法在真实业务中会产生&lt;strong&gt;海量的误报 (False Positives)&lt;/strong&gt;，直接被同事们喷到下线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么会误报？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;普通工具函数调用&lt;/strong&gt;：开发者在一个普通的非 React &lt;code&gt;utils&lt;/code&gt; 函数里，调用了一个提供外置状态的 &lt;code&gt;getStore()&lt;/code&gt;，这根本不需要 &lt;code&gt;observer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高阶组件隔空包裹&lt;/strong&gt;：组件被 &lt;code&gt;forwardRef&lt;/code&gt; 或者 &lt;code&gt;withRouter&lt;/code&gt; 包裹了好几层，&lt;code&gt;observer&lt;/code&gt; 写在了最外层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 Hook 内部提取&lt;/strong&gt;：开发者写了一个 &lt;code&gt;useUserData&lt;/code&gt; 的 Hook，里面调用了 &lt;code&gt;useStore&lt;/code&gt;，此时警告应该抛给使用这个 Hook 的组件，而不是这个 Hook 本身！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：AST 节点攀爬与黑白名单过滤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了实现**“零误报”&lt;strong&gt;的 Lint 拦截，我编写了一段硬核的 AST 作用域攀爬代码。当我们在 AST 树中捕获到 Store 提取逻辑时，我们利用 &lt;code&gt;node.parent&lt;/code&gt; 指针，像爬树一样&lt;/strong&gt;逐层向上寻找当前代码执行的真实上下文 (Context)**。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 核心攀爬探测器：向上寻找真正的 React 组件宿主
function findReactComponentHost(node) {
  let currentNode = node.parent;
  
  while (currentNode) {
    // 1. 如果爬到了一个函数声明 (FunctionDeclaration / ArrowFunctionExpression)
    if (currentNode.type === &apos;FunctionDeclaration&apos; || currentNode.type === &apos;ArrowFunctionExpression&apos;) {
      
      // 获取函数的名称
      const funcName = getFunctionName(currentNode);
      
      // 2. 误报排雷：如果是以 use 开头的自定义 Hook，绝对安全，立刻放行！
      if (/^use[A-Z]/.test(funcName)) {
        return { type: &apos;HOOK&apos;, node: currentNode };
      }
      
      // 3. 命中目标：首字母大写，且有 return JSX 语句，确认为 React 组件！
      if (/^[A-Z]/.test(funcName) &amp;amp;&amp;amp; hasJSXReturn(currentNode)) {
        return { type: &apos;COMPONENT&apos;, node: currentNode, name: funcName };
      }
    }
    
    // 继续向上爬一层
    currentNode = currentNode.parent;
  }
  
  return { type: &apos;UNKNOWN&apos;, node: null };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种严密的 Scope Climbing 算法，插件拥有了近乎人类 Code Review 的上下文感知能力。它能精准识别出当前这行 &lt;code&gt;useStore&lt;/code&gt; 到底是在 Hook 里、在普通函数里、还是在真正的 React 渲染周期里，从而真正做到了可用性极高的工业级架构守卫。&lt;/p&gt;
&lt;h2&gt;5. “潜在问题”问题排查：老版本 ESLint 的字节数限制 Bug&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Start([&quot;拦截 VariableDeclarator&quot;]) --&amp;gt; CheckStore{&quot;匹配到 Store 提取?&quot;}
    CheckStore --&amp;gt;|&quot;是 (如 useStores)&quot;| Climb[&quot;AST 向上作用域攀爬 (node.parent)&quot;]
    CheckStore --&amp;gt;|&quot;否&quot;| End((&quot;放行&quot;))
    
    Climb --&amp;gt; CheckComp{&quot;遇到 React 组件或Hook?&quot;}
    CheckComp --&amp;gt;|&quot;是 Hook (use开头)&quot;| End
    CheckComp --&amp;gt;|&quot;是 React 组件 (首字母大写)&quot;| CheckWrap{&quot;父级是否被 observer 包裹?&quot;}
    CheckComp --&amp;gt;|&quot;未找到&quot;| End
    
    CheckWrap --&amp;gt;|&quot;已包裹&quot;| End
    CheckWrap --&amp;gt;|&quot;未被显式包裹&quot;| RegexCheck{&quot;源码正则兜底校验&quot;}
    
    RegexCheck --&amp;gt;|&quot;匹配成功&quot;| End
    RegexCheck --&amp;gt;|&quot;匹配失败&quot;| Report([&quot;抛出 Lint 报错&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// config/eslint-rules/mobx-require-observer.mjs (节选逻辑展示)
// 1. 向上攀爬寻找父级 React 组件
let currentNode = node;
let componentNode = null;
while (currentNode) {
  if (currentNode.type === &apos;FunctionDeclaration&apos; &amp;amp;&amp;amp; /^[A-Z]/.test(currentNode.id.name)) {
    componentNode = currentNode;
    break;
  }
  currentNode = currentNode.parent;
}

// 2. 检查 AST 结构是否被 observer 显式包裹
let isWrappedDirectly = false;
let checkParent = componentNode.parent;
while (checkParent) {
  if (checkParent.callee &amp;amp;&amp;amp; (checkParent.callee.name === &apos;observer&apos; || checkParent.callee.name === &apos;memoWithObserver&apos;)) {
    isWrappedDirectly = true;
    break;
  }
  checkParent = checkParent.parent;
}

// 3. 终极兜底：剥离所有注释的纯净源码正则扫描
if (!isWrappedDirectly) {
  const cleanText = context.getSourceCode().text.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, &apos;&apos;);
  const observerRegex = new RegExp(`(observer|memoWithObserver)\\s*\\(\\s*${compName}`);
  if (!observerRegex.test(cleanText)) {
    context.report({ node, messageId: &apos;requireObserver&apos; });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这套规则的落地，彻底根治了我们在日常迭代中因为漏写 &lt;code&gt;observer&lt;/code&gt; 导致的“难以排查的”刷新 Bug，节省了大量的排查时间。&lt;/p&gt;
&lt;h3&gt;性能防劣化 (Early Return / Bailout)&lt;/h3&gt;
&lt;p&gt;自定义 ESLint 规则如果写得不够收敛，会在开发者每次按下 &lt;code&gt;Cmd+S&lt;/code&gt; 时触发全量的 AST 深层遍历，导致 VSCode 的 Lint 提示严重卡顿（甚至耗时几秒钟）。
为了保障极致的研发体验，我在所有自定义规则的入口处都做了 &lt;strong&gt;尽早退出 (Bailout)&lt;/strong&gt; 优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特征预检&lt;/strong&gt;：在进入昂贵的 AST 树遍历之前，先通过 &lt;code&gt;context.getSourceCode().text&lt;/code&gt; 获取全文的纯字符串。利用高效率的正则表达式粗筛（例如 &lt;code&gt;if (!/Store|use[A-Z]/i.test(text)) return {}&lt;/code&gt;），如果文件内压根没有相关的关键字，直接跳过整棵 AST 的解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种深度的性能防劣化处理，确保了即便在非常庞大的单体仓库中，Lint 进程也始终保持在毫秒级响应。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. “潜在问题”问题排查：老版本 ESLint 的字节数限制 Bug&lt;/h2&gt;
&lt;p&gt;在这些自定义规则刚推上测试流水线时，我们遇到了一个非常异常的 Bug。在我们公司内部使用的基于 Phabricator 的代码审查系统 (&lt;code&gt;arc lint&lt;/code&gt;) 结合老版本的 lint 检查器时，抛出了如下异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Exception: Parameter (...) passed to &quot;setCode()&quot; when constructing a lint message must be a scalar with a maximum string length of 128 bytes, but is 163 bytes in length.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因分析&lt;/strong&gt;：
这是因为我在自定义 ESLint 报错信息中，附带了异常代码的物理路径作为错误追踪，导致单条 Lint Message 的内容长度超出了当时 &lt;code&gt;arc&lt;/code&gt; 校验工具硬编码的 128 bytes 的极限阈值，直接导致流水线崩溃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：
如果你所在的基础架构组也遇到了此类报错，不用怀疑自己写的 ESLint 规则有问题，这是基础设施的历史技术债。你需要做的是去重新拉取更新（拉取新的 arc 相关仓库代码）来打破这个老旧的版本限制。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 总结与落地推广&lt;/h2&gt;
&lt;p&gt;为了让这些阻断性规则在不打断团队当前开发节奏的前提下平滑落地，我们将这些自定义规则默认配置为了 &lt;code&gt;warn&lt;/code&gt; 级别。&lt;/p&gt;
&lt;p&gt;并在 CI 和日常脚本中加入了专属的可视化检测命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 生成一份专门展示不规范代码架构的可视化 HTML 报告
npm run lint:report

# 修复所有可以自动修复的规则
npm run lint-fix
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>TubeWidget YouTube 双语字幕插件架构设计与实现</title><link>https://nollieleo.github.io/posts/tubewidget-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/tubewidget-architecture/</guid><description>深度解析已上架 Chrome 商店的 TubeWidget (Dual Subtitles) 插件架构。探讨如何通过 Plasmo 的 Main World 注入突破 YouTube 严苛的 API 签名防爬虫机制，以及高精度时间轴同步、滑动窗口预加载流水线和零冲突 Shadow DOM 渲染方案。</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与项目概述&lt;/h2&gt;
&lt;p&gt;在海外视频学习和日常娱乐中，原生的 YouTube 播放器只能单选一种字幕，这对于非母语学习者来说是一个巨大的痛点。为了解决这个问题，我从零到一独立设计、开发并开源了 &lt;strong&gt;TubeWidget - Dual Subtitles&lt;/strong&gt;（目前已成功上架 Chrome 应用商店）。&lt;/p&gt;
&lt;p&gt;整个项目采用了当今最前沿的现代浏览器插件技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心框架&lt;/strong&gt;：&lt;strong&gt;Plasmo&lt;/strong&gt;（完美支持 Manifest V3，拥有极佳的开发者体验）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 与样式&lt;/strong&gt;：&lt;strong&gt;React 18&lt;/strong&gt; + &lt;strong&gt;TailwindCSS&lt;/strong&gt; + &lt;strong&gt;Ant Design&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态与数据&lt;/strong&gt;：&lt;strong&gt;Zustand&lt;/strong&gt; (响应式状态) + &lt;strong&gt;Dexie.js&lt;/strong&gt; (IndexedDB 本地高频缓存)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;翻译引擎路由&lt;/strong&gt;：支持 Google 免费版、DeepL、Microsoft 以及 Google Cloud 等多通道分发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是一个简单的 DOM 爬虫工具，而是一个深入视频播放器骨髓的工程化产品。本文将详细拆解该插件底层非常底层的拦截架构与优化细节。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心痛点一：突破隔离沙箱，强行拦截官方网络请求&lt;/h2&gt;
&lt;h3&gt;业务痛点与技术封锁&lt;/h3&gt;
&lt;p&gt;YouTube 具有非常严苛的防爬虫机制。其官方的字幕数据接口（&lt;code&gt;/api/timedtext&lt;/code&gt;）强依赖于前端播放器动态生成的加密签名（Signature）。如果我们像普通的插件那样，在 Content Script 中手动发起 &lt;code&gt;fetch&lt;/code&gt; 请求去拉取字幕数据，必然会遭遇 &lt;code&gt;403 Forbidden&lt;/code&gt; 的无情拦截。&lt;/p&gt;
&lt;p&gt;同时，Chrome 插件的 Content Script 默认运行在安全隔离的 &lt;strong&gt;ISOLATED World&lt;/strong&gt; 中，根本无法触碰或修改宿主页面的 &lt;code&gt;window.fetch&lt;/code&gt; 或 &lt;code&gt;XMLHttpRequest&lt;/code&gt;，这就断绝了“普通抓包”的可能。&lt;/p&gt;
&lt;h3&gt;架构攻坚：Main World 注入与底层 Hook&lt;/h3&gt;
&lt;p&gt;为了拿到带有官方合法签名的字幕数据，我利用了 Plasmo 提供的 &lt;code&gt;world: &quot;MAIN&quot;&lt;/code&gt; 能力，在页面加载的第一时间（&lt;code&gt;run_at: &quot;document_start&quot;&lt;/code&gt;），将一段名为 &lt;code&gt;youtube-main-world.ts&lt;/code&gt; 的探针脚本&lt;strong&gt;注入 YouTube 真实的主执行环境&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在这个没有沙箱限制的世界里，我直接重写了原生的 &lt;code&gt;window.fetch&lt;/code&gt;，并在不破坏官方请求的前提下，将返回的 JSON 字幕流通过 &lt;code&gt;clone()&lt;/code&gt; 提取出来，最后使用一个安全的 Token 握手机制，通过 &lt;code&gt;postMessage&lt;/code&gt; 传递回插件的隔离上下文中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph MAIN [&quot;YouTube 真实页面环境 (MAIN World)&quot;]
        YT[&quot;YouTube 原生播放器&quot;] --&amp;gt;|&quot;发起带签名的请求&quot;| FetchHook[&quot;被拦截的 window.fetch&quot;]
        FetchHook --&amp;gt;|&quot;发送真实网络请求&quot;| Server[(&quot;YouTube Backend&quot;)]
        Server --&amp;gt;|&quot;返回 JSON 字幕数据&quot;| FetchHook
        FetchHook --&amp;gt;|&quot;Response.clone() 提取数据&quot;| Dispatcher[&quot;postMessage (附带 Token)&quot;]
    end

    subgraph ISOLATED [&quot;插件沙箱环境 (ISOLATED World)&quot;]
        Dispatcher --&amp;gt;|&quot;监听 window message&quot;| CS[&quot;Content Script 中枢&quot;]
        CS --&amp;gt;|&quot;解析字幕流存入 Zustand&quot;| Store[(&quot;Zustand Store&quot;)]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心注入代码 (&lt;code&gt;src/contents/main-world/hook-fetch.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function setupFetchHook(getHandshakeToken: () =&amp;gt; string) {
  const originalFetch = window.fetch

  // 强行重写宿主环境的 fetch
  window.fetch = async function (...args) {
    const url = args[0] instanceof Request ? args[0].url : args[0]?.toString() || &quot;&quot;
    const response = await originalFetch.apply(this, args)

    try {
      // 如果拦截到的是目标 timedtext (字幕) 接口
      if (isTimedTextUrl(url)) {
        // 规避掉贴片广告的无关字幕
        if (!isAdVideo(url)) {
          const clone = response.clone()
          clone.json().then((data) =&amp;gt;
            // 通过握手 Token 将原汁原味的官方数据派发回插件沙箱
            dispatchCaptionEvent(url, data, getHandshakeToken())
          ).catch(() =&amp;gt; {})
        }
      }
    } catch {}

    // 将毫无篡改的 Response 返回给 YouTube 官方逻辑，保证播放器不出错
    return response
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，为了保证 YouTube &lt;strong&gt;必然会发起字幕请求&lt;/strong&gt;，我还编写了 &lt;code&gt;ensureCCEnabled&lt;/code&gt; 轮询方法，模拟真实的物理点击，去自动点亮原生播放器上的 &lt;code&gt;CC&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 核心痛点二：零冲突的 UI 渲染与原生字幕“隐形”&lt;/h2&gt;
&lt;h3&gt;业务痛点&lt;/h3&gt;
&lt;p&gt;YouTube 原生播放器的 DOM 非常复杂且动态变化。如果我们贸然删除其原生的字幕节点，会导致官方内部的代码逻辑（如位置计算、重绘机制）报出 TypeError 并导致播放器崩溃。
同时，我们自己用 Tailwind 编写的 React 双语字幕 UI，绝不能被宿主页面（YouTube）原有的全局 CSS 污染，也不能去污染宿主。&lt;/p&gt;
&lt;h3&gt;架构攻坚：Shadow DOM 与动态 Class 屏蔽&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;绝对隔离的 Shadow DOM&lt;/strong&gt;：
我利用 Plasmo 的 &lt;code&gt;getInlineAnchor&lt;/code&gt; 特性，将整个 React 渲染树挂载在 &lt;code&gt;#movie_player&lt;/code&gt; 内部的影子节点中（Shadow Root）。这样外部的 CSS 穿透不进插件，插件的 Tailwind 工具类也不会泄露到外部。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;原生降级策略（“隐形”而非“抹杀”）&lt;/strong&gt;：
我没有去删除原生的 CC 节点，而是在全局动态注入了一段高优先级的脱离流样式 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;。当用户开启了 TubeWidget 双语字幕时，通过赋予宿主播放器 &lt;code&gt;.tube-widget-active&lt;/code&gt; 标志，&lt;strong&gt;彻底将原生字幕在视觉和交互上隐形，但保留其在 DOM 树中的物理存在&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 动态隐藏 YouTube 原生字幕渲染（配合 UI 层控制）
function injectDynamicSubtitleHider() {
  if (document.getElementById(&quot;ytranslater-dynamic-cc-hider&quot;)) return

  const style = document.createElement(&quot;style&quot;)
  style.id = &quot;ytranslater-dynamic-cc-hider&quot;
  // 当播放器具有 tube-widget-active 标志时，彻底隐藏所有原生字幕容器及指针事件
  style.textContent = `
    .tube-widget-active .ytp-caption-window-container { opacity: 0 !important; pointer-events: none !important; }
    .tube-widget-active .caption-window { opacity: 0 !important; pointer-events: none !important; }
  `
  document.head.appendChild(style)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3.1 业务踩坑：突破 CSP 限制与 Main World 劫持的竞态条件&lt;/h3&gt;
&lt;p&gt;在实现 Main World 注入时，新手最容易踩的坑是&lt;strong&gt;竞态条件 (Race Condition)&lt;/strong&gt;。
YouTube 并不是传统的刷新式网页，它重度使用了 SPF (Structured Page Fragments) 和自研的 Polymer 框架进行基于 &lt;code&gt;history.pushState&lt;/code&gt; 的无刷新前端路由跳转。&lt;/p&gt;
&lt;p&gt;如果你只是在 &lt;code&gt;document_start&lt;/code&gt; 时注入了一次 &lt;code&gt;fetch&lt;/code&gt; 拦截器，当用户从首页点击进入一个新视频时，YouTube 的前端路由接管了页面，&lt;strong&gt;此时你的拦截器很容易在某些边界情况下失效，或者你的 UI 挂载点 (&lt;code&gt;Mount Point&lt;/code&gt;) 直接被 YouTube 动态更新的 DOM 树连根拔起销毁掉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：双重守护机制&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;底层探针的持久化&lt;/strong&gt;：我们在注入 &lt;code&gt;fetch&lt;/code&gt; 拦截器时，不仅要重写原生的 &lt;code&gt;window.fetch&lt;/code&gt;，还要同时拦截浏览器的 &lt;code&gt;history.pushState&lt;/code&gt; 和 &lt;code&gt;history.replaceState&lt;/code&gt;。一旦检测到 URL 发生带有 &lt;code&gt;/watch?v=&lt;/code&gt; 的切换，拦截器会主动向插件的 Isolated World 发送一个 &lt;code&gt;PAGE_NAVIGATED&lt;/code&gt; 心跳，唤醒处于休眠状态的 Content Script。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOM 挂载点的 MutationObserver 霸体&lt;/strong&gt;：在 UI 层，为了防止我们的双语字幕容器被 YouTube 删掉，我们不能只执行一次 &lt;code&gt;document.body.appendChild&lt;/code&gt;。必须利用 &lt;code&gt;MutationObserver&lt;/code&gt; 死死盯住 YouTube 播放器的内部节点（如 &lt;code&gt;.html5-video-player&lt;/code&gt;）。一旦发现我们的字幕容器 &lt;code&gt;#tube-widget-root&lt;/code&gt; 消失了，立刻在下一帧（Next Tick）把它原地复活重建。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 业务踩坑：Shadow DOM 的事件穿透与 React 18 挂载冲突&lt;/h3&gt;
&lt;p&gt;为了保证我们的 Tailwind CSS 绝对不污染 YouTube，也绝对不被 YouTube 那套写得非常宽泛的全局 CSS 污染，&lt;strong&gt;Shadow DOM&lt;/strong&gt; 是唯一解。&lt;/p&gt;
&lt;p&gt;但在将 React 18 应用渲染到 Shadow DOM 内部时，有一个极其著名的历史遗留大坑：&lt;strong&gt;React 的合成事件系统 (Synthetic Events) 默认是绑定在 &lt;code&gt;document&lt;/code&gt; 根节点上的。&lt;/strong&gt;
由于 Shadow DOM 的边界隔离特性（Event Retargeting），当你点击 Shadow DOM 里的一个按钮时，事件冒泡到 &lt;code&gt;document&lt;/code&gt; 时，事件的 &lt;code&gt;target&lt;/code&gt; 会变成整个 Shadow Host 本身，而不是那个按钮。这会导致 React 内部的事件系统完全错乱，你的 &lt;code&gt;onClick&lt;/code&gt; 根本不会触发！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：利用 React 18 的 createRoot 穿透&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;幸运的是，React 18 修改了事件委托机制。事件不再绑定到 &lt;code&gt;document&lt;/code&gt;，而是绑定到了你调用 &lt;code&gt;createRoot&lt;/code&gt; 的那个&lt;strong&gt;根节点容器&lt;/strong&gt;上。
我们在 Plasmo 中，必须极其小心地手动接管 Shadow Root 的挂载逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 必须挂载在 Shadow DOM 的内部容器上，而不是 Shadow Host 上！
const shadowHost = document.createElement(&apos;div&apos;);
shadowHost.id = &apos;tube-widget-host&apos;;
const shadowRoot = shadowHost.attachShadow({ mode: &apos;open&apos; });

// 必须在 Shadow Root 里再套一层 div 作为 React 的 Root
const reactRootContainer = document.createElement(&apos;div&apos;);
reactRootContainer.id = &apos;tube-widget-react-root&apos;;
shadowRoot.appendChild(reactRootContainer);

// Tailwind 样式流必须动态打入这个 Shadow Root 内部
const styleSheet = document.createElement(&apos;style&apos;);
styleSheet.textContent = tailwindCssString;
shadowRoot.appendChild(styleSheet);

// 最后，将 React 18 挂载在这个被完全隔离的内部节点上
// 此时 React 的合成事件监听器会绑定在 reactRootContainer 上，完美避开 Shadow DOM 穿透问题
const root = createRoot(reactRootContainer);
root.render(&amp;lt;App /&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套严密的嵌套解法，我们不仅实现了像素级的样式物理隔离，还保全了 React 复杂的交互能力。&lt;/p&gt;
&lt;h2&gt;4. 核心痛点三：高精度时间同步与翻译预加载流水线&lt;/h2&gt;
&lt;h3&gt;业务痛点&lt;/h3&gt;
&lt;p&gt;调用外部的大模型或 DeepL API 翻译一句话存在不可忽视的网络延迟（通常在几百毫秒到一两秒不等）。如果在字幕出现的那一瞬间才去触发 &lt;code&gt;translate&lt;/code&gt; 请求，双语字幕必然会严重脱节，用户体验极差。&lt;/p&gt;
&lt;h3&gt;架构攻坚：Look-ahead Prefetch Pipeline (滑动窗口预加载)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;毫秒级时间帧同步与跳变阻断 (Seek Handling)&lt;/strong&gt;：
除了抛弃粗糙的 &lt;code&gt;setInterval&lt;/code&gt; 改用 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 同步 &lt;code&gt;&amp;lt;video&amp;gt;.currentTime&lt;/code&gt;，这里还有一个非常复杂的边界场景（Edge Case）：如果用户突然在进度条上拖拽跳跃了 30 分钟呢？
我的底层状态机能够精准侦测到这类&lt;strong&gt;非线性的时间跳变 (Time Seek)&lt;/strong&gt;。当检测到 &lt;code&gt;currentTime&lt;/code&gt; 与上一帧的差值大于设定的阈值时，系统会立刻中断旧的预加载队列，废弃掉正在路上的无关网络请求，并在跳跃后的新基准线上重新建立 &lt;code&gt;baseIndex&lt;/code&gt; 的 5 句缓存流水线。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;超前滑动窗口翻译&lt;/strong&gt;：
在 UI 渲染的骨干 &lt;code&gt;Overlay&lt;/code&gt; 组件中，我设计了一个超前滑动窗口流水线。当前视频线性播放到某一句字幕时，程序会自动向前探知之后的 &lt;strong&gt;5 句字幕 (&lt;code&gt;PREFETCH_COUNT = 5&lt;/code&gt;)&lt;/strong&gt;，并在后台静默派发翻译任务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    CurrentTime[&quot;当前播放时间轴 (currentTime)&quot;] --&amp;gt; SubtitleN[&quot;当前渲染: 句子 N&quot;]
    SubtitleN --&amp;gt; Window{&quot;触发预加载机制&quot;}
    Window --&amp;gt;|&quot;预发网络请求&quot;| PrefetchN1[&quot;预发网络请求: 句子 N+1&quot;]
    Window --&amp;gt;|&quot;预发网络请求&quot;| PrefetchN2[&quot;预发网络请求: 句子 N+2&quot;]
    Window --&amp;gt;|&quot;预发网络请求&quot;| PrefetchN3[&quot;预发网络请求: 句子 N+3...&quot;]
    
    PrefetchN1 --&amp;gt; Cache[(&quot;IndexedDB 本地缓存&quot;)]
    PrefetchN2 --&amp;gt; Cache
    
    TimelineMove[&quot;时间轴推进到 N+1&quot;] --&amp;gt; ReadCache[&quot;直接从本地缓存读取译文，实现 0 毫秒渲染延迟&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心预加载引擎代码 (&lt;code&gt;src/contents/overlay/index.tsx&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  // ...省略环境与边界判断

  // 找到当前时间轴对应的基准字幕下标
  let baseIndex = currentSegment 
    ? segments.findIndex((s) =&amp;gt; s.id === currentSegment.id)
    : segments.findIndex((s) =&amp;gt; s.tStartMs &amp;gt;= currentTimeMs);

  if (baseIndex === -1) return;

  const PREFETCH_COUNT = 5; // 永远提前预加载后续的 5 句字幕
  
  for (let i = 0; i &amp;lt;= PREFETCH_COUNT; i++) {
    const nextIndex = baseIndex + i;
    // 如果该片段尚未被预加载过，则推入预加载队列
    if (nextIndex &amp;lt; segments.length &amp;amp;&amp;amp; nextIndex &amp;gt; maxPrefetchedIndexRef.current) {
      const nextSegment = segments[nextIndex];
      // 底层自动查库/发起网络请求，并缓存到 IndexedDB
      prefetchTranslation(nextSegment.text, settings.targetLang);
      maxPrefetchedIndexRef.current = nextIndex;
    }
  }
}, [currentSegment?.id, segments.length, settings?.targetLang, currentTimeMs]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合传递到打字机 UI 组件 (&lt;code&gt;Subtitle&lt;/code&gt;) 上的 &lt;code&gt;durationMs&lt;/code&gt;（该段字幕的物理寿命），实现了原生般流畅、绝不卡顿的双语同轨显示。&lt;/p&gt;
&lt;h2&gt;5. 极致体验：动态打字机特效 (Typewriter Effect) 与 UI 隔离&lt;/h2&gt;
&lt;p&gt;除了后端的预加载和底层拦截，TubeWidget 在 UI 表现上也下足了功夫，特别是字幕的呈现方式。&lt;/p&gt;
&lt;p&gt;如果我们直接把外部模型翻译好的长句子“啪”地一下怼到屏幕上，会对用户的视觉产生强烈的突兀感，并且和原生 YouTube 字幕那种随语音节奏出现的感觉完全脱节。&lt;/p&gt;
&lt;p&gt;为此，我在 &lt;code&gt;Subtitle&lt;/code&gt; 组件中实现了一套&lt;strong&gt;基于真实物理寿命的自适应打字机特效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心算法 (&lt;code&gt;src/contents/overlay/components/Subtitle/index.tsx&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const Subtitle = ({ text, translation, durationMs }: SubtitleProps) =&amp;gt; {
  const [currentIndex, setCurrentIndex] = useState(0)

  useEffect(() =&amp;gt; {
    if (!translation) return
    setCurrentIndex(0)

    // durationMs 是底层从拦截到的官方字幕时间轴提取出的这句字幕的总存活时间
    // 我们强制打字机特效在字幕总寿命的 75% 内播放完毕，留下 25% 的时间让用户阅读全句
    const targetTypingTime = durationMs * 0.75
    let speed = targetTypingTime / translation.length
    
    // 限制单字敲击的物理速度，最快 15ms，最慢 50ms 防止过于缓慢
    speed = Math.max(15, Math.min(speed, 50))

    const interval = setInterval(() =&amp;gt; {
      setCurrentIndex((prev) =&amp;gt; {
        if (prev &amp;lt; translation.length) return prev + 1
        clearInterval(interval)
        return prev
      })
    }, speed)

    return () =&amp;gt; clearInterval(interval)
  }, [translation, durationMs])

  return (
    // 渲染时：已敲击出的文字不透明，未敲击的保留占位但透明，防止容器疯狂抖动
    &amp;lt;&amp;gt;
      &amp;lt;span&amp;gt;{translation.slice(0, currentIndex)}&amp;lt;/span&amp;gt;
      &amp;lt;span style={{ opacity: 0 }}&amp;gt;{translation.slice(currentIndex)}&amp;lt;/span&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这套自适应速率算法，保证了不管一句话有多长或者语速有多快，双语字幕的出现节奏始终能和视频发音的起伏保持着非常舒适的同步律动感。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 成本与性能优化：Dexie.js 的本地缓存降频&lt;/h2&gt;
&lt;p&gt;作为一款支持多翻译引擎（包括付费调用的 OpenAI 和 Google Cloud）的插件，重播同一视频或遇到极高频出现的语句（如 &quot;Subscribe to my channel&quot; 或 &quot;Thank you for watching&quot;）时，如果每次都发起真实的 API 调用，将会烧毁大量的 Token 额度。&lt;/p&gt;
&lt;p&gt;我在插件的底层接入了基于 IndexedDB 封装的 &lt;strong&gt;Dexie.js&lt;/strong&gt;。所有经由 &lt;code&gt;TranslateRouter&lt;/code&gt; 的请求，都会通过 &lt;code&gt;MD5(sourceText + targetLanguage)&lt;/code&gt; 算法生成一个极短且唯一的 Hash 摘要（Digest）作为主键。
在真正发起 HTTP 请求前，路由会先拦截并查询本地 IndexedDB 数据库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同视频击中&lt;/strong&gt;：用户反复拖拽进度条、或者是重温某一段精彩对话时，100% 缓存击中，零网络开销、零延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨视频击中&lt;/strong&gt;：不同 YouTuber 说的相同打招呼日常用语，只要语种一致，Hash 就一致。即使用户看的是一个全新的视频，只要这句话曾经被翻译过，同样能瞬间从本地提库！这种跨域（Cross-Pollination）的缓存策略，把使用付费大模型 API 的成本压缩到了物理极限。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 总结&lt;/h2&gt;
&lt;p&gt;YouTube 的环境犹如一个庞大的、充满了代码混淆和动态防御黑盒。&lt;/p&gt;
&lt;p&gt;从利用 &lt;code&gt;world: &quot;MAIN&quot;&lt;/code&gt; 打破隔离沙箱窃取原生接口流，到非常克制的原生 CSS “隐形降级”，再到结合 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 和&lt;strong&gt;超前滑动窗口&lt;/strong&gt;打造的零延迟翻译预加载体系——TubeWidget 完美跨越了扩展开发的深水区，呈现出了非常成熟、稳定且高性能的双语字幕产品体验。&lt;/p&gt;
</content:encoded></item><item><title>沉浸式 YouTube 翻译插件：TubeWidget 使用手册</title><link>https://nollieleo.github.io/posts/tubewidget-manual/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/tubewidget-manual/</guid><description>TubeWidget 是一款强大的 YouTube 原生双语字幕与点词翻译插件。本文是 TubeWidget 的官方完全使用指南，教你如何配置多款翻译引擎（DeepL/Google/OpenAI），自定义打字机特效字幕，以及利用沉浸式交互字典进行外语学习。</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 简介与安装&lt;/h2&gt;
&lt;p&gt;欢迎使用 &lt;strong&gt;TubeWidget - Dual Subtitles&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;这是一款专门为 YouTube 打造的沉浸式双语字幕扩展程序。它不仅能为你提供零延迟的双语对照字幕，还内置了&lt;strong&gt;智能打字机动效&lt;/strong&gt;、&lt;strong&gt;多款大厂翻译引擎&lt;/strong&gt;（支持 DeepL、Google、Microsoft 等）以及强大的&lt;strong&gt;点词即译交互式字典&lt;/strong&gt;。无论是追剧、看 Vlog 还是深度学习外语，TubeWidget 都是你不可或缺的利器。&lt;/p&gt;
&lt;p&gt;👉 &lt;strong&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/tubewidget-dual-subtitles/jfadikahfcmphjjaoelplocfpebfchml?hl=zh-CN&amp;amp;utm_source=ext_sidebar&quot;&gt;点击前往 Chrome 网上应用店安装 TubeWidget&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./store-banner-placeholder.png&quot; alt=&quot;TubeWidget Chrome 商店首页展示图&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 基础使用与双语字幕&lt;/h2&gt;
&lt;h3&gt;2.1 开启双语字幕&lt;/h3&gt;
&lt;p&gt;安装插件后，打开任意一个带有 CC 字幕的 YouTube 视频页面。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TubeWidget 会在后台自动探测并为你解析原文字幕。&lt;/li&gt;
&lt;li&gt;插件会自动隐去 YouTube 呆板的原生字幕，在视频画面底部渲染出我们为你定制的&lt;strong&gt;高清晰度双语对照字幕&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./dual-subtitles-placeholder.png&quot; alt=&quot;双语字幕播放效果图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.2 自由拖拽与个性化主题定制&lt;/h3&gt;
&lt;p&gt;如果你觉得字幕挡住了视频的关键画面（例如进度条或比分板），别担心！
&lt;strong&gt;将鼠标悬浮在双语字幕区域&lt;/strong&gt;，即可自由拖拽字幕框，将其移动到视频画面的最合适角落。&lt;/p&gt;
&lt;p&gt;此外，TubeWidget 提供了极其强大的&lt;strong&gt;字幕主题定制能力&lt;/strong&gt;。在侧边栏设置中，你可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一键切换预设主题&lt;/strong&gt;：内置了包括 &lt;code&gt;Netflix 风格&lt;/code&gt;、&lt;code&gt;沉浸式 (Immersive)&lt;/code&gt;、&lt;code&gt;学习模式 (Study)&lt;/code&gt;、&lt;code&gt;赛博朋克霓虹 (Neon)&lt;/code&gt;、&lt;code&gt;毛玻璃 (Glassmorphism)&lt;/code&gt;、&lt;code&gt;高对比度 (HighContrast)&lt;/code&gt; 等多款专业设计的预设主题，满足你在观影、学习、夜间模式等不同场景下的视觉需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高自由度精调&lt;/strong&gt;：除了预设，你还可以分别对“译文”和“原文”进行像素级的自定义。包括字体颜色、字号大小、字体粗细、文字阴影（柔和、发光或描边）、背景透明度，以及是否开启&lt;strong&gt;动态打字机特效&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./drag-dual-subtitles-placeholder.png&quot; alt=&quot;拖拽字幕与主题效果图&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 沉浸式侧边栏：完整双语字幕列表 (Side Panel)&lt;/h2&gt;
&lt;p&gt;除了视频底部的动态字幕，TubeWidget 还为你提供了一个强大的&lt;strong&gt;浏览器侧边栏 (Side Panel) 工作台&lt;/strong&gt;。点击 Chrome 浏览器右上角的插件图标，即可唤出专属的侧边栏。&lt;/p&gt;
&lt;h3&gt;3.1 自动滚动的原文/译文对照&lt;/h3&gt;
&lt;p&gt;侧边栏会实时提取当前视频的所有双语字幕，并将它们按时间轴拼接成一篇&lt;strong&gt;易于阅读的文章列表&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动跟随播放进度 (Auto-Scroll)&lt;/strong&gt;：当你观看视频时，侧边栏会自动高亮当前正在朗读的句子，并向下滚动，就像看卡拉 OK 歌词一样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点击空降跳转 (Seek to Time)&lt;/strong&gt;：想要重温某一句台词？直接在侧边栏中点击该句字幕左侧的&lt;strong&gt;播放按钮&lt;/strong&gt;，视频便会瞬间跳转至该时间点重新播放。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 侧边栏内的点词即译&lt;/h3&gt;
&lt;p&gt;同样的，侧边栏里的长文章也支持&lt;strong&gt;实时查词功能&lt;/strong&gt;。在侧边栏阅读时遇到生词，鼠标点击同样会呼出翻译字典卡片，帮你全方位理解视频内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sidepanel-subtitle-list-placeholder.png&quot; alt=&quot;侧边栏字幕列表效果图&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 进阶功能：点词即译 (交互式字典)&lt;/h2&gt;
&lt;p&gt;在观看外语视频时，遇到不认识的生词怎么办？去查字典？太慢了！&lt;/p&gt;
&lt;p&gt;TubeWidget 将原生的一长串文本&lt;strong&gt;实时解析成了可交互的单词序列&lt;/strong&gt;。
当你遇到不懂的生词时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接在视频播放中（或侧边栏长文中）&lt;strong&gt;点击那个英文单词&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;视频会自动为你暂停（可选配置），并在单词上方瞬间弹出一个精致的&lt;strong&gt;翻译释义卡片 (WordPopup)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;字典卡片关闭后，视频即可继续流畅播放，让你的学习过程绝不被打断。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./word-popup-placeholder.png&quot; alt=&quot;点词即译交互词典效果图&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 翻译引擎配置 (DeepL / Google / Microsoft)&lt;/h2&gt;
&lt;p&gt;TubeWidget 默认提供免费的 Google 基础翻译通道。但如果你追求&lt;strong&gt;信达雅的高质量翻译&lt;/strong&gt;，插件同样支持接入你自己的大厂 API 密钥。&lt;/p&gt;
&lt;h3&gt;5.1 如何切换翻译引擎&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在插件的&lt;strong&gt;侧边栏 (SidePanel)&lt;/strong&gt; 顶部工具栏中，点击配置按钮进入设置页。&lt;/li&gt;
&lt;li&gt;在「翻译引擎 (Translation Engine)」设置中，你可以看到多个选项：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google 免费版 (默认)&lt;/strong&gt;：即插即用，无需配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepL (推荐)&lt;/strong&gt;：极其精准自然的神经机器翻译。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Cloud / Microsoft&lt;/strong&gt;：企业级云翻译通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 配置私有 API Key&lt;/h3&gt;
&lt;p&gt;如果你选择了 DeepL 等高级通道：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;填入你申请好的对应 &lt;code&gt;API Key&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;插件会将你的 Key &lt;strong&gt;安全地存储在浏览器本地&lt;/strong&gt;，绝不会上传到任何第三方服务器。&lt;/li&gt;
&lt;li&gt;由于我们底层的 IndexedDB (Dexie.js) &lt;strong&gt;全量缓存机制&lt;/strong&gt;，相同的句子在本地只会消耗一次 API 请求，帮你极大限度地节省 Token 与计费成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./settings-panel-placeholder.png&quot; alt=&quot;配置项面板截图&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 个性化样式与偏好设置&lt;/h2&gt;
&lt;p&gt;在 TubeWidget 的设置面板中，你完全可以掌控字幕的外观与行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标语言 (Target Language)&lt;/strong&gt;：支持将字幕实时翻译为中文、日语、韩语、西班牙语等数十种语言。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字幕外观设置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;调整&lt;strong&gt;字体大小 (Font Size)&lt;/strong&gt;，适应你的视力与屏幕。&lt;/li&gt;
&lt;li&gt;随时切换&lt;strong&gt;仅显示译文&lt;/strong&gt;或&lt;strong&gt;双语对照&lt;/strong&gt;模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 常见问题 (FAQ)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q：为什么我打开 YouTube 视频，没有出现 TubeWidget 的双语字幕？&lt;/strong&gt;
&lt;strong&gt;A：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请确保当前播放的视频&lt;strong&gt;本身带有 CC 官方字幕&lt;/strong&gt;（自动生成或人工上传均可）。如果没有官方字幕，我们无法拦截并进行翻译。&lt;/li&gt;
&lt;li&gt;请确保 YouTube 播放器右下角的 &lt;code&gt;[CC]&lt;/code&gt; 按钮是开启状态。插件通常会帮你自动点亮它。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Q：翻译速度感觉变慢了？&lt;/strong&gt;
&lt;strong&gt;A：&lt;/strong&gt; 如果使用默认的免费公共通道，在网络高峰期可能会有轻微延迟。强烈建议在配置页中填入自己的 &lt;strong&gt;DeepL 免费 API Key&lt;/strong&gt;，体验毫秒级、零延迟的极致解析！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：字幕卡在屏幕中间了怎么复原？&lt;/strong&gt;
&lt;strong&gt;A：&lt;/strong&gt; 你可以随时拖拽字幕将其拉回画面底部。如果排版彻底错乱，可以刷新页面或在设置面板中点击“恢复默认设置”。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;赶快去享受属于你的无国界视频漫游之旅吧！如果觉得好用，别忘了去 Chrome 商店给我们一个五星好评 ⭐⭐⭐⭐⭐！&lt;/p&gt;
</content:encoded></item><item><title>Zion DevTools 架构设计与技术分析</title><link>https://nollieleo.github.io/posts/zed-devtools-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zed-devtools-architecture/</guid><description>深度解析为 Zion Editor 量身定制的 Chrome 开发者工具 zed-devtools 的架构设计。涵盖跨隔离世界的 React Fiber 劫持、基于 Web Worker 的百万级 JSON 虚拟化渲染，以及基于 CRDT 架构的 Time Travel 时间漫游技术。</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与概述&lt;/h2&gt;
&lt;p&gt;在复杂的大型前端项目中，特别是像 &lt;strong&gt;Zion Editor&lt;/strong&gt; 这种低代码/无代码可视化编辑器，传统的 React DevTools 或 Redux DevTools 往往显得力不从心。Zion Editor 底层包含了庞大的项目配置树（Project Store）、基于 CRDT（冲突无关复制数据类型）的协作数据流（Schema Store），以及复杂的可视化画布渲染逻辑。&lt;/p&gt;
&lt;p&gt;为了解决日常开发和调试中的痛点，我从零到一独立设计并开发了 &lt;strong&gt;Zion DevTools&lt;/strong&gt; (&lt;code&gt;zed-devtools&lt;/code&gt;)。这是一个基于 Plasmo 和 React 构建的定制化 Chrome 浏览器插件，它通过“桥接”（Bridge）与 Zion 的运行时内核 (&lt;code&gt;zed&lt;/code&gt;) 深度绑定，实现了三大核心能力：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DOM 到业务数据的逆向穿透 (Inspector)&lt;/strong&gt;：一键抓取画布 DOM 背后隐藏的 React 数据绑定信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;巨型状态树的丝滑审查 (Store Inspector)&lt;/strong&gt;：支撑百万级节点的 Mobx/Zustand 状态树，实现零卡顿渲染与双向数据同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协作数据的时光漫游 (Time Travel)&lt;/strong&gt;：基于 CRDT 底层机制，实现协作 Patch 的微秒级正向重放与逆向回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文将详细拆解该项目的架构设计、核心难点以及技术实现细节。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 整体架构设计：四层通信隧道&lt;/h2&gt;
&lt;p&gt;浏览器插件有着严格的安全沙箱（Isolated World）限制。插件的 UI 面板（Panel）无法直接访问页面的内部变量（如 &lt;code&gt;window.Zed&lt;/code&gt; ），Content Script 也只能操作 DOM 而无法读取页面的 JS 执行上下文。&lt;/p&gt;
&lt;p&gt;为了实现 DevTools 面板与 Zion Editor 运行时的双向通信，我设计了一套 &lt;strong&gt;“四层通信隧道”&lt;/strong&gt; 架构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Panel as DevTools Panel (React)
    participant BG as Background Service Worker
    participant CS as Content Script (Isolated World)
    participant Main as Main World Script (Zion Editor)

    Note over Panel,BG: chrome.runtime.sendMessage
    Panel-&amp;gt;&amp;gt;BG: 发起读取 Store 请求
    Note over BG,CS: chrome.tabs.sendMessage
    BG-&amp;gt;&amp;gt;CS: 路由转发至当前活动 Tab
    Note over CS,Main: window.postMessage
    CS-&amp;gt;&amp;gt;Main: 穿透沙箱发送请求
    Note over Main: Zion DevToolsBridge 拦截&amp;lt;br&amp;gt;调用 Mobx Store 序列化数据
    Main--&amp;gt;&amp;gt;CS: 返回 Store 数据包
    CS--&amp;gt;&amp;gt;BG: 向上回传数据
    BG--&amp;gt;&amp;gt;Panel: 更新面板 UI
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Panel (UI 层)&lt;/strong&gt;：开发者交互的界面，负责数据的展示与指令的下发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background (路由层)&lt;/strong&gt;：作为整个插件的中枢神经，负责鉴权、跨 Tab 消息路由以及代码注入（Script Injection）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Script (中继层)&lt;/strong&gt;：运行在目标页面的隔离沙箱中，负责监听 URL 变化并作为 &lt;code&gt;chrome.runtime&lt;/code&gt; 和 &lt;code&gt;window.postMessage&lt;/code&gt; 之间的翻译网关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Main World (执行层)&lt;/strong&gt;：包含两部分：一是我通过 Background 强行注入的 Helper 脚本；二是 Zion 源码内置的 &lt;code&gt;DevToolsBridge&lt;/code&gt; 监听器，它们直接运行在页面的真实执行环境中，拥有最高的数据访问权限。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 核心难点与攻克细节&lt;/h2&gt;
&lt;h3&gt;难题一：跨越隔离世界，精准抓取组件数据绑定 (React Fiber Inspector)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：
在 Zion 画布上，渲染出的真实 DOM 并没有存放复杂的 &lt;code&gt;DataBinding&lt;/code&gt; 业务配置。普通的 Content Script 跑在隔离环境，完全碰不到页面里的 React 运行态。我们要实现“按住特定快捷键，鼠标悬浮元素即可查看底层绑定数据”，面临极大的权限和定位挑战。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案&lt;/strong&gt;：
我编写了一段硬核的注入代码 (&lt;code&gt;helper.ts&lt;/code&gt;)，并在 Background 中利用 &lt;code&gt;chrome.scripting.executeScript&lt;/code&gt; 的 &lt;code&gt;world: &quot;MAIN&quot;&lt;/code&gt; 属性，将其强行打入 &lt;strong&gt;MAIN World&lt;/strong&gt;。
在这段脚本中，我劫持了 React 底层的内部机制：通过检索 DOM 节点上隐藏的 &lt;code&gt;__reactFiber$&lt;/code&gt; 属性，获取真实的 Fiber Node，并沿着父级指针 (&lt;code&gt;fiber.return&lt;/code&gt;) 向上层层级联遍历，直到精准提取出 Zion 内部独有的业务属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心代码 (&lt;code&gt;zed-devtools/src/contents/inspector/main-world/helper.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 寻找 DOM 节点上隐藏的 React Fiber 引用
function getFiber(dom: Element): FiberNode | null {
  const key = Object.keys(dom).find((k) =&amp;gt; k.startsWith(&quot;__reactFiber$&quot;));
  return key ? (dom as unknown as Record&amp;lt;string, FiberNode&amp;gt;)[key] : null;
}

// 向上级联查找，直到提取出 Zion 内部的 dataBinding 属性
function findDataBinding(dom: Element): Record&amp;lt;string, unknown&amp;gt; | null {
  let fiber = getFiber(dom);
  let depth = 0;
  // 控制遍历深度(最大15层)，防止无穷回溯带来的性能损耗
  while (fiber &amp;amp;&amp;amp; depth &amp;lt; 15) {
    if (fiber.memoizedProps?.dataBinding) {
      return fiber.memoizedProps.dataBinding;
    }
    fiber = fiber.return ?? null;
    depth++;
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 &lt;code&gt;keydown&lt;/code&gt; / &lt;code&gt;mousemove&lt;/code&gt; / &lt;code&gt;click&lt;/code&gt; 的事件劫持（使用 &lt;code&gt;stopImmediatePropagation&lt;/code&gt; 阻断画布默认点击），最终实现了极其惊艳的无缝查探体验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./databinding-inspector.png&quot; alt=&quot;DataBinding 探查器展示&quot; /&gt;
&lt;img src=&quot;./databinding-inspector2.png&quot; alt=&quot;DataBinding 探查器详情展示&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;难题二：百万级巨型 JSON 树的无卡顿渲染 (Store Inspector)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：
Zion 的 Store 是一棵极其庞大且嵌套极深的对象树。如果直接在 DevTools Panel 中进行 JSON 的解析、展平和渲染，主线程会瞬间被阻塞，导致插件和浏览器假死。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：Web Worker 多线程 + 视窗虚拟列表&lt;/strong&gt;
我采用了“降维打击”的策略：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多线程计算&lt;/strong&gt;：引入 Web Worker (&lt;code&gt;storeWorker.ts&lt;/code&gt;)，将巨型 JSON 的深度遍历、路径生成、状态展开计算全部丢进子线程。主线程只负责接收处理好的、轻量的一维数组 (&lt;code&gt;VirtualLine[]&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟化渲染&lt;/strong&gt;：结合 &lt;code&gt;virtua&lt;/code&gt; 库，把树形结构的渲染降维成了对一维数组的&lt;strong&gt;按需切片加载&lt;/strong&gt;。即便 Store 包含十万个节点，DOM 树上也只存在当前视窗可见的那几十个元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    subgraph Zion [&quot;Main World (Zion Runtime)&quot;]
        A[&quot;巨型 JSON Store 树&quot;] --&amp;gt;|&quot;Message 传输&quot;| B
    end
    
    subgraph Background [&quot;Background Layer&quot;]
        B[&quot;中转站&quot;] --&amp;gt;|&quot;分发&quot;| C
    end
    
    subgraph DevTools [&quot;Panel UI (Isolated World)&quot;]
        direction TB
        C[&quot;UI 主线程&quot;] --&amp;gt;|&quot;传入 data + 展开状态&quot;| W
        W(&quot;🔨 Web Worker 子线程&quot;) --&amp;gt;|&quot;DFS 遍历降维&quot;| D[&quot;轻量级 1D VirtualLine[]&quot;]
        D --&amp;gt;|&quot;传回&quot;| C
        C --&amp;gt;|&quot;丢给 virtua&quot;| E[&quot;按需渲染几十个 DOM&quot;]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心代码 (&lt;code&gt;zed-devtools/src/panels/views/StoreInspector/hooks/useStoreWorker.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const useStoreWorker = ({ data, expandedPaths }) =&amp;gt; {
  const [virtualLines, setVirtualLines] = useState&amp;lt;VirtualLine[] | null&amp;gt;(null);

  // 初始化 Web Worker，将繁重的 JSON 解析与展平操作移出 UI 线程
  const { postMessage, isProcessing } = useWorker({
    workerFactory: () =&amp;gt;
      new Worker(new URL(&quot;../utils/storeWorker.ts&quot;, import.meta.url), {
        type: &quot;module&quot;,
      }),
    onMessage: (data) =&amp;gt; {
      // 接收 Worker 展平好的一维视图数组，UI 层仅需绑定到虚拟列表上
      if (data.success) {
        setVirtualLines(data.result);
      }
    },
  });

  const refreshView = useCallback(() =&amp;gt; {
    if (!data) return;
    postMessage({ data, expandedPaths: Array.from(expandedPaths) });
  }, [data, expandedPaths]);

  return { virtualLines, isProcessing, refreshView };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./store-inspector.png&quot; alt=&quot;Store 状态树虚拟化与多线程加载效果&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;难题三：基于 CRDT 架构的时间旅行调试 (Time Travel)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：
像 Redux DevTools 那样的时光回溯，本质上是暴力的“全局 State 快照替换”。但 Zion 的协作底层是基于 CRDT 的，数据的演变是由无数个原子化的 &lt;code&gt;Patch&lt;/code&gt;（补丁）串联而成。如果直接替换全局状态，会彻底破坏 CRDT 的协作一致性与内部时钟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：精准的 Patch 回放与逆向剥离&lt;/strong&gt;
我在 Zion 端（&lt;code&gt;useTimeTravel.ts&lt;/code&gt;）打通了底层的协同引擎。在时间线中穿梭时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向未来跳转（正向）&lt;/strong&gt;：依次执行 &lt;code&gt;applyLocalCrdtDiff&lt;/code&gt; 叠加 Patch。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向过去跳转（逆向）&lt;/strong&gt;：依次执行 &lt;code&gt;executeReverseNetworkPatch&lt;/code&gt; 进行精准的反向剥离运算。这里的反向并不是简单的“快照覆盖”，而是&lt;strong&gt;绝对严谨的数学逆运算&lt;/strong&gt;。比如如果正向的 Patch 是 &lt;code&gt;Array.insert(2, &apos;a&apos;)&lt;/code&gt;，那么逆运算会自动解算为 &lt;code&gt;Array.delete(2)&lt;/code&gt;；如果是 &lt;code&gt;Map.set(&apos;color&apos;, &apos;red&apos;)&lt;/code&gt;，底层引擎会从历史日志中提取旧值，将其解算为 &lt;code&gt;Map.set(&apos;color&apos;, &apos;blue&apos;)&lt;/code&gt;。这保证了即便是回退操作，CRDT 的冲突解决时钟（Logical Clock）也依然是向前走且合法递增的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;业务流程展示&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Start((&quot;触发 Time Travel Jump&quot;)) --&amp;gt; Check{&quot;目标 Patch 索引与当前相比?&quot;}
    Check --&amp;gt;|&quot;大于当前 (向未来)&quot;| Forward[&quot;正向应用 Patch&quot;]
    Check --&amp;gt;|&quot;小于当前 (向过去)&quot;| Backward[&quot;反向回滚 Patch&quot;]
    Check --&amp;gt;|&quot;等于当前&quot;| Done((&quot;无操作&quot;))
    
    Forward --&amp;gt; LoopF[&quot;循环执行 applyLocalCrdtDiff&quot;]
    LoopF --&amp;gt; Notify[&quot;更新 DevTools 状态&quot;]
    
    Backward --&amp;gt; LoopB[&quot;循环执行 executeReverseNetworkPatch&quot;]
    LoopB --&amp;gt; Notify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心逻辑 (&lt;code&gt;zion-all/zed/src/zed/views/DevToolsBridge/hooks/useTimeTravel.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const onTimeTravelJump = useCallback(({ targetIndex }) =&amp;gt; {
  const currentIndex = lastPatchIndexRef.current;
  const patches = currentNetworkPatchesRef.current;

  // 如果目标节点在未来，执行正向补丁应用
  if (targetIndex &amp;gt; currentIndex) {
    for (let i = currentIndex + 1; i &amp;lt;= targetIndex; i += 1) {
      applyLocalCrdtDiff(patches[i].content, {
        isPendingApplication: false,
        skipValidation: true, // 时间旅行时跳过常规校验
      });
    }
  } 
  // 如果目标节点在过去，执行网络补丁的反向重算与剥离
  else {
    for (let i = currentIndex; i &amp;gt; targetIndex; i -= 1) {
      const result = executeReverseNetworkPatch(patches[i]);
      if (!result.successful) {
        console.error(&apos;[DevTools Bridge] Failed to reverse patch at index&apos;, i);
        return;
      }
    }
  }

  // 记录当前游标并广播通知 DevTools 更新 UI
  lastPatchIndexRef.current = targetIndex;
  onBroadcastTimeTravelStatus();
}, [onBroadcastTimeTravelStatus]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./project-debug.png&quot; alt=&quot;时间旅行与项目调试面板&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配合 &lt;code&gt;StoreRehydrate.rehydrate&lt;/code&gt; 进行深度模型重水化，开发者可以在协作数据的任意一个历史变更节点之间自如穿梭，这对于排查多人协同冲突和复杂状态 Bug 而言是极大的效率提升。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;难题四：SPA 路由侦听与企业级安全沙箱隔离&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：
Zion Editor 是一个重度的单页应用（SPA）。由于 Chrome Extension 的 Content Script 运行在隔离沙箱中，我们无法直接覆写（Hook）Main World 中的 &lt;code&gt;history.pushState&lt;/code&gt; 来监听路由变化。另外，作为一个具有高权限的数据桥接插件，如果任意一个恶意网页都能向插件发送或接收调试指令，将带来极大的安全渗透隐患。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：轮询降级与严格的白名单校验&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SPA 路由监听&lt;/strong&gt;：在 &lt;code&gt;bridge-relay.ts&lt;/code&gt; 中，我采用原生 &lt;code&gt;popstate&lt;/code&gt; 监听，并结合兜底的高频轮询 (&lt;code&gt;setInterval&lt;/code&gt; 500ms) 的方式，来捕捉画布或工作区的 URL 切换。一旦 URL 发生变化，立刻通知 Background 重置插件的缓存和连接状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需执行与环境校验&lt;/strong&gt;：在 &lt;code&gt;background.ts&lt;/code&gt; 中，所有跨 Tab 转发的消息 (&lt;code&gt;RELAY_TO_TAB&lt;/code&gt;) 以及代码注入请求，都会经过一层严苛的 &lt;code&gt;SUPPORTED_URL_PATTERNS&lt;/code&gt; 正则校验。确保插件的超能力只在 Zion 的本地开发 (&lt;code&gt;localhost&lt;/code&gt;)、预发和生产域名下激活，从物理层面隔离安全风险。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心代码 (&lt;code&gt;zed-devtools/src/background.ts&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 支持的 URL 匹配模式（安全白名单） */
const SUPPORTED_URL_PATTERNS = [
  /^http:\/\/localhost(:\d+)?\//,
  /^https:\/\/zion\.functorz\.work\//,
  /^https:\/\/zion\.functorz\.com\//,
  /^https:\/\/editor\.momen\.app\//,
];

const isUrlSupported = (url?: string): boolean =&amp;gt; {
  if (!url) return false;
  return SUPPORTED_URL_PATTERNS.some((pattern) =&amp;gt; pattern.test(url));
};

chrome.runtime.onMessage.addListener((message, sender) =&amp;gt; {
  // 路由转发前严格校验宿主环境
  if (message.type === DevToolsMessageType.RELAY_TO_TAB &amp;amp;&amp;amp; message.tabId) {
    chrome.tabs.get(message.tabId).then((tab) =&amp;gt; {
      // 拦截非法域名的渗透，静默忽略
      if (!isUrlSupported(tab.url)) return; 
      chrome.tabs.sendMessage(message.tabId, message.payload);
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;细节打磨：利用 Esbuild 动态编译与静态隔离注入&lt;/h3&gt;
&lt;p&gt;通常在 Chrome 插件开发中，注入脚本会作为静态资源放在 &lt;code&gt;manifest.json&lt;/code&gt; 的 &lt;code&gt;web_accessible_resources&lt;/code&gt; 中。但为了确保我的 Inspector 核心探针 (&lt;code&gt;helper.ts&lt;/code&gt;) 能够始终获得最新的 TypeScript 类型支持、不受外部打包工具污染，且不向宿主环境暴露全局变量，我编写了一个独立构建流：&lt;/p&gt;
&lt;p&gt;在执行 &lt;code&gt;pnpm build&lt;/code&gt; 或 &lt;code&gt;pnpm dev&lt;/code&gt; 时，利用 &lt;strong&gt;esbuild&lt;/strong&gt; 脚本 (&lt;code&gt;scripts/build-inspector.ts&lt;/code&gt;)，将 &lt;code&gt;helper.ts&lt;/code&gt; 实时编译、打包、转义成一段纯文本的立即执行函数 (IIFE) 字符串（&lt;code&gt;helperCode.ts&lt;/code&gt;），然后再由 Background 通过 &lt;code&gt;chrome.scripting.executeScript&lt;/code&gt; 的 &lt;code&gt;func&lt;/code&gt; 和 &lt;code&gt;args&lt;/code&gt; 动态注入到页面的 Main World。&lt;/p&gt;
&lt;p&gt;这种高度工程化的做法，不仅做到了逻辑的绝对隔离，还完美兼顾了现代 TypeScript 开发的丝滑体验。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zed-devtools&lt;/code&gt; 的开发不仅是一次简单的 Chrome 扩展实践，更是一次对 React 渲染机制、浏览器线程模型和前端数据协同底层的深度剖析与重构。&lt;/p&gt;
&lt;p&gt;通过引入四层架构打破沙箱壁垒、利用 Web Worker 和虚拟列表突破性能瓶颈、结合 CRDT 实现时光倒流，最终呈现的是一个拥有极致性能和强大洞察力的开发者利器。这套技术方案同样适用于其他大型前端应用的专属调试工具开发，极具工程借鉴价值。&lt;/p&gt;
</content:encoded></item><item><title>构建提效：Zion 编辑器从 Webpack 到 Rsbuild 的架构迁移实践</title><link>https://nollieleo.github.io/posts/zed-rsbuild-migration/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zed-rsbuild-migration/</guid><description>深度解析 Zion Editor 大型前端单体仓库从 Webpack 迁移至 Rsbuild (基于 Rspack) 的架构升级之路。探讨百万行代码级应用的构建瓶颈、Babel/TS-Loader 的剥离策略、AST 级别自定义 Loader 的重构，以及深度 Webpack 优化的降级兼容方案。</description><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与痛点：大型单体前端应用的构建困境&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Zion Editor (&lt;code&gt;zed&lt;/code&gt;)&lt;/strong&gt; 是一个非常复杂的低代码/无代码可视化编辑器底座。随着多年的业务迭代，整个工程已经膨胀为一个包含数十万行代码、高度依赖 Webpack 深度定制（涵盖各类复杂 Loaders、Plugins 和 AST 转换）的大型单体（Monolith）应用。&lt;/p&gt;
&lt;p&gt;随着代码量的剧增，传统的 Webpack 构建架构暴露出严重的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;冷启动耗时较长&lt;/strong&gt;：开发阶段的 &lt;code&gt;npm start&lt;/code&gt; 往往需要等待数分钟才能完成首次编译，严重拖慢了研发同学进入开发状态的节奏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HMR（热更新）迟钝&lt;/strong&gt;：在编写复杂的组件树（如画布区域代码）时，一次保存带来的增量编译甚至需要十几秒，开发体验出现显著下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产环境构建阻塞&lt;/strong&gt;：CI/CD 阶段的 &lt;code&gt;build&lt;/code&gt; 时间随着模块数量呈线性增长，单次打包高度消耗内存（常常触发 OOM 崩溃），导致流水线效率低下。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决这一核心问题，我主导了 &lt;code&gt;zed&lt;/code&gt; 仓库的底层构建引擎升级，将其从老旧的 &lt;strong&gt;Webpack&lt;/strong&gt; 架构整体迁移至基于 Rust 编写的高性能构建工具 &lt;strong&gt;Rsbuild (底层为 Rspack)&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 迁移策略与架构设计&lt;/h2&gt;
&lt;p&gt;Rsbuild 是字节跳动开源的基于 Rspack 的构建工具，虽然官方宣称对 Webpack 生态有极高的兼容性，但对于 Zion Editor 这样“深度定制”过大量底层配置的重度工程来说，平滑迁移绝非易事。&lt;/p&gt;
&lt;p&gt;我将整个迁移过程拆解为&lt;strong&gt;四个核心维度的重构&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mindmap
  root((&quot;Rsbuild 架构迁移&quot;))
    [&quot;编译层 (Compiler)&quot;]
      [&quot;废弃 babel-loader&quot;]
      [&quot;废弃 ts-loader&quot;]
      [&quot;引入 SWC 底层支持&quot;]
    [&quot;配置层 (Config)&quot;]
      [&quot;分离 optimization.ts&quot;]
      [&quot;分离 plugins.ts&quot;]
      [&quot;分离 loaders.ts&quot;]
    [&quot;适配层 (Polyfill &amp;amp; Patch)&quot;]
      [&quot;Node 全局变量 Mock&quot;]
      [&quot;Brotli 算法兼容&quot;]
      [&quot;AST/Loader 深度定制兼容&quot;]
    [&quot;优化层 (Performance)&quot;]
      [&quot;基于 Module 的 SplitChunks&quot;]
      [&quot;ImageMinimizer -&amp;gt; plugin-image-compress&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.1 解耦庞杂的 &lt;code&gt;webpack.config.js&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;以前的 Webpack 配置杂糅在 &lt;code&gt;webpack.base.ts&lt;/code&gt;, &lt;code&gt;webpack.dev.ts&lt;/code&gt;, &lt;code&gt;webpack.prod.ts&lt;/code&gt; 甚至自定义的 &lt;code&gt;utils.ts&lt;/code&gt; 中，逻辑交织，极难维护。
在本次迁移中，我重新设计了配置目录结构，将其拆分为纯粹、语义化的 &lt;code&gt;rsbuild.config.ts&lt;/code&gt; 及其附属模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zed/config/
├── constants.ts      // 环境变量、路径、补丁等常量收敛
├── loaders.ts        // 自定义 Rules 和 Loaders
├── optimization.ts   // 分包 (SplitChunks)、忽略警告 (IgnorePlugin)、NoParse
└── plugins.ts        // 插件生态聚合 (Brotli压缩、Less/Sass、SVGR)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模块化设计让长达上千行的构建配置文件变得一目了然，也为后续的灰度发布提供了切入点。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 核心难点与攻克细节&lt;/h2&gt;
&lt;h3&gt;难题一：Loader 体系的“去 Babel”与“全面拥抱 SWC”&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点：&lt;/strong&gt;
原先的项目严重依赖 &lt;code&gt;babel-loader&lt;/code&gt; 和 &lt;code&gt;ts-loader&lt;/code&gt;，这是导致编译缓慢的元凶之一。但在彻底移除它们时，面临着 Zion 业务中大量特殊语法糖和内部库无法被 Rsbuild 默认的 SWC 转换器正确识别的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：&lt;/strong&gt;
我们利用了 Rsbuild 内置的 &lt;code&gt;@rsbuild/plugin-react&lt;/code&gt; 和 &lt;code&gt;@rsbuild/plugin-type-check&lt;/code&gt;。Rsbuild 底层直接使用 Rspack 原生的 SWC 转换，免去了 Babel 繁重的 AST 解析与序列化开销。
对于 TypeScript 的类型检查，我们彻底将其从编译主线程中剥离（以往使用 &lt;code&gt;fork-ts-checker-webpack-plugin&lt;/code&gt;），转而使用 Rsbuild 官方的隔离检查插件，极大提升了主进程的编译吞吐量。&lt;/p&gt;
&lt;h3&gt;难题二：定制化 AST Loader 和 第三方库的黑盒 Bug&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点：&lt;/strong&gt;
在迁移过程中，发现 Zion 强依赖的 &lt;code&gt;json-joy&lt;/code&gt; 库在 Rspack 的打包机制下，其内部的一段基于 Node.js &lt;code&gt;Buffer&lt;/code&gt; 的 UTF-8 解码代码会引发主要的运行时异常。由于不能直接去修改 &lt;code&gt;node_modules&lt;/code&gt; 里的代码，在过去的 Webpack 里我们可能通过复杂的 plugin 甚至自定义 loader 解决。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：精准的文本级 Patch 替换&lt;/strong&gt;
我利用了 &lt;code&gt;string-replace-loader&lt;/code&gt;，在构建流水线的 &lt;code&gt;module.rules&lt;/code&gt; 中实现了一个&lt;strong&gt;精准的代码注入&lt;/strong&gt;（见 &lt;code&gt;config/loaders.ts&lt;/code&gt;）。在构建时拦截对应文件，将其核心 &lt;code&gt;decodeUtf8&lt;/code&gt; 函数强行替换为安全的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/config/loaders.ts
import { JSON_JOY_UTF8_PATCH } from &apos;./constants&apos;;

export const getJsonJoyPatchRule = (): RuleSetRule =&amp;gt; ({
  // 精准匹配出问题的 json-joy 依赖路径
  test: /node_modules\/@jsonjoy\.com\/util\/lib\/buffers\/utf8\/decodeUtf8\/v18\.js$/,
  loader: require.resolve(&apos;string-replace-loader&apos;),
  options: {
    search: /[\s\S\n]*/, // 匹配全文
    replace: JSON_JOY_UTF8_PATCH, // 替换为安全的 JS 实现
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;这一做法既避开了繁重的 AST 树解析开销，又以最轻量的方式跨越了 Rsbuild 和特定第三方老旧依赖兼容的差异。&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;难题三：百万行代码的分包策略 (SplitChunks) 重塑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点：&lt;/strong&gt;
对于 Zion 这样的超大型 Web IDE 来说，良好的 Chunk 分割是保证首屏加载速度的关键。Rsbuild 默认的 &lt;code&gt;strategy: &apos;split-by-experience&apos;&lt;/code&gt; 在面对 Zion 动辄 10MB 的第三方图表(&lt;code&gt;@antv&lt;/code&gt;)、画布拖拽 (&lt;code&gt;@dnd-kit&lt;/code&gt;)、富文本引擎时，往往拆得过碎或者合并得过大，导致缓存命中率极低。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：底层能力接管，精细化 Module Chunking&lt;/strong&gt;
我在 &lt;code&gt;rsbuild.config.ts&lt;/code&gt; 中果断禁用了 Rsbuild 默认的分割策略（&lt;code&gt;strategy: &apos;custom&apos;&lt;/code&gt;），并利用 Rspack 的底层钩子重新编写了极为严苛的业务分包策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/config/optimization.ts
export const getSplitChunksConfig = (): Rspack.OptimizationSplitChunksOptions =&amp;gt; ({
  chunks: &apos;all&apos;,
  cacheGroups: {
    // 提取最核心不变的 React 生态
    react: {
      name: &apos;lib-react&apos;,
      test: /[\\/]node_modules[\\/](core-js|react.*|redux.*|immer)[\\/]/,
      priority: 0,
    },
    // 将非常厚重的编辑器相关库单独抽离，按需加载
    editor: {
      name: &apos;lib-editor&apos;,
      test: /[\\/]node_modules[\\/](@codemirror|codemirror|@uiw)[\\/]/,
      priority: 5,
    },
    // 兜底的 Node_modules 处理，限制单文件不超过 5MB
    vendors: {
      name: &apos;chunk-vendors&apos;,
      test: /[\\/]node_modules[\\/]/,
      minChunks: 1,
      minSize: 100 * 1024,
      maxSize: 5 * 1000 * 1024,
      priority: -10,
    },
    common: {
      name: &apos;chunk-common&apos;,
      minChunks: 2,
      priority: -20,
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;难题四：CSS 的兼容与优化（保留 SCSS SourceMap）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;细节打磨：&lt;/strong&gt;
Rsbuild 默认会使用高性能的 &lt;code&gt;lightningcss-loader&lt;/code&gt; 来处理样式。但在我们实际迁移中发现，&lt;code&gt;lightningcss&lt;/code&gt; 破坏了我们在深度定制 SCSS 主题时的 Source Map 映射，导致开发阶段调试样式非常痛苦。&lt;/p&gt;
&lt;p&gt;为了极致的开发者体验，我通过阅读 Rsbuild 底层源码和 GitHub Issue，在配置层灵活将其降级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/rsbuild.config.ts
export default defineConfig({
  tools: {
    // 禁用 lightningcss-loader 以保留真实的 SCSS Source Map
    // 参考：https://github.com/web-infra-dev/rsbuild/issues/4451
    lightningcssLoader: false,
    cssLoader: {
      sourceMap: IS_DEV,
      importLoaders: 3,
    },
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;难题五：前端路由入口的 React-Router 兼容重构&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点：&lt;/strong&gt;
原先在 Webpack 环境下，&lt;code&gt;zed&lt;/code&gt; 的应用入口代码存在一些依赖于 Webpack 模块解析机制的陈旧写法，特别是在动态路由加载和 App 初始化流程中。在迁移到 Rsbuild 且升级了相关的构建依赖后，这些非标准的入口文件引发了加载异常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻克方案：拥抱 React-Router 现代标准&lt;/strong&gt;
在这次迁移中，我一并&lt;strong&gt;拆分和重构了之前 App 入口中杂乱的代码&lt;/strong&gt;。针对路由模块，我移除了老旧的写法，采用 &lt;code&gt;react-router&lt;/code&gt; 最新推荐的方式来重新组织路由树和按需加载模块。这不仅解决了新构建系统下的白屏风险，还从代码层面还清了一部分历史技术债。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 迁移成果与收益：显著的性能提升&lt;/h2&gt;
&lt;p&gt;经过这一轮全面而深度的重构，去除了近千行的陈旧 Webpack 及其配套 Loader 的配置文件（删除了 &lt;code&gt;config/webpack.dev.ts&lt;/code&gt;, &lt;code&gt;config/webpack.prod.ts&lt;/code&gt;, &lt;code&gt;config/webpack.common.ts&lt;/code&gt;），并且顺带清理了 400 多个历史遗留的无用 SVG，带来的业务提效是非常显著的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标对比 (Production Build)&lt;/th&gt;
&lt;th&gt;Webpack&lt;/th&gt;
&lt;th&gt;Rsbuild&lt;/th&gt;
&lt;th&gt;变化与收益&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际耗时 (real)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;153.42s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38.66s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;减少了 114.76s (&lt;strong&gt;约提升 75%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU 利用率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;275%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;338%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+63%&lt;/strong&gt; (更好地利用了多核并发优势)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;产物体积&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;57 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;减少了 5MB (&lt;strong&gt;约降低 8.8%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;冷启动时间 (Dev)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt; 30s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极速秒开 (&lt;strong&gt;约提升 80%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;热更新 (HMR)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2~5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 200ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;毫秒级响应 (&lt;strong&gt;约提升 90%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;说明：Rsbuild 的 CPU 利用率更高，意味着它在底层（Rust）拥有更加优秀的并行多线程调度能力，从而在更短的实际时间 (real) 内充分利用了机器性能（比如 M4 Pro 的多核性能）。&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;深度原理解析：为什么 Rsbuild 能够解决 OOM 崩溃？&lt;/h3&gt;
&lt;p&gt;在 Webpack 时代，无论我们怎么优化 &lt;code&gt;thread-loader&lt;/code&gt; 或 &lt;code&gt;cache&lt;/code&gt;，底层依然受限于 Node.js 的单线程 Event Loop 以及 V8 引擎对老生代内存（Heap Limit，通常默认限制在 1.4GB 左右）的严格管控。当 Zion 这样包含数十万个模块的大型应用在 CI 流水线进行 AST 解析、依赖收集和压缩时，极易触碰 V8 的垃圾回收（GC）红线，导致频繁的 “Stop-the-World” 甚至直接抛出 &lt;code&gt;JavaScript heap out of memory&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而 &lt;strong&gt;Rsbuild/Rspack 的底层是用 Rust 编写的&lt;/strong&gt;。它不仅脱离了 V8 的内存分配与 GC 限制（由 Rust 原生的所有权机制管理内存，占用极低且确定），还实现了真正的 OS 级别多线程架构（这也是为什么监控中 CPU 利用率能提升到 338% 的原因）。这种底层运行时的效率，能够把构建时间压缩 75% 。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 迁移过程中的“潜在问题”与遗留问题&lt;/h2&gt;
&lt;p&gt;迁移并不是完美无缺的，在脱离了 Webpack 庞大的生态后，我也遇到了一些底层编译机制差异带来的“潜在问题”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;待解决的难题：Mobx Observer HOC 导致的热更新 (HMR) 崩溃&lt;/strong&gt;
在原有代码中，我们大量使用了基于 Mobx 的 &lt;code&gt;memoWithObserver&lt;/code&gt; 包装高阶组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const DMContentComp = () =&amp;gt; {
  const { isDetailsOpen } = useDMStore();
  useInitDMState();
  // ...
};
export const DMContent = memoWithObserver(DMContentComp, &apos;DMContent&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Rsbuild（底层依赖 SWC 和 React Refresh）的机制下，如果在该组件内新增一个 &lt;code&gt;useEffect&lt;/code&gt; 钩子并保存，页面会直接抛出经典的 &lt;code&gt;Rendered more hooks than during the previous render&lt;/code&gt; 崩溃错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;根本原因分析&lt;/strong&gt;：
Rsbuild 的 Compiler 在处理 React Refresh 签名时，没有正确识别出被自定义 &lt;code&gt;observer&lt;/code&gt; HOC 包裹的函数是一个合法的组件。当代码发生变更时，热更新模块本该选择**“销毁并重建组件树”&lt;strong&gt;，但却错误地选择了&lt;/strong&gt;“保留状态复用”**，导致新增加的 Hook 跑在了旧的 Fiber 节点状态上，引发了 React 规则报错。
目前这个问题由于涉及到大面积的历史代码规范，暂时保留，计划在后续的专项技术债清理中通过编写 SWC 插件或重构业务组件写法来彻底修复。&lt;/p&gt;
&lt;h3&gt;遗留的 Eslint Flat Config 平迁与兜底工作&lt;/h3&gt;
&lt;p&gt;在本次迁移中，我将老旧的 &lt;code&gt;.eslintrc.js&lt;/code&gt; 直接重构成了最新版 ESLint v9 强制要求的 Flat Config 格式（&lt;code&gt;eslint.config.mjs&lt;/code&gt;）。同时我移除了 &lt;code&gt;eslint-plugin-unused-import&lt;/code&gt; 这类已经过时的校验插件，升级了核心的 &lt;code&gt;@eslint/js&lt;/code&gt;、&lt;code&gt;typescript-eslint&lt;/code&gt; 以及 React Hook 插件。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;虽然理论上我是按照老项目的规则做了“平迁”处理，但之前的规则集因为长期积累导致体系比较松散，并没有非常严格的限制。所以在转换为新机制后，可能会存在校验规则的遗漏。这需要研发团队在日常业务迭代中，互相帮忙兼顾与持续补充，才能构建起一套完美的最新规则集。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;将大型单体仓库从 Webpack 迁移至 Rsbuild 绝对不仅仅是换一个命令行工具那么简单。它是一场&lt;strong&gt;全方位的架构翻新&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在这次重构过程中，我不仅仅完成了基础包的替换，更深入到 Loader 机制层、SplitChunks 的按需拆分、甚至特定库的内存 Patch。这次技术重构不仅卸下了 Zion Editor 沉重的历史构建包袱，更为团队未来快速演进和研发迭代提供了基于 Rust 的底层支持。&lt;/p&gt;
</content:encoded></item><item><title>AI 学习助手架构设计与问题解决记录</title><link>https://nollieleo.github.io/posts/ai-agent-architecture-record/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/ai-agent-architecture-record/</guid><description>记录 AI Agent 开发过程中的交互重构、输出解析及容错架构设计。</description><pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;AI 学习助手架构设计与问题解决记录&lt;/h1&gt;
&lt;p&gt;本文档记录了 AI 学习助手在开发过程中遇到的核心架构问题、重构原因及最终的解决方案。内容以实际工程问题为导向，说明各项设计的意图与收益。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 业务需求背景 (Business Context)&lt;/h2&gt;
&lt;p&gt;本项目是一个“AI 学习助手 (AI Learning Assistant)”。核心业务流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需求表达&lt;/strong&gt;：用户通过自然语言聊天表达自己想要学习的知识、技能或目标（例如：“我想学前端开发”）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顾问式沟通&lt;/strong&gt;：为了保证学习计划的有效性，AI 扮演学习顾问的角色。它不会立刻盲目生成计划，而是主动向用户提问，收集制定计划所需的关键基线信息（如：当前的知识基础、每天可用的学习时间、期望的项目目标等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定制化生成&lt;/strong&gt;：当关键信息收集完整后，AI 结束追问阶段，并基于收集到的精确信息，为用户生成一份高度定制化、结构化的专属学习计划。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（目前阶段需求到这里）&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 交互架构：从弹窗表单到对话流状态机 (UX Problem)&lt;/h2&gt;
&lt;p&gt;在早期版本中，当 AI 收集完用户信息准备生成学习计划时，系统会弹出一个表单弹窗（Modal）让用户确认。这种设计切断了对话的上下文，导致体验割裂。&lt;/p&gt;
&lt;h3&gt;为什么这么设计&lt;/h3&gt;
&lt;p&gt;我将交互重构为基于状态机的对话流。系统维护 &lt;code&gt;EXPLORING_PLAN&lt;/code&gt; 和 &lt;code&gt;READY_FOR_PLAN&lt;/code&gt; 两个核心状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;EXPLORING_PLAN&lt;/code&gt; 阶段，系统通过纯文本与用户对话，收集需求。&lt;/li&gt;
&lt;li&gt;在状态切换至 &lt;code&gt;READY_FOR_PLAN&lt;/code&gt; 时，前端不再弹出阻断式的表单，而是在聊天流中直接渲染内联 UI（Inline UI）组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;预防了什么问题以及什么好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预防的问题&lt;/strong&gt;：避免了模态弹窗（Modal）遮挡聊天记录，防止用户在确认计划时遗忘之前的对话上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带来的好处&lt;/strong&gt;：维持了单向滚动的对话心智模型，用户交互更连贯；状态机的引入使得前端 UI 的渲染逻辑更加确定和可预测。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构与流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; EXPLORING_PLAN : 初始化对话
    EXPLORING_PLAN --&amp;gt; EXPLORING_PLAN : 多轮问答收集信息
    EXPLORING_PLAN --&amp;gt; READY_FOR_PLAN : 触发生成计划工具

    state READY_FOR_PLAN {
        [*] --&amp;gt; RenderInlineUI : 渲染内联卡片
        RenderInlineUI --&amp;gt; UserConfirm : 用户核对配置
        UserConfirm --&amp;gt; GeneratePlan : 确认并生成
    }

    READY_FOR_PLAN --&amp;gt; EXPLORING_PLAN : 用户修改需求
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. LLM 输出控制 - 解决 XML 输出与工具调用的冲突&lt;/h2&gt;
&lt;p&gt;在集成结构化表单配置生成时，LLM 没有调用预期的 Tool（函数调用），而是直接在回复中输出了包含 &lt;code&gt;&amp;lt;form_config&amp;gt;&lt;/code&gt; 的原生 XML 文本，导致前端无法渲染组件。这是一开始不熟悉工具采用的策略。也算是踩的一个很标准的坑&lt;/p&gt;
&lt;h3&gt;为什么这么设计&lt;/h3&gt;
&lt;p&gt;这是由于 Prompt 冲突导致的。当系统提示词（System Prompt）中包含过多的 XML 示例，且未明确界定“回复文本”与“工具调用”的边界时，LLM 会倾向于模仿示例直接输出文本。
我将架构调整为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;移除 System Prompt 中容易引起误导的 XML 代码块示例。&lt;/li&gt;
&lt;li&gt;强制使用严格的 JSON Schema 定义 Tool 的参数结构。&lt;/li&gt;
&lt;li&gt;在系统级明确指令：“当需要生成配置时，必须且只能通过调用工具实现，禁止在文本中输出代码块”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;预防了什么问题以及什么好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预防的问题&lt;/strong&gt;：防止前端解析器因收到混杂着 Markdown 或 XML 的非标准文本而发生正则匹配错误或解析崩溃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带来的好处&lt;/strong&gt;：保证了前后端数据交换格式的绝对标准化（纯 JSON），解耦了 LLM 文本生成行为与系统功能触发行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构与流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[用户请求生成计划] --&amp;gt; B{LLM 意图识别}

    B --&amp;gt;|旧架构: 错误匹配| C[直接输出包含 XML 的文本]
    C --&amp;gt; D[前端解析失败 / 暴露源码给用户]

    B --&amp;gt;|新架构: 严格 Tool Call| E[触发 generate_form_config 工具]
    E --&amp;gt; F[返回标准化 JSON 参数]
    F --&amp;gt; G[前端精确渲染内联 UI]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 数据校验层：Zod 运行时校验与 Schema 规范化&lt;/h2&gt;
&lt;p&gt;前端曾多次抛出 &lt;code&gt;ZodError&lt;/code&gt; 导致页面白屏或崩溃。根本原因是 LLM 返回的 JSON 缺少 &lt;code&gt;required&lt;/code&gt; 字段或数据类型错误（例如将文本框类型错写为 &quot;text&quot; 而非 &quot;input&quot;），而前端代码使用了 TypeScript 的类型断言（&lt;code&gt;as Type&lt;/code&gt;）而非运行时校验。&lt;/p&gt;
&lt;h3&gt;为什么这么设计&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;废弃类型断言&lt;/strong&gt;：将所有 &lt;code&gt;const data = rawData as Config&lt;/code&gt; 修改为 &lt;code&gt;const data = ConfigSchema.parse(rawData)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema 补齐与对齐&lt;/strong&gt;：在给 LLM 提供的 JSON Schema 描述中，严格对齐前端的类型枚举（如 &lt;code&gt;[&apos;input&apos;, &apos;select&apos;]&lt;/code&gt;），并利用 Zod 的 &lt;code&gt;.parse()&lt;/code&gt; 在后端强行注入 &lt;code&gt;.default(true)&lt;/code&gt; 的默认值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;预防了什么问题以及什么好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预防的问题&lt;/strong&gt;：TypeScript 的类型断言只在编译时有效，无法拦截运行时 LLM 生成的脏数据。此设计防止了脏数据进入视图层导致 React 渲染崩溃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带来的好处&lt;/strong&gt;：Zod 在数据边界处提供了坚固的防护。一旦数据结构不符，会在逻辑层尽早抛出清晰的错误（Fail Fast），而不是在渲染层产生不可预知的副作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构与流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant LLM as LLM (Tool Call)
    participant Backend as 后端接口
    participant Zod as Zod 校验层
    participant UI as React 视图层

    LLM-&amp;gt;&amp;gt;Backend: 返回 JSON 数据
    Backend-&amp;gt;&amp;gt;Zod: 传递 Payload

    alt 数据合法
        Zod--&amp;gt;&amp;gt;UI: 返回类型安全的 Data
        UI-&amp;gt;&amp;gt;UI: 正常渲染内联表单
    else 数据缺失/类型错误
        Zod--&amp;gt;&amp;gt;UI: 报错并中断 (旧架构)
        Zod-&amp;gt;&amp;gt;Backend: 触发后端的自愈重试 (新架构)
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 加入容错与自愈架构 - SSE 流中的 LLM 自动修复&lt;/h2&gt;
&lt;p&gt;当上述 Zod 校验失败或后端业务逻辑报错时，如果直接抛出 Exception，会导致 Server-Sent Events (SSE) 流断开，对话非正常中止。&lt;/p&gt;
&lt;h3&gt;为什么这么设计&lt;/h3&gt;
&lt;p&gt;我实现了一个基于 &lt;code&gt;while&lt;/code&gt; 循环的自愈架构（Auto-Healing）。当工具调用发生错误（如必填字段缺失或格式校验失败）时，系统不抛出中断级异常，而是捕获该错误，将其包装为一条 &lt;code&gt;ToolMessage&lt;/code&gt;（附带具体的错误信息）发送回 LLM。&lt;/p&gt;
&lt;h3&gt;预防了什么问题以及什么好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预防的问题&lt;/strong&gt;：防止后端报错直接切断 SSE 网络连接，避免前端用户看到“网络错误”或断流的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带来的好处&lt;/strong&gt;：赋予了 Agent 自我修正的能力。LLM 能够阅读自己上一次调用产生的错误日志，并在下一次迭代中输出正确格式的参数，全程对用户透明，极大地提升了系统的容错率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构与流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Start((开始)) --&amp;gt; CallLLM[调用 LLM]
    CallLLM --&amp;gt; CheckTool{是否触发 Tool?}
    CheckTool --&amp;gt;|否| StreamResponse[返回普通文本响应流]
    CheckTool --&amp;gt;|是| ExecuteTool[执行 Zod 解析与校验]

    ExecuteTool --&amp;gt; CheckResult{校验是否成功?}
    CheckResult --&amp;gt;|成功| ReturnData[返回配置并渲染 UI]

    CheckResult --&amp;gt;|失败 (ZodError)| WrapError[捕获异常并伪造 ToolMessage]
    WrapError --&amp;gt; AppendHistory[将错误信息追加到提示词上下文]
    AppendHistory --&amp;gt;|在同一个 SSE 流中重试| CallLLM

    ReturnData --&amp;gt; EndNode((结束))
    StreamResponse --&amp;gt; EndNode
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 6：常用辅助类库盘点</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part6/</guid><description>盘点在 NestJS 企业级开发中不可或缺的辅助类库：数据校验与转换 (class-validator/class-transformer)、配置校验 (Joi)、高性能日志 (nestjs-pino)、安全加密 (bcryptjs) 以及接口文档生成 (@nestjs/swagger)。</description><pubDate>Mon, 16 Feb 2026 12:25:00 GMT</pubDate><content:encoded>&lt;p&gt;在 NestJS 项目开发中，除了框架自身提供的核心模块外，我们通常还会引入一系列强大的第三方类库来解决特定的工程问题。本文将盘点项目中常用的几个核心辅助类库，探讨它们的使用场景及底层逻辑。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 数据校验与转换：class-validator &amp;amp; class-transformer&lt;/h2&gt;
&lt;p&gt;在处理客户端发来的 HTTP 请求体（Body）时，数据验证和类型转换是第一道防线。NestJS 官方推荐使用 &lt;code&gt;class-validator&lt;/code&gt; 和 &lt;code&gt;class-transformer&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { IsString, IsNotEmpty, MinLength } from &quot;class-validator&quot;;
import { Transform } from &quot;class-transformer&quot;;

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  @Transform(({ value }) =&amp;gt; (typeof value === &quot;string&quot; ? value.trim() : value))
  username: string;

  @MinLength(6)
  password: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这两者的分工是什么？&lt;/strong&gt;
&lt;code&gt;class-transformer&lt;/code&gt; 负责“变形”。由于网络传输的都是纯 JSON，它能将普通的 JavaScript 对象转换为类的实例（Instance），并执行 &lt;code&gt;@Transform&lt;/code&gt; 之类的自定义逻辑清洗数据（如去除空格、字符串转数字）。
&lt;code&gt;class-validator&lt;/code&gt; 负责“质检”。在数据变为类实例后，它根据 &lt;code&gt;@IsString&lt;/code&gt; 等装饰器上的规则进行严格校验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不直接写 if-else 校验？&lt;/strong&gt;
如果业务代码里充斥着 &lt;code&gt;if (!dto.username) throw Error&lt;/code&gt;，会导致代码臃肿不堪。基于装饰器的声明式校验完美契合 NestJS 的全局 &lt;code&gt;ValidationPipe&lt;/code&gt;，实现了解耦。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 环境变量强校验：Joi 的 Fail-Fast 机制&lt;/h2&gt;
&lt;p&gt;加载环境变量时，除了自带的 &lt;code&gt;@nestjs/config&lt;/code&gt;，我们通常还会配合 &lt;code&gt;joi&lt;/code&gt; 进行强校验。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as Joi from &quot;joi&quot;;
import { ConfigModule } from &quot;@nestjs/config&quot;;

ConfigModule.forRoot({
  validationSchema: Joi.object({
    NODE_ENV: Joi.valid(&quot;development&quot;, &quot;production&quot;).default(&quot;development&quot;),
    DB_PASSWORD: Joi.string().required(),
    JWT_SECRET: Joi.string().required(),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Joi 的作用是什么？&lt;/strong&gt;
&lt;code&gt;Joi&lt;/code&gt; 是一个功能强大的数据模型描述和校验库。在这里，它作为应用启动的安全门。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么要用它校验环境变量？&lt;/strong&gt;
如果没有强校验，一旦运维人员在生产环境中漏配了核心变量（如数据库密码、JWT 秘钥），应用依然能正常启动，但会在用户访问时触发 500 崩溃。引入 &lt;code&gt;Joi&lt;/code&gt; 实现了 Fail-Fast（快速失败）机制：只要缺失必填项，应用在启动的瞬间就会报错并阻断，大幅度降低了排错成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 高性能结构化日志：nestjs-pino 的工程化应用&lt;/h2&gt;
&lt;p&gt;生产环境中，我们需要高性能且结构化的日志方案，通常会选用 &lt;code&gt;nestjs-pino&lt;/code&gt;（底层基于 &lt;code&gt;pino&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { LoggerModule } from &quot;nestjs-pino&quot;;

LoggerModule.forRootAsync({
  useFactory: () =&amp;gt; ({
    pinoHttp: {
      redact: [&quot;req.headers.authorization&quot;, &quot;req.body.password&quot;],
      transport:
        process.env.NODE_ENV === &quot;development&quot;
          ? { target: &quot;pino-pretty&quot; }
          : {
              target: &quot;pino-roll&quot;,
              options: { file: &quot;logs/app.log&quot;, frequency: &quot;daily&quot; },
            },
    },
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原生 &lt;code&gt;console.log&lt;/code&gt; 的缺陷：&lt;/strong&gt;
原生输出在处理大量并发请求时，其同步特性可能会阻塞 Event Loop。且彩色文本日志难以被 ELK 等日志系统解析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pino 的优势：&lt;/strong&gt;
&lt;code&gt;pino&lt;/code&gt; 主打极高的性能（完全异步非阻塞），并且天生输出 JSON 格式（结构化日志）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态周边配套：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pino-pretty&lt;/code&gt;：在开发环境下将 JSON 日志转化回人类易读的彩色文本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pino-roll&lt;/code&gt;：在生产环境下提供按天、按体积的自动文件切割轮转，防止硬盘撑爆。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 单向散列加密：bcryptjs 的安全防御策略&lt;/h2&gt;
&lt;p&gt;在用户注册和登录时，必须对密码进行单向散列处理。项目中使用了 &lt;code&gt;bcryptjs&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as bcrypt from &quot;bcryptjs&quot;;

// 加密
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(&quot;123456&quot;, salt);

// 对比
const isValid = await bcrypt.compare(&quot;123456&quot;, hash);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不用普通的 MD5 或 SHA？&lt;/strong&gt;
普通哈希算法运算速度极快，黑客很容易通过预先计算好的“彩虹表”反查出原始密码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bcrypt 的核心防御机制：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加盐 (Salt)&lt;/strong&gt;：随机生成一段字符串混入明文再运算，使得相同的密码产生完全不同的密文，使彩虹表失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢速算法&lt;/strong&gt;：&lt;code&gt;10&lt;/code&gt; 是 Cost Factor，故意拉长 CPU 计算时间。对正常登录（算 1 次）无感，但对于试图在一秒内暴力破解几百万次的黑客来说，时间成本是难以承受的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 接口文档自动化：@nestjs/swagger 的代码即文档&lt;/h2&gt;
&lt;p&gt;为了与前端高效对接，通常会使用 &lt;code&gt;@nestjs/swagger&lt;/code&gt; 自动生成 OpenAPI 接口文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ApiTags, ApiOperation, ApiQuery } from &quot;@nestjs/swagger&quot;;

@ApiTags(&quot;行政区划&quot;)
@Controller(&quot;regions&quot;)
export class RegionController {
  @Get()
  @ApiOperation({ summary: &quot;获取省级行政区划列表&quot; })
  findAll() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不手写 API 文档？&lt;/strong&gt;
手写 Markdown 或使用独立工具写文档，极其容易滞后于代码的变更，导致前后端信息不同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于装饰器的优势：&lt;/strong&gt;
利用 TypeScript 的反射机制，Swagger 可以直接读取 Controller 的路由结构、DTO 的类型定义，并在运行时自动生成网页版的可交互文档。代码即文档，保证了接口和文档的 100% 同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 获取真实客户端 IP：request-ip&lt;/h2&gt;
&lt;p&gt;在记录操作日志或实现限流策略时，我们需要准确获取客户端的真实 IP 地址。项目中使用了 &lt;code&gt;request-ip&lt;/code&gt; 这个专门的辅助库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Request } from &quot;express&quot;;
import { getClientIp } from &quot;request-ip&quot;;

export function sendFormattedExceptionResponse(
  response: Response,
  request: Request,
  logger: Logger,
) {
  const clientIp = getClientIp(request);
  // ... 记录日志
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不能直接用 &lt;code&gt;request.ip&lt;/code&gt;？&lt;/strong&gt;
在真实的生产环境中，我们的 Node.js 服务通常部署在 Nginx、HAProxy 或云服务商的负载均衡（LB）之后。如果直接读取 &lt;code&gt;request.ip&lt;/code&gt;，拿到的往往是网关或负载均衡器的内网 IP（例如 &lt;code&gt;127.0.0.1&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;request-ip&lt;/code&gt; 的底层逻辑：&lt;/strong&gt;
它会自动去解析 HTTP 请求头中的 &lt;code&gt;X-Forwarded-For&lt;/code&gt;、&lt;code&gt;X-Real-IP&lt;/code&gt;、&lt;code&gt;CF-Connecting-IP&lt;/code&gt; (Cloudflare) 等一系列由反向代理服务器转发过来的真实 IP 标头，抹平了不同代理环境下的获取差异，保证了审计日志中源 IP 的准确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 身份验证策略抽象：@nestjs/passport &amp;amp; passport-jwt&lt;/h2&gt;
&lt;p&gt;在处理 JWT 登录与鉴权时，我们引入了 &lt;code&gt;@nestjs/passport&lt;/code&gt; 和 &lt;code&gt;passport-jwt&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { PassportStrategy } from &quot;@nestjs/passport&quot;;
import { ExtractJwt, Strategy } from &quot;passport-jwt&quot;;

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: &quot;secretKey&quot;,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么要引入 Passport 框架？&lt;/strong&gt;
虽然自己手写代码从 Header 提取 &lt;code&gt;Bearer Token&lt;/code&gt; 并使用 &lt;code&gt;jsonwebtoken&lt;/code&gt; 去校验也完全可以，但扩展性极差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略模式（Strategy Pattern）的优势：&lt;/strong&gt;
&lt;code&gt;Passport&lt;/code&gt; 提供了一套标准化的“策略模式”架构。今天你使用 JWT 进行鉴权，只需配置 &lt;code&gt;passport-jwt&lt;/code&gt; 策略；明天如果需要增加 OAuth2 (如 Github、Google 登录) 或是 Local (本地账号密码登录)，你只需要新增对应的策略模块即可，原有的 Guard 鉴权体系和业务逻辑不需要做任何改动。它完美践行了开闭原则（对扩展开放，对修改封闭）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>LgdTree 大型虚拟树与复杂拖拽架构解析</title><link>https://nollieleo.github.io/posts/lgdtree-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/lgdtree-architecture/</guid><description>深度剖析无代码编辑器 Zion 底层图层树组件 LgdTree 的架构设计。从 Dnd-kit 拖拽引擎的定制化改造、树结构的 1D 虚拟化扁平降维，到拖拽放置意图（Projected）的精准推导和循环依赖防腐，展现大型前端应用中的深度性能优化与数据同步策略。</description><pubDate>Thu, 12 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与痛点：为什么我们需要重造一棵“树”？&lt;/h2&gt;
&lt;p&gt;在开发 &lt;strong&gt;Zion 无代码编辑器&lt;/strong&gt; 时，画布左侧的“图层结构树 (Layers Tree)”以及“页面路由树 (Pages Tree)”是开发者最依赖的视图模块。
起初，我们考虑过使用 Ant Design 的 &lt;code&gt;Tree&lt;/code&gt; 或市面上开源的树组件。但在低代码/无代码的真实业务场景下，这些传统组件很快就暴露出了主要的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DOM 节点溢出&lt;/strong&gt;：一个复杂的业务页面很容易拥有上千个互相嵌套的 UI 组件。如果不使用虚拟渲染，仅仅是展开整个大树就会导致浏览器主线程严重卡顿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高度自由的拖放互动 (DnD)&lt;/strong&gt;：用户可以在树中随意拖拽一个层级极深的子节点，不仅能改变它的同级顺序（排序），还能将其拖拽并跨级放入另外一个父节点中（层级嵌套）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据一致性灾难&lt;/strong&gt;：在拖拽过程中，如果用户试图把一个“父容器”拖进它自己的“子容器”里（引发&lt;strong&gt;循环依赖 / 循环引用&lt;/strong&gt;），或者把不允许包含子元素的组件（如纯文本节点）当做父容器，系统需要毫秒级的精准拦截，否则中间的可视化画布将直接崩溃。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决这些底层难题，我从零开始主导并设计了 &lt;strong&gt;LgdTree (Large Graph Data Tree)&lt;/strong&gt; 这个专门服务于 Zion 画布生态的高性能虚拟树组件。它基于现代化拖拽引擎 &lt;code&gt;@dnd-kit/core&lt;/code&gt; 与虚拟列表技术，完美承载了 Zion 百万级组件配置的视图树重任。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心架构一：多叉树的“1D”虚拟化扁平降维&lt;/h2&gt;
&lt;p&gt;要想让一棵拥有上千节点的深层多叉树不卡顿，唯一的解法就是&lt;strong&gt;虚拟列表 (Virtual List)&lt;/strong&gt;。但虚拟列表只能渲染一维数组（1D Array），无法直接理解树的 &lt;code&gt;children&lt;/code&gt; 嵌套结构。&lt;/p&gt;
&lt;p&gt;在 LgdTree 的架构中，我设计了一个非常高效的实时扁平化管道 (&lt;code&gt;flattenTree&lt;/code&gt;)。在每次数据源发生变化或者节点折叠/展开状态改变时，我们会结合 DFS（深度优先遍历）算法，把树形结构扁平化为一个包含 &lt;code&gt;depth&lt;/code&gt; (缩进深度) 属性的一维数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    subgraph OriginalTree [&quot;原始多叉树结构&quot;]
        A[&quot;Root&quot;] --&amp;gt; B[&quot;Container&quot;]
        B --&amp;gt; C[&quot;Button&quot;]
        A --&amp;gt; D[&quot;Text&quot;]
    end
    
    subgraph Flattening [&quot;实时扁平化管道&quot;]
        DFS[&quot;DFS 递归展开&quot;]
        State[&quot;检查展开/折叠状态&quot;]
        DFS --&amp;gt; State
    end
    
    subgraph VirtualArray [&quot;一维渲染数组 (VirtualLine)&quot;]
        Line1[&quot;[depth: 0] Root&quot;]
        Line2[&quot;[depth: 1] Container&quot;]
        Line3[&quot;[depth: 2] Button&quot;]
        Line4[&quot;[depth: 1] Text&quot;]
    end
    
    OriginalTree --&amp;gt; Flattening
    Flattening --&amp;gt; VirtualArray
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心降维逻辑&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/components/LgdTree/utils/index.ts
function flatten(items: TreeData, parentId: string | null = null, depth = 0, result: TreeNode[] = []): TreeNode[] {
  items.forEach((item, index) =&amp;gt; {
    const children = item.children || [];
    const newItem = { ...item, children, parentId, depth, index };
    result.push(newItem);
    if (children.length &amp;gt; 0) {
      flatten(children, item.id, depth + 1, result);
    }
  });
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用生成的 &lt;code&gt;depth&lt;/code&gt;，我们在 React 渲染时通过给每行 &lt;code&gt;VirtualLine&lt;/code&gt; 加上 &lt;code&gt;padding-left: ${depth * 20}px&lt;/code&gt; 的缩进，在视觉上视觉上呈现成了一棵树，但在浏览器的 DOM 树上，它永远只有视窗里可见的那十几行简洁的 &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; 节点。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 核心架构二：跨级拖拽与意图投影 (Projected Insertion)&lt;/h2&gt;
&lt;p&gt;LgdTree 最复杂的逻辑在于其定制的拖拽引擎。传统的排序库（如 &lt;code&gt;react-sortable-hoc&lt;/code&gt;）只能在同层级改变顺序，而我们需要支持&lt;strong&gt;跨层级的嵌套拖拽&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了实现这一点，我在 &lt;code&gt;components/DndMonitor/index.tsx&lt;/code&gt; 中深度定制了 &lt;code&gt;dnd-kit&lt;/code&gt; 的核心钩子。我们不直接操作真实数据，而是引入了 &lt;strong&gt;意图投影 (Projection)&lt;/strong&gt; 的概念。&lt;/p&gt;
&lt;h3&gt;意图推导法则 (The Projection Logic)&lt;/h3&gt;
&lt;p&gt;当用户拖拽一个节点并在某个目标节点上悬浮时，系统必须根据鼠标的 &lt;strong&gt;X 轴偏移量 (OffsetX)&lt;/strong&gt; 和 &lt;strong&gt;Y 轴碰撞 (Collision)&lt;/strong&gt;，推导出用户的三个核心意图：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;上方插入&lt;/strong&gt;：插入为同级前驱节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下方插入&lt;/strong&gt;：插入为同级后继节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部嵌套&lt;/strong&gt;：插入为目标节点的子元素 (Child)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心拦截代码演示&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/src/zed/components/LgdTree/components/DndMonitor/index.tsx
function onDragMove({ delta }: DragMoveEvent) {
  // 利用节流阀实时记录用户的横向拖拽偏移量
  // 横向移动的距离决定了用户是想保持同级 (同 depth)，还是想缩进成为子元素 (depth + 1)
  handleDragMove(delta.x);
}

function onDragOver({ over, active }: DragOverEvent) {
  // 结合当前的 activeId, overId 和 deltaX, 推算出实时投影位置 Projected
  const projected = getProjection(
    items,
    activeId,
    overId,
    dragOffset,   // Y轴偏移
    indentWidth   // 层级缩进单位 (比如 20px)
  );

  // 1. 循环依赖防腐层：一旦查明拖入的是自身的后代节点，立刻终止！
  if (isCircularDependency(activeId, projected.parentId)) {
     return;
  }

  // 2. 派发渲染层，渲染一条带有目标缩进深度的 &quot;蓝色的拖拽落点指示线&quot;
  setProjectedState(projected);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合高频节流 (&lt;code&gt;throttle&lt;/code&gt;)，我们把繁重的坐标解算和防腐校验从 UI 线程解耦，确保拖拽时依然能保持 60 FPS 的流畅体验。&lt;/p&gt;
&lt;h3&gt;微交互容错：防抖传感器与“点击/拖拽”冲突隔离 (Activation Constraints)&lt;/h3&gt;
&lt;p&gt;在真实的复杂树形组件交互中，有一个非常隐蔽的难点：&lt;strong&gt;点击与拖拽的冲突&lt;/strong&gt;。
用户的鼠标往往是不精确的。当他们只想“单击”选中某一行时，手部轻微的抖动会产生 &lt;code&gt;1~2px&lt;/code&gt; 的偏移，导致系统误判为“拖拽开始”，瞬间打断了选中逻辑并闪烁出拖拽占位符。
为了解决这个微交互痛点，我深度定制了 &lt;code&gt;@dnd-kit/core&lt;/code&gt; 的 &lt;code&gt;PointerSensor&lt;/code&gt;（指针传感器）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: {
      distance: 5, // 核心防抖：必须按住并实际移动超过 5 像素，才正式激活拖拽引擎
    },
  })
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅仅是加了这一个 &lt;code&gt;distance: 5&lt;/code&gt; 的激活约束阈值，就完美隔离了“轻微手抖的点击”和“明确意图的拖放”，从非常微小的细节处保障了低代码平台“稳如泰山”的交互质感。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 核心难点突破：领域解耦与数据双向同步 (&lt;code&gt;useNodeDnd&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;在 Zion 的编辑器中，左侧的 &lt;code&gt;LayersTree&lt;/code&gt; (图层树) 和中间的 &lt;code&gt;CanvasPro&lt;/code&gt; (画布) 实际上是对同一份 Schema Meta 数据的两套不同视图的映射。
为了保证拖拽完成后，数据能够安全地同步回 MobX Store 并触发全局重绘，我把具体的落地逻辑抽象成了领域特定的 Hook：&lt;code&gt;useNodeDnd&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;优雅的事件分发机制&lt;/h3&gt;
&lt;p&gt;LgdTree 组件本身是一个“纯视觉与逻辑计算组件”，它不关心具体的业务含义。拖放结束时，它只会向上传递两种原子级的事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onReorder({ src, dst })&lt;/code&gt;：发生在同层级的拖拽排序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onReparent({ dragItem, src, dst })&lt;/code&gt;：发生了跨层级的改变父节点行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而在业务消费侧，&lt;code&gt;useNodeDnd.ts&lt;/code&gt; 将这些泛化的 UI 动作翻译为非常精准的底层 Mutation 事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/src/zed/views/CanvasPro/views/LeftSidebar/views/MetaHierarchy/views/LayersTree/hooks/useNodeDnd.ts
export const useNodeDnd = (): Pick&amp;lt;LgdTreeProps, &apos;onReorder&apos; | &apos;onReparent&apos;&amp;gt; =&amp;gt; {
  const { onMoveMetaChild } = useMetaChildsUpdate();
  const { onUpdateComponentParent } = useUpdateComponentParent();

  // 1. 同级排序事务
  const onReorder = useCallback((params) =&amp;gt; {
    const parentMeta = getParentMeta(dragItem.id);
    // 触发底层画布同级的 Mobx Mutation，并由中间的可视化画布响应刷新
    onMoveMetaChild({
      metaId: parentMeta.id,
      from: params.src.index,
      to: params.dst.index,
    });
  }, [...]);

  // 2. 跨层级嵌套事务
  const onReparent = useCallback((params) =&amp;gt; {
    const { dragItem, dst } = params;
    // 解析树节点 ID 到底层画布 Component ID 的映射
    const dragCompId = decodeMetaId(dragItem.id).componentId;
    const dropParentId = decodeMetaId(dst.parentId).componentId;

    // ...组装新的 Schema 数据拓扑，通知后端和画布重绘
    onUpdateComponentParent({
      componentId: dragCompId,
      newParentId: dropParentId,
      index: dst.index
    });
  }, [...]);

  return { onReorder, onReparent };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种 &lt;strong&gt;UI 抽象 -&amp;gt; 事件抛出 -&amp;gt; 领域 Hook 接管 -&amp;gt; Store 变异更新&lt;/strong&gt; 的标准单向数据流设计，彻底终止了原本视图组件和业务模型之间复杂的耦合。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 攻克难题四：双向联动视角下的自动展开与精准定位 (Auto-Scroll in Virtual Tree)&lt;/h2&gt;
&lt;p&gt;在可视化编辑器中，画布和左侧图层树必须是&lt;strong&gt;双向联动&lt;/strong&gt;的：当用户在画布上点击某个极深层级的子组件时，左侧的图层树需要自动滚动并将该节点高亮显示。&lt;/p&gt;
&lt;p&gt;在普通的树组件中，这只需要调用 &lt;code&gt;element.scrollIntoView()&lt;/code&gt;。但在 &lt;strong&gt;虚拟化树组件 (Virtualized Tree)&lt;/strong&gt; 中，这是一个技术难点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;节点可能根本不存在&lt;/strong&gt;：目标节点可能被折叠在某个父级文件夹内，并没有被渲染在 DOM 上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不知道滚动距离&lt;/strong&gt;：因为树被折叠，目标节点的全局 Index 是未知的，也就无法计算它的 &lt;code&gt;scrollTop&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决这个痛点，我在 &lt;code&gt;useScrollToNode.ts&lt;/code&gt; 中设计了一套非常巧妙的&lt;strong&gt;自动追踪与重绘路由算法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/src/zed/components/LgdTree/hooks/useScrollToNode.ts (精简版)
export const useScrollToNode = (params: UseScrollToNodeParams) =&amp;gt; {
  const { listRef, expandTreeNodes, indentationWidth, itemHeight } = params;

  const onScrollToNode = useCallback(
    (key: string) =&amp;gt; {
      // 1. 业务层首先向上查找目标节点的所有父节点 ID，并更新到 expandedKeys 中
      // 2. 这会触发 flattenTree 重新计算 1D 数组 (expandTreeNodes)
      
      // 3. 我们利用 requestAnimationFrame 等待全新的 1D 数组生成完毕
      requestAnimationFrame(() =&amp;gt; {
        // 获取目标节点在全新 1D 数组中的真实下标
        const idx = expandTreeNodes.findIndex(({ id }) =&amp;gt; id === key);
        if (idx === -1 || !listRef.current) return;

        // 计算目标节点的物理 Y 轴高度和 X 轴横向缩进
        const offsetLeft = (expandTreeNodes[idx].depth + 1) * indentationWidth;
        const scrollTop = idx * itemHeight;

        // 调用 rc-virtual-list 的底层方法，进行像素级坐标跳跃
        listRef.current?.scrollTo({
          top: scrollTop,
          left: offsetLeft,
        });
      });
    },
    [expandTreeNodes, indentationWidth, itemHeight, listRef],
  );

  return { onScrollToNode };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套算法，无论组件隐藏得有多深，系统都能在毫秒间完成**“祖先节点展开 -&amp;gt; 扁平树重算 -&amp;gt; 像素坐标解算 -&amp;gt; 滚动 API 调用”**的完整链路，实现了所见即所得的双向高亮驱动。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 总结：前端重型基建的破局之道&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LgdTree&lt;/strong&gt; 是我在 Zion 无代码平台中核心前端基建项目之一。
它不仅仅是一个长得好看的左侧菜单，它实际上是一个&lt;strong&gt;披着树形外衣的高性能计算引擎&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过引入 &lt;strong&gt;1D 扁平化虚拟渲染&lt;/strong&gt;、&lt;strong&gt;基于 offsetX/offsetY 的投影意图推导算法&lt;/strong&gt;、以及&lt;strong&gt;严格的事务防腐与解耦 Hook 体系&lt;/strong&gt;，它完美解决了大量 DOM 卡顿、交叉拖拽死循环等痛点。正有了如此坚如磐石的底层树形调度组件，Zion 的可视化画布能够支撑起数以千计的复杂业务节点和随心所欲的用户操作。&lt;/p&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 5：CLI 环境割裂与请求参数陷阱</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part5/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part5/</guid><description>记录 NestJS 项目中的两个常见易错点：为什么 TypeORM CLI 运行迁移时总是读不到环境变量？为什么处理 GET 数组参数时还要手动判断是否为数组？以及如何防御 SQL 注入。</description><pubDate>Mon, 09 Feb 2026 12:20:00 GMT</pubDate><content:encoded>&lt;p&gt;本文记录在 NestJS 配合 TypeORM 开发中遇到的两个常见问题：TypeORM CLI 环境导致的环境变量读取问题，以及处理 HTTP GET 请求数组参数时的边界处理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么 TypeORM CLI 运行迁移时总是读不到环境变量？&lt;/h2&gt;
&lt;p&gt;在开发环境中，NestJS 应用能正常连接数据库，因为我们在 &lt;code&gt;AppModule&lt;/code&gt; 中配置了 &lt;code&gt;ConfigModule.forRoot()&lt;/code&gt; 来加载环境变量。&lt;/p&gt;
&lt;p&gt;但在终端执行 &lt;code&gt;npm run typeorm migration:run&lt;/code&gt; 时，可能会报错：&lt;code&gt;Access denied for user &apos;undefined&apos;@&apos;localhost&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在专门提供给 TypeORM CLI 使用的 &lt;code&gt;data-source.ts&lt;/code&gt; 中，必须这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/data-source.ts
import { DataSource, DataSourceOptions } from &quot;typeorm&quot;;
import * as dotenv from &quot;dotenv&quot;;

// ⚠️ 必须先加载环境变量，再导入配置
// 因为 CLI 独立运行，不经过 NestJS 的 ConfigModule
const envFilePath = `.env.${process.env.NODE_ENV || &quot;development&quot;}`;
dotenv.config({ path: envFilePath });
dotenv.config({ path: &quot;.env&quot; });

// 环境变量加载后，再导入配置
import { getDatabaseConfig } from &quot;./config/database.config&quot;;

export default new DataSource({
  ...getDatabaseConfig(),
  synchronize: false, // ⚠️ CLI 永远不应该自动同步
  logging: true,
} as DataSourceOptions);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;环境的割裂：&lt;/strong&gt;
&lt;code&gt;TypeORM CLI&lt;/code&gt; 是一个完全独立的纯 Node.js 脚本环境。执行 CLI 命令时，它不会加载 &lt;code&gt;app.module.ts&lt;/code&gt; 中的模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么要先 &lt;code&gt;dotenv&lt;/code&gt; 再 &lt;code&gt;import&lt;/code&gt; 配置？&lt;/strong&gt;
Node.js 的模块加载是静态的。如果先 &lt;code&gt;import { getDatabaseConfig }&lt;/code&gt;，此时 &lt;code&gt;process.env.DB_PASSWORD&lt;/code&gt; 拿到的是 &lt;code&gt;undefined&lt;/code&gt;。
因此，必须要在提供给 CLI 的入口文件顶部，手动调用 &lt;code&gt;dotenv.config()&lt;/code&gt; 将配置注入到 &lt;code&gt;process.env&lt;/code&gt; 中，然后再 &lt;code&gt;import&lt;/code&gt; 配置文件，确保配置函数正常获取到环境变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么接 GET 数组参数时还要手动 Array.isArray 判断？&lt;/h2&gt;
&lt;p&gt;在实现“行政区划回显”（根据传入的几个 &lt;code&gt;code&lt;/code&gt; 查询对应的省市区名字）时，Controller 中的参数接收逻辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/region/region.controller.ts
@Get()
findAll(@Query(&apos;codes&apos;) codes?: string | string[]) {
  if (codes) {
    // 抹平单参数和多参数的差异
    const codesArray = Array.isArray(codes) ? codes : [codes];
    return this.regionService.getRegionsByCodes(codesArray);
  }
  return this.regionService.getProvinces();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service 中的查询逻辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/region/region.service.ts
async getRegionsByCodes(codes: string[]) {
  if (!codes || codes.length === 0) return [];

  // TypeORM 的 In 查询可以直接用 array
  const qb = this.regionRepository
    .createQueryBuilder(&apos;region&apos;)
    .where(&apos;region.code IN (:...codes)&apos;, { codes });

  return qb.getMany();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么需要判断数组？&lt;/strong&gt;
在 HTTP 协议中：
&lt;ul&gt;
&lt;li&gt;如果请求 &lt;code&gt;/regions?codes=110000&lt;/code&gt;，后端拿到的 &lt;code&gt;codes&lt;/code&gt; 是&lt;strong&gt;字符串&lt;/strong&gt; (&lt;code&gt;&quot;110000&quot;&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;如果请求 &lt;code&gt;/regions?codes=110000&amp;amp;codes=120000&lt;/code&gt;，后端拿到的 &lt;code&gt;codes&lt;/code&gt; 是&lt;strong&gt;数组&lt;/strong&gt; (&lt;code&gt;[&quot;110000&quot;, &quot;120000&quot;]&lt;/code&gt;)。
如果后端直接调用 &lt;code&gt;codes.map()&lt;/code&gt; 或传给 TypeORM，遇到单一参数时会报 &lt;code&gt;.map is not a function&lt;/code&gt; 错误。&lt;code&gt;Array.isArray(codes) ? codes : [codes]&lt;/code&gt; 统一了边界情况，将其统一转为了数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;IN (:...codes)&lt;/code&gt; 的作用与防注入：&lt;/strong&gt;
当把数组传入 SQL 的 &lt;code&gt;IN&lt;/code&gt; 语句时，原生字符串拼接易引发 SQL 注入。TypeORM 的 &lt;code&gt;:...codes&lt;/code&gt; 展开语法会在底层进行参数化绑定（Parameterized Queries），避免了 SQL 注入的风险。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 4：TypeORM 避坑与配置安全</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part4/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part4/</guid><description>记录 NestJS 项目中的 TypeORM 查询踩坑经验与环境配置安全：多对多级联查询数据的过滤丢失问题、密码字段的按需查询（select: false），以及通过 Joi 实现 Fail-Fast 的启动级环境变量校验。</description><pubDate>Sun, 01 Feb 2026 12:15:00 GMT</pubDate><content:encoded>&lt;p&gt;本文记录在 NestJS 配合 TypeORM 开发中遇到的数据库查询坑点，以及在应用启动阶段如何保障环境配置的安全与健壮性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;TypeORM 多对多查询过滤为什么会丢数据？&lt;/h2&gt;
&lt;p&gt;在获取用户列表并支持“按角色过滤”时，初学者很容易写出这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 错误写法 ❌
const queryBuilder = this.userRepository
  .createQueryBuilder(&quot;user&quot;)
  .leftJoinAndSelect(&quot;user.roles&quot;, &quot;roles&quot;)
  .where(&quot;roles.id = :roleId&quot;, { roleId: role });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述写法隐藏了一个极大的坑：假设一个用户同时拥有 &lt;code&gt;Admin&lt;/code&gt; 和 &lt;code&gt;Developer&lt;/code&gt; 两个角色，当你传入 &lt;code&gt;Developer&lt;/code&gt; 的 roleId 进行过滤时，查出来的这个用户，他的 &lt;code&gt;roles&lt;/code&gt; 数组里&lt;strong&gt;只剩下了 Developer 角色&lt;/strong&gt;，而 Admin 角色被过滤掉了！这会导致返回给前端的用户数据不完整。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们在 &lt;code&gt;UserService&lt;/code&gt; 中引入了 &lt;strong&gt;“innerJoin 替身”&lt;/strong&gt; 技巧：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/user.service.ts
const queryBuilder = this.userRepository
  .createQueryBuilder(&quot;user&quot;)
  .leftJoinAndSelect(&quot;user.roles&quot;, &quot;roles&quot;); // 负责把用户的完整角色数据带出来

if (role) {
  // ⚠️ 不要使用 .andWhere(&apos;roles.id = :roleId&apos;)！
  // 正确做法：新建一个无副作用的 innerJoin 替身 &apos;roleFilter&apos; 专门用来筛选主表，而不影响 SELECT 的 &apos;roles&apos; 数据。
  queryBuilder.innerJoin(
    &quot;user.roles&quot;,
    &quot;roleFilter&quot;,
    &quot;roleFilter.id = :roleId&quot;,
    { roleId: role },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么会丢数据？&lt;/strong&gt;
&lt;code&gt;leftJoinAndSelect&lt;/code&gt; 在底层不仅做了连表，还负责构造返回的实体对象树。当你对其别名（&lt;code&gt;roles&lt;/code&gt;）加上 &lt;code&gt;where&lt;/code&gt; 条件时，TypeORM 构造对象树时就会直接丢弃不符合条件的关联数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;替身方案的优势：&lt;/strong&gt;
我们新建了一个纯用于连接筛选的别名 &lt;code&gt;roleFilter&lt;/code&gt;，用它来进行内连接（&lt;code&gt;innerJoin&lt;/code&gt;）来决定主表 &lt;code&gt;user&lt;/code&gt; 哪条记录该留下，而原本用于 Select 返回的别名 &lt;code&gt;roles&lt;/code&gt; 不受任何影响，从而保证了用户拥有多少个角色，返回的数据里就有多少个角色。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么在查询用户时还要单独 addSelect 密码？&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;UserService&lt;/code&gt; 的登录查询方法中，有一段特殊的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/user.service.ts
findByUsername(username: string, selectPassword = false) {
  const qb = this.userRepository
    .createQueryBuilder(&apos;user&apos;)
    .leftJoinAndSelect(&apos;user.roles&apos;, &apos;roles&apos;)
    .where(&apos;user.username = :username&apos;, { username });

  if (selectPassword) {
    qb.addSelect(&apos;user.password&apos;);
  }

  return qb.getOne();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么查密码要这么费劲？&lt;/strong&gt;
在定义 &lt;code&gt;User&lt;/code&gt; 实体类时，我们通常会将密码字段配置为 &lt;code&gt;@Column({ select: false })&lt;/code&gt;。这意味着在普通的查询（如获取用户列表、查询详情）中，数据库引擎&lt;strong&gt;绝对不会&lt;/strong&gt;把密码字段 select 出来。这从根源上杜绝了因为开发人员疏忽导致密码被意外序列化并返回给前端的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需取出的安全策略：&lt;/strong&gt;
只有在登录校验或修改密码这种明确需要读取密码哈希值的高危操作中，我们才显式地传入 &lt;code&gt;selectPassword = true&lt;/code&gt;，并通过 &lt;code&gt;addSelect(&apos;user.password&apos;)&lt;/code&gt; 将其临时从数据库取出。这是一种非常极致且有效的安全兜底设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么环境变量不能直接用 process.env 读取？&lt;/h2&gt;
&lt;p&gt;在启动 NestJS 应用时，我们引入了 &lt;code&gt;@nestjs/config&lt;/code&gt; 并结合 &lt;code&gt;Joi&lt;/code&gt; 进行了强校验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/app.module.ts
ConfigModule.forRoot({
  isGlobal: true,
  validationSchema: Joi.object({
    NODE_ENV: Joi.valid(&quot;development&quot;, &quot;production&quot;).default(&quot;development&quot;),
    DB_PORT: Joi.number().default(3306),
    DB_USERNAME: Joi.string().required(),
    DB_PASSWORD: Joi.string().required(),
    DB_NAME: Joi.string().required(),
    JWT_SECRET: Joi.string().required(),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果不用 Joi 校验会怎样？&lt;/strong&gt;
如果新部署的一台服务器在 &lt;code&gt;.env&lt;/code&gt; 文件中漏配了 &lt;code&gt;JWT_SECRET&lt;/code&gt;，而代码里又直接通过 &lt;code&gt;process.env.JWT_SECRET&lt;/code&gt; 读取。应用在启动时&lt;strong&gt;不会报错&lt;/strong&gt;，依然会正常提供服务。直到有用户尝试登录，系统调用签名算法时才会突然崩溃。这给运维排查带来了极大的心智负担。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fail-Fast (快速失败) 架构思想：&lt;/strong&gt;
配置了 &lt;code&gt;Joi&lt;/code&gt; 校验后，一旦缺失核心的环境变量，NestJS 在服务&lt;strong&gt;启动的瞬间&lt;/strong&gt;就会直接报错并阻断运行，清晰地在控制台告诉你少了哪些配置项。这种“有错及早报错”的设计，避免了带着隐患上线的“毒应用”。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 3：业务实践细节</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part3/</guid><description>记录 NestJS 项目中的具体业务实践：密码加密存储、操作日志的自动化记录以及前端传参的格式清洗。</description><pubDate>Wed, 28 Jan 2026 12:10:00 GMT</pubDate><content:encoded>&lt;p&gt;本文探讨在具体业务开发中，如何通过 NestJS 的机制来处理密码加密、自动化操作日志采集以及数据格式清洗。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么绝对不能明文存储密码？&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;AuthService&lt;/code&gt; 中，我们使用 &lt;code&gt;bcryptjs&lt;/code&gt; 单向散列算法处理密码。不论是用户登录验证还是密码修改，都不会接触到密码的明文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/auth/auth.service.ts
@Injectable()
export class AuthService {
  // 验证密码
  async validateUser(username: string, password: string) {
    const user = await this.userService.findByUsername(username, true);
    // ...
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) throw new BadRequestException(&quot;用户名或密码错误&quot;);
    return user;
  }

  // 修改密码
  async updatePassword(userId: number, dto: UpdatePasswordDto) {
    // ...
    const salt = await bcrypt.genSalt(10);
    const hashedNewPassword = await bcrypt.hash(dto.newPassword, salt);
    await this.userService.updatePasswordRaw(userId, hashedNewPassword);
    return { success: true };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不明文存储密码？&lt;/strong&gt;
明文存储密码一旦发生数据库泄露，会导致用户账号信息直接暴露（甚至引发跨站点的“撞库”安全事故）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不使用普通的 MD5 或 SHA-256？&lt;/strong&gt;
MD5 的计算速度较快，黑客容易通过预先计算的彩虹表哈希字典反查出原密码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bcrypt 的优势：&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内置加盐 (Salt)&lt;/strong&gt;：&lt;code&gt;genSalt(10)&lt;/code&gt; 生成随机字符串混入密码进行哈希，保证相同明文密码生成的密文也截然不同，抵御查表攻击。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可调复杂度&lt;/strong&gt;：通过设置成本因子，增加计算的 CPU 消耗，大幅提高暴力破解的时间成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防时序攻击&lt;/strong&gt;：&lt;code&gt;bcrypt.compare&lt;/code&gt; 的比较时间是恒定的，防止通过接口响应时长推测密码。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;如何在不侵入业务的情况下记录操作日志？&lt;/h2&gt;
&lt;p&gt;在后台系统中，通常需要审计操作日志。如果直接在每个 Controller 里添加 &lt;code&gt;this.logsService.create(...)&lt;/code&gt; 会导致代码深度耦合。为此，我们使用了 &lt;code&gt;OperationLogInterceptor&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/interceptors/operation-log.interceptor.ts
@Injectable()
export class OperationLogInterceptor implements NestInterceptor {
  // ...
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;unknown&amp;gt; {
    const metadata = this.reflector.get&amp;lt;OperationLogMetadata&amp;gt;(
      OPERATION_LOG_KEY,
      context.getHandler(),
    );
    if (!metadata) return next.handle();

    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();

    // 简单脱敏
    const bodyCopy = { ...request.body };
    if (&quot;password&quot; in bodyCopy) bodyCopy.password = &quot;***&quot;;
    const data = JSON.stringify(bodyCopy);

    return next.handle().pipe(
      tap(() =&amp;gt; {
        // 请求成功：记录 HTTP 200 及相关信息
        const statusCode = response.statusCode || HttpStatus.OK;
        this.saveLog({
          path: request.path,
          method: request.method,
          data,
          result: statusCode,
          userId: request.user?.id,
          description: metadata.description,
        });
      }),
      catchError((error: unknown) =&amp;gt; {
        // 请求失败：记录实际的错误状态码并抛出异常
        const statusCode =
          error instanceof HttpException
            ? error.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
        this.saveLog({
          path: request.path,
          method: request.method,
          data,
          result: statusCode,
          userId: request.user?.id,
          description: metadata.description,
        });
        return throwError(() =&amp;gt; error);
      }),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设计思路：&lt;/strong&gt;
采用非侵入式设计，只要在接口上贴 &lt;code&gt;@OperationLog(&apos;创建新用户&apos;)&lt;/code&gt;，拦截器就会自动提取元数据并打日志。业务代码完全无感。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tap&lt;/code&gt; 和 &lt;code&gt;catchError&lt;/code&gt; 的作用：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tap&lt;/code&gt; (旁路执行)：在不改变原本返回数据流的情况下，记录数据库日志。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;catchError&lt;/code&gt; (错误捕获)：哪怕业务层抛出异常，拦截器也能记录失败日志，然后通过 &lt;code&gt;throwError&lt;/code&gt; 重新抛出，不阻断全局异常过滤器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么要在 DTO 里做数据清洗而不是 Service 层？&lt;/h2&gt;
&lt;p&gt;用户在前端表单输入时，经常会多敲空格（如 &lt;code&gt;&quot; admin &quot;&lt;/code&gt;）。这会严重影响后续的匹配与查询。我们在 DTO 中使用 &lt;code&gt;class-transformer&lt;/code&gt; 的 &lt;code&gt;@Transform&lt;/code&gt; 来处理这类问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/utils/transformer.util.ts
import { TransformFnParams } from &quot;class-transformer&quot;;

export const trimString = ({ value }: TransformFnParams): unknown =&amp;gt; {
  return typeof value === &quot;string&quot; ? value.trim() : value;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在入参校验 DTO 中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/dto/create-user.dto.ts
export class CreateUserDto implements RegisterRequest {
  @IsString()
  @IsNotEmpty()
  @Transform(trimString)
  username: string;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不在 Service 层进行 trim 操作？&lt;/strong&gt;
在业务逻辑层（如 &lt;code&gt;UserService&lt;/code&gt;）手动对特定字段进行 &lt;code&gt;.trim()&lt;/code&gt; 处理，不仅代码啰嗦容易遗漏，还会使核心业务代码被“清洗脏数据”这种边缘逻辑污染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计思路：&lt;/strong&gt;
在数据进入 Controller 之前（即框架的最外层边界），利用管道与 DTO 转换直接完成数据清洗。流入系统内部的数据将永远是干净的。这完美契合了系统架构设计中的&lt;strong&gt;防腐层 (Anti-Corruption Layer)&lt;/strong&gt; 思想。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 2：工程化实践</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part2/</guid><description>记录 NestJS 项目中的工程化实践：全局响应拦截器、异常过滤器链、ValidationPipe 的配置以及生产环境日志框架 nestjs-pino 的应用。</description><pubDate>Sat, 24 Jan 2026 12:05:00 GMT</pubDate><content:encoded>&lt;p&gt;本文记录在 NestJS 项目中应用的四个工程化实践，探讨其配置原因及替代方案。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么要用响应拦截器统一 API 格式？&lt;/h2&gt;
&lt;p&gt;为了保持前后端接口格式的一致性（如 &lt;code&gt;{ code: 0, message: &apos;success&apos;, data: ... }&lt;/code&gt;），通常需要统一封装返回结果。下面是我们项目中的拦截器实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/interceptors/transform.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from &quot;@nestjs/common&quot;;
import { Observable } from &quot;rxjs&quot;;
import { map } from &quot;rxjs/operators&quot;;

export interface Response&amp;lt;T&amp;gt; {
  code: number;
  message: string;
  data: T | null;
}

@Injectable()
export class TransformInterceptor&amp;lt;T&amp;gt; implements NestInterceptor&amp;lt;
  T,
  Response&amp;lt;T&amp;gt;
&amp;gt; {
  intercept(
    _context: ExecutionContext,
    next: CallHandler,
  ): Observable&amp;lt;Response&amp;lt;T&amp;gt;&amp;gt; {
    return next.handle().pipe(
      map((data: unknown) =&amp;gt; ({
        code: 0,
        message: &quot;success&quot;,
        // 边界处理：将 undefined 转换为 null，保证前端 JSON 序列化的稳定性
        data: data === undefined ? null : (data as T),
      })),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不在 Controller 里手动拼装？&lt;/strong&gt;
手动组装会导致大量模板代码，违反 DRY 原则。且如果结构变更，修改成本较高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么使用 RxJS 的 Observable 流？&lt;/strong&gt;
拦截器包装了路由处理器的执行流，借助 &lt;code&gt;map&lt;/code&gt; 操作符在数据返回给前端前进行拦截与格式化。Controller 只需专注于返回业务数据结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;异常过滤器是怎么知道该进哪一个的？&lt;/h2&gt;
&lt;p&gt;服务端业务通常配置一组层级化的异常过滤器来处理报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/main.ts
app.useGlobalFilters(
  new AllExceptionFilter(logger, httpAdapterHost), // 范围最大，兜底处理
  new TypeormExceptionFilter(logger), // 针对数据库报错拦截
  new EntityNotFoundExceptionFilter(logger), // 针对特定实体的拦截
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以数据库异常拦截器为例，我们将底层的 MySQL 错误码转译为了友好的 HTTP 提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/filters/typeorm-exception.filter.ts
@Catch(QueryFailedError)
export class TypeormExceptionFilter implements ExceptionFilter {
  // ...
  catch(exception: QueryFailedError, host: ArgumentsHost) {
    const err = exception.driverError as TypeORMError;
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = &quot;Internal database error&quot;;

    if (err) {
      switch (err.code) {
        // [1062] 唯一索引冲突 (Unique Constraint)
        case &quot;ER_DUP_ENTRY&quot;:
          status = HttpStatus.CONFLICT;
          message = &quot;数据已经存在，请勿重复创建&quot;;
          break;
        // [1452] 外键约束失败 (Foreign Key Constraint)
        case &quot;ER_NO_REFERENCED_ROW_2&quot;:
          status = HttpStatus.BAD_REQUEST;
          message = &quot;关联的数据不存在，请检查提交的参数 (外键约束失败)&quot;;
          break;
      }
    }
    // ...发送异常响应
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么要设置多个过滤器？&lt;/strong&gt;
全局过滤器的匹配从后往前执行。抛出数据库错误时，会优先被 &lt;code&gt;TypeormExceptionFilter&lt;/code&gt; 捕获。如果是特定过滤器无法处理的普通 Error，最终会被 &lt;code&gt;AllExceptionFilter&lt;/code&gt; 兜底，返回统一的 HTTP 500 错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不在 Service 里全局使用 &lt;code&gt;try-catch&lt;/code&gt;？&lt;/strong&gt;
在业务层过度使用 &lt;code&gt;try-catch&lt;/code&gt; 会导致代码臃肿。将错误向上抛出，由全局过滤器统一处理，符合错误统一管理的规范。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;ValidationPipe 里的 whitelist 是干什么用的？&lt;/h2&gt;
&lt;p&gt;通过在应用入口配置全局验证管道处理请求参数验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    transform: true,
    transformOptions: { enableImplicitConversion: true },
  }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开启 &lt;code&gt;whitelist: true&lt;/code&gt; 的作用：&lt;/strong&gt;
如果没有白名单，前端提交 DTO 类中未定义的字段时，这些额外字段可能会被更新至数据库，引发批量赋值漏洞 (Mass Assignment)。&lt;code&gt;whitelist&lt;/code&gt; 会自动剔除所有未声明的非法字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;enableImplicitConversion&lt;/code&gt; 的作用：&lt;/strong&gt;
HTTP 协议中的 Query 字符串和 Path 参数通常被解析为字符串。开启隐式转换后，NestJS 会根据 DTO 的类型定义（如 &lt;code&gt;number&lt;/code&gt;），自动完成类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么不用原生 Logger 要换成 Pino？&lt;/h2&gt;
&lt;p&gt;在生产环境中，项目中使用了 &lt;code&gt;nestjs-pino&lt;/code&gt; 替代原生的 &lt;code&gt;ConsoleLogger&lt;/code&gt;。我们在 &lt;code&gt;LogsModule&lt;/code&gt; 中进行了详细配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/logs/logs.module.ts
const pinoLogger = LoggerModule.forRootAsync({
  useFactory: () =&amp;gt; {
    const isDev = process.env.NODE_ENV === &quot;development&quot;;
    return {
      pinoHttp: {
        redact: [&quot;req.headers.authorization&quot;, &quot;req.body.password&quot;],
        transport: {
          targets: isDev
            ? [
                {
                  target: &quot;pino-pretty&quot;,
                  level: &quot;info&quot;,
                  options: { colorize: true },
                },
              ]
            : [
                {
                  target: &quot;pino-roll&quot;,
                  level: &quot;info&quot;,
                  options: {
                    file: join(process.cwd(), &quot;logs&quot;, &quot;application.log&quot;),
                    frequency: &quot;daily&quot;,
                    size: &quot;10m&quot;,
                    mkdir: true,
                  },
                },
              ],
        },
      },
    };
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么替换原生 Logger？&lt;/strong&gt;
原生 &lt;code&gt;console.log&lt;/code&gt; 的同步特性在高并发时可能会影响性能。彩色文本日志也不利于使用正则或日志系统解析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pino 的优势：&lt;/strong&gt;
采用异步非阻塞性能更好；默认输出 JSON 结构化日志，便于直接接入 ELK。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脱敏与切割：&lt;/strong&gt;
&lt;code&gt;redact&lt;/code&gt; 会在落盘前自动将敏感字段替换为 &lt;code&gt;[Redacted]&lt;/code&gt;，防止密码或 Token 在日志中明文留存。&lt;code&gt;pino-roll&lt;/code&gt; 按天或大小进行切割轮转，避免单个日志文件过大。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NestJS 学习记录 Part 1：核心原理解析</title><link>https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nestjs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95-part1/</guid><description>记录在学习 NestJS 时遇到的核心概念解析：身份验证流、装饰器执行顺序、跨平台执行上下文，以及元数据反射机制。探讨其设计原因及替代方案。</description><pubDate>Thu, 22 Jan 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么能在 request 中拿到 user 呢？&lt;/h2&gt;
&lt;p&gt;在实现 &lt;code&gt;RolesGuard&lt;/code&gt; 时，常常需要在 &lt;code&gt;request&lt;/code&gt; 对象中获取 &lt;code&gt;user&lt;/code&gt; 对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/auth/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context
      .switchToHttp()
      .getRequest&amp;lt;{ user?: { roles?: string[] } }&amp;gt;();
    const user = request.user;

    // 省略后续鉴权逻辑...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这实际上是由 NestJS 和 Passport.js 构筑的身份验证流水线完成的。一切的源头在 &lt;code&gt;JwtStrategy&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/auth/strategies/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get&amp;lt;string&amp;gt;(&quot;JWT_SECRET&quot;)!,
    });
  }

  /**
   * 解析 JWT payload，返回值会挂载到 req.user
   */
  validate(payload: JwtPayload) {
    return {
      id: payload.sub,
      username: payload.username,
      roles: payload.roles || [],
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拦截请求&lt;/strong&gt;：&lt;code&gt;JwtAuthGuard&lt;/code&gt; 率先拦截请求，提取 &lt;code&gt;Authorization&lt;/code&gt; 头的 Token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证与解析&lt;/strong&gt;：底层触发 &lt;code&gt;JwtStrategy&lt;/code&gt; 的 &lt;code&gt;validate&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂载上下文&lt;/strong&gt;：Passport 验证成功后，自动将 &lt;code&gt;validate&lt;/code&gt; 返回的对象赋值给 &lt;code&gt;request.user&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限校验&lt;/strong&gt;：后续的 &lt;code&gt;RolesGuard&lt;/code&gt; 从 &lt;code&gt;request.user&lt;/code&gt; 提取身份信息进行鉴权。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么这么做？&lt;/strong&gt;
基于“关注点分离”原则。身份验证（解析 Token）和授权控制（校验权限）分离。将解析 Token 抽离到 Strategy 中，&lt;code&gt;Guard&lt;/code&gt; 保持轻量和复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果在 Controller 里直接解析 Token 会怎样？&lt;/strong&gt;
会导致代码冗余。每个需要鉴权的接口都要写解码 Token 的逻辑，业务与鉴权强耦合，后续重构成本高。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;装饰器到底是按照什么顺序执行的？&lt;/h2&gt;
&lt;p&gt;控制器上经常会有多个装饰器，比如我的项目中是这样写的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/user/user.controller.ts
@ApiTags(&quot;用户管理 (Admin)&quot;) // 5. 最后执行
@ApiBearerAuth() // 4. 第四个执行
@UseGuards(JwtAuthGuard, RolesGuard) // 3. 第三个执行
@Roles(RoleEnum.ADMIN) // 2. 第二个执行
@Controller(&quot;user&quot;) // 1. 最先执行
export class UserController {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码加载阶段&lt;/strong&gt;遵循 TypeScript 规范，采用洋葱模型&lt;strong&gt;从下往上&lt;/strong&gt;执行。此时，装饰器并不执行业务逻辑，仅调用 &lt;code&gt;Reflect.defineMetadata&lt;/code&gt; 添加元数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时阶段&lt;/strong&gt;由 NestJS 框架接管请求的生命周期。比如 &lt;code&gt;@UseGuards(JwtAuthGuard, RolesGuard)&lt;/code&gt; 内部是&lt;strong&gt;从左到右&lt;/strong&gt;依次执行，因为存在依赖关系（先登录，后鉴权）。像 &lt;code&gt;@ApiTags&lt;/code&gt; 仅在应用启动扫描 Swagger 时生效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这种设计的好处&lt;/strong&gt;是声明式编程。开发者通过装饰器声明接口属性和权限，而非编写大量的 &lt;code&gt;if-else&lt;/code&gt; 判断逻辑，提高了代码的直观性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;switchToHttp() 到底是干嘛用的？&lt;/h2&gt;
&lt;p&gt;在编写 Guard 或 Interceptor 时，NestJS 提供的是 &lt;code&gt;context: ExecutionContext&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const request = context.switchToHttp().getRequest();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么这么设计？&lt;/strong&gt;
NestJS 是一个与传输层无关的框架。同一套业务代码可以支撑 HTTP、WebSockets、微服务 (TCP/gRPC) 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么要 &lt;code&gt;switchToHttp()&lt;/code&gt;？&lt;/strong&gt;
NestJS 通过 &lt;code&gt;ExecutionContext&lt;/code&gt; 将底层协议统一。调用 &lt;code&gt;switchToHttp()&lt;/code&gt; 明确获取 HTTP 上下文。如果增加 WebSocket 模块，只需改为 &lt;code&gt;context.switchToWs()&lt;/code&gt;，即可复用守卫逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免直接注入 &lt;code&gt;@Req()&lt;/code&gt;&lt;/strong&gt;：
大量依赖 &lt;code&gt;express&lt;/code&gt; 的原生请求对象，会导致代码和 HTTP 协议深度绑定，降低后续向微服务架构演进的灵活性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;getAllAndOverride 和 getAllAndMerge 有啥区别？&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;RolesGuard&lt;/code&gt; 中，我们需要获取当前路由需要的角色信息。当 Controller 类和具体的 Route 方法都加了 &lt;code&gt;@Roles&lt;/code&gt; 装饰器时，该听谁的？&lt;/p&gt;
&lt;h3&gt;思考与对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;getAllAndOverride&lt;/code&gt; (覆盖优先)&lt;/strong&gt;：
我们在项目中使用的是这个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/server/src/auth/guards/roles.guard.ts
const requiredRoles = this.reflector.getAllAndOverride&amp;lt;RoleEnum[]&amp;gt;(
  ROLES_KEY,
  [context.getHandler(), context.getClass()],
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：局部特例。例如整个 &lt;code&gt;UserController&lt;/code&gt; 默认需要 &lt;code&gt;ADMIN&lt;/code&gt;，但某个特定接口允许 &lt;code&gt;USER&lt;/code&gt; 访问。方法级配置覆盖类级配置（因为 &lt;code&gt;getHandler()&lt;/code&gt; 传在前面）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;getAllAndMerge&lt;/code&gt; (合并累加)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const roles = this.reflector.getAllAndMerge&amp;lt;RoleEnum[]&amp;gt;(&quot;roles&quot;, [
  context.getHandler(),
  context.getClass(),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：多重限制叠加。整个模块要求 &lt;code&gt;ADMIN&lt;/code&gt;，而某个接口贴了 &lt;code&gt;SUPER_ADMIN&lt;/code&gt;，合并后要求同时具备 &lt;code&gt;[&apos;ADMIN&apos;, &apos;SUPER_ADMIN&apos;]&lt;/code&gt;，全部满足才能放行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Astro 与 Islands 架构原理解析</title><link>https://nollieleo.github.io/posts/astro-islands-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/astro-islands-architecture/</guid><description>探讨内容驱动型网站的性能瓶颈，理解 Astro 是如何通过局部水合 (Partial Hydration) 实现极简负载的。</description><pubDate>Wed, 05 Mar 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在内容驱动型网站（如博客、文档、电商）中，传统的单页应用 (SPA) 架构面临严重的性能挑战。SPA 要求客户端下载完整的框架运行时并执行全量水合 (Hydration)，这对于大部分是静态内容的页面来说是极大的资源消耗。Astro 通过 Islands 架构改变了这一现状。&lt;/p&gt;
&lt;h2&gt;1. 核心理念：Zero-JS By Default&lt;/h2&gt;
&lt;p&gt;Astro 默认在服务端将所有组件渲染为纯 HTML。除非开发者显式指定，否则不会向客户端发送任何 JavaScript。这种模式极大地提升了首屏加载速度，并优化了 SEO 表现。&lt;/p&gt;
&lt;h2&gt;2. Islands 架构与局部水合&lt;/h2&gt;
&lt;p&gt;Islands 架构（群岛架构）允许在静态的 HTML 页面中嵌入独立的交互式组件。这些组件被称为“岛屿”，它们之间相互独立，且仅在需要时才进行水合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph Browser_DOM
        Static_Header[Static HTML Header]
        Island_A[React Search Bar - Hydrated]
        Static_Content[Static HTML Content]
        Island_B[Vue Image Gallery - Hydrated]
        Static_Footer[Static HTML Footer]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.1 客户端指令 (Client Directives)&lt;/h3&gt;
&lt;p&gt;Astro 通过指令精准控制每个岛屿的水合时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client:load&lt;/code&gt;：立即加载并水合 JS。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client:idle&lt;/code&gt;：在浏览器空闲时加载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client:visible&lt;/code&gt;：仅当组件进入视口时才加载（利用 Intersection Observer）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client:only&lt;/code&gt;：跳过服务端渲染，仅在客户端运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;---
// index.astro
import StaticContent from &apos;../components/StaticContent.astro&apos;;
import InteractiveChart from &apos;../components/InteractiveChart.jsx&apos;;
---

&amp;lt;StaticContent /&amp;gt; &amp;lt;!-- 渲染为纯 HTML --&amp;gt;

&amp;lt;!-- 仅当用户滚动到图表位置时，才下载 React 运行时和组件逻辑 --&amp;gt;
&amp;lt;InteractiveChart client:visible /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 技术细节：DOM 注入与通信&lt;/h2&gt;
&lt;p&gt;Astro 在处理岛屿时，会在 HTML 中插入特殊的自定义元素标记（如 &lt;code&gt;&amp;lt;astro-island&amp;gt;&lt;/code&gt;）。这些标记包含了组件的 Props 数据和水合指令。&lt;/p&gt;
&lt;h3&gt;3.1 局部水合的实现&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;client:visible&lt;/code&gt; 触发时，Astro 的轻量级运行时会执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动态导入&lt;/strong&gt;：通过 &lt;code&gt;import()&lt;/code&gt; 加载对应的框架运行时（如 React）和组件代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据恢复&lt;/strong&gt;：从 HTML 标记中解析出序列化后的 Props。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂载&lt;/strong&gt;：调用框架的 &lt;code&gt;hydrate&lt;/code&gt; 方法，将交互逻辑绑定到已有的 DOM 节点上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种方式避免了 SPA 中常见的“先清空再重新渲染”的闪烁问题，同时也减少了主线程的阻塞时间。&lt;/p&gt;
&lt;h2&gt;3. 业务踩坑：多框架混用下的跨岛屿通信&lt;/h2&gt;
&lt;p&gt;Islands 架构有一个天生的硬伤：&lt;strong&gt;既然每个“岛屿”都是彼此独立的水合沙箱，那它们之间怎么通信？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在传统的 SPA 中，如果顶部导航栏有一个“购物车图标”，底部有一个“加入购物车”按钮，我们通常用一个外层包裹的 &lt;code&gt;&amp;lt;Provider&amp;gt;&lt;/code&gt; 或者全局的 Redux Store 来同步状态。
但在 Astro 中，包裹在它们外面的全都是冰冷的静态 HTML（Zero JS）。此时，如果“购物车图标”是用 Vue 写的，“加入购物车”按钮是用 React 写的，它们该如何对话？&lt;/p&gt;
&lt;h3&gt;3.1 官方推荐方案：Nano Stores&lt;/h3&gt;
&lt;p&gt;Astro 官方推荐使用 &lt;strong&gt;Nano Stores&lt;/strong&gt; 来解决跨岛屿状态共享。这是一个极其轻量级（不到 1KB）的状态库，专为去中心化的原子化状态设计，并且原生支持 React、Vue、Svelte、Solid 等各种框架。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// store.ts (公共状态文件)
import { atom } from &apos;nanostores&apos;;

export const cartCount = atom(0);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ReactButton.tsx
import { useStore } from &apos;@nanostores/react&apos;;
import { cartCount } from &apos;../store&apos;;

export default function ReactButton() {
  return &amp;lt;button onClick={() =&amp;gt; cartCount.set(cartCount.get() + 1)}&amp;gt;加购&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- VueCart.vue --&amp;gt;
&amp;lt;script setup&amp;gt;
import { useStore } from &apos;@nanostores/vue&apos;;
import { cartCount } from &apos;../store&apos;;

const count = useStore(cartCount);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;cart&quot;&amp;gt;🛒 {{ count }}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 底层通信机制：DOM 事件代理&lt;/h3&gt;
&lt;p&gt;如果你不想引入外部状态库，最原生的做法是利用&lt;strong&gt;浏览器原生的 CustomEvent&lt;/strong&gt;。由于所有的岛屿最终都挂载在同一个 &lt;code&gt;window&lt;/code&gt; 对象下，基于 DOM 的发布订阅模式是极其稳健的后备方案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// React 端发送
window.dispatchEvent(new CustomEvent(&apos;cart-update&apos;, { detail: { count: 1 } }));

// Vue 端监听
window.addEventListener(&apos;cart-update&apos;, (e) =&amp;gt; {
  count.value += e.detail.count;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;踩坑提醒&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;序列化丢失&lt;/strong&gt;：由于 Astro 在构建时会静态序列化 Props，如果你向一个组件传递了一个函数（如 &lt;code&gt;onClick&lt;/code&gt;）或者复杂的类的实例（如 &lt;code&gt;new Date()&lt;/code&gt;），它在客户端水合时会报错或者变成字符串。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;水合时机的不一致性&lt;/strong&gt;：如果一个组件是 &lt;code&gt;client:load&lt;/code&gt;（立刻加载），另一个组件是 &lt;code&gt;client:visible&lt;/code&gt;（滚到才加载）。当事件发出时，接收方的 JS 可能根本还没下载完！所以对于跨岛通信，要么确保双方的水合指令一致，要么使用 Nano Stores 这种自带持久化或懒加载订阅机制的库。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 架构优势与边界&lt;/h2&gt;
&lt;p&gt;Astro 的核心优势在于其框架不可知 (Framework Agnostic) 的特性。你可以在同一个页面中混合使用 React、Vue、Svelte 等现代框架。&lt;/p&gt;
&lt;p&gt;Astro 的核心优势在于其框架不可知 (Framework Agnostic) 的特性。你可以在同一个页面中混合使用 React、Vue、Svelte 等现代框架。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;极低的 TTI (Time to Interactive)&lt;/strong&gt;：由于减少了 JS 执行量，页面能更快响应用户操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发体验&lt;/strong&gt;：类 HTML 的 &lt;code&gt;.astro&lt;/code&gt; 语法非常直观，同时支持现代前端的所有工程化能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需加载&lt;/strong&gt;：只为必要的交互加载 JS。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于以内容展示为主的应用，Astro 提供的 Islands 架构是目前优化前端加载性能的有效方案。&lt;/p&gt;
</content:encoded></item><item><title>现代 CSS 架构：Tailwind CSS 与 CSS-in-JS 的取舍</title><link>https://nollieleo.github.io/posts/css-architecture-tailwind-cssinjs/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/css-architecture-tailwind-cssinjs/</guid><description>对比运行时 CSS-in-JS 库与 Utility-first CSS 框架的优缺点，探讨组件库样式设计的最佳实践。</description><pubDate>Sat, 15 Feb 2025 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;前端样式的组织方式经历了从全局预处理器、CSS Modules 到 CSS-in-JS 与 Utility-first 的演进。在当今复杂组件库和业务开发中，架构的取舍直接影响到应用的渲染性能和开发体验。&lt;/p&gt;
&lt;h2&gt;1. 运行时 CSS-in-JS 的性能代价&lt;/h2&gt;
&lt;p&gt;以 &lt;code&gt;styled-components&lt;/code&gt; 或 &lt;code&gt;emotion&lt;/code&gt; 为代表的运行时方案，通过 JS 动态生成样式。虽然它提供了强大的逻辑处理能力，但在高性能场景下存在瓶颈。&lt;/p&gt;
&lt;h3&gt;1.1 CSSOM 重构开销&lt;/h3&gt;
&lt;p&gt;当组件状态改变触发样式更新时，运行时库会执行以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;哈希计算&lt;/strong&gt;：根据 props 计算样式的唯一哈希值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式注入&lt;/strong&gt;：生成 CSS 字符串并将其插入到文档的 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 标签中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浏览器重绘&lt;/strong&gt;：浏览器检测到样式表变化，触发 CSSOM (CSS Object Model) 的重新构建。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在拥有数千个节点的复杂页面（如大型数据表格）中，这种频繁的 JS 到 CSS 的转换会导致明显的掉帧。&lt;/p&gt;
&lt;h2&gt;2. Utility-first 与构建时提取：Tailwind CSS&lt;/h2&gt;
&lt;p&gt;Tailwind CSS 采用了完全不同的思路。它通过 AOT (Ahead of Time) 编译，在构建阶段扫描源码并生成静态 CSS 文件。&lt;/p&gt;
&lt;h3&gt;2.1 JIT (Just-in-Time) 引擎原理&lt;/h3&gt;
&lt;p&gt;Tailwind 的 JIT 引擎不再预生成庞大的 CSS，而是根据代码中的类名按需生成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[Source Code] --&amp;gt; B{Tailwind JIT}
    B --&amp;gt;|Scan ClassNames| C[Generate Atomic CSS]
    C --&amp;gt; D[PostCSS Minification]
    D --&amp;gt; E[Final Static CSS]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式的优势在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;零运行时开销&lt;/strong&gt;：样式在构建时已确定，浏览器只需解析静态 CSS 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存友好&lt;/strong&gt;：生成的 CSS 文件体积通常小于 50KB，且不随项目规模线性增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 业务踩坑：Tailwind 的动态类名截断灾难 (Dynamic Class Purging)&lt;/h2&gt;
&lt;p&gt;很多从传统 CSS 过渡到 Tailwind 的新手，最容易写出这种引发线上事故的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 灾难写法：动态拼接类名
function StatusBadge({ status, color }) {
  // 假设 color 是 &apos;red&apos;, &apos;green&apos;, &apos;blue&apos; 之一
  return &amp;lt;span className={`bg-${color}-500 text-white`}&amp;gt;{status}&amp;lt;/span&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么背景色全丢了？&lt;/strong&gt;
Tailwind 不是在浏览器里运行的，而是在**构建时（Build-time）**通过正则扫描你的源码字符串。
它只会无脑地提取所有完整的单词（比如它看到了 &lt;code&gt;text-white&lt;/code&gt;，就会去生成这个类的 CSS）。
但它根本不懂 JavaScript 的运行逻辑！它扫描你的代码时，看到的是 &lt;code&gt;bg-${color}-500&lt;/code&gt; 这个死板的字符串，它完全不知道 &lt;code&gt;color&lt;/code&gt; 在运行时会变成 &lt;code&gt;red&lt;/code&gt; 还是 &lt;code&gt;blue&lt;/code&gt;。因此，在最终打包出的 &lt;code&gt;.css&lt;/code&gt; 文件里，&lt;code&gt;bg-red-500&lt;/code&gt; 这些样式被无情地剔除（Purged）了。&lt;/p&gt;
&lt;h3&gt;3.1 工业级解法：完整写出或白名单 (Safelist)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方案一：永远写完整的类名（推荐）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是 Tailwind 官方最推荐的做法。牺牲一点代码的 DRY（Don&apos;t Repeat Yourself），换取绝对的构建安全。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ 正确写法：映射字典
const colorMap = {
  red: &apos;bg-red-500&apos;,
  green: &apos;bg-green-500&apos;,
  blue: &apos;bg-blue-500&apos;,
};

function StatusBadge({ status, color }) {
  return &amp;lt;span className={`${colorMap[color]} text-white`}&amp;gt;{status}&amp;lt;/span&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tailwind 在扫描这个文件时，看到了 &lt;code&gt;bg-red-500&lt;/code&gt; 这个完整的字符串字面量，就会乖乖地把它打包进去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案二：配置 &lt;code&gt;tailwind.config.js&lt;/code&gt; 的 Safelist&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你的颜色真的是由后端下发的一组动态枚举（比如几十种业务状态色），映射表写起来太长，可以通过正则表达式强制打包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// tailwind.config.js
module.exports = {
  safelist: [
    // 强行打包所有 bg-X-500 的类名（哪怕源码里没写全）
    {
      pattern: /bg-(red|green|blue)-500/,
      // 可选：还可以强行打包它们在 hover 时的状态
      variants: [&apos;hover&apos;],
    },
  ],
  // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 架构进阶：Zero-runtime CSS-in-JS 的崛起&lt;/h2&gt;
&lt;p&gt;虽然传统的 CSS-in-JS (如 Styled-components) 性能太差，但很多人还是怀念能在 JS 里写 CSS 的开发体验（比如强类型校验、自动提取未使用的样式）。&lt;/p&gt;
&lt;p&gt;于是，这两年诞生了一个完美的折中方案：&lt;strong&gt;构建时提取（Zero-runtime / Build-time CSS-in-JS）&lt;/strong&gt;。代表作有 &lt;code&gt;Vanilla Extract&lt;/code&gt;、&lt;code&gt;Linaria&lt;/code&gt;，以及字节跳动开源的 &lt;code&gt;Panda CSS&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;4.1 鱼与熊掌兼得的魔法&lt;/h3&gt;
&lt;p&gt;以 Panda CSS 为例，它的写法完全是 CSS-in-JS 的体验，但在底层，它结合了 Tailwind 的 JIT 扫描机制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 Panda CSS
import { css } from &apos;../styled-system/css&apos;;

function Button({ children }) {
  return (
    &amp;lt;button className={css({
      bg: &apos;blue.500&apos;, // 支持设计系统 Token
      color: &apos;white&apos;,
      _hover: { bg: &apos;blue.600&apos; }, // 伪类
      padding: &apos;4&apos;
    })}&amp;gt;
      {children}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;它是怎么运行的？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在编写代码时，&lt;code&gt;css()&lt;/code&gt; 函数提供了完美的 TypeScript 类型提示（甚至比 Tailwind 的 VSCode 插件更准）。&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;构建阶段&lt;/strong&gt;，Panda CSS 的分析器（基于 AST）会提取出你写在 &lt;code&gt;css()&lt;/code&gt; 里的对象。&lt;/li&gt;
&lt;li&gt;它在本地悄悄生成了原子化 CSS（如 &lt;code&gt;.bg_blue_500 { background-color: #3b82f6; }&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;运行阶段&lt;/strong&gt;，那个 &lt;code&gt;css()&lt;/code&gt; 函数会被直接编译成一串纯静态的字符串：&lt;code&gt;&quot;bg_blue_500 text_white ...&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终，浏览器收到的就是一个干干净净的 &lt;code&gt;className=&quot;bg_blue_500&quot;&lt;/code&gt;，没有任何 JS 运行时的哈希计算、也没有 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 标签的动态插入，性能与 Tailwind 齐平！&lt;/p&gt;
&lt;h2&gt;5. 深度对比与选型建议&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;CSS-in-JS (Runtime)&lt;/th&gt;
&lt;th&gt;Tailwind CSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;开发体验&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;逻辑与样式高度耦合，支持复杂计算&lt;/td&gt;
&lt;td&gt;快速原型开发，约束感强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;运行时有损耗，JS 包体积大&lt;/td&gt;
&lt;td&gt;零运行时损耗，CSS 体积极小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极强 (基于 Props)&lt;/td&gt;
&lt;td&gt;较弱 (需预定义类名或使用 style 属性)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;学习曲线&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;低 (纯 JS/TS)&lt;/td&gt;
&lt;td&gt;中 (需记忆工具类名)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.2 混合架构实践&lt;/h3&gt;
&lt;p&gt;在大型项目中，我们通常采取折中方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础布局与原子组件&lt;/strong&gt;：使用 Tailwind CSS 保证渲染性能和样式一致性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高度动态的交互组件&lt;/strong&gt;：对于需要根据复杂业务逻辑实时计算位置、颜色的组件（如拖拽画布、图表），使用内联样式 (Inline Styles) 或轻量级的 CSS 变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 推荐的混合写法
function DynamicBox({ color, size }) {
  return (
    &amp;lt;div 
      className=&quot;flex items-center justify-center transition-all&quot; // 静态样式用 Tailwind
      style={{ backgroundColor: color, width: `${size}px` }}      // 动态样式用内联
    &amp;gt;
      Content
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;CSS 架构的演进方向正在从“运行时灵活性”回归到“构建时确定性”。Tailwind CSS 凭借其优秀的性能表现和原子化的设计约束，已成为目前构建现代设计系统的首选方案。对于追求极致动态能力的场景，CSS-in-JS 依然有其应用价值，但在性能敏感的项目中需谨慎使用。&lt;/p&gt;
</content:encoded></item><item><title>TypeScript 高阶特性实战：类型体操在复杂业务 Schema 中的应用</title><link>https://nollieleo.github.io/posts/advanced-typescript-patterns/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/advanced-typescript-patterns/</guid><description>结合实际业务场景，深入讲解 TypeScript 的高阶特性（如模板字面量类型、条件类型、satisfies 操作符），展示如何构建类型安全的复杂数据 Schema。</description><pubDate>Mon, 10 Feb 2025 10:15:00 GMT</pubDate><content:encoded>&lt;p&gt;在前端工程化中，TypeScript 已经成为了不可或缺的基础设施。然而，许多开发者仍然停留在定义基础 &lt;code&gt;interface&lt;/code&gt; 和 &lt;code&gt;type&lt;/code&gt; 的阶段，遇到复杂的动态数据结构时，往往会退化为使用 &lt;code&gt;any&lt;/code&gt; 或 &lt;code&gt;Record&amp;lt;string, any&amp;gt;&lt;/code&gt;，这被称为“AnyScript”现象。&lt;/p&gt;
&lt;p&gt;本文将结合实际业务场景，探讨如何利用 TypeScript 的高阶特性，在复杂业务 Schema 中实现深度的类型安全。&lt;/p&gt;
&lt;h2&gt;1. 模板字面量类型：构建强类型路由系统&lt;/h2&gt;
&lt;p&gt;模板字面量类型（Template Literal Types）允许我们在类型层面进行字符串拼接和模式匹配。这在处理路由路径或事件派发系统时极为有用。&lt;/p&gt;
&lt;p&gt;假设我们需要实现一个强类型的路由跳转函数，要求能够从路径字符串中自动提取参数类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 提取路由参数的类型计算
type ExtractRouteParams&amp;lt;T extends string&amp;gt; = 
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams&amp;lt;`/${Rest}`&amp;gt;]: string }
    : T extends `${infer _Start}:${infer Param}`
      ? { [K in Param]: string }
      : {};

// 测试类型推导
type UserRouteParams = ExtractRouteParams&amp;lt;&quot;/user/:userId/post/:postId&quot;&amp;gt;;
// 推导结果: { userId: string; postId: string; }

// 强类型跳转函数
function navigate&amp;lt;T extends string&amp;gt;(
  path: T, 
  params: ExtractRouteParams&amp;lt;T&amp;gt; extends Record&amp;lt;string, never&amp;gt; ? void : ExtractRouteParams&amp;lt;T&amp;gt;
) {
  // 实现逻辑...
}

// ✅ 类型检查通过
navigate(&quot;/user/:id&quot;, { id: &quot;123&quot; });

// ❌ 类型报错：缺少 postId
navigate(&quot;/user/:userId/post/:postId&quot;, { userId: &quot;123&quot; }); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;infer&lt;/code&gt; 关键字结合模板字面量，我们让编译器在编写代码时就能捕获路由参数遗漏的错误。&lt;/p&gt;
&lt;h2&gt;2. 递归条件类型：深层对象属性的提取&lt;/h2&gt;
&lt;p&gt;在处理后端返回的复杂 JSON Schema 或表单配置时，我们经常需要获取对象所有可能的深层路径（如 &lt;code&gt;user.address.city&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;利用递归条件类型，我们可以生成一个对象所有深层路径的联合类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Path&amp;lt;T&amp;gt; = T extends object
  ? {
      [K in keyof T]: K extends string
        ? T[K] extends object
          ? K | `${K}.${Path&amp;lt;T[K]&amp;gt;}`
          : K
        : never;
    }[keyof T]
  : never;

interface UserSchema {
  id: number;
  profile: {
    name: string;
    contact: {
      email: string;
      phone: string;
    };
  };
}

type UserPaths = Path&amp;lt;UserSchema&amp;gt;;
// 推导结果: &quot;id&quot; | &quot;profile&quot; | &quot;profile.name&quot; | &quot;profile.contact&quot; | &quot;profile.contact.email&quot; | &quot;profile.contact.phone&quot;

// 结合 lodash 的 get 函数实现强类型
declare function get&amp;lt;T, P extends Path&amp;lt;T&amp;gt;&amp;gt;(obj: T, path: P): any;

const user: UserSchema = /* ... */;
get(user, &quot;profile.contact.email&quot;); // ✅ 合法路径
get(user, &quot;profile.age&quot;); // ❌ 类型报错：路径不存在
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 业务踩坑：深层可选与嵌套推断的性能深渊&lt;/h2&gt;
&lt;p&gt;当我们在写 &lt;code&gt;Path&amp;lt;T&amp;gt;&lt;/code&gt; 这种递归类型时，如果 &lt;code&gt;T&lt;/code&gt; 是一个极其庞大的业务对象（比如包含了上百个字段、几十层嵌套的 GraphQL 生成的 Schema），你的 VSCode 可能会突然卡死，或者风扇狂转。&lt;/p&gt;
&lt;p&gt;接着，TypeScript 编译器会无情地抛出一个红线错误：
&lt;code&gt;Type instantiation is excessively deep and possibly infinite.&lt;/code&gt; （类型实例化过深，可能无限循环）。&lt;/p&gt;
&lt;h3&gt;3.1 为什么会性能爆炸？&lt;/h3&gt;
&lt;p&gt;TypeScript 的类型系统本质上是一门&lt;strong&gt;图灵完备&lt;/strong&gt;的函数式编程语言。
在计算 &lt;code&gt;Path&amp;lt;UserSchema&amp;gt;&lt;/code&gt; 时，TS 会像展开多项式一样，把每一层对象结构暴力展开。如果遇到交叉类型（Intersection Types, &lt;code&gt;A &amp;amp; B&lt;/code&gt;）或者复杂的联合类型（Union Types, &lt;code&gt;A | B&lt;/code&gt;），计算量会呈指数级增长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法 1：人为阻断递归深度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在写企业级通用类型库时，千万不要让递归无限进行下去。我们必须手动传入一个“深度计数器”来强制中断递归（利用元组的 &lt;code&gt;length&lt;/code&gt; 属性模拟数字递减）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 利用元组长度模拟数字，限制最大递归层级
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

type SafePath&amp;lt;T, D extends number = 3&amp;gt; = 
  [D] extends [never] // 如果深度耗尽，立即停止
    ? never 
    : T extends object
      ? {
          [K in keyof T]-?: K extends string | number
            ? `${K}` | `${K}.${SafePath&amp;lt;T[K], Prev[D]&amp;gt;}`
            : never;
        }[keyof T]
      : never;

// 即使 UserSchema 有 100 层，推导也会在第 3 层强行停止，挽救了编译器性能
type SafePaths = SafePath&amp;lt;UserSchema&amp;gt;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 工业级解法 2：延迟推断与 infer 优化&lt;/h3&gt;
&lt;p&gt;当你的条件类型写得很复杂时（比如 &lt;code&gt;A extends B ? X : Y&lt;/code&gt;），TS 会在判断前先急切地去计算 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt;。
如果把复杂的计算提取到 &lt;code&gt;infer&lt;/code&gt; 中，就能实现&lt;strong&gt;局部计算的缓存和延迟求值&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 差的写法：T[K] 被计算了两次，性能翻倍
type BadGet&amp;lt;T, K extends keyof T&amp;gt; = T[K] extends Function ? never : T[K];

// ✅ 好的写法：用 infer 缓存了结果
type GoodGet&amp;lt;T, K extends keyof T&amp;gt; = T[K] extends infer U 
  ? U extends Function ? never : U
  : never;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是在编写像 React Hook Form 这种底层重度依赖类型体操的库时，必须掌握的性能优化秘籍。&lt;/p&gt;
&lt;h2&gt;4. &lt;code&gt;satisfies&lt;/code&gt; 操作符：精确推导的关键&lt;/h2&gt;
&lt;p&gt;在 TypeScript 4.9 中引入的 &lt;code&gt;satisfies&lt;/code&gt; 操作符，解决了长期以来配置对象类型声明的一个核心问题：&lt;strong&gt;如何在验证对象结构的同时，保留其最精确的字面量类型？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设我们有一个主题配置对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Color = string | { r: number; g: number; b: number };

// 传统做法：使用类型注解
const theme: Record&amp;lt;string, Color&amp;gt; = {
  primary: &quot;blue&quot;,
  secondary: { r: 255, g: 0, b: 0 }
};

// ❌ 报错：theme.primary 被推导为 Color，丢失了字符串特有的方法
theme.primary.toUpperCase(); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果去掉类型注解，虽然能保留精确类型，但失去了对对象结构的校验。&lt;code&gt;satisfies&lt;/code&gt; 有效解决了这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const theme = {
  primary: &quot;blue&quot;,
  secondary: { r: 255, g: 0, b: 0 },
  // error: 123 // 如果添加不符合 Color 的属性，这里会报错
} satisfies Record&amp;lt;string, Color&amp;gt;;

// ✅ 正常工作：编译器知道 primary 确切是 string
theme.primary.toUpperCase();

// ✅ 正常工作：编译器知道 secondary 确切是对象
console.log(theme.secondary.r);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;在底层架构、公共组件库和复杂业务 Schema 的设计中，合理运用模板字面量、条件类型和 &lt;code&gt;satisfies&lt;/code&gt; 等高级特性，能够将大量的运行时错误提前到编译阶段暴露。&lt;/p&gt;
&lt;p&gt;然而，作为架构师或高级工程师，我们也需要注意架构的平衡。复杂的类型计算会增加代码的阅读门槛和编译耗时。最佳实践是：&lt;strong&gt;在核心基础库和高频复用的业务模块中追求深度的类型安全，而在普通的业务线代码中保持清晰直白。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>Service Worker 离线可用与资源缓存策略</title><link>https://nollieleo.github.io/posts/service-worker-caching-strategies/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/service-worker-caching-strategies/</guid><description>深挖 Service Worker 的生命周期，解析 Network First、Cache First 等高阶缓存控制在 PWA 中的应用。</description><pubDate>Tue, 28 Jan 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Service Worker 是运行在浏览器后台的独立线程，作为 Web 应用与网络之间的代理层。它是构建 PWA (Progressive Web Apps) 的核心，通过拦截网络请求并管理本地缓存，实现了离线可用和极快的二次加载体验。&lt;/p&gt;
&lt;h2&gt;1. Service Worker 生命周期&lt;/h2&gt;
&lt;p&gt;理解 SW 的生命周期是实现缓存策略的前提。它与普通 JS 脚本不同，具有严格的阶段划分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Registration&lt;/strong&gt;：在主线程调用 &lt;code&gt;navigator.serviceWorker.register()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Installation&lt;/strong&gt;：触发 &lt;code&gt;install&lt;/code&gt; 事件，通常用于预缓存 (Pre-cache) 静态资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Activation&lt;/strong&gt;：触发 &lt;code&gt;activate&lt;/code&gt; 事件，用于清理旧版本的缓存空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redundant&lt;/strong&gt;：被新版本取代或安装失败。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// sw.js
const CACHE_NAME = &apos;v2-assets&apos;;

self.addEventListener(&apos;install&apos;, (event) =&amp;gt; {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) =&amp;gt; {
      return cache.addAll([
        &apos;/&apos;,
        &apos;/styles/main.css&apos;,
        &apos;/scripts/app.js&apos;,
        &apos;/offline.html&apos;
      ]);
    })
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 核心缓存策略解析&lt;/h2&gt;
&lt;p&gt;通过监听 &lt;code&gt;fetch&lt;/code&gt; 事件，我们可以根据业务需求定制不同的响应逻辑。&lt;/p&gt;
&lt;h3&gt;2.1 Cache First (缓存优先)&lt;/h3&gt;
&lt;p&gt;适用于字体、图片等不常变动的静态资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Browser
    participant SW as Service Worker
    participant Cache
    participant Network

    Browser-&amp;gt;&amp;gt;SW: Fetch Resource
    SW-&amp;gt;&amp;gt;Cache: Match Request
    alt Cache Hit
        Cache--&amp;gt;&amp;gt;SW: Return Cached Response
        SW--&amp;gt;&amp;gt;Browser: Response
    else Cache Miss
        SW-&amp;gt;&amp;gt;Network: Fetch from Server
        Network--&amp;gt;&amp;gt;SW: Response
        SW-&amp;gt;&amp;gt;Cache: Update Cache
        SW--&amp;gt;&amp;gt;Browser: Response
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 Network First (网络优先)&lt;/h3&gt;
&lt;p&gt;适用于对实时性要求较高的 API 请求。如果网络不可用，则回退到缓存。&lt;/p&gt;
&lt;h3&gt;2.3 Stale-While-Revalidate (SWR)&lt;/h3&gt;
&lt;p&gt;这是性能与实时性的最佳平衡方案。它立即返回缓存内容，同时在后台发起网络请求更新缓存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&apos;fetch&apos;, (event) =&amp;gt; {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) =&amp;gt; {
      return cache.match(event.request).then((cachedResponse) =&amp;gt; {
        const fetchPromise = fetch(event.request).then((networkResponse) =&amp;gt; {
          // 更新缓存
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        // 返回缓存或等待网络
        return cachedResponse || fetchPromise;
      });
    })
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 缓存失效与更新机制&lt;/h2&gt;
&lt;p&gt;Service Worker 的更新遵循“字节对比”原则。如果 &lt;code&gt;sw.js&lt;/code&gt; 文件发生 1 字节的变化，浏览器就会尝试安装新版本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skip Waiting&lt;/strong&gt;：新 SW 安装后默认进入等待状态，调用 &lt;code&gt;self.skipWaiting()&lt;/code&gt; 可强制立即接管页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clients Claim&lt;/strong&gt;：在激活阶段调用 &lt;code&gt;self.clients.claim()&lt;/code&gt;，让新 SW 立即控制所有已打开的标签页。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 业务踩坑：僵尸更新危机与 Opaque Response&lt;/h2&gt;
&lt;p&gt;Service Worker 看似完美，但在真正上线给百万用户使用时，绝大多数开发者都会被它的&lt;strong&gt;更新机制&lt;/strong&gt;和&lt;strong&gt;缓存污染&lt;/strong&gt;折磨得痛不欲生。&lt;/p&gt;
&lt;h3&gt;4.1 僵尸更新：为什么用户总是看到旧页面？&lt;/h3&gt;
&lt;p&gt;假设你用 &lt;code&gt;SWR (Stale-While-Revalidate)&lt;/code&gt; 策略缓存了 &lt;code&gt;index.html&lt;/code&gt;。
昨天你发了 v1.0，今天紧急发了 v2.0 修复一个致命 Bug。&lt;/p&gt;
&lt;p&gt;用户的浏览器行为如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户访问网站，SW 拦截了请求。&lt;/li&gt;
&lt;li&gt;SW 发现缓存里有 v1.0 的 &lt;code&gt;index.html&lt;/code&gt;，&lt;strong&gt;立刻返回给浏览器&lt;/strong&gt;。用户看到了旧版的页面！&lt;/li&gt;
&lt;li&gt;同时，SW 在后台悄悄拉取了 v2.0 的 &lt;code&gt;index.html&lt;/code&gt; 并更新了缓存，顺便发现 &lt;code&gt;sw.js&lt;/code&gt; 也变了。&lt;/li&gt;
&lt;li&gt;浏览器下载了新的 &lt;code&gt;sw.js&lt;/code&gt;，触发了 &lt;code&gt;install&lt;/code&gt;，但因为旧的 SW 还在控制当前页面，新 SW 会进入 &lt;strong&gt;&lt;code&gt;waiting&lt;/code&gt; (等待态)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;致命问题来了&lt;/strong&gt;：只要用户不&lt;strong&gt;把所有相关的 Tab 页全部关掉&lt;/strong&gt;，这个新的 SW 就永远不会 &lt;code&gt;activate&lt;/code&gt;！即使用户疯狂点浏览器的刷新按钮 (F5)，看到的依然是旧页面！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：弹窗提示 + 强制接管 (Skip Waiting)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不要指望用户会主动关闭所有 Tab，你必须在 UI 层做强制干预。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在注册 SW 时，监听 &lt;code&gt;updatefound&lt;/code&gt; 事件。如果发现有一个新 Worker 处于 &lt;code&gt;installed&lt;/code&gt; 且正在等待，就在页面顶部弹出一个提示条：&lt;strong&gt;“发现新版本，点击刷新”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当用户点击“刷新”按钮时，通过 &lt;code&gt;postMessage&lt;/code&gt; 告诉那个正在等待的新 SW：“快给我 &lt;code&gt;skipWaiting&lt;/code&gt;！”。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 主线程 (main.js)
navigator.serviceWorker.register(&apos;/sw.js&apos;).then(reg =&amp;gt; {
  reg.addEventListener(&apos;updatefound&apos;, () =&amp;gt; {
    const newWorker = reg.installing;
    newWorker.addEventListener(&apos;statechange&apos;, () =&amp;gt; {
      // 检查它是否处于等待接管状态
      if (newWorker.state === &apos;installed&apos; &amp;amp;&amp;amp; navigator.serviceWorker.controller) {
        showUpdateToast(() =&amp;gt; {
          // 用户点击更新，发送消息强制 skipWaiting
          newWorker.postMessage({ type: &apos;SKIP_WAITING&apos; });
        });
      }
    });
  });
});

// 监听控制权转移，强制刷新页面加载新资源
let refreshing = false;
navigator.serviceWorker.addEventListener(&apos;controllerchange&apos;, () =&amp;gt; {
  if (!refreshing) {
    refreshing = true;
    window.location.reload();
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Service Worker (sw.js)
self.addEventListener(&apos;message&apos;, (event) =&amp;gt; {
  if (event.data &amp;amp;&amp;amp; event.data.type === &apos;SKIP_WAITING&apos;) {
    // 强制杀掉旧 SW，自己立刻接管
    self.skipWaiting();
  }
});

// 一旦接管，立刻宣布控制所有 clients
self.addEventListener(&apos;activate&apos;, (event) =&amp;gt; {
  event.waitUntil(self.clients.claim());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是唯一一种能够保证 100% 版本一致性的 PWA 热更新方案。&lt;/p&gt;
&lt;h3&gt;4.2 Opaque Response (不透明响应) 的缓存雪崩&lt;/h3&gt;
&lt;p&gt;有时候你会用 SW 去缓存第三方的 CDN 图片或 API。由于存在跨域，且第三方没配 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; (CORS)，此时 &lt;code&gt;fetch&lt;/code&gt; 拿到的 &lt;code&gt;Response&lt;/code&gt; 类型会变成 &lt;strong&gt;&lt;code&gt;opaque&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可怕的陷阱：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Opaque 响应的 HTTP 状态码永远是 &lt;code&gt;0&lt;/code&gt;，你根本不知道这个请求是成功还是 404 / 500！&lt;/li&gt;
&lt;li&gt;浏览器出于安全考虑，如果把 Opaque 响应存进 Cache Storage，为了防止黑客进行跨域大小推测攻击，浏览器会给它分配一个极度膨胀的配额（哪怕一张图片只有 10KB，浏览器也会按 &lt;strong&gt;7MB&lt;/strong&gt; 来计算存储空间！）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你不小心用 &lt;code&gt;cache.put(request, opaqueResponse)&lt;/code&gt; 存了几张跨域图，用户的存储空间瞬间就被吃掉了几百兆，触发上面提到的 &lt;code&gt;QuotaExceededError&lt;/code&gt; 和浏览器静默删库！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;防爆盾策略：永远只缓存状态码为 200 或明确允许的跨域响应&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&apos;fetch&apos;, (event) =&amp;gt; {
  event.respondWith(
    caches.match(event.request).then(cached =&amp;gt; {
      return cached || fetch(event.request).then(response =&amp;gt; {
        // 关键防御：如果是跨域的不透明响应 (type === &apos;opaque&apos;) 且不是 200，绝不缓存！
        // 除非你百分百确认这是一个有效的 CDN 资源
        if (!response || response.status !== 200 || response.type === &apos;opaque&apos;) {
          return response; 
        }

        // 可以安全缓存
        const responseToCache = response.clone();
        caches.open(CACHE_NAME).then(cache =&amp;gt; cache.put(event.request, responseToCache));
        return response;
      });
    })
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 最佳实践建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要缓存 sw.js 本身&lt;/strong&gt;：确保服务器对 SW 脚本设置 &lt;code&gt;Cache-Control: no-cache&lt;/code&gt;，否则会导致应用无法更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层缓存&lt;/strong&gt;：将核心 UI 框架资源设为预缓存，将业务数据设为运行时缓存 (Runtime Cache)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优雅降级&lt;/strong&gt;：始终提供一个 &lt;code&gt;offline.html&lt;/code&gt; 页面，在网络和缓存均失效时展示，提升用户体验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;合理运用 Service Worker 不仅能提升加载速度，更能让 Web 应用在弱网环境下保持高度的可用性。&lt;/p&gt;
</content:encoded></item><item><title>无代码编辑器基建：组件样式 Schema 重构与样式模板架构设计</title><link>https://nollieleo.github.io/posts/zion-component-style-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zion-component-style-architecture/</guid><description>深度解析 Zion 无代码编辑器底层组件样式系统的重构历程。从打破陈旧的 mRef 结构到建立强类型的 CSS 数据模型，再到设计支持多断点、多变体的 Comp Style Templates 抽象层，最后剖析大型 Component-Property 样式支持矩阵的 TS 类型实现。</description><pubDate>Mon, 20 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与历史包袱&lt;/h2&gt;
&lt;p&gt;在低代码/无代码平台的演进过程中，“组件样式系统（Styling System）”往往是第一个遇到架构瓶颈的模块。&lt;/p&gt;
&lt;p&gt;随着平台内置组件数量的增加和用户界面自定义需求的膨胀，Zion 早期基于扁平化 &lt;code&gt;mRef&lt;/code&gt; 属性存储样式的方案很快暴露出了非常严重的技术债：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;样式无法复用与 Schema 膨胀&lt;/strong&gt;：如果用户希望 50 个按钮保持统一的样式，旧版架构下这 50 个按钮实例的 Schema 会完全复制这几十个 CSS 属性。这种重复导致整个页面的 JSON Schema 体积呈指数级增长，严重拖慢了网络解析与渲染性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型安全与结构化定义的缺失&lt;/strong&gt;：旧版定义非常僵化且缺乏约束（例如 &lt;code&gt;width&lt;/code&gt; 强制为 &lt;code&gt;number&lt;/code&gt;，或者直接使用松散的 &lt;code&gt;string&lt;/code&gt;）。这导致在配置复杂样式（如渐变色、阴影、多单位尺寸）时极易出错，且无法在代码层面对不合法的样式组合进行拦截。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;断点与交互变体（Hover/Active）管理的混乱&lt;/strong&gt;：早期的设计缺乏对响应式（Breakpoints）和伪类（Variants/Triggers）的抽象层支持。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决以上问题，我主导设计并落地了 Zion 全新的组件样式架构方案，核心围绕 &lt;strong&gt;数据结构抽象&lt;/strong&gt; 与 &lt;strong&gt;Comp Style Templates&lt;/strong&gt; 展开。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心解法一：CSS 属性的强类型抽象与结构重塑&lt;/h2&gt;
&lt;p&gt;为了建立健壮的样式基建，必须打破“纯 CSS 字符串”的限制。我将原本松散的样式字段与标准 CSS 规范对齐，进行了深度的结构化与强类型抽象。&lt;/p&gt;
&lt;h3&gt;2.1 颜色与数值的结构重塑&lt;/h3&gt;
&lt;p&gt;在重构中，我们摒弃了简单的 &lt;code&gt;string&lt;/code&gt; 或 &lt;code&gt;number&lt;/code&gt; 定义。
例如，针对宽度/高度等布局尺寸，拆分出了明确的单位枚举（&lt;code&gt;SizeUnit&lt;/code&gt;）和结构化的尺寸定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export enum SizeUnit {
  FIXED = &apos;px&apos;,
  RELATIVE = &apos;%&apos;,
  FILL = &apos;fr&apos;,
  AUTO = &apos;auto&apos;,
}

export interface SizeDefaultMeasure {
  value: number; 
  unit: SizeUnit; 
}

export interface SizeStyle {
  width: SizeDefaultMeasure | &apos;auto&apos;;
  height: SizeDefaultMeasure | &apos;auto&apos;;
  minWidth?: SizeDefaultMeasure;
  maxWidth?: SizeDefaultMeasure;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对颜色等需要复杂渲染的属性，我们也引入了枚举化分类（如 &lt;code&gt;DefaultColorStyle&lt;/code&gt;、&lt;code&gt;LinearGradientColorStyle&lt;/code&gt;），使其能够安全地支持纯色与渐变色。&lt;/p&gt;
&lt;h3&gt;2.2 复杂类型的精细拆解与聚合&lt;/h3&gt;
&lt;p&gt;为了贴合 TypeScript 的严格校验体系，我将原先庞大的字段进行了分类聚合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layout 布局&lt;/strong&gt;：整合 &lt;code&gt;display&lt;/code&gt;, &lt;code&gt;justifyContent&lt;/code&gt;, &lt;code&gt;alignItems&lt;/code&gt;, &lt;code&gt;flexDirection&lt;/code&gt;, &lt;code&gt;flexWrap&lt;/code&gt;, &lt;code&gt;rowGap&lt;/code&gt;, &lt;code&gt;columnGap&lt;/code&gt;，形成完整的 &lt;code&gt;FlexLayoutStyle&lt;/code&gt; / &lt;code&gt;GridLayoutStyle&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Position 定位&lt;/strong&gt;：根据 &lt;code&gt;PositionKeyword&lt;/code&gt; (relative/absolute/fixed/sticky)，拆分对应的方向坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BoxShadow 阴影&lt;/strong&gt;：拆分 &lt;code&gt;offsetX&lt;/code&gt;, &lt;code&gt;offsetY&lt;/code&gt;, &lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typography 字体&lt;/strong&gt;：统一收敛到 &lt;code&gt;TextStyle&lt;/code&gt; 下，合并 &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;fontFamily&lt;/code&gt;, &lt;code&gt;fontWeight&lt;/code&gt;, &lt;code&gt;lineHeight&lt;/code&gt; 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终，所有的 CSS 能力被整合成了大一统的 &lt;code&gt;Styles&lt;/code&gt; 基础数据结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface Styles {
  layout: LayoutStyle;
  size: SizeStyle;
  position: PositionStyle;
  margin: MarginStyle;
  padding: PaddingStyle;
  text: TextStyle;
  background: BackgroundStyle;
  border: BorderStyle;
  // ... 其他属性
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 核心解法二：Comp Style Templates 抽象层架构&lt;/h2&gt;
&lt;p&gt;为了解决 Schema 的无限膨胀，我引入了 &lt;strong&gt;Comp Style Templates (组件样式模板)&lt;/strong&gt; 概念。
这套架构的核心思想是将“抽象层面的样式定义”与“组件的实例数据”彻底剥离，形成 &lt;strong&gt;抽象层 (Abstract Layer)&lt;/strong&gt; 与 &lt;strong&gt;实例层 (Instance Layer)&lt;/strong&gt; 的双层架构。&lt;/p&gt;
&lt;h3&gt;3.1 抽象层 (Abstract Layer)：模板配置设计&lt;/h3&gt;
&lt;p&gt;我们在 Schema 根节点建立了一个统一的 &lt;code&gt;styleTemplates&lt;/code&gt; 字典。所有的样式定义（如 primary button、outline button）都作为独立的实体存放在此，由 &lt;code&gt;CompStyleTemplateConfig&lt;/code&gt; 进行描述。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Schema {
  styleTemplates: CompStyleTemplates;
  mRefsMap: Record&amp;lt;string, ComponentMeta&amp;gt;;
}

// 模板基于组件类型 (ComponentType) 进行隔离存放
type CompStyleTemplates = Record&amp;lt;
  ComponentType,
  Record&amp;lt;CompStyleTemplateConfig[&apos;id&apos;], CompStyleAttributes&amp;gt;
&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 实例层 (Instance Layer)：多断点与交互变体矩阵&lt;/h3&gt;
&lt;p&gt;组件实例本身不再存储海量的 CSS 代码，它只保留一个 &lt;code&gt;template&lt;/code&gt; 的引用 ID。同时，为了支持响应式和特殊状态，实例层 (&lt;code&gt;CompStylesDefault&lt;/code&gt;) 引入了对 &lt;strong&gt;断点 (Breakpoints)&lt;/strong&gt; 和 &lt;strong&gt;交互变体 (Interaction Variants)&lt;/strong&gt; 的原生支持。&lt;/p&gt;
&lt;p&gt;在断点设计上，我们预设了标准化的枚举（如 &lt;code&gt;PresetBreakpoint.WEB1&lt;/code&gt;, &lt;code&gt;WEB2&lt;/code&gt;），允许开发者针对不同屏幕尺寸进行样式重写。在交互变体上，支持了 &lt;code&gt;Hover&lt;/code&gt;, &lt;code&gt;Active&lt;/code&gt; 等状态的精细化配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum PresetBreakpoint {
  WEB1 = &apos;WEB1&apos;, // e.g., &amp;gt;= 1440px
  WEB2 = &apos;WEB2&apos;, // e.g., &amp;gt;= 1024px
  MOBILE = &apos;MOBILE&apos;,
}

enum InteractionVariant {
  HOVER = &apos;Hover&apos;,
  ACTIVE = &apos;Active&apos;,
  DISABLED = &apos;Disabled&apos;,
}

// 实例层的样式定义结构
interface CompStylesDefault {
  template: CompStyleTemplateConfig[&apos;id&apos;]; // 指向全局抽象模板
  breakpoint: Record&amp;lt;PresetBreakpoint, Partial&amp;lt;CompStyleAttributes&amp;gt;&amp;gt;;
  variant?: Record&amp;lt;PresetBreakpoint, Record&amp;lt;InteractionVariant, Partial&amp;lt;CompStyleAttributes&amp;gt;&amp;gt;&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终应用在 DOM 上的样式，遵循一套严格的优先级覆盖顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    T[&quot;Template (全局基础样式模板)&quot;] --&amp;gt; B[&quot;Breakpoint (当前设备断点覆盖)&quot;]
    B --&amp;gt; V[&quot;Variant (当前交互变体, 如 Hover)&quot;]
    V --&amp;gt; I[&quot;Instance (当前组件自身的强制覆盖)&quot;]
    I --&amp;gt; F((&quot;最终计算渲染样式&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种设计使得当我们想要一键替换全站按钮的背景色时，只需修改一条 &lt;code&gt;styleTemplates&lt;/code&gt; 记录即可。所有继承该模板的组件实例样式将自动发生变更，这极大地减小了项目的 JSON 体积。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 业务踩坑：Schema 压缩与运行时的“样式类名继承”降维&lt;/h2&gt;
&lt;p&gt;这套 &lt;code&gt;Template &amp;lt; Breakpoint &amp;lt; Variant&lt;/code&gt; 的双层架构看起来很完美，但在真正渲染到 DOM 上时，我们遇到了一个极其棘手的性能问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;痛点：渲染时的重复计算与 React DOM 臃肿&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在最初的实现中，为了把这套复杂的嵌套结构渲染到网页上，我们在每个 &lt;code&gt;Button&lt;/code&gt; 组件内部写了一个极其庞大的 &lt;code&gt;useComputedStyle&lt;/code&gt; Hook。这个 Hook 会在运行时，把 Template 的 JSON、当前 Breakpoint 的 JSON、以及 Hover 时的 JSON，执行三次 &lt;code&gt;mergeDeep&lt;/code&gt;，最后转换成一个巨大的 React &lt;code&gt;style={{...}}&lt;/code&gt; 对象塞给 DOM。&lt;/p&gt;
&lt;p&gt;这导致了两个致命后果：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;React 渲染极慢&lt;/strong&gt;：页面里有 1000 个按钮，这 1000 个按钮在每次浏览器 Resize 时，都要各自在 JS 线程里执行深度对象合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML 节点体积爆炸&lt;/strong&gt;：1000 个按钮的 HTML 里，塞满了长达几百个字符的 &lt;code&gt;style=&quot;background: red; font-size: 14px; margin-top: 10px; ...&quot;&lt;/code&gt;，导致 DOM 树极其臃肿，滚动卡顿。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：运行时 CSS-in-JS 引擎 (即时编译为原子类)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们在渲染引擎层引入了一个&lt;strong&gt;动态样式表编译器 (Dynamic StyleSheet Compiler)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;其核心思路是：&lt;strong&gt;绝对不要把 Schema 里的样式对象直接以 &lt;code&gt;style&lt;/code&gt; 的形式传给 React 节点，而是要在渲染前，将其编译为动态注入的 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 标签中的 CSS 类名！&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模板的预编译&lt;/strong&gt;：当系统加载到 &lt;code&gt;styleTemplates&lt;/code&gt; 字典时，底层引擎会立刻遍历这些模板，将它们转化为真实的 CSS 字符串，并为其生成唯一的 Hash 类名（如 &lt;code&gt;.zion-btn-tpl-a1b2c&lt;/code&gt;），然后通过 &lt;code&gt;document.head.appendChild&lt;/code&gt; 注入到全局。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;伪类的原生支持&lt;/strong&gt;：对于 Schema 中的 &lt;code&gt;Hover&lt;/code&gt; 或 &lt;code&gt;Active&lt;/code&gt; 变体，我们不再用 JS 去监听 &lt;code&gt;onMouseEnter&lt;/code&gt;（这极度消耗性能），而是直接在编译阶段生成 &lt;code&gt;.zion-btn-tpl-a1b2c:hover { ... }&lt;/code&gt; 的原生 CSS 规则！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件的极简渲染&lt;/strong&gt;：当真实的 Button 组件渲染时，它的逻辑变得极度简单：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 组件渲染层不再进行任何复杂的对象合并
function ZionButton({ meta }) {
  // 只需要拿到模板生成的原子类名 Hash
  const templateClassName = getTemplateClassName(meta.templateId);
  
  // 实例自身的特殊覆盖样式（通常很少），才通过 inline-style 挂载
  const instanceStyle = parseInstanceStyle(meta.instanceStyle);

  // 最终的 DOM 极其干净
  return &amp;lt;button className={`zion-btn ${templateClassName}`} style={instanceStyle}&amp;gt;
    {meta.text}
  &amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套**“配置期是结构化 JSON，运行期降维打击为原生 CSS 类名”**的架构，我们不仅实现了 JSON Schema 体积的百倍压缩（几千个组件现在只存一个极短的 &lt;code&gt;templateId&lt;/code&gt; 字符串），还将渲染耗时从数百毫秒级别瞬间降到了趋近于 0（因为大量的样式计算被转移给了浏览器最底层的 CSS 渲染引擎）。&lt;/p&gt;
&lt;h2&gt;5. 架构优势：Component-Property 矩阵的 TS 类型实现&lt;/h2&gt;
&lt;p&gt;在无代码平台中，并非所有组件都支持所有样式。比如，一个纯文本 (&lt;code&gt;Text&lt;/code&gt;) 不应该有内部容器（Container）颜色配置；一个输入框 (&lt;code&gt;Input&lt;/code&gt;) 需要特有的 &lt;code&gt;placeholderColor&lt;/code&gt;；而滚动容器则需要定制 &lt;code&gt;scrollbar&lt;/code&gt; 样式。&lt;/p&gt;
&lt;p&gt;为了在代码层杜绝错误配置，我将这一套样式矩阵完全用 TypeScript 类型实现了约束。不同的组件类型拥有独立的 &lt;code&gt;xxxStyles&lt;/code&gt; 接口定义，精确控制了哪些区域可以使用哪些样式，甚至包括特定的伪元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 以 Input 组件为例，包含特有的 placeholderColor
export interface InputStyles {
  wrapper: {
    layout: BlockLayoutStyle;
    size: SizeStyle;
    // ...
  };
  input: {
    text: TextStyle;
    background: BackgroundStyle;
    placeholderColor: DefaultColorStyle; // 特有伪元素样式
  };
}

// 滚动容器特有样式
export interface ScrollableContainerStyles {
  container: {
    scrollbar: ScrollbarStyle; // 滚动条定制
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种&lt;strong&gt;基于 &lt;code&gt;Pick&lt;/code&gt; 和 &lt;code&gt;Omit&lt;/code&gt; 的组合式类型定义&lt;/strong&gt;，前端的属性面板（Right Sidebar）能够通过类型推导，精准地为不同组件的特定层级提供合法的样式配置控件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;架构演进细节：&lt;/strong&gt;
在这次重构中，我们对历史遗留的组件结构也进行了清理。例如，早期用于占位和布局的 &lt;code&gt;blank-container&lt;/code&gt; 组件被正式废弃，全面拥抱基于 &lt;code&gt;view&lt;/code&gt; 模板的 &lt;code&gt;CustomView&lt;/code&gt; 架构。&lt;code&gt;CustomView&lt;/code&gt; 配合全新的样式矩阵，不仅能够替代 &lt;code&gt;blank-container&lt;/code&gt; 的所有功能，还能提供更清晰的 DOM 结构和更好的样式扩展能力。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;Zion 的 &lt;code&gt;Comp Style Templates&lt;/code&gt; 重构是一次深度的 Schema 数据模型改造。&lt;/p&gt;
&lt;p&gt;它通过强类型的结构化抽象打破了纯 CSS 字符串的局限，利用 &lt;code&gt;Abstract Layer&lt;/code&gt; 与 &lt;code&gt;Instance Layer&lt;/code&gt; 的分离，以及 &lt;code&gt;Template &amp;lt; Breakpoint &amp;lt; Variant&lt;/code&gt; 的多维矩阵模型，解决了样式复用与 JSON 膨胀问题。配合底层严格的 &lt;code&gt;Component-Property&lt;/code&gt; 类型矩阵和对 &lt;code&gt;CustomView&lt;/code&gt; 等现代组件架构的升级，这套基建为 Zion 未来更复杂的 UI 搭建需求（如多端适配、设计系统导入）提供了底层支持。&lt;/p&gt;
</content:encoded></item><item><title>基于 SWC/Rust 的前端工具链演进浅析</title><link>https://nollieleo.github.io/posts/rust-based-frontend-tooling/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/rust-based-frontend-tooling/</guid><description>从 Babel 和 Webpack 到 SWC 与 Rolldown，探讨 Rust 在前端基础设施重构中的性能降维优势。</description><pubDate>Fri, 10 Jan 2025 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前端工具链正在经历一场由底层系统语言驱动的重构浪潮。随着大型 Monorepo 规模的膨胀，基于 V8 引擎执行的 Node.js 工具在内存碎片处理和单线程执行效能上已经触及了物理瓶颈。Rust 凭借其内存安全特性和极高的执行效率，成为了下一代前端基础设施的首选。&lt;/p&gt;
&lt;h2&gt;1. 性能瓶颈：V8 GC 与单线程限制&lt;/h2&gt;
&lt;p&gt;传统的 JavaScript 工具（如 Babel, ESLint）运行在 Node.js 环境中。虽然 V8 引擎极其强大，但在处理数万个文件的 AST (Abstract Syntax Tree) 转换时，面临两个核心问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;垃圾回收 (GC) 压力&lt;/strong&gt;：频繁创建和销毁 AST 节点会导致大量的内存分配，触发 V8 的 Stop-the-world GC，造成明显的卡顿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单线程模型&lt;/strong&gt;：虽然 Node.js 有 Worker Threads，但在 JS 层进行大规模数据交换的开销抵消了并行计算的收益。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. Rust 的底层优势：内存安全与并行化&lt;/h2&gt;
&lt;p&gt;Rust 通过所有权 (Ownership) 系统在编译期管理内存，消除了运行时的 GC 开销。在前端编译场景下，这意味着更稳定的内存占用和更快的执行速度。&lt;/p&gt;
&lt;h3&gt;2.1 SWC 的架构优势&lt;/h3&gt;
&lt;p&gt;SWC (Speedy Web Compiler) 是 Babel 的 Rust 替代品。它利用了 Rust 的以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rayon 并行计算&lt;/strong&gt;：自动将 AST 遍历任务分发到所有 CPU 核心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SIMD 指令集优化&lt;/strong&gt;：在字符串处理和哈希计算中使用单指令多数据流优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A[Source Code] --&amp;gt; B{Parser}
    B --&amp;gt;|JS/V8| C[Babel AST - Single Threaded]
    B --&amp;gt;|Rust/SWC| D[SWC AST - Multi-threaded]
    C --&amp;gt; E[GC Overhead]
    D --&amp;gt; F[Zero-copy Transformation]
    E --&amp;gt; G[Slow Build]
    F --&amp;gt; H[Fast Build]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 零拷贝解析与内存布局&lt;/h3&gt;
&lt;p&gt;Rust 允许开发者精细控制数据的内存布局。SWC 在解析代码时，尽量使用 &lt;code&gt;&amp;amp;str&lt;/code&gt; 切片引用原始文件内容，而不是为每个 Token 创建新的字符串对象。这种“零拷贝”思路极大地减少了内存分配次数。&lt;/p&gt;
&lt;h2&gt;3. 现代工具链对比：Rspack 与 Rolldown&lt;/h2&gt;
&lt;p&gt;目前业界正在从“JS 编写的工具”转向“Rust 编写的核心 + JS 插件系统”。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Webpack&lt;/th&gt;
&lt;th&gt;Rspack&lt;/th&gt;
&lt;th&gt;Rolldown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;核心语言&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;兼容性&lt;/td&gt;
&lt;td&gt;完整生态&lt;/td&gt;
&lt;td&gt;兼容 Webpack 插件&lt;/td&gt;
&lt;td&gt;兼容 Rollup API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;较慢 (依赖持久化缓存)&lt;/td&gt;
&lt;td&gt;极快 (原生多线程)&lt;/td&gt;
&lt;td&gt;极快 (旨在统一 Vite 生产/开发)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.1 Rspack 配置示例&lt;/h3&gt;
&lt;p&gt;Rspack 旨在提供 Webpack 的平替体验，同时将构建速度显著提升。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rspack.config.js
module.exports = {
  entry: { main: &apos;./src/index.tsx&apos; },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: &apos;builtin:swc-loader&apos;,
          options: {
            jsc: {
              parser: { syntax: &apos;typescript&apos;, tsx: true },
              transform: { react: { runtime: &apos;automatic&apos; } }
            }
          }
        }
      }
    ]
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：V8 与 Rust 的跨语言通信灾难 (FFI 瓶颈)&lt;/h2&gt;
&lt;p&gt;很多公司看到 SWC 和 Rspack 这么快，兴冲冲地把业务项目里的 Webpack 换掉，结果发现：&lt;strong&gt;构建速度不仅没变快，反而更慢了！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是因为，他们虽然用了 Rust 写的核心构建引擎，但业务代码里配置了大量&lt;strong&gt;自己用 JavaScript 写的 Custom Loader（比如某个把 &lt;code&gt;.vue&lt;/code&gt; 转成 JS 的私有 Loader）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;4.1 昂贵的序列化开销&lt;/h3&gt;
&lt;p&gt;当 Rust 引擎在多线程极速解析 AST 时，突然遇到了一个 JS Loader。
此时，Rust 必须停下来，把当前文件的源码、AST 和上下文配置&lt;strong&gt;序列化成字符串（JSON）&lt;/strong&gt;，通过 NAPI-RS (Node.js API for Rust) 发送给 V8 引擎。
V8 收到字符串后，&lt;strong&gt;反序列化&lt;/strong&gt;，单线程跑完 JS 代码，然后再&lt;strong&gt;序列化&lt;/strong&gt;回传给 Rust。&lt;/p&gt;
&lt;p&gt;在这个过程中，原本 Rust &quot;零拷贝&quot;带来的性能红利，被这来回跨越语言边界（Foreign Function Interface, FFI）的极其昂贵的拷贝和反序列化开销彻底吞噬！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：全链路 Rust 化 (Built-in Loaders)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了解决这个问题，Rspack 和 SWC 提出了终极方案：&lt;strong&gt;将常用的 Loader 用 Rust 重写，内置进引擎中（builtin:swc-loader）。&lt;/strong&gt;
如果你想让构建快到起飞，你必须尽量少用、甚至不用任何 JS 写的 Loader 和 Plugin，而是尽可能寻找其对应的 Rust 替代品。&lt;/p&gt;
&lt;p&gt;这就是为什么这几年，前端圈疯狂用 Rust 重写各种工具（如 Biome 替代 Prettier+ESLint，Oxc 替代 Babel parser）的根本原因——&lt;strong&gt;只有当整个工具链都在 Rust 内存空间内闭环流转，不再回传给 V8 时，前端基建的性能天花板才会被真正打破。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;5. 演进趋势：从脚本化到工程化&lt;/h2&gt;
&lt;p&gt;前端工程化正式从松散的 JS 脚本执行，走向更底层、更注重系统级内存与多线程调度的编译新时代。虽然 Rust 增加了工具链的开发门槛，但其带来的构建效率提升直接转化为开发者的生产力。未来，诸如类型检查（如 STC）、代码格式化（如 Biome）等领域都将全面拥抱 Rust。&lt;/p&gt;
</content:encoded></item><item><title>状态机 (XState) 在复杂组件交互中的应用</title><link>https://nollieleo.github.io/posts/xstate-state-machines-in-frontend/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/xstate-state-machines-in-frontend/</guid><description>分析大量 boolean 状态变量带来的维护噩梦，介绍有限状态机如何规范前端逻辑分支。</description><pubDate>Wed, 25 Dec 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在处理复杂的前端 UI 交互时，开发者经常面临逻辑分支爆炸的问题。多步骤表单、复杂的拖拽引擎或带有权限判断的支付链路，如果仅依赖离散的 &lt;code&gt;boolean&lt;/code&gt; 变量（如 &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt;），会导致代码难以维护且极易产生非法状态。&lt;/p&gt;
&lt;h2&gt;1. 离散变量引发的“不可能状态”&lt;/h2&gt;
&lt;p&gt;常规的 React &lt;code&gt;useState&lt;/code&gt; 模式通常如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [data, setData] = useState(null);

// 逻辑漏洞：可能同时存在 isLoading 为 true 且 isError 为 true 的情况
if (isLoading &amp;amp;&amp;amp; isError) {
  // 这种物理意义上不可能存在的状态组合，在逻辑上却被允许了
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种隐式的逻辑缺陷随着业务迭代会演变成“意大利面条”代码。有限状态机 (Finite State Machine, FSM) 提供了一种数学模型，通过显式定义状态和转换路径来消除这些隐患。&lt;/p&gt;
&lt;h2&gt;2. 有限状态机 (FSM) 的数学模型&lt;/h2&gt;
&lt;p&gt;一个标准的 FSM 由五个核心要素组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;States (S)&lt;/strong&gt;：系统所有可能存在的有限状态集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events (E)&lt;/strong&gt;：触发状态转换的输入信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transitions (T)&lt;/strong&gt;：定义在特定状态下接收特定事件后，系统应迁移到的新状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actions (A)&lt;/strong&gt;：在状态转换或进入/退出状态时执行的副作用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context (C)&lt;/strong&gt;：状态机携带的扩展数据（在 XState 中称为 Extended State）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; idle
    idle --&amp;gt; loading : FETCH
    loading --&amp;gt; success : RESOLVE
    loading --&amp;gt; failure : REJECT
    failure --&amp;gt; loading : RETRY
    success --&amp;gt; [*]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. XState 核心实现与分层架构&lt;/h2&gt;
&lt;p&gt;XState 不仅仅是 FSM 的实现，它还支持分层状态机 (Hierarchical State Machines) 和并行状态 (Parallel States)。&lt;/p&gt;
&lt;h3&gt;3.1 状态机配置示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { createMachine, assign } from &apos;xstate&apos;;

interface FetchContext {
  retries: number;
  errorMessage?: string;
}

const fetchMachine = createMachine&amp;lt;FetchContext&amp;gt;({
  id: &apos;fetch&apos;,
  initial: &apos;idle&apos;,
  context: { retries: 0 },
  states: {
    idle: {
      on: { FETCH: &apos;loading&apos; }
    },
    loading: {
      on: {
        RESOLVE: &apos;success&apos;,
        REJECT: {
          target: &apos;failure&apos;,
          actions: assign({
            errorMessage: (_, event: any) =&amp;gt; event.data
          })
        }
      }
    },
    failure: {
      on: {
        RETRY: {
          target: &apos;loading&apos;,
          cond: (ctx) =&amp;gt; ctx.retries &amp;lt; 3,
          actions: assign({ retries: (ctx) =&amp;gt; ctx.retries + 1 })
        }
      }
    },
    success: { type: &apos;final&apos; }
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 React 集成&lt;/h3&gt;
&lt;p&gt;通过 &lt;code&gt;@xstate/react&lt;/code&gt; 提供的 &lt;code&gt;useMachine&lt;/code&gt; 钩子，可以将控制流逻辑与 UI 渲染彻底解耦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useMachine } from &apos;@xstate/react&apos;;

function DataFetcher() {
  const [state, send] = useMachine(fetchMachine);

  return (
    &amp;lt;div&amp;gt;
      {state.matches(&apos;idle&apos;) &amp;amp;&amp;amp; &amp;lt;button onClick={() =&amp;gt; send(&apos;FETCH&apos;)}&amp;gt;Start&amp;lt;/button&amp;gt;}
      {state.matches(&apos;loading&apos;) &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Loading... (Attempt: {state.context.retries})&amp;lt;/p&amp;gt;}
      {state.matches(&apos;failure&apos;) &amp;amp;&amp;amp; (
        &amp;lt;button onClick={() =&amp;gt; send(&apos;RETRY&apos;)}&amp;gt;Retry&amp;lt;/button&amp;gt;
      )}
      {state.matches(&apos;success&apos;) &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Data loaded successfully.&amp;lt;/p&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：Context 滥用与状态黑洞&lt;/h2&gt;
&lt;p&gt;XState 最容易被新手误用的地方，就是把它当成 Redux 来用，把所有业务数据都塞进 &lt;code&gt;context&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;陷阱：&lt;/strong&gt;
假设你在做一个电商系统，你把长达 1000 条的商品列表数组存进了 XState 的 &lt;code&gt;context.productList&lt;/code&gt; 中。
XState 的哲学是“不可变数据（Immutable）”。每次状态机发生转换（哪怕只是从 &lt;code&gt;idle&lt;/code&gt; 变成 &lt;code&gt;loading&lt;/code&gt;），底层的解释器都会对整个 &lt;code&gt;context&lt;/code&gt; 进行浅拷贝更新。如果你的 &lt;code&gt;context&lt;/code&gt; 里塞满了巨大的业务对象，不仅会导致严重的性能开销（内存抖动），还会让状态机的调试器面板直接卡死。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践：分离“控制状态”与“数据状态”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;状态机只应该管理&lt;strong&gt;控制流（Control Flow）&lt;/strong&gt;，而不应该管理&lt;strong&gt;数据流（Data Flow）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应该放进 XState Context 的&lt;/strong&gt;：重试次数（retries）、当前选中的步骤索引（stepIndex）、表单的校验有效性（isValid）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不应该放进 XState Context 的&lt;/strong&gt;：巨大的 JSON 列表、富文本编辑器的内部实例、File 对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于巨大的业务数据，最佳的做法是：&lt;strong&gt;用 XState 控制状态，用 Zustand/React Context 存储数据。&lt;/strong&gt; XState 的 &lt;code&gt;actions&lt;/code&gt; 负责触发数据层的更新。&lt;/p&gt;
&lt;h2&gt;5. React 18 严格模式下的并发陷阱&lt;/h2&gt;
&lt;p&gt;在 React 18 的 Strict Mode 下，组件在开发环境会被挂载、卸载、再挂载。这对于普通的 &lt;code&gt;useState&lt;/code&gt; 影响不大，但对于 &lt;code&gt;useMachine&lt;/code&gt; 却是致命的。&lt;/p&gt;
&lt;p&gt;如果你的状态机在 &lt;code&gt;entry&lt;/code&gt; 动作中包含了一个发送网络请求的 &lt;code&gt;invoke&lt;/code&gt; 服务，那么在 Strict Mode 下，这个网络请求会被&lt;strong&gt;意外地发送两次&lt;/strong&gt;，甚至导致状态机收到乱序的回调而卡在死胡同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;避免在极短的 entry 动作中做不可逆的副作用&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用单例模式或 &lt;code&gt;spawn&lt;/code&gt;&lt;/strong&gt;：如果某个状态机掌管的是全局级或模块级的核心逻辑，不要在组件树深处使用 &lt;code&gt;useMachine&lt;/code&gt; 动态创建它。应该在组件外部使用 &lt;code&gt;createActor&lt;/code&gt; 实例化它，然后在 React 中通过 &lt;code&gt;@xstate/react&lt;/code&gt; 的 &lt;code&gt;useActor&lt;/code&gt; 引入这个单例的引用。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// store/authMachine.ts (组件外部)
import { createActor } from &apos;xstate&apos;;
import { authMachine } from &apos;./machine&apos;;

// 全局单例 Actor
export const authActor = createActor(authMachine).start();

// Component.tsx
import { useSelector } from &apos;@xstate/react&apos;;
import { authActor } from &apos;./store/authMachine&apos;;

function UserProfile() {
  // 只订阅自己关心的状态，避免由于其他状态变更导致组件无意义的重渲染
  const isReady = useSelector(authActor, (state) =&amp;gt; state.matches(&apos;ready&apos;));
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 架构优势与确定性&lt;/h2&gt;
&lt;p&gt;引入状态机后，前端架构从“基于变量的条件判断”转向了“基于状态的驱动模型”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;消除非法状态&lt;/strong&gt;：系统永远处于定义的有效状态之一，不存在 &lt;code&gt;loading&lt;/code&gt; 且 &lt;code&gt;error&lt;/code&gt; 的中间态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑可视化&lt;/strong&gt;：XState 提供的可视化工具可以将代码直接转换为状态图，方便产品经理与开发人员对齐逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可测试性&lt;/strong&gt;：状态转换是纯函数式的，可以通过编写针对事件序列的测试用例，覆盖所有可能的交互路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种模式在处理如低代码编辑器、复杂审批流等高交互场景时，能显著降低系统的认知负荷。&lt;/p&gt;
</content:encoded></item><item><title>前端监控与异常上报体系建设实践</title><link>https://nollieleo.github.io/posts/frontend-monitoring-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/frontend-monitoring-architecture/</guid><description>从底层错误捕获 API 到 SourceMap 解析，梳理如何构建高可用、无死角的线上稳定性监控体系。</description><pubDate>Sun, 08 Dec 2024 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;前端异常监控系统的核心，不仅在于拦截报错，更在于建立完整的运行时上下文恢复体系，以便开发者能快速复现与定位。一个成熟的监控体系应涵盖：错误捕获、性能度量、行为追踪以及自动化告警。&lt;/p&gt;
&lt;h2&gt;1. 全方位错误捕获策略&lt;/h2&gt;
&lt;p&gt;在复杂的 Web 应用中，异常可能来自 JS 引擎、网络请求、资源加载或异步任务。我们需要构建多层拦截机制。&lt;/p&gt;
&lt;h3&gt;JS 运行时错误与资源加载异常&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;window.onerror&lt;/code&gt; 只能捕获 JS 错误，而 &lt;code&gt;addEventListener(&apos;error&apos;)&lt;/code&gt; 在捕获阶段可以拦截到资源（如图片、脚本）加载失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;error&apos;, (event) =&amp;gt; {
  const target = event.target;
  // 区分资源加载错误与 JS 运行时错误
  if (target &amp;amp;&amp;amp; (target.src || target.href)) {
    report({
      type: &apos;RESOURCE_ERROR&apos;,
      url: target.src || target.href,
      tagName: target.tagName
    });
  } else {
    report({
      type: &apos;JS_ERROR&apos;,
      message: event.message,
      stack: event.error?.stack
    });
  }
}, true); // 必须在捕获阶段监听
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;未处理的 Promise 拒绝&lt;/h3&gt;
&lt;p&gt;现代应用大量使用 &lt;code&gt;async/await&lt;/code&gt;，未被 &lt;code&gt;catch&lt;/code&gt; 的 Promise 异常需要通过 &lt;code&gt;unhandledrejection&lt;/code&gt; 捕获。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;unhandledrejection&apos;, (event) =&amp;gt; {
  report({
    type: &apos;PROMISE_ERROR&apos;,
    reason: event.reason?.message || event.reason,
    stack: event.reason?.stack
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;跨域脚本错误 (Script Error)&lt;/h3&gt;
&lt;p&gt;如果加载了 CDN 上的第三方脚本且未配置 CORS，&lt;code&gt;onerror&lt;/code&gt; 只能拿到 &quot;Script error.&quot;。
&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务端设置 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;客户端 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 标签添加 &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 性能监控：Web Vitals&lt;/h2&gt;
&lt;p&gt;除了错误，性能也是稳定性的重要组成部分。利用 &lt;code&gt;PerformanceObserver&lt;/code&gt; API，我们可以收集核心指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCP (First Contentful Paint)&lt;/strong&gt;：首次内容绘制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LCP (Largest Contentful Paint)&lt;/strong&gt;：最大内容绘制，衡量加载性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLS (Cumulative Layout Shift)&lt;/strong&gt;：累计布局偏移，衡量视觉稳定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;new PerformanceObserver((entryList) =&amp;gt; {
  for (const entry of entryList.getEntries()) {
    if (entry.name === &apos;largest-contentful-paint&apos;) {
      reportPerformance(&apos;LCP&apos;, entry.startTime);
    }
  }
}).observe({ type: &apos;largest-contentful-paint&apos;, buffered: true });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 行为追踪与面包屑 (Breadcrumbs)&lt;/h2&gt;
&lt;p&gt;单纯的堆栈信息往往不足以定位复杂问题。我们需要记录用户在报错前的操作流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由跳转&lt;/strong&gt;：监听 &lt;code&gt;popstate&lt;/code&gt; 或覆写 &lt;code&gt;pushState&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户交互&lt;/strong&gt;：全局监听 &lt;code&gt;click&lt;/code&gt; 事件，记录目标元素的 ID 或 Class。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口请求&lt;/strong&gt;：覆写 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 和 &lt;code&gt;fetch&lt;/code&gt;，记录请求 URL 和状态码。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    UserAction[用户点击/输入] --&amp;gt; Breadcrumbs[面包屑队列]
    Network[接口请求] --&amp;gt; Breadcrumbs
    Route[路由切换] --&amp;gt; Breadcrumbs
    ErrorOccur{发生异常}
    Breadcrumbs --&amp;gt; ErrorOccur
    ErrorOccur --&amp;gt; Report[上报: 错误堆栈 + 面包屑]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. SourceMap 反解析体系&lt;/h2&gt;
&lt;p&gt;生产环境的代码经过压缩混淆，报错堆栈通常形如 &lt;code&gt;at a.b (main.js:1:350)&lt;/code&gt;。要还原源码，必须在服务端进行 SourceMap 解析。&lt;/p&gt;
&lt;h3&gt;解析流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建阶段&lt;/strong&gt;：CI 流水线生成 &lt;code&gt;.map&lt;/code&gt; 文件，并将其上传至内部监控中心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上报阶段&lt;/strong&gt;：SDK 上报原始堆栈及版本号（Release ID）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;还原阶段&lt;/strong&gt;：监控平台利用 &lt;code&gt;source-map&lt;/code&gt; 库，结合 VLQ 编码的映射表，将行列号转换为源码位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 服务端伪代码
const sourceMap = require(&apos;source-map&apos;);
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
const originalPos = consumer.originalPositionFor({
  line: 1,
  column: 350
});
console.log(originalPos.source, originalPos.line, originalPos.name);
// 输出: src/utils/auth.ts, 42, login
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：SourceMap 的安全分发与线上反解机制&lt;/h2&gt;
&lt;p&gt;很多公司在做前端监控时，最纠结的就是 SourceMap。
如果把 &lt;code&gt;.map&lt;/code&gt; 文件直接跟着静态资源一起部署到线上的 CDN，等于向全世界开源了整个前端项目的未混淆源码。
但如果不上传，在 Sentry 或自研监控后台看到的堆栈全都是 &lt;code&gt;at a.b (main.js:1:350)&lt;/code&gt;，排查难度堪比看天书。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解决方案：自动化构建隔离传输与隐式映射&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在现代前端监控架构中，SourceMap 的生成与线上应用必须严格物理隔离。典型的 CI/CD 流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构建阶段（生成与剥离）&lt;/strong&gt;
在执行 &lt;code&gt;webpack&lt;/code&gt; 或 &lt;code&gt;vite&lt;/code&gt; 打包时，开启 SourceMap 生成。打包完成后，运行一个后置脚本（Post-build Script）。
这个脚本负责遍历 &lt;code&gt;dist/&lt;/code&gt; 目录下所有的 &lt;code&gt;.js&lt;/code&gt; 文件，利用正则 &lt;strong&gt;强行剥离&lt;/strong&gt; 末尾的 &lt;code&gt;//# sourceMappingURL=...&lt;/code&gt; 注释。
这样，浏览器在执行线上代码时，根本不知道 SourceMap 的存在，更不会尝试去下载它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分发阶段（私有上传与清洗）&lt;/strong&gt;
在剥离注释之前，脚本已经将所有的 &lt;code&gt;.map&lt;/code&gt; 文件与其对应的 &lt;code&gt;.js&lt;/code&gt; 文件名（通常带有 Hash，如 &lt;code&gt;main.a1b2c3d4.js.map&lt;/code&gt;）打包上传到了&lt;strong&gt;仅内网可访问的监控分析后台&lt;/strong&gt;（或 Sentry 服务器）。
上传成功后，立刻在 CI 服务器的本地 &lt;code&gt;dist/&lt;/code&gt; 目录中删除所有 &lt;code&gt;.map&lt;/code&gt; 文件。最后再把干净的 &lt;code&gt;dist/&lt;/code&gt; 推送到线上的生产环境 CDN。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;反解阶段（隐式匹配与栈还原）&lt;/strong&gt;
当线上用户触发了报错，前端的监控 SDK 捕获到了混淆的错误堆栈：
&lt;code&gt;at a.b (https://cdn.example.com/assets/main.a1b2c3d4.js:1:350)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;SDK 将这个堆栈发送给监控后台。
监控后台收到堆栈后，提取出报错的脚本 URL 中的文件名 &lt;code&gt;main.a1b2c3d4.js&lt;/code&gt;。
接着，后台在其内部的高速存储（如 MinIO 或 Redis）中查找之前 CI 上传的对应的 &lt;code&gt;main.a1b2c3d4.js.map&lt;/code&gt;。
找到后，利用 Mozilla 的 &lt;code&gt;source-map&lt;/code&gt; 库，将 &lt;code&gt;line: 1, column: 350&lt;/code&gt; 还原成对应的源码位置，并将翻译后的清爽堆栈呈现给开发者。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套机制既保证了线上源码的绝对安全，又完美保留了快速定位 Bug 的能力，是构建企业级监控体系的必经之路。&lt;/p&gt;
&lt;h2&gt;5. 数据清洗与告警策略&lt;/h2&gt;
&lt;p&gt;大量的监控数据需要有效的处理策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;聚合去重&lt;/strong&gt;：根据错误类型、消息和堆栈生成哈希值，将相同的错误聚合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;采样 (Sampling)&lt;/strong&gt;：对于高频的性能数据或资源错误，按比例上报以节省资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分级告警&lt;/strong&gt;：基于错误增长率、影响用户数设置阈值，实时通知。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;构建前端监控体系是一项系统工程。它涵盖了底层的异常拦截、中层的上下文关联，以及后层的自动化解析与分析。这套完整的机制有助于在用户反馈之前，先一步发现并定位线上问题。&lt;/p&gt;
</content:encoded></item><item><title>现代前端 Monorepo 架构演进：基于 Turborepo 与 pnpm</title><link>https://nollieleo.github.io/posts/modern-monorepo-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/modern-monorepo-architecture/</guid><description>探讨在复杂业务场景下，如何利用 pnpm workspace 和 Turborepo 构建高效、可扩展的现代前端 Monorepo 架构，解决依赖地狱与构建缓慢的痛点。</description><pubDate>Wed, 20 Nov 2024 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;随着前端业务复杂度的指数级增长，多项目（Multi-repo）管理模式的弊端日益凸显：代码复用困难、依赖版本不一致、跨项目重构成本极高。Monorepo（单体仓库）架构因此成为了现代前端工程化的标配。&lt;/p&gt;
&lt;p&gt;在 2024 年的今天，&lt;code&gt;pnpm&lt;/code&gt; + &lt;code&gt;Turborepo&lt;/code&gt; 已经成为了构建前端 Monorepo 的黄金组合。本文将深入探讨这套架构的优势及其实战落地经验。&lt;/p&gt;
&lt;h2&gt;1. 核心基石：pnpm Workspace&lt;/h2&gt;
&lt;p&gt;在包管理工具的演进史中，npm 和 Yarn 早期采用的扁平化 &lt;code&gt;node_modules&lt;/code&gt; 结构虽然解决了嵌套过深的问题，但也引入了“幽灵依赖”（Phantom Dependencies）的隐患。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pnpm&lt;/code&gt; 通过基于内容寻址的全局存储（Store）和软链接（Symlink）机制，完美解决了这一问题。在 Monorepo 场景下，&lt;code&gt;pnpm workspace&lt;/code&gt; 提供了极其优雅的多包管理能力。&lt;/p&gt;
&lt;h3&gt;配置 Workspace&lt;/h3&gt;
&lt;p&gt;在项目根目录创建 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packages:
  - &apos;apps/*&apos;      # 业务应用层
  - &apos;packages/*&apos;  # 共享基础层
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;workspace:*&lt;/code&gt; 协议，我们可以轻松实现内部包的相互引用，且保证在本地开发时始终使用最新代码，无需繁琐的 &lt;code&gt;npm link&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/package.json
{
  &quot;dependencies&quot;: {
    &quot;@my-org/ui&quot;: &quot;workspace:*&quot;,
    &quot;@my-org/utils&quot;: &quot;workspace:*&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 构建加速器：Turborepo 深度解析&lt;/h2&gt;
&lt;p&gt;Monorepo 最大的挑战在于构建性能。当仓库中包含数十个包时，全量构建的时间是不可接受的。Vercel 开源的 Turborepo 通过智能的任务编排和缓存机制，显著提升了构建速度。&lt;/p&gt;
&lt;h3&gt;任务拓扑排序&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;turbo.json&lt;/code&gt; 中，我们可以定义任务之间的依赖关系。Turborepo 会根据依赖图（Dependency Graph）最大化地并行执行任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;pipeline&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;], // 依赖于所有内部依赖包的 build 任务
      &quot;outputs&quot;: [&quot;dist/**&quot;, &quot;.next/**&quot;]
    },
    &quot;lint&quot;: {
      &quot;dependsOn&quot;: [] // 无依赖，可立即并行执行
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false,
      &quot;persistent&quot;: true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;远程缓存（Remote Caching）&lt;/h3&gt;
&lt;p&gt;Turborepo 最具优势的特性是缓存。它不仅能在本地缓存构建产物，还能通过 Vercel 或自定义的 Remote Cache Server 在团队成员和 CI/CD 之间共享缓存。&lt;/p&gt;
&lt;p&gt;如果同事 A 已经构建过某个特定的 commit，同事 B 拉取代码后执行 &lt;code&gt;turbo build&lt;/code&gt;，耗时将从几分钟缩短至几百毫秒（Cache Hit）。&lt;/p&gt;
&lt;h2&gt;3. 业务踩坑：Turborepo 的缓存穿透与隔离部署&lt;/h2&gt;
&lt;p&gt;Turborepo 虽然快得像魔法，但如果配置不当，在企业级 CI/CD 中很容易翻车。&lt;/p&gt;
&lt;h3&gt;3.1 幽灵构建 (Ghost Builds) 与环境污染&lt;/h3&gt;
&lt;p&gt;在前后端分离的项目中，我们经常使用环境变量（如 &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt;）来区分测试环境和生产环境。
假设你在本地跑了 &lt;code&gt;turbo build&lt;/code&gt;，打包出了一个连着&lt;strong&gt;测试后端&lt;/strong&gt;的产物。
这时，CI 服务器开始打包&lt;strong&gt;生产环境&lt;/strong&gt;，Turborepo 计算了一下源码 Hash：“咦，源码没变，直接命中缓存！”
结果是：&lt;strong&gt;生产环境被部署了一个连着测试后端的包。这是致命的生产事故。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;破局方案：显式声明环境变量依赖&lt;/strong&gt;
你必须在 &lt;code&gt;turbo.json&lt;/code&gt; 中，将所有会影响构建产物内容的环境变量声明在 &lt;code&gt;env&lt;/code&gt; 字段中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;pipeline&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],
      &quot;outputs&quot;: [&quot;dist/**&quot;, &quot;.next/**&quot;],
      // 告诉 Turbo：如果下面任何一个环境变量变了，缓存立刻失效，必须重新打包！
      &quot;env&quot;: [
        &quot;NODE_ENV&quot;,
        &quot;NEXT_PUBLIC_API_URL&quot;,
        &quot;CI&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在 &lt;code&gt;.env&lt;/code&gt; 或 &lt;code&gt;package.json&lt;/code&gt; 同级放一个 &lt;code&gt;.env.local&lt;/code&gt; 文件，并在 &lt;code&gt;turbo.json&lt;/code&gt; 配置 &lt;code&gt;globalEnv&lt;/code&gt;，它会影响所有的 pipeline。&lt;/p&gt;
&lt;h3&gt;3.2 Docker 部署的痛点：依赖体积膨胀与 &lt;code&gt;turbo prune&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在 Monorepo 里单独部署某一个微应用（比如 &lt;code&gt;apps/admin&lt;/code&gt;）时，最粗暴的做法是把整个仓库的 &lt;code&gt;10GB&lt;/code&gt; 的 &lt;code&gt;node_modules&lt;/code&gt; 连同各种不相关的子包全打进一个 Docker 镜像里。
这会导致镜像体积庞大，拉取极慢。&lt;/p&gt;
&lt;p&gt;Turborepo 提供了强大的 &lt;code&gt;prune&lt;/code&gt; 命令来优雅地进行依赖瘦身。它的核心思想是：&lt;strong&gt;“只拷贝这个子应用真正依赖的源码和 package.json”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Dockerfile 的构建阶段（Builder Stage）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. Prune 阶段
FROM node:18-alpine AS pruner
WORKDIR /app
RUN yarn global add turbo
# 会在 out/ 目录下生成一个“极简版”的 Monorepo，只包含 admin 及其依赖的 packages
RUN turbo prune --scope=admin --docker

# 2. 安装与构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
# 首先只拷贝 package.json 结构（利用 Docker 缓存层加速 pnpm install）
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm &amp;amp;&amp;amp; pnpm install --frozen-lockfile

# 然后拷贝真正的源码进行构建
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=admin...

# 3. 运行阶段 (Runner)
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式构建出的 Docker 镜像，体积通常能缩减 70% 以上，且完美利用了 Docker 缓存层。&lt;/p&gt;
&lt;h2&gt;4. 架构实战：目录结构设计&lt;/h2&gt;
&lt;p&gt;一个健壮的 Monorepo 需要清晰的边界划分。以下是我们团队目前采用的标准目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    Root[Monorepo Root] --&amp;gt; Apps[apps/ 业务应用]
    Root --&amp;gt; Packages[packages/ 共享包]
    
    Apps --&amp;gt; Web[web / Next.js]
    Apps --&amp;gt; Admin[admin / Vite + React]
    Apps --&amp;gt; Docs[docs / Astro]
    
    Packages --&amp;gt; UI[ui / 组件库]
    Packages --&amp;gt; Utils[utils / 核心逻辑]
    Packages --&amp;gt; Config[config / 工程化配置]
    Packages --&amp;gt; Types[types / 全局类型]
    
    Web -.依赖.-&amp;gt; UI
    Web -.依赖.-&amp;gt; Utils
    Admin -.依赖.-&amp;gt; UI
    UI -.依赖.-&amp;gt; Config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;共享配置的最佳实践&lt;/h3&gt;
&lt;p&gt;为了保证代码风格的一致性，我们将 ESLint、Prettier、TypeScript 的配置抽离到 &lt;code&gt;packages/config&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;例如，在 &lt;code&gt;packages/config/tsconfig.base.json&lt;/code&gt; 中定义基础类型规范，然后在各个子包中继承：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// apps/web/tsconfig.json
{
  &quot;extends&quot;: &quot;@my-org/config/tsconfig.base.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;preserve&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm&lt;/code&gt; 解决了底层依赖的物理隔离与链接问题，而 &lt;code&gt;Turborepo&lt;/code&gt; 解决了上层任务的调度与缓存问题。两者的结合，使得前端团队能够在享受 Monorepo 带来的代码复用与一致性红利的同时，依然保持极佳的开发与构建体验。&lt;/p&gt;
&lt;p&gt;对于中大型前端团队而言，这套架构已经成为支撑业务快速迭代的关键基础设施。&lt;/p&gt;
</content:encoded></item><item><title>前端依赖治理：幽灵依赖的产生与 pnpm 解决方案</title><link>https://nollieleo.github.io/posts/pnpm-phantom-dependencies/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/pnpm-phantom-dependencies/</guid><description>探讨 npm 扁平化 node_modules 带来的隐患，以及 pnpm 如何通过 Symlink 和内容寻址实现严格隔离。</description><pubDate>Fri, 15 Nov 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Node.js 生态中，&lt;code&gt;node_modules&lt;/code&gt; 的结构演进史就是一部解决“依赖地狱”的斗争史。从 npm v3 开始引入的扁平化结构虽然解决了路径过长的问题，却带来了更隐蔽的工程隐患：&lt;strong&gt;幽灵依赖 (Phantom Dependencies)&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1. 扁平化结构的代价&lt;/h2&gt;
&lt;p&gt;在 npm v2 时代，依赖是嵌套安装的。如果 A 依赖 B，B 依赖 C，结构如下：
&lt;code&gt;node_modules/A/node_modules/B/node_modules/C&lt;/code&gt;。
这导致了严重的依赖冗余和 Windows 下的路径长度限制（260 字符）。&lt;/p&gt;
&lt;p&gt;为了解决这些问题，npm v3 和 Yarn 引入了 &lt;strong&gt;Hoisting (提升)&lt;/strong&gt; 机制，将所有间接依赖尽可能地安装到根目录的 &lt;code&gt;node_modules&lt;/code&gt; 中。&lt;/p&gt;
&lt;h3&gt;什么是幽灵依赖？&lt;/h3&gt;
&lt;p&gt;由于模块被提升到了顶层，开发者可以在代码中直接 &lt;code&gt;import&lt;/code&gt; 那些并未在 &lt;code&gt;package.json&lt;/code&gt; 中显式声明的库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// package.json
{
  &quot;dependencies&quot;: {
    &quot;express&quot;: &quot;^4.18.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 开发者可以直接导入 body-parser，因为它是 express 的依赖，被提升到了顶层
import bodyParser from &apos;body-parser&apos;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;风险点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不可靠性&lt;/strong&gt;：一旦 &lt;code&gt;express&lt;/code&gt; 在升级时移除了对 &lt;code&gt;body-parser&lt;/code&gt; 的依赖，你的项目会立即在本地或 CI 环境中报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本冲突&lt;/strong&gt;：如果项目中有多个库依赖了不同版本的同一个间接依赖，提升机制可能会导致版本覆盖，引发难以排查的 Bug。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. pnpm 的破局：基于 Symlink 的严格隔离&lt;/h2&gt;
&lt;p&gt;pnpm 抛弃了完全的扁平化，引入了 &lt;strong&gt;基于内容寻址的存储 (Content-addressable storage)&lt;/strong&gt; 和 &lt;strong&gt;严格的符号链接 (Symlink)&lt;/strong&gt; 机制。&lt;/p&gt;
&lt;h3&gt;pnpm 的 node_modules 布局&lt;/h3&gt;
&lt;p&gt;当你使用 pnpm 安装依赖时，根目录的 &lt;code&gt;node_modules&lt;/code&gt; 仅包含你在 &lt;code&gt;package.json&lt;/code&gt; 中声明的包。这些包实际上是到 &lt;code&gt;.pnpm&lt;/code&gt; 目录下的软链接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node_modules
├── express -&amp;gt; .pnpm/express@4.18.0/node_modules/express
└── .pnpm
    ├── express@4.18.0
    │   └── node_modules
    │       ├── express (实际文件或硬链接)
    │       └── body-parser -&amp;gt; ../../body-parser@1.20.0/node_modules/body-parser
    └── body-parser@1.20.0
        └── node_modules
            └── body-parser
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    Root[&quot;项目根目录/node_modules&quot;]
    ExpressLink[&quot;express (Symlink)&quot;]
    PnpmStore[&quot;.pnpm 虚拟存储区&quot;]
    ExpressReal[&quot;express@4.18.0/node_modules/express&quot;]
    BPReal[&quot;body-parser@1.20.0/node_modules/body-parser&quot;]
    GlobalStore[(&quot;全局内容寻址存储 (~/.pnpm-store)&quot;)]

    Root --&amp;gt; ExpressLink
    ExpressLink -.-&amp;gt; ExpressReal
    ExpressReal --&amp;gt;|依赖| BPReal
    ExpressReal --&amp;gt;|硬链接| GlobalStore
    BPReal --&amp;gt;|硬链接| GlobalStore
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心优势&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;严格限制访问&lt;/strong&gt;：由于根目录 &lt;code&gt;node_modules&lt;/code&gt; 下没有 &lt;code&gt;body-parser&lt;/code&gt;，代码中 &lt;code&gt;import &apos;body-parser&apos;&lt;/code&gt; 会直接报错。这强制开发者必须显式声明依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决多版本冗余&lt;/strong&gt;：在扁平化结构中，如果一个包无法被提升（版本冲突），它会被多次安装。pnpm 通过全局 Store 确保同一个文件在磁盘上只存在一份。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极速安装&lt;/strong&gt;：由于使用了硬链接，安装过程主要是创建链接的操作，速度显著优于 npm/yarn。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 深度解析：Symlink 寻址原理&lt;/h2&gt;
&lt;p&gt;Node.js 的模块解析算法在遇到符号链接时，会解析出其 &lt;strong&gt;真实路径 (realpath)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;express&lt;/code&gt; 尝试加载 &lt;code&gt;body-parser&lt;/code&gt; 时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它在自己的 &lt;code&gt;node_modules&lt;/code&gt;（即 &lt;code&gt;.pnpm/express@4.18.0/node_modules/&lt;/code&gt;）中寻找。&lt;/li&gt;
&lt;li&gt;找到了指向 &lt;code&gt;../../body-parser@1.20.0/...&lt;/code&gt; 的软链接。&lt;/li&gt;
&lt;li&gt;Node.js 顺着链接找到真实文件并加载。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种设计巧妙地利用了 Node.js 的原生机制，既实现了物理上的单实例存储，又实现了逻辑上的严格隔离。&lt;/p&gt;
&lt;h2&gt;4. 业务踩坑与兼容性处理&lt;/h2&gt;
&lt;p&gt;虽然 pnpm 的隔离机制极其优秀，但在老旧项目的迁移过程中，不可避免会遇到一些“历史遗留问题”。&lt;/p&gt;
&lt;h3&gt;4.1 兼容“幽灵依赖”：shamefully-hoist&lt;/h3&gt;
&lt;p&gt;很多老旧的 Webpack 插件（如 &lt;code&gt;eslint-plugin-*&lt;/code&gt; 或某些 Babel preset）在内部硬编码了寻找子依赖的逻辑，或者默认其子依赖一定会被提升到根目录。&lt;/p&gt;
&lt;p&gt;在迁移 pnpm 时，这些工具往往会报 &lt;code&gt;Module not found&lt;/code&gt;。作为临时过渡方案，你可以配置 &lt;code&gt;.npmrc&lt;/code&gt; 放宽隔离限制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .npmrc
# 创建半隔离的提升结构，让不规范的第三方包能找到依赖
shamefully-hoist=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但请注意，这是&lt;strong&gt;饮鸩止渴&lt;/strong&gt;。长期来看，应该通过显式声明依赖或使用 &lt;code&gt;packageExtensions&lt;/code&gt; 来彻底解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// package.json (pnpm 的补丁机制)
&quot;pnpm&quot;: {
  &quot;packageExtensions&quot;: {
    &quot;bad-webpack-plugin@1.0.0&quot;: {
      &quot;peerDependencies&quot;: {
        &quot;webpack&quot;: &quot;^4.0.0&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 Peer Dependencies 的严格校验&lt;/h3&gt;
&lt;p&gt;在 npm/Yarn 中，如果 A 依赖 React 17，B 依赖 React 18，包管理器可能会强行提升一个版本，导致另一个组件在运行时崩溃。&lt;/p&gt;
&lt;p&gt;pnpm 处理 &lt;code&gt;peerDependencies&lt;/code&gt; 的策略极其严格：&lt;strong&gt;它会为不同的 Peer 依赖组合，创建不同的虚拟实例。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如，如果两个不同的包同时依赖了 &lt;code&gt;button-ui&lt;/code&gt;，但提供了不同版本的 &lt;code&gt;react&lt;/code&gt; 作为 peer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.pnpm
├── button-ui@1.0.0(react@17.0.2)
└── button-ui@1.0.0(react@18.2.0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pnpm 会在 &lt;code&gt;.pnpm&lt;/code&gt; 目录下创建两个 &lt;strong&gt;独立的硬链接实例&lt;/strong&gt;。这完美解决了 React Hooks 中的 &lt;code&gt;Invalid hook call&lt;/code&gt;（通常由于项目中存在多个 React 实例导致）问题。&lt;/p&gt;
&lt;p&gt;但这也意味着，在开发 Monorepo 组件库时，如果你没有在子包中正确声明 &lt;code&gt;peerDependencies&lt;/code&gt;，pnpm 绝对不会像 Yarn 那样“默默帮你找齐”，而是直接抛出错误。这就倒逼开发者必须保证依赖声明的绝对严谨。&lt;/p&gt;
&lt;h2&gt;5. 依赖治理的最佳实践&lt;/h2&gt;
&lt;p&gt;在使用 pnpm 构建大型 Monorepo 或复杂项目时，建议遵循以下原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;严格模式&lt;/strong&gt;：保持 &lt;code&gt;hoist=false&lt;/code&gt;（pnpm 默认行为），确保依赖边界清晰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peer Dependencies 处理&lt;/strong&gt;：pnpm 会为不同的 peer 依赖组合创建不同的虚拟路径，这解决了 npm 中长期存在的 peer 依赖冲突问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;利用 pnpm-workspace.yaml&lt;/strong&gt;：在多包管理中，通过 &lt;code&gt;workspace:*&lt;/code&gt; 协议引用内部包，确保版本同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;幽灵依赖是前端工程化中的隐患。pnpm 通过结构优化，并辅以 Symlink 和硬链接技术，在不牺牲性能的前提下，有效解决了依赖隔离与复用的矛盾。对于追求稳定性的中大型项目，迁移至 pnpm 是合理的架构选择。&lt;/p&gt;
</content:encoded></item><item><title>Next.js App Router 架构与服务端组件数据流</title><link>https://nollieleo.github.io/posts/nextjs-app-router-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nextjs-app-router-architecture/</guid><description>深度剖析 Next.js 从 Pages 迁移至 App Router 后，Layout 嵌套与 Server Actions 带来的开发范式变革。</description><pubDate>Mon, 28 Oct 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Next.js 的 App Router 引入了 React Server Components (RSC) 作为核心的默认架构，彻底改变了全栈 Web 应用的开发范式。与传统的 Pages Router 相比，App Router 不仅仅是路由定义的改变，更是组件渲染模型、数据获取方式以及客户端/服务端边界的一次重构。&lt;/p&gt;
&lt;h2&gt;1. 嵌套布局与局部路由 (Nested Layouts)&lt;/h2&gt;
&lt;p&gt;在早期的 Pages Router 中，页面切换往往意味着整个 React 树的重新挂载，或是需要通过自定义的高阶组件维持状态。App Router 通过文件系统路由实现了真正的嵌套布局。&lt;/p&gt;
&lt;p&gt;通过在目录中定义 &lt;code&gt;layout.tsx&lt;/code&gt;，Next.js 能够识别出哪些部分是持久化的，哪些部分是动态变化的。在路由跳转时，只有变化的 &lt;code&gt;page.tsx&lt;/code&gt; 会被重新渲染，而上层的 &lt;code&gt;layout.tsx&lt;/code&gt; 保持挂载状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/dashboard/layout.tsx
// 这是一个 Server Component，仅在服务端获取侧边栏数据
import Sidebar from &apos;./Sidebar&apos;;

export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
  const user = await fetchCurrentUser(); // 服务端直接获取数据，无需 API 中转
  
  return (
    &amp;lt;div className=&quot;flex h-screen&quot;&amp;gt;
      &amp;lt;Sidebar user={user} /&amp;gt;
      &amp;lt;main className=&quot;flex-1 overflow-y-auto p-6&quot;&amp;gt;
        {children}
      &amp;lt;/main&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种架构带来了 &lt;strong&gt;Partial Rendering (局部渲染)&lt;/strong&gt;：当用户在 &lt;code&gt;/dashboard/settings&lt;/code&gt; 和 &lt;code&gt;/dashboard/profile&lt;/code&gt; 之间切换时，&lt;code&gt;DashboardLayout&lt;/code&gt; 及其内部的 &lt;code&gt;Sidebar&lt;/code&gt; 状态（如滚动位置、输入框内容）将得以保留，仅有 &lt;code&gt;children&lt;/code&gt; 部分被重新请求和渲染。&lt;/p&gt;
&lt;h2&gt;2. React Server Components (RSC) 的运行机制&lt;/h2&gt;
&lt;p&gt;RSC 是 App Router 的灵魂。不同于传统的 SSR（服务端渲染后发送 HTML），RSC 允许组件在服务端执行并生成一种特殊的二进制格式（RSC Payload），随后在客户端进行流式解析。&lt;/p&gt;
&lt;h3&gt;RSC Payload 包含什么？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;渲染后的组件树结构&lt;/strong&gt;：描述了 UI 的层级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传递给 Client Components 的 Props&lt;/strong&gt;：序列化后的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;占位符&lt;/strong&gt;：指示哪些部分是客户端组件，需要加载对应的 JS Bundle。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Browser
    participant NextServer
    participant Database

    Browser-&amp;gt;&amp;gt;NextServer: GET /dashboard
    NextServer-&amp;gt;&amp;gt;Database: Fetch Data (Parallel)
    Database--&amp;gt;&amp;gt;NextServer: Result
    NextServer-&amp;gt;&amp;gt;NextServer: Render RSC to Payload
    NextServer--&amp;gt;&amp;gt;Browser: Stream HTML (Initial) + RSC Payload
    Browser-&amp;gt;&amp;gt;Browser: Hydrate Client Components
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式最大的优势在于 &lt;strong&gt;Zero Bundle Size&lt;/strong&gt;：所有仅在服务端运行的逻辑（如数据库驱动、大型 Markdown 解析库）都不会被包含在发送给浏览器的 JS 包中。&lt;/p&gt;
&lt;h3&gt;2.1 业务踩坑：Server 向 Client 传值的序列化黑洞&lt;/h3&gt;
&lt;p&gt;在 RSC 架构中，最让新手抓狂的报错莫过于：
&lt;code&gt;Error: Event handlers cannot be passed to Client Component props.&lt;/code&gt;
或者
&lt;code&gt;Error: Only plain objects can be passed to Client Components from Server Components.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么会报错？&lt;/strong&gt;
当你在 Server Component 里查询了数据库，拿到了一个 &lt;code&gt;user&lt;/code&gt; 对象，然后把它当做 prop 传给了一个带有 &lt;code&gt;&apos;use client&apos;&lt;/code&gt; 指令的客户端组件时，这个 &lt;code&gt;user&lt;/code&gt; 对象必须跨越 &lt;strong&gt;网络边界（Network Boundary）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Next.js 在底层必须把这个对象序列化（变成 JSON 字符串），通过 HTTP 传给浏览器，浏览器再反序列化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;支持序列化的&lt;/strong&gt;：字符串、数字、布尔值、纯对象（Plain Object）、数组、Promise（没错，Promise 可以传！）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不支持序列化的&lt;/strong&gt;：函数（Function / 方法）、Class 实例（比如 &lt;code&gt;new Date()&lt;/code&gt; 或者 Prisma 返回的带有内部方法的实体对象）。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 灾难写法
import ClientProfile from &apos;./ClientProfile&apos;;

export default async function ServerPage() {
  const user = await db.user.findUnique({ id: 1 });
  // user 如果是一个复杂的 Prisma 模型，它内部可能挂载了某些不可序列化的 getter
  // 这会导致整个页面渲染崩溃！
  return &amp;lt;ClientProfile user={user} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：DTO 转换与 &lt;code&gt;server-only&lt;/code&gt; 防御&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了防止服务端那些不可见人的秘密（比如带有密码哈希、数据库实例方法的对象）意外泄漏到客户端，最佳实践是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;永远在传递前将对象手动解构为纯对象（DTO, Data Transfer Object）。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;server-only&lt;/code&gt; 包作为防御性编程。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// lib/data.ts
import &apos;server-only&apos;; // 如果有客户端组件试图 import 这个文件，构建时直接报错！

export async function getUserDTO(id) {
  const user = await db.user.findUnique({ id });
  // 剥离敏感信息和类方法，只返回纯数据
  return {
    id: user.id,
    name: user.name,
    createdAt: user.createdAt.toISOString() // Date 转字符串
  };
}

// app/page.tsx
import ClientProfile from &apos;./ClientProfile&apos;;
import { getUserDTO } from &apos;@/lib/data&apos;;

export default async function ServerPage() {
  const safeUser = await getUserDTO(1);
  return &amp;lt;ClientProfile user={safeUser} /&amp;gt; // ✅ 安全穿越边界
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Streaming 与 Suspense&lt;/h2&gt;
&lt;p&gt;App Router 原生支持流式渲染。通过 React &lt;code&gt;Suspense&lt;/code&gt;，我们可以将页面拆分为多个独立的区块，优先渲染静态部分，而将耗时的数据获取逻辑包裹在 &lt;code&gt;Suspense&lt;/code&gt; 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Suspense } from &apos;react&apos;;
import { PostList, PostSkeleton } from &apos;./components&apos;;

export default function Page() {
  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Latest Posts&amp;lt;/h1&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;PostSkeleton /&amp;gt;}&amp;gt;
        &amp;lt;PostList /&amp;gt; {/* 内部进行异步数据获取 */}
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next.js 会立即发送页面的 HTML 骨架，并在服务端数据准备就绪后，通过同一个 HTTP 连接将剩余的 HTML 片段“推”给浏览器。这种方式显著提升了 &lt;strong&gt;TTFB (Time to First Byte)&lt;/strong&gt; 和 &lt;strong&gt;FCP (First Contentful Paint)&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;4. Server Actions：重塑数据突变&lt;/h2&gt;
&lt;p&gt;传统的表单提交需要开发者编写 API 路由、处理请求验证、执行数据库操作，然后再通过前端 &lt;code&gt;fetch&lt;/code&gt; 获取结果。Server Actions 将这一过程简化为函数调用。&lt;/p&gt;
&lt;p&gt;Server Actions 本质上是基于 HTTP POST 的 RPC（远程过程调用）。当你在客户端调用一个声明了 &lt;code&gt;&apos;use server&apos;&lt;/code&gt; 的函数时，Next.js 会自动发起一个请求，并处理参数序列化与结果返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/actions.ts
&apos;use server&apos;

import { db } from &apos;@/lib/db&apos;;
import { revalidatePath } from &apos;next/cache&apos;;

export async function createPost(formData: FormData) {
  const title = formData.get(&apos;title&apos;) as string;
  const content = formData.get(&apos;content&apos;) as string;

  // 1. 校验与持久化
  await db.post.create({ data: { title, content } });

  // 2. 触发服务端缓存失效
  // 这会通知 Next.js 重新生成 /posts 路径的 RSC Payload
  revalidatePath(&apos;/posts&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在客户端使用时，只需将其绑定到 &lt;code&gt;form&lt;/code&gt; 的 &lt;code&gt;action&lt;/code&gt; 属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/components/PostForm.tsx
import { createPost } from &apos;../actions&apos;;

export default function PostForm() {
  return (
    &amp;lt;form action={createPost}&amp;gt;
      &amp;lt;input name=&quot;title&quot; required /&amp;gt;
      &amp;lt;textarea name=&quot;content&quot; required /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;发布文章&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式不仅消除了 API 胶水代码，还支持 &lt;strong&gt;Progressive Enhancement (渐进式增强)&lt;/strong&gt;：即使在浏览器禁用 JavaScript 的情况下，表单提交依然可以通过标准的 HTML Form 机制工作。&lt;/p&gt;
&lt;h2&gt;5. 业务踩坑：丧心病狂的四层缓存体系&lt;/h2&gt;
&lt;p&gt;很多开发者从 Vite 或 CRA 转到 Next.js App Router 后，最大的疑惑就是：“我明明数据库里的数据已经改了，为什么页面刷新还是旧的？”&lt;/p&gt;
&lt;p&gt;这是因为 Next.js 默认开启了极为激进的&lt;strong&gt;四层缓存体系&lt;/strong&gt;。如果你不理解它们，你的应用将充满幽灵数据。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Request Memoization (请求记忆)&lt;/strong&gt;：
在同一个 Server Component 渲染周期内，如果你 &lt;code&gt;fetch&lt;/code&gt; 了同一个 URL 两次，Next.js 会拦截第二次请求，直接复用第一次的结果。这个缓存&lt;strong&gt;只在单次渲染期间存活&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Cache (数据缓存)&lt;/strong&gt;：
这是跨越多次请求的&lt;strong&gt;持久化缓存&lt;/strong&gt;。Next.js 拦截了原生的 &lt;code&gt;fetch&lt;/code&gt;，默认把所有返回结果存到文件系统或 CDN。除非你配置 &lt;code&gt;fetch(url, { cache: &apos;no-store&apos; })&lt;/code&gt;，否则它永远不会向源站发新请求！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full Route Cache (完整路由缓存)&lt;/strong&gt;：
如果在构建阶段，页面没有使用任何动态函数（如 &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;），Next.js 会把整个页面的 RSC Payload 和 HTML 静态化存起来（类似早期的 SSG）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Router Cache (客户端路由缓存)&lt;/strong&gt;：
这是浏览器内存里的缓存。当用户在客户端 &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; 跳转时，被访问过的 RSC Payload 会被缓存在内存中（默认存活 30 秒 到 5 分钟），按浏览器的返回键瞬间秒开。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：按需爆破缓存 (On-demand Revalidation)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在真实业务中，我们通常会在 Server Actions 里修改数据后，精准地炸掉特定的缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;use server&apos;
import { revalidateTag, revalidatePath } from &apos;next/cache&apos;;

export async function updateArticle(id, content) {
  await db.article.update(id, content);
  
  // 💣 爆破 Data Cache：重新拉取带有这个 tag 的所有 fetch 请求
  revalidateTag(`article-${id}`);
  
  // 💣 爆破 Full Route Cache 和 客户端 Router Cache
  // 让用户下次访问 /blog 列表页时看到最新数据
  revalidatePath(&apos;/blog&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;熟练掌控这四层缓存，是精通 Next.js App Router 的终极分水岭。&lt;/p&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;Next.js App Router 通过 RSC 实现了组件级的服务端渲染，通过嵌套布局优化了客户端导航体验，并通过 Server Actions 统一了前后端交互模型。虽然它引入了更高的架构复杂度（如理解 &lt;code&gt;use client&lt;/code&gt; 边界），但其带来的性能上限和开发效率提升，标志着 React 开发进入了真正的全栈时代。&lt;/p&gt;
</content:encoded></item><item><title>大模型时代的前端基建：SSE 流式输出的拦截与解析</title><link>https://nollieleo.github.io/posts/sse-websocket-llm-streaming/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/sse-websocket-llm-streaming/</guid><description>针对大语言模型 (LLM) 的交互场景，对比 WebSocket 与 SSE 协议，介绍流式数据的分块处理与 Markdown 渲染。</description><pubDate>Sat, 05 Oct 2024 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;大语言模型 (LLM) 的交互范式要求前端具备处理“流式响应”的能力。传统的 JSON 整包返回模式在面对长文本生成时，会导致明显的首字节等待（Time to First Byte, TTFB）延迟。Server-Sent Events (SSE) 凭借其轻量化和基于标准 HTTP 的特性，已成为 LLM 应用前端交互的重要组成部分。&lt;/p&gt;
&lt;h2&gt;1. 协议选型：为什么是 SSE 而非 WebSocket？&lt;/h2&gt;
&lt;p&gt;在 LLM 对话场景中，数据流向主要是单向的（服务端向客户端推送 Token）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SSE (Server-Sent Events)&lt;/th&gt;
&lt;th&gt;WebSocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;协议&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;标准 HTTP (HTTP/1.1 / HTTP/2)&lt;/td&gt;
&lt;td&gt;独立的二进制协议 (ws://)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;连接性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;单向 (Server -&amp;gt; Client)&lt;/td&gt;
&lt;td&gt;全双工 (双向)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;防火墙&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;易穿越，兼容现有 LB 和网关&lt;/td&gt;
&lt;td&gt;需特殊配置，易被代理拦截&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;重连机制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;内置自动重连&lt;/td&gt;
&lt;td&gt;需手动实现心跳与重连逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多路复用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;配合 HTTP/2 支持良好&lt;/td&gt;
&lt;td&gt;需独立连接&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于 LLM 而言，SSE 能够复用现有的鉴权（Cookie/Auth Header）体系，是一种资源消耗较低且高效的选择。&lt;/p&gt;
&lt;h2&gt;2. 前端流式解析深度实践&lt;/h2&gt;
&lt;p&gt;现代浏览器通过 &lt;code&gt;fetch&lt;/code&gt; API 的 &lt;code&gt;body&lt;/code&gt; 属性暴露了 &lt;code&gt;ReadableStream&lt;/code&gt;，允许我们在数据到达时立即处理。&lt;/p&gt;
&lt;h3&gt;2.1 核心解析逻辑&lt;/h3&gt;
&lt;p&gt;处理流式数据时，最关键的是解决 &lt;strong&gt;UTF-8 字符截断问题&lt;/strong&gt;。一个中文字符可能被拆分到两个不同的数据块中，直接解码会导致乱码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function consumeStream(response) {
  const reader = response.body.getReader();
  // TextDecoder 会自动处理跨 chunk 的字节状态
  const decoder = new TextDecoder(&apos;utf-8&apos;);
  let buffer = &apos;&apos;;

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    // 解码当前 chunk，注意 stream: true 参数
    const chunk = decoder.decode(value, { stream: true });
    buffer += chunk;

    // 处理 SSE 格式 (data: { ... }\n\n)
    const lines = buffer.split(&apos;\n&apos;);
    // 保留最后一个可能不完整的行
    buffer = lines.pop() || &apos;&apos;;

    for (const line of lines) {
      const message = line.replace(/^data: /, &apos;&apos;).trim();
      if (message === &apos;[DONE]&apos;) return;
      if (message) {
        try {
          const json = JSON.parse(message);
          updateUI(json.choices[0].delta.content);
        } catch (e) {
          console.error(&apos;Parse error&apos;, e);
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 流程时序图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant C as Client (Browser)
    participant S as Server (LLM API)
    
    C-&amp;gt;&amp;gt;S: POST /api/chat (prompt)
    S--&amp;gt;&amp;gt;C: HTTP 200 OK (Content-Type: text/event-stream)
    S--&amp;gt;&amp;gt;C: data: {&quot;content&quot;: &quot;Hello&quot;}
    Note over C: ReadableStream 拦截并解码
    C-&amp;gt;&amp;gt;C: 更新 UI (增量渲染)
    S--&amp;gt;&amp;gt;C: data: {&quot;content&quot;: &quot; world&quot;}
    S--&amp;gt;&amp;gt;C: data: [DONE]
    Note over C: 关闭 Reader
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 业务踩坑：大模型流式输出的硬核挑战&lt;/h2&gt;
&lt;p&gt;在真实的 ChatGPT 级产品中，网络的不稳定性和后端缓冲池的设计会导致无数的边界情况。&lt;/p&gt;
&lt;h3&gt;3.1 跨 Chunk 的 JSON 截断灾难&lt;/h3&gt;
&lt;p&gt;我们在前面的代码中用了 &lt;code&gt;buffer.split(&apos;\n&apos;)&lt;/code&gt; 来按行分割。但在极端的网络环境或者高并发下，TCP 报文会被无情劈开。
假设后端推送了一个包含中文字符的完整 JSON：
&lt;code&gt;data: {&quot;content&quot;: &quot;我是一个人工智能&quot;}\n\n&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;它可能被切成了两个 &lt;code&gt;Uint8Array&lt;/code&gt; Chunk 到达前端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chunk 1: &lt;code&gt;data: {&quot;content&quot;: &quot;我是一&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Chunk 2: &lt;code&gt;个人工智能&quot;}\n\n&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果前端粗暴地在 Chunk 1 到达时立刻尝试去 &lt;code&gt;JSON.parse&lt;/code&gt;，就会遭遇 &lt;code&gt;Unexpected end of JSON input&lt;/code&gt; 的致命崩溃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解决方案：双重缓冲（Buffer）+ 游标切割&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最佳实践是维护一个外部的 &lt;code&gt;string&lt;/code&gt; 缓冲区。在每次接收到 Chunk 并通过 &lt;code&gt;TextDecoder({stream: true})&lt;/code&gt;（这个 &lt;code&gt;stream: true&lt;/code&gt; 非常关键，它会把被截断的 UTF-8 中文字节保留到下一次解码中）解码后，将字符串拼接到 Buffer。
然后通过正则或循环去寻找&lt;strong&gt;完整的双换行符&lt;/strong&gt; &lt;code&gt;\n\n&lt;/code&gt;，只有找到双换行符，才把这部分字符串切下来送去 &lt;code&gt;JSON.parse&lt;/code&gt;，剩下的继续留在 Buffer 里等待下一个 Chunk。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 伪代码：稳健的切帧逻辑
let buffer = &apos;&apos;;
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  
  // 寻找 SSE 的消息边界
  let boundary = buffer.indexOf(&apos;\n\n&apos;);
  while (boundary !== -1) {
    const rawMessage = buffer.slice(0, boundary);
    buffer = buffer.slice(boundary + 2); // 截断消费过的数据
    
    if (rawMessage.startsWith(&apos;data: &apos;)) {
      const jsonStr = rawMessage.slice(6);
      if (jsonStr === &apos;[DONE]&apos;) return;
      try {
        const payload = JSON.parse(jsonStr);
        updateUI(payload.content);
      } catch(e) {
        // 如果这里还报错，说明后端的 JSON 格式本身就是坏的
      }
    }
    boundary = buffer.indexOf(&apos;\n\n&apos;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 优雅中断：AbortController 的双端协同&lt;/h3&gt;
&lt;p&gt;当模型生成到一半，用户点击了“停止生成”。如果前端只是默默地把 UI 的加载动画关掉，那是灾难性的。
后端的 GPU 会继续把剩下的 1000 个字算完，极大地浪费了昂贵的算力资源。&lt;/p&gt;
&lt;p&gt;前端&lt;strong&gt;必须&lt;/strong&gt;主动掐断 TCP 连接。在 fetch 中传入 &lt;code&gt;AbortController.signal&lt;/code&gt;，调用 &lt;code&gt;abort()&lt;/code&gt; 会直接终止底层的网络流。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();
fetch(&apos;/api/chat&apos;, { signal: controller.signal })
  // ...
  
// 用户点击停止
controller.abort(); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;后端配合&lt;/strong&gt;：在 Node.js 或 Go 等后端服务中，必须监听 Request 的 &lt;code&gt;close&lt;/code&gt; 或 &lt;code&gt;aborted&lt;/code&gt; 事件。一旦检测到客户端主动断开，立刻向下游的 LLM 引擎发送取消信号（Cancel Signal），停止推理。&lt;/p&gt;
&lt;h2&gt;4. 渲染性能与 UX 优化&lt;/h2&gt;
&lt;p&gt;流式输出通常伴随着高频的状态更新（每秒数十次），这会对前端渲染性能造成压力。&lt;/p&gt;
&lt;h3&gt;3.1 避免主线程阻塞&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增量渲染&lt;/strong&gt;：避免每次都重新解析整个 Markdown 字符串。可以使用支持增量更新的解析器，或者对输入进行节流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Worker 解析&lt;/strong&gt;：将 Markdown 转 HTML 的繁重任务移至 Web Worker 中，释放主线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 交互细节优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动滚动控制&lt;/strong&gt;：当用户手动向上滚动查看历史记录时，应暂停自动滚动到底部，避免干扰阅读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AbortController&lt;/strong&gt;：支持用户手动中断生成。通过 &lt;code&gt;controller.abort()&lt;/code&gt; 停止 fetch 请求，服务端感知到连接断开后停止模型推理。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();
fetch(&apos;/api/chat&apos;, { signal: controller.signal })
  .then(response =&amp;gt; consumeStream(response))
  .catch(err =&amp;gt; {
    if (err.name === &apos;AbortError&apos;) console.log(&apos;User cancelled&apos;);
  });

// 用户触发中断操作
// controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;SSE 流式输出已成为大模型应用的标准配置。掌握 &lt;code&gt;ReadableStream&lt;/code&gt; 的底层 API，并关注高频数据流下的渲染性能优化与异常处理机制，是构建稳定、流畅的 LLM 交互界面的关键。&lt;/p&gt;
</content:encoded></item><item><title>前端测试策略演进：从 Jest 到 Vitest 与 Playwright</title><link>https://nollieleo.github.io/posts/vitest-playwright-testing-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/vitest-playwright-testing-architecture/</guid><description>回顾前端测试框架的发展，解析 Vitest 基于 esbuild 的冷启动优势及 Playwright 现代 E2E 体系。</description><pubDate>Tue, 10 Sep 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在过去很长一段时间内，Jest 凭借其“开箱即用”的特性统治了前端测试领域。然而，随着 ESM 的普及和 Vite 等新型构建工具的兴起，基于 CommonJS 架构的 Jest 在大型项目中表现出明显的滞后性。前端测试工具链正经历一场从“模拟环境”向“原生管线”的转变。&lt;/p&gt;
&lt;h2&gt;1. Vitest：打破测试与构建的隔阂&lt;/h2&gt;
&lt;p&gt;Vitest 的核心优势在于它直接复用了 Vite 的转换器、插件系统和配置文件。&lt;/p&gt;
&lt;h3&gt;1.1 统一的转换管线&lt;/h3&gt;
&lt;p&gt;在 Jest 中，我们需要配置 &lt;code&gt;babel-jest&lt;/code&gt; 或 &lt;code&gt;ts-jest&lt;/code&gt; 来处理代码转换，这往往导致测试环境与生产环境的编译行为不一致。Vitest 直接调用 Vite 的 &lt;code&gt;dev server&lt;/code&gt; 逻辑，确保测试运行在与开发环境完全相同的代码路径上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph Traditional [&quot;Traditional (Jest)&quot;]
        J_Code[Source Code] --&amp;gt; J_Babel[Babel/ts-jest]
        J_Babel --&amp;gt; J_Runtime[&quot;Node.js Runtime&quot;]
    end
    subgraph Modern [&quot;Modern (Vitest)&quot;]
        V_Code[Source Code] --&amp;gt; V_Vite[Vite Pipeline / esbuild]
        V_Vite --&amp;gt; V_Runtime[&quot;V8 / Node.js&quot;]
        V_Vite --&amp;gt; V_HMR[Instant HMR]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 性能优势：esbuild 与并行化&lt;/h3&gt;
&lt;p&gt;Vitest 利用 &lt;code&gt;esbuild&lt;/code&gt; 进行极速转换，并基于 &lt;code&gt;tinypool&lt;/code&gt; 实现多线程并行执行。对于拥有数千个单元测试的项目，冷启动时间通常能显著缩短。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest.config.ts 深度配置示例
import { defineConfig } from &apos;vitest/config&apos;;

export default defineConfig({
  test: {
    // 启用多线程并行
    threads: true,
    // 模拟浏览器环境
    environment: &apos;jsdom&apos;,
    // 自动清理 mock
    restoreMocks: true,
    // 覆盖率配置
    coverage: {
      provider: &apos;v8&apos;,
      reporter: [&apos;text&apos;, &apos;json&apos;, &apos;html&apos;],
      all: true,
    },
    // 针对大型项目的分片执行
    shard: {
      index: 1,
      size: 4,
    }
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 业务踩坑：Jest 的内存泄漏噩梦与 Vitest 的架构救赎&lt;/h2&gt;
&lt;p&gt;如果你在一个有超过 1000 个单元测试文件的大型仓库里用过 Jest，你一定遇到过这个极其崩溃的场景：
&lt;strong&gt;在本地跑单个文件秒出结果，但在 CI 上跑全量测试时，跑到第 500 个文件，Node.js 突然卡死，接着爆出 &lt;code&gt;FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.1 为什么 Jest 必然会 OOM？&lt;/h3&gt;
&lt;p&gt;Jest 为了保证每个测试文件之间互不干扰（避免你在 test A 里修改了 &lt;code&gt;window.location&lt;/code&gt; 影响了 test B），它在底层使用了 Node.js 的 &lt;code&gt;vm&lt;/code&gt; 模块。
每次执行一个测试文件，Jest 就会创建一个全新的 &lt;code&gt;vm&lt;/code&gt; 隔离沙箱，并在里面注入一整套极其庞大的 JSDOM 环境。&lt;/p&gt;
&lt;p&gt;致命的问题在于：&lt;strong&gt;Node.js 的 &lt;code&gt;vm&lt;/code&gt; 模块在频繁创建和销毁时，存在严重的内存泄漏问题（V8 引擎的 Context 无法被垃圾回收）。&lt;/strong&gt;
只要你的测试文件够多，内存占用就会像滚雪球一样从 500MB 飙升到 4GB，直到把 CI 机器撑爆。&lt;/p&gt;
&lt;h3&gt;2.2 Vitest 的 Tinypool 线程池拯救世界&lt;/h3&gt;
&lt;p&gt;Vitest 从一开始就吸取了 Jest 的血泪教训。它不仅抛弃了沉重的 Babel 转换，还引入了基于 &lt;code&gt;Piscina&lt;/code&gt; 的 &lt;code&gt;tinypool&lt;/code&gt; 架构。&lt;/p&gt;
&lt;p&gt;在 Vitest 中，每个测试文件不再是一个沉重的 &lt;code&gt;vm&lt;/code&gt; 沙箱，而是被分发到独立且轻量级的 &lt;strong&gt;Worker Threads（工作线程）&lt;/strong&gt; 中运行。
更绝的是，Vitest 提供了 &lt;code&gt;poolOptions.threads.isolate: false&lt;/code&gt; 和全新的 &lt;code&gt;browser&lt;/code&gt; 模式。当你在 CI 上遇到内存瓶颈时，可以&lt;strong&gt;关闭严格的隔离模式&lt;/strong&gt;，让多个测试文件在一个 Worker 内复用 V8 上下文，彻底消灭了 OOM 的可能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest.config.ts
export default defineConfig({
  test: {
    // 开启 pool 机制，告别 Jest 的 vm 内存泄漏
    pool: &apos;threads&apos;,
    poolOptions: {
      threads: {
        // 在极端大型项目中，可以关闭隔离以换取极致的内存和速度
        isolate: false, 
      }
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Playwright：重塑 E2E 测试体验&lt;/h2&gt;
&lt;p&gt;如果说 Vitest 解决了单元测试的效率问题，那么 Playwright 则大幅优化了端到端测试（E2E）的稳定性。&lt;/p&gt;
&lt;h3&gt;2.1 架构差异：CDP vs WebDriver&lt;/h3&gt;
&lt;p&gt;不同于 Selenium 依赖的 WebDriver 协议，Playwright 通过浏览器底层的调试协议（如 Chrome DevTools Protocol）直接控制浏览器引擎。这种方式响应更快，且能捕获更细粒度的网络请求和控制台日志。&lt;/p&gt;
&lt;h3&gt;3.2 核心特性分析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原生并发&lt;/strong&gt;：Playwright 可以在单个进程中启动多个独立的浏览器上下文（Browser Context），实现真正的并行测试，而无需为每个测试用例启动完整的浏览器实例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动等待 (Auto-waiting)&lt;/strong&gt;：内置了对元素可见性、可点击性的智能轮询，减少了测试脚本中的 &lt;code&gt;sleep&lt;/code&gt; 或手动等待逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trace Viewer&lt;/strong&gt;：在 CI 环境失败时，Playwright 会生成完整的追踪文件，包含每一帧的 DOM 快照、网络请求和动作回放，提升调试效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// playwright.config.ts 生产级配置
import { defineConfig, devices } from &apos;@playwright/test&apos;;

export default defineConfig({
  testDir: &apos;./e2e&apos;,
  timeout: 30 * 1000,
  expect: { timeout: 5000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: &apos;html&apos;,
  use: {
    actionTimeout: 0,
    baseURL: &apos;http://localhost:3000&apos;,
    trace: &apos;on-first-retry&apos;,
    video: &apos;on-first-retry&apos;,
  },
  projects: [
    { name: &apos;chromium&apos;, use: { ...devices[&apos;Desktop Chrome&apos;] } },
    { name: &apos;firefox&apos;, use: { ...devices[&apos;Desktop Firefox&apos;] } },
    { name: &apos;webkit&apos;, use: { ...devices[&apos;Desktop Safari&apos;] } },
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：Flaky Tests 与视觉回归 (Visual Regression) 测试&lt;/h2&gt;
&lt;p&gt;写过 E2E 测试的人都知道，E2E 最让人头疼的不是不会写，而是 &lt;strong&gt;Flaky Tests（时过时挂的幽灵测试）&lt;/strong&gt;。
你点了一个保存按钮，有时候弹窗 0.1 秒就出来了，测试通过；有时候服务器卡了，弹窗 1.5 秒才出来，测试就挂了。&lt;/p&gt;
&lt;h3&gt;4.1 Playwright 的超能力：自动等待 (Auto-waiting)&lt;/h3&gt;
&lt;p&gt;在以前的 Selenium 或 Cypress 时代，我们的代码里布满了丑陋的 &lt;code&gt;await sleep(1000)&lt;/code&gt;。
Playwright 彻底改变了这一点，它内置了&lt;strong&gt;严格的行动前置校验 (Actionability Checks)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当你执行 &lt;code&gt;await page.locator(&apos;.submit-btn&apos;).click()&lt;/code&gt; 时，Playwright 会在底层以极高的频率疯狂轮询，直到满足以下所有条件才执行点击：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;元素存在于 DOM 中（Attached）&lt;/li&gt;
&lt;li&gt;元素是可见的（Visible，没有 display: none 或被其他元素遮挡）&lt;/li&gt;
&lt;li&gt;元素是稳定的（Stable，没有在执行 CSS transform 动画）&lt;/li&gt;
&lt;li&gt;元素接收到了指针事件（Receives Events，没有 &lt;code&gt;pointer-events: none&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果超过了 &lt;code&gt;actionTimeout&lt;/code&gt;（默认 30 秒）还没满足，它才会报错。这直接消灭了 90% 因为时序问题导致的 Flaky Tests。&lt;/p&gt;
&lt;h3&gt;4.2 终极挑战：像素级视觉比对 (Snapshot Testing)&lt;/h3&gt;
&lt;p&gt;在核心的 C 端业务（比如电商首页）中，仅仅断言“元素存在”是不够的。老板最怕的是：按钮在，但是 CSS 样式全乱了，字盖在了一起。&lt;/p&gt;
&lt;p&gt;Playwright 提供了极其强大的 &lt;code&gt;toHaveScreenshot&lt;/code&gt; 能力。但在团队协作时，一个巨大的坑是：&lt;strong&gt;Mac 上截的图，拿到 Linux (CI 机器) 上去比对，100% 会报错。&lt;/strong&gt;
因为不同的操作系统、不同的显卡驱动，甚至连字体的抗锯齿边缘渲染（Anti-aliasing）都有细微的像素差异！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：用 Docker 抹平渲染环境差异&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;千万不要把本地截的 Base 图片提交到 Git 仓库让 CI 去比对。
必须使用 Playwright 官方提供的 Docker 镜像来统一截图和比对环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 在本地使用 Docker 生成 Base 截图（macOS/Windows 统一用 Linux 渲染引擎）
docker run -v $PWD:/work/ -w /work -it mcr.microsoft.com/playwright:v1.40.0-jammy npx playwright test --update-snapshots

# 2. 在 CI (也是 Ubuntu Jammy 环境) 中执行比对
npx playwright test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合代码里的容差配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// playwright.config.ts
import { expect } from &apos;@playwright/test&apos;;

// 允许不超过 5% 的像素有细微的颜色偏差（解决字体抗锯齿造成的误报）
expect.extend({
  toHaveScreenshot(received, name) {
    return expect(received).toHaveScreenshot(name, {
      maxDiffPixelRatio: 0.05, 
    });
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 Docker 容器化 + 合理的抗锯齿容差，你可以把 E2E 测试推进到“视觉无损”的最高境界。&lt;/p&gt;
&lt;h2&gt;5. 现代测试金字塔的重构&lt;/h2&gt;
&lt;p&gt;随着工具链的演进，我们的测试策略也应随之调整：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单元测试 (Vitest)&lt;/strong&gt;：关注纯函数、工具类及业务逻辑。追求高覆盖率和执行速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件测试 (Vitest + Browser Mode)&lt;/strong&gt;：利用 Vitest 的浏览器模式直接在真实引擎中运行组件测试，替代部分资源消耗较高的 E2E 测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端测试 (Playwright)&lt;/strong&gt;：仅覆盖核心业务路径（如注册、下单、支付）。利用 Playwright 的并发能力优化 CI 耗时。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;从 Jest 迁移到 Vitest + Playwright 是对开发反馈环的深度优化。Vitest 带来的高效 HMR 体验和 Playwright 提供的稳定 E2E 验证，共同构成了一套支撑现代复杂前端应用的工程化底座。&lt;/p&gt;
</content:encoded></item><item><title>WebAssembly 在前端密集型计算中的应用场景探讨</title><link>https://nollieleo.github.io/posts/wasm-frontend-computation/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/wasm-frontend-computation/</guid><description>解析 WASM 的内存模型与执行机制，探讨其在视频处理、数据加密等非传统前端领域的落地。</description><pubDate>Sun, 18 Aug 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WebAssembly (WASM) 的出现并非为了取代 JavaScript，而是为了填补 Web 平台在处理 CPU 密集型任务时的性能短板。作为一种低级的类汇编二进制格式，WASM 为浏览器引入了近乎原生的执行效率，使得原本只能在桌面端运行的复杂应用能够平滑迁移至 Web 环境。&lt;/p&gt;
&lt;h2&gt;1. WASM 核心架构与执行机制&lt;/h2&gt;
&lt;p&gt;WASM 之所以高效，源于其设计上的预编译与强类型特性。与 JavaScript 需要经过解析（Parsing）、解释（Interpreting）和即时编译（JIT）的复杂过程不同，WASM 在下载后即可直接进入基线编译器。&lt;/p&gt;
&lt;h3&gt;1.1 线性内存模型 (Linear Memory)&lt;/h3&gt;
&lt;p&gt;WASM 使用一个连续、可扩展的原始字节数组作为其内存空间。这种模型与 C/C++ 的内存布局高度一致，允许开发者手动管理内存分配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph BrowserEnvironment [&quot;Browser Environment&quot;]
        JS[JavaScript Engine] &amp;lt;--&amp;gt; Buffer[SharedArrayBuffer / ArrayBuffer]
        subgraph WASMInstance [&quot;WASM Instance&quot;]
            Memory[Linear Memory]
            Stack[Expression Stack]
            Code[Compiled Machine Code]
        end
        Buffer &amp;lt;--&amp;gt; Memory
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 验证与编译&lt;/h3&gt;
&lt;p&gt;WASM 模块在执行前会经过严格的单次扫描验证，确保其不违反类型安全或访问越界。由于其二进制格式已经过高度优化，现代浏览器（如 Chrome 的 V8 引擎）可以利用多线程并行编译 WASM 模块，甚至在下载过程中就开始编译（Streaming Compilation）。&lt;/p&gt;
&lt;h2&gt;2. 业务踩坑：JS 与 WASM 的“过路费”陷阱 (Serialization Overhead)&lt;/h2&gt;
&lt;p&gt;很多前端第一次把一段复杂的加解密逻辑用 Rust 写成 WASM 后，在浏览器里一跑，结果发现：&lt;strong&gt;比纯 JavaScript 还要慢！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就是经典的 &lt;strong&gt;WASM 边界通信开销（FFI Overhead）&lt;/strong&gt; 问题。&lt;/p&gt;
&lt;h3&gt;2.1 昂贵的内存拷贝&lt;/h3&gt;
&lt;p&gt;在 JS 中调用 WASM 函数时，数字类型（如 &lt;code&gt;int32&lt;/code&gt;）可以直接通过 V8 的寄存器传递，速度极快。
但是，如果你想传一个长度为 10MB 的字符串，或者一张 4K 分辨率的图片（Uint8Array）给 WASM 处理，灾难就来了。&lt;/p&gt;
&lt;p&gt;WASM 运行在一个隔离的沙箱中，它不能直接读取 JS 的堆内存（Heap）。你必须：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 WASM 的线性内存（Linear Memory）中 &lt;code&gt;malloc&lt;/code&gt; 分配出一块 10MB 的空白区域。&lt;/li&gt;
&lt;li&gt;JS 引擎将这 10MB 的图片数据，&lt;strong&gt;逐字节拷贝（Copy）&lt;/strong&gt; 到 WASM 的内存空间中。&lt;/li&gt;
&lt;li&gt;WASM 执行极速的图像处理。&lt;/li&gt;
&lt;li&gt;处理完后，JS 引擎再次从 WASM 内存中把那 10MB 数据&lt;strong&gt;拷贝&lt;/strong&gt;出来，转成 JS 的 &lt;code&gt;Uint8Array&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你的图片处理本身只需要 2 毫秒，但这两次内存拷贝花费了 10 毫秒，WASM 的性能红利就彻底变成了负数！&lt;/p&gt;
&lt;h3&gt;2.2 工业级解法：SharedArrayBuffer 与零拷贝 (Zero-copy)&lt;/h3&gt;
&lt;p&gt;在处理音视频、3D 渲染等真正的高密集型场景时，我们必须彻底消灭内存拷贝。
现代浏览器提供了 &lt;code&gt;SharedArrayBuffer&lt;/code&gt;，它允许 JS 的 Web Worker 和 WASM 实例&lt;strong&gt;从物理上映射到同一块内存地址&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 在 JS 主线程预分配 16MB 的共享内存
const sharedMemory = new WebAssembly.Memory({
  initial: 256, // 256 pages * 64KB = 16MB
  maximum: 512,
  shared: true  // 关键参数：开启共享
});

// 2. 将这块内存的控制权传递给 WASM 实例
const wasmInstance = await WebAssembly.instantiateStreaming(fetch(&apos;image-processor.wasm&apos;), {
  env: { memory: sharedMemory }
});

// 3. JS 直接通过视图操作这块内存（不需要任何拷贝！）
const jsView = new Uint8Array(sharedMemory.buffer);
jsView.set(hugeImageData, 0); // 将图片流直接打入这块物理内存

// 4. 通知 WASM 开始干活，WASM 会直接原地修改这块内存
wasmInstance.exports.processImage(0, hugeImageData.length);

// 5. JS 直接从 jsView 里读取被 WASM 修改后的结果，渲染到 Canvas 上
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，JS 和 WASM 就像是在同一个房间里的两个人，直接在一个黑板（共享内存）上读写，彻底省去了“传小纸条（拷贝）”的开销。&lt;/p&gt;
&lt;h2&gt;3. 杀手级业务落地：突破前端的物理极限&lt;/h2&gt;
&lt;p&gt;除了传统的计算密集型任务，WASM 正在改变前端能做的事情的边界。&lt;/p&gt;
&lt;h3&gt;3.1 视频本地转码：FFmpeg.wasm&lt;/h3&gt;
&lt;p&gt;以前，用户上传了一个 500MB 的 &lt;code&gt;.avi&lt;/code&gt; 视频，前端只能原封不动地传给后端，由后端的集群消耗巨量 CPU 去转码成 &lt;code&gt;.mp4&lt;/code&gt;。不仅服务器成本高昂，用户的上传等待时间也极长。&lt;/p&gt;
&lt;p&gt;现在，借助 &lt;code&gt;FFmpeg.wasm&lt;/code&gt;，我们直接把整个用 C 写的视频转码引擎搬到了浏览器里。
用户选完视频，&lt;strong&gt;利用用户电脑的 CPU 甚至 GPU（借助 WebGPU）&lt;/strong&gt; 在本地将其压缩到 50MB 的 &lt;code&gt;.mp4&lt;/code&gt;，然后再上传。这为视频类网站（如 B站、YouTube 创作者中心）节省了海量的 CDN 带宽和服务器算力。&lt;/p&gt;
&lt;h3&gt;3.2 浏览器端的关系型数据库：SQLite-WASM&lt;/h3&gt;
&lt;p&gt;在 Local-first（本地优先）架构中，IndexedDB 的 API 极度难用，且不支持复杂的 &lt;code&gt;JOIN&lt;/code&gt; 表查询。
通过将 SQLite 编译为 WASM，并结合现代浏览器的 &lt;code&gt;OPFS (Origin Private File System)&lt;/code&gt; API 进行高性能的本地磁盘落盘。我们现在可以直接在浏览器里执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT users.name, orders.total FROM users LEFT JOIN orders ON users.id = orders.user_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这让前端拥有了和后端一模一样的高性能数据查询和强事务能力。Figma 和 Notion 的 Web 端底层都已经重度依赖这种架构。&lt;/p&gt;
&lt;h2&gt;4. 落地建议与工具链选择&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rust 生态&lt;/strong&gt;：目前最成熟的选择。&lt;code&gt;wasm-pack&lt;/code&gt; 提供了完整的构建、打包及发布流程，生成的胶水代码极大地简化了 JS 互操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Emscripten&lt;/strong&gt;：适用于将现有的大型 C/C++ 项目（如 AutoCAD, Google Earth）迁移至 Web。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AssemblyScript&lt;/strong&gt;：语法接近 TypeScript，适合不熟悉底层语言的前端开发者快速上手，但需注意其内存管理仍需手动介入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;WASM 并非万能药。在评估是否引入 WASM 时，应遵循“计算密度”原则：只有当计算任务的耗时远大于 JS-WASM 边界通信开销时，WASM 才能带来真正的收益。随着 WASI（WebAssembly System Interface）和组件模型（Component Model）的推进，WASM 的边界将进一步扩展至服务端与边缘计算领域。&lt;/p&gt;
</content:encoded></item><item><title>React 19 核心特性解析：从 React Compiler 到 Actions 数据流</title><link>https://nollieleo.github.io/posts/react-19-core-features/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react-19-core-features/</guid><description>深入分析 React 19 的底层架构更新。探讨 React Compiler 的静态分析原理、Actions 对表单数据突变的规范化处理，以及全新的 use 与 useOptimistic Hook 在复杂异步场景中的工程实践。</description><pubDate>Thu, 15 Aug 2024 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;React 19 是 React 演进史上的一个重要里程碑。与以往仅仅增加上层 API 不同，React 19 在编译层和异步数据流控制上进行了深度的架构调整。其核心目标是降低开发者在状态同步和性能优化上的心智负担，使代码逻辑更加贴近原生 JavaScript 的编写直觉。&lt;/p&gt;
&lt;p&gt;本文将从工程实践与底层原理的角度，详细解析 React 19 的核心特性，并探讨其对现代前端架构的影响。&lt;/p&gt;
&lt;h2&gt;1. React Compiler：从手动缓存到 AOT 静态分析&lt;/h2&gt;
&lt;p&gt;在 React 19 之前，React 的渲染模型依赖于开发者手动进行 Memoization（记忆化）。为了防止父组件渲染导致子组件的不必要更新，或者防止复杂计算在每次渲染时重复执行，代码中往往充斥着 &lt;code&gt;useMemo&lt;/code&gt;、&lt;code&gt;useCallback&lt;/code&gt; 以及 &lt;code&gt;React.memo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这种模式存在两个主要问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码侵入性强&lt;/strong&gt;：业务逻辑被大量的缓存依赖数组（Dependency Arrays）包裹，可读性下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖维护困难&lt;/strong&gt;：开发者容易遗漏依赖项，导致闭包陷阱（Stale Closures），或者添加了不必要的依赖，导致缓存失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.1 编译器工作原理&lt;/h3&gt;
&lt;p&gt;React Compiler（曾用名 React Forget）是一个基于 Babel 的预先（AOT）编译器。它在构建阶段对 React 组件和 Hook 进行静态分析（Static Analysis），构建出控制流图（Control Flow Graph）和数据依赖图。&lt;/p&gt;
&lt;p&gt;Compiler 的核心机制是将组件内部的变量分配到不同的“响应式作用域（Reactive Scopes）”中。如果某个作用域内的输入依赖没有发生变化，Compiler 会利用底层注入的 &lt;code&gt;useMemoCache&lt;/code&gt; Hook 直接返回缓存的输出，从而跳过当前作用域的重新计算和子组件的 Diff 过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[&quot;源代码 (JSX/TSX)&quot;] --&amp;gt; B[&quot;Babel 解析生成 AST&quot;]
    B --&amp;gt; C[&quot;React Compiler 插件拦截&quot;]
    C --&amp;gt; D[&quot;构建 HIR (High-level Intermediate Representation)&quot;]
    D --&amp;gt; E[&quot;推导变量别名与类型分析&quot;]
    E --&amp;gt; F[&quot;构建响应式作用域 (Reactive Scopes)&quot;]
    F --&amp;gt; G[&quot;注入底层 useMemoCache 逻辑&quot;]
    G --&amp;gt; H[&quot;生成优化后的可执行 JS 代码&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 编译前后的代码映射&lt;/h3&gt;
&lt;p&gt;以一个常见的数据过滤和事件绑定组件为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 优化前的常规写法 (React 18)
function ProductList({ products, filter }) {
  // 需要手动处理计算缓存
  const filteredProducts = useMemo(() =&amp;gt; {
    return products.filter(p =&amp;gt; p.category === filter);
  }, [products, filter]);

  // 需要手动保持引用稳定
  const handleSelect = useCallback((id: string) =&amp;gt; {
    trackEvent(&apos;select&apos;, id);
  }, []);

  return (
    &amp;lt;ul&amp;gt;
      {filteredProducts.map(product =&amp;gt; (
        &amp;lt;ProductItem key={product.id} item={product} onSelect={handleSelect} /&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 React 19 环境下，开发者只需编写最基础的业务逻辑，Compiler 会在编译后生成类似以下的底层代码（抽象伪代码表示）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Compiler 编译后的底层逻辑 (伪代码)
import { c as _useMemoCache } from &quot;react/compiler-runtime&quot;;

function ProductList({ products, filter }) {
  // 分配一个包含 4 个插槽的缓存数组
  const $ = _useMemoCache(4);

  let filteredProducts;
  // 校验依赖插槽 $[0] 和 $[1]
  if ($[0] !== products || $[1] !== filter) {
    filteredProducts = products.filter(p =&amp;gt; p.category === filter);
    // 更新缓存
    $[0] = products;
    $[1] = filter;
    $[2] = filteredProducts;
  } else {
    // 命中缓存
    filteredProducts = $[2];
  }

  let handleSelect;
  if ($[3] === Symbol.for(&quot;react.memo_cache_sentinel&quot;)) {
    handleSelect = (id) =&amp;gt; { trackEvent(&apos;select&apos;, id); };
    $[3] = handleSelect;
  } else {
    handleSelect = $[3];
  }

  return /* JSX 渲染 */;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compiler 的引入使得 UI 的更新链路更加确定，大幅减少了因为人为疏忽导致的性能退化。&lt;/p&gt;
&lt;h3&gt;1.3 业务踩坑：Compiler 的“逃生舱”与失效场景 (Bailouts)&lt;/h3&gt;
&lt;p&gt;很多开发者以为接入了 React Compiler，就可以随便写代码了。实际上，Compiler 的静态分析非常严格，如果你的代码触犯了 &lt;strong&gt;Rules of React&lt;/strong&gt;，编译器会直接放弃优化（这个过程被称为 &lt;strong&gt;Bailout&lt;/strong&gt;），并且在没有任何报错的情况下退回到原始的未缓存状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最常见的导致 Compiler 罢工的坏味道：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;直接修改外部变量 (Mutating external variables)&lt;/strong&gt;：
React 要求组件必须是纯函数。如果你在组件渲染期，修改了一个定义在组件外部的变量，Compiler 会检测到这种非纯行为并 Bailout。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let renderCount = 0; // 外部变量
function BadComponent() {
  renderCount++; // ❌ 违反纯函数规则，Compiler 放弃优化
  return &amp;lt;div&amp;gt;{renderCount}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;直接修改 Props 或 State (Mutating Props/State)&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function UserProfile({ user }) {
  user.name = &quot;Alice&quot;; // ❌ 直接修改 props 对象，Bailout!
  return &amp;lt;div&amp;gt;{user.name}&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动态调用 Hooks&lt;/strong&gt;：
把 Hooks 放在 &lt;code&gt;if&lt;/code&gt; 或 &lt;code&gt;for&lt;/code&gt; 循环里，这不仅会引发运行时的 React 报错，Compiler 在静态分析阶段也会直接拦截并 Bailout。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;如何排查 Bailout？&lt;/strong&gt;
在工业级项目中，我们必须引入官方的 ESLint 插件 &lt;code&gt;eslint-plugin-react-compiler&lt;/code&gt;。它不仅能在编写代码时标红这些破坏规则的代码，还能明确告诉你“这行代码导致了编译器罢工”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .eslintrc.json
{
  &quot;plugins&quot;: [&quot;react-compiler&quot;],
  &quot;rules&quot;: {
    &quot;react-compiler/react-compiler&quot;: &quot;error&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果确实遇到了极其复杂的第三方库导致 Compiler 误判，你可以使用 &lt;code&gt;&quot;use no memo&quot;&lt;/code&gt; 指令作为逃生舱，显式地告诉编译器跳过当前组件的优化。&lt;/p&gt;
&lt;h2&gt;2. Actions 架构：标准化数据突变 (Data Mutations)&lt;/h2&gt;
&lt;p&gt;在客户端应用中，数据获取（Query）通常与数据突变（Mutation）交织在一起。React 18 通过 Suspense 规范了数据获取，但在表单提交、数据更新等 Mutation 场景，开发者仍需要手动管理 &lt;code&gt;isPending&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt; 状态，并在请求完成后手动重置 UI。&lt;/p&gt;
&lt;p&gt;React 19 引入了 &lt;strong&gt;Actions&lt;/strong&gt; 概念，将异步数据流与原生 HTML &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; 元素深度绑定，提供了一套标准化的状态流转范式。&lt;/p&gt;
&lt;h3&gt;2.1 useActionState 与原生表单的结合&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useActionState&lt;/code&gt; 取代了早期的 &lt;code&gt;useFormState&lt;/code&gt;，专门用于管理带有异步副作用的表单状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useActionState } from &apos;react&apos;;

// 异步 Action 函数
async function updateProfile(prevState: any, formData: FormData) {
  const username = formData.get(&apos;username&apos;);
  try {
    const res = await fetch(&apos;/api/profile&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ username }),
    });
    if (!res.ok) throw new Error(&apos;Update failed&apos;);
    return { success: true, message: &apos;Saved successfully&apos; };
  } catch (err) {
    return { success: false, error: err.message };
  }
}

export default function ProfileEditor() {
  // state 包含 action 的返回值，formAction 绑定至 &amp;lt;form&amp;gt;，isPending 提供提交状态
  const [state, formAction, isPending] = useActionState(updateProfile, null);

  return (
    &amp;lt;form action={formAction}&amp;gt;
      &amp;lt;input type=&quot;text&quot; name=&quot;username&quot; disabled={isPending} /&amp;gt;
      &amp;lt;button type=&quot;submit&quot; disabled={isPending}&amp;gt;
        {isPending ? &apos;Saving...&apos; : &apos;Save&apos;}
      &amp;lt;/button&amp;gt;
      {state?.error &amp;amp;&amp;amp; &amp;lt;div className=&quot;text-red-500&quot;&amp;gt;{state.error}&amp;lt;/div&amp;gt;}
      {state?.success &amp;amp;&amp;amp; &amp;lt;div className=&quot;text-green-500&quot;&amp;gt;{state.message}&amp;lt;/div&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，表单的默认提交行为会被 React 拦截，且异步请求期间的 &lt;code&gt;isPending&lt;/code&gt; 状态更新不再阻塞其他的交互输入，保证了应用在网络延迟期间的响应性。&lt;/p&gt;
&lt;h3&gt;2.2 useOptimistic：乐观 UI 更新的最佳实践&lt;/h3&gt;
&lt;p&gt;在复杂的业务系统中（如即时通讯、点赞操作），为了提升感知性能，通常会在网络请求完成前预先更新 UI（即“乐观更新”）。如果请求失败，则回滚到之前的状态。&lt;/p&gt;
&lt;p&gt;React 19 原生提供了 &lt;code&gt;useOptimistic&lt;/code&gt; Hook，将这一复杂的状态机逻辑封装为标准 API。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useOptimistic, useState } from &apos;react&apos;;

export function LikeButton({ initialLikes, postId }) {
  const [likes, setLikes] = useState(initialLikes);

  // optimisticLikes 是乐观状态，addOptimisticLike 是触发器
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    // 定义乐观状态的更新规则
    (currentState, optimisticValue: number) =&amp;gt; currentState + optimisticValue
  );

  const handleLike = async () =&amp;gt; {
    // 1. 立即更新 UI（无需等待网络请求）
    addOptimisticLike(1);
    
    try {
      // 2. 发起真实的网络请求
      const newLikes = await api.submitLike(postId);
      // 3. 请求成功，同步真实数据
      setLikes(newLikes);
    } catch (e) {
      // 若请求失败，组件重新渲染时，useOptimistic 会自动丢弃乐观状态，回退到原始的 likes 值
      console.error(&quot;Failed to like&quot;);
    }
  };

  return &amp;lt;button onClick={handleLike}&amp;gt;Likes: {optimisticLikes}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 全新的 use Hook：解决条件异步读取的痛点&lt;/h2&gt;
&lt;p&gt;在 React 的规则体系中，Hooks 不能在条件语句（&lt;code&gt;if&lt;/code&gt;）或循环中使用。这在处理按需加载的数据或 Context 时带来了架构上的冗余。&lt;/p&gt;
&lt;p&gt;React 19 提供了一个名为 &lt;code&gt;use&lt;/code&gt; 的特殊 API。它既可以用于读取 Promise，也可以用于读取 Context。最重要的是，&lt;strong&gt;&lt;code&gt;use&lt;/code&gt; 可以在条件语句内部调用&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3.1 配合 Suspense 的按需读取&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;use&lt;/code&gt; 接收一个尚未 Resolve 的 Promise 时，它会抛出该 Promise，触发外层最近的 &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; 回退逻辑（Fallback）。一旦 Promise 完成，React 会从挂起点恢复渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { use, Suspense } from &apos;react&apos;;

function UserComments({ commentsPromise }) {
  // use 可以在 if 语句内使用。当 commentsPromise 未完成时，组件挂起
  const comments = use(commentsPromise);
  
  if (comments.length === 0) {
    return &amp;lt;div&amp;gt;No comments found.&amp;lt;/div&amp;gt;;
  }

  return (
    &amp;lt;ul&amp;gt;
      {comments.map(c =&amp;gt; &amp;lt;li key={c.id}&amp;gt;{c.text}&amp;lt;/li&amp;gt;)}
    &amp;lt;/ul&amp;gt;
  );
}

export default function Post({ showComments, commentsPromise }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Post Content&amp;lt;/h1&amp;gt;
      {showComments &amp;amp;&amp;amp; (
        &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading comments...&amp;lt;/div&amp;gt;}&amp;gt;
          {/* 将 Promise 传递给子组件进行拆解 */}
          &amp;lt;UserComments commentsPromise={commentsPromise} /&amp;gt;
        &amp;lt;/Suspense&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式鼓励开发者将数据获取（Fetching）的逻辑向上提升，将数据解包（Unwrapping）的逻辑下沉，进一步实现了视图与数据流的解耦。&lt;/p&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;React 19 的更新核心可以概括为“底层自动化”与“数据流规范化”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React Compiler&lt;/strong&gt; 在编译阶段解决了由于闭包和对象引用变更引发的不必要重渲染问题，开发者不再需要过度依赖人工 Memoization。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actions&lt;/strong&gt; 和 &lt;strong&gt;useOptimistic&lt;/strong&gt; 为表单和异步交互提供了状态机的标准解法，避免了离散的布尔值状态导致的代码复杂性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;use Hook&lt;/strong&gt; 结合 Suspense，构建了一套更符合直觉的异步资源加载模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于前端架构而言，这些特性意味着我们可以将架构的重心从“如何避免组件卡顿”转移到“如何设计更合理的数据流与领域模型”上，推动整体工程质量向更成熟的方向演进。&lt;/p&gt;
</content:encoded></item><item><title>复杂企业级中后台的 Schema 驱动表单架构设计</title><link>https://nollieleo.github.io/posts/schema-driven-complex-forms/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/schema-driven-complex-forms/</guid><description>分析大型表单在状态管理和联动校验上的痛点，介绍 Schema 驱动与状态机分离的架构实践。</description><pubDate>Thu, 25 Jul 2024 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;在企业级中后台系统中，表单是承载业务逻辑最密集的组件。随着业务复杂度的增加，开发者往往会面临以下挑战：几百个字段的联动逻辑、动态增删的自增列表、复杂的跨字段异步校验，以及由于 React 顶层状态更新导致的严重渲染卡顿。本文将探讨如何通过 &lt;strong&gt;Schema 驱动&lt;/strong&gt; 与 &lt;strong&gt;状态机分离&lt;/strong&gt; 的架构设计来应对这些挑战。&lt;/p&gt;
&lt;h2&gt;1. 传统受控模式的困局&lt;/h2&gt;
&lt;p&gt;在传统的 React 开发中，我们习惯于将表单状态（State）提升至父组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统模式：任何一个 Input 改变都会导致 Form 整体重渲染
const [formData, setFormData] = useState({});
return (
  &amp;lt;form&amp;gt;
    &amp;lt;Input value={formData.a} onChange={v =&amp;gt; setFormData({...formData, a: v})} /&amp;gt;
    &amp;lt;Input value={formData.b} /&amp;gt;
    {/* 当表单项达到 100+ 时，输入会产生明显延迟 */}
  &amp;lt;/form&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式的架构优势是简单直观，但在复杂场景下存在两个主要缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能瓶颈&lt;/strong&gt;：全量渲染（Re-render）的开销随字段数量呈线性增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑耦合&lt;/strong&gt;：联动逻辑（如“选择 A 后隐藏 B”）散落在各个组件的 &lt;code&gt;onChange&lt;/code&gt; 中，难以维护。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. Schema 驱动架构：MVVM 的回归&lt;/h2&gt;
&lt;p&gt;Schema 驱动的核心思想是将表单的&lt;strong&gt;结构（Structure）&lt;/strong&gt;、**逻辑（Logic）&lt;strong&gt;与&lt;/strong&gt;渲染（UI）**解耦。我们使用 JSON Schema 来描述表单，而表单的运行状态则由一个独立的状态机管理。&lt;/p&gt;
&lt;h3&gt;2.1 状态机下沉与局部更新&lt;/h3&gt;
&lt;p&gt;借鉴 MVVM 模式，我们将每个表单项（Field）视为一个独立的 Observable 节点。UI 组件通过订阅（Subscribe）特定节点的状态来决定何时重绘。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    subgraph FormCore [&quot;领域模型层 (Form Core)&quot;]
        State[&quot;Form State (Observable)&quot;]
        Graph[&quot;Dependency Graph (拓扑图)&quot;]
    end
    
    subgraph SchemaLayer [&quot;配置层 (JSON Schema)&quot;]
        JSON[&quot;Field Rules &amp;amp; Linkages&quot;]
    end
    
    subgraph UILayer [&quot;渲染层 (React Components)&quot;]
        InputA[&quot;Input A (Observer)&quot;]
        InputB[&quot;Input B (Observer)&quot;]
    end

    JSON --&amp;gt; State
    State --&amp;gt; Graph
    InputA --&amp;gt;|修改| State
    State --&amp;gt;|精准通知| InputB
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 JSON Schema 的扩展协议&lt;/h3&gt;
&lt;p&gt;标准的 JSON Schema 主要用于校验，我们在其基础上扩展了 &lt;code&gt;x-component&lt;/code&gt;（指定组件）和 &lt;code&gt;x-reactions&lt;/code&gt;（描述联动）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;role&quot;: {
      &quot;type&quot;: &quot;string&quot;,
      &quot;title&quot;: &quot;用户角色&quot;,
      &quot;enum&quot;: [{ &quot;label&quot;: &quot;管理员&quot;, &quot;value&quot;: &quot;admin&quot; }, { &quot;label&quot;: &quot;普通用户&quot;, &quot;value&quot;: &quot;user&quot; }],
      &quot;x-component&quot;: &quot;Select&quot;
    },
    &quot;permissionCode&quot;: {
      &quot;type&quot;: &quot;string&quot;,
      &quot;title&quot;: &quot;权限代码&quot;,
      &quot;x-component&quot;: &quot;Input&quot;,
      &quot;x-reactions&quot;: {
        &quot;dependencies&quot;: [&quot;role&quot;],
        &quot;fulfill&quot;: {
          &quot;state&quot;: {
            &quot;visible&quot;: &quot;{{$deps[0] === &apos;admin&apos;}}&quot;,
            &quot;required&quot;: &quot;{{$deps[0] === &apos;admin&apos;}}&quot;
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 核心机制：依赖收集与响应式引擎&lt;/h2&gt;
&lt;p&gt;为了实现高效联动，架构内部维护了一个&lt;strong&gt;依赖关系拓扑图&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：解析 Schema 中的 &lt;code&gt;dependencies&lt;/code&gt;，建立节点间的指向关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖收集&lt;/strong&gt;：当 &lt;code&gt;permissionCode&lt;/code&gt; 声明依赖 &lt;code&gt;role&lt;/code&gt; 时，它会自动注册到 &lt;code&gt;role&lt;/code&gt; 的观察者列表中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精准派发&lt;/strong&gt;：当用户修改 &lt;code&gt;role&lt;/code&gt; 的值，状态机仅触发 &lt;code&gt;permissionCode&lt;/code&gt; 的计算逻辑和重渲染，表单的其他部分保持静默。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种机制类似于 Vue 的响应式系统，但在表单场景下，它能处理更复杂的路径匹配（如数组下标联动 &lt;code&gt;users.*.name&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;4. 业务踩坑：依赖收集与路径引擎 (Path Engine) 的深水区&lt;/h2&gt;
&lt;p&gt;当你真正尝试去写一个 Schema 引擎时，最难的不是“监听状态变化”，而是**“数组循环与深层路径匹配”**。&lt;/p&gt;
&lt;p&gt;设想一个经典的动态增删列表场景：&lt;code&gt;用户有多个联系人，每个联系人有名字和手机号。如果名字是“张三”，手机号就必须必填。&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在扁平的表单里，这个联动叫 &lt;code&gt;name -&amp;gt; phone&lt;/code&gt;。
但在动态数组里，这个联动变成了：&lt;code&gt;users[0].name -&amp;gt; users[0].phone&lt;/code&gt;，&lt;code&gt;users[1].name -&amp;gt; users[1].phone&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统的 EventEmitter 或 Redux 此时会瞬间崩溃。&lt;/strong&gt; 因为你不可能为数组的每一行去手动注册独立的事件监听器，尤其是当数组发生 &lt;code&gt;push&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;splice&lt;/code&gt; 时，索引(Index) 会全盘错乱。&lt;/p&gt;
&lt;h3&gt;4.1 工业级解法：路径匹配器 (Path Matcher)&lt;/h3&gt;
&lt;p&gt;成熟的表单架构（如 Formily）内部一定会实现一个极其强悍的 &lt;strong&gt;Path Engine&lt;/strong&gt;。它允许使用类似于正则的通配符语法来声明联动依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;users&quot;: {
    &quot;type&quot;: &quot;array&quot;,
    &quot;items&quot;: {
      &quot;type&quot;: &quot;object&quot;,
      &quot;properties&quot;: {
        &quot;name&quot;: { &quot;x-component&quot;: &quot;Input&quot; },
        &quot;phone&quot;: { 
          &quot;x-component&quot;: &quot;Input&quot;,
          &quot;x-reactions&quot;: {
            // 神奇的通配符：只依赖“当前行的 name”
            &quot;dependencies&quot;: [&quot;.name&quot;], 
            // 相对路径的底层会被 Path Engine 解析为真正的绝对路径：
            // 如果当前在 users[1].phone，它会自动去拿 users[1].name 的值
            &quot;fulfill&quot;: {
              &quot;state&quot;: { &quot;required&quot;: &quot;{{$deps[0] === &apos;张三&apos;}}&quot; }
            }
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;底层的核心思想是：&lt;strong&gt;状态机不存储组件树，只存储一颗巨大的 JSON 状态树。&lt;/strong&gt; 组件在渲染时，通过 &lt;code&gt;Context&lt;/code&gt; 注入自己当前的绝对路径（如 &lt;code&gt;users.1.phone&lt;/code&gt;）。当它需要依赖数据时，状态机会通过路径系统计算出兄弟节点的绝对路径，并精确返回数据，同时利用 Proxy 完成依赖收集。&lt;/p&gt;
&lt;h2&gt;5. 架构反思：隔离副作用 (Effects) 的面条代码&lt;/h2&gt;
&lt;p&gt;随着业务的堆叠，表单的联动逻辑会越来越像一团乱麻：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果选了 A，清空 B，隐藏 C，并且向后端发一个请求去获取 D 的下拉列表。&lt;/li&gt;
&lt;li&gt;如果请求 D 失败了，弹出一个 Toast，并把 A 重置为初始值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你把这些逻辑全部写在 Schema 的 &lt;code&gt;x-reactions&lt;/code&gt; 或者组件的 &lt;code&gt;onChange&lt;/code&gt; 里，代码将彻底失去可读性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践：基于生命周期的 Effects 拦截器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;表单不应该只是一堆数据的集合，它是一个拥有完整生命周期（onInit, onMount, onFieldValueChange, onSubmit, unMount）的沙箱。
我们应该在 Schema 之外，独立注入一层 &lt;code&gt;Effects&lt;/code&gt; 副作用拦截器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { onFieldValueChange, setFieldState } from &apos;@form-core&apos;;

const myFormEffects = () =&amp;gt; {
  // 监听任意路径符合 pattern 的字段变化
  onFieldValueChange(&apos;users.*.role&apos;, (field) =&amp;gt; {
    // 提取当前行号
    const index = field.path.segments[1]; 
    const currentRole = field.value;
    
    // 发起异步请求
    fetchPermissions(currentRole).then(res =&amp;gt; {
      // 精确操作兄弟节点的状态
      setFieldState(`users.${index}.permissions`, state =&amp;gt; {
        state.dataSource = res.list;
        state.loading = false;
      });
    });
  });
};

// 渲染表单时注入
&amp;lt;SchemaForm schema={schema} effects={myFormEffects} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过将 &lt;strong&gt;UI 的描述（Schema）&lt;/strong&gt; 与 &lt;strong&gt;行为的流转（Effects）&lt;/strong&gt; 彻底剥离，即使是拥有 500+ 字段的巨型表单，也能保持代码的极度清晰和高可维护性。&lt;/p&gt;
&lt;h2&gt;6. 架构优势总结&lt;/h2&gt;
&lt;p&gt;在企业级应用中，校验往往涉及异步接口（如“检查用户名是否重复”）。Schema 驱动架构支持将校验规则声明化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const schema = {
  &quot;x-validator&quot;: [
    { &quot;required&quot;: true, &quot;message&quot;: &quot;必填项&quot; },
    { &quot;format&quot;: &quot;url&quot;, &quot;message&quot;: &quot;请输入合法的 URL&quot; },
    {
      &quot;validator&quot;: &quot;{{asyncValidateFromServer}}&quot; // 引用外部注入的异步校验函数
    }
  ]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过将校验逻辑从 UI 组件中抽离，我们可以实现“校验规则随 Schema 动态下发”，这对于低代码平台或动态表单场景非常有用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;显著提升性能&lt;/strong&gt;：通过局部渲染技术，支撑大规模字段表单依然保持流畅交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑高度内聚&lt;/strong&gt;：所有联动规则都在 Schema 中定义，业务逻辑一目了然。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨端/跨框架能力&lt;/strong&gt;：核心状态机（Form Core）是纯 JS 编写的，可以轻松适配 React、Vue 甚至原生环境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易于测试&lt;/strong&gt;：可以脱离 UI 直接对 Form Core 进行逻辑单元测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 结语&lt;/h2&gt;
&lt;p&gt;Schema 驱动表单架构在处理复杂企业级业务时，它所带来的&lt;strong&gt;可维护性&lt;/strong&gt;和&lt;strong&gt;性能优势&lt;/strong&gt;是传统模式难以替代的。选择合适的工具（如 Formily）或自研一套轻量级的 Schema 引擎，是提升中后台开发效率的关键。&lt;/p&gt;
</content:encoded></item><item><title>GitHub Actions 前端自动化构建与缓存优化</title><link>https://nollieleo.github.io/posts/github-actions-cache-optimization/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/github-actions-cache-optimization/</guid><description>分享在 CI/CD 流程中，如何利用哈希计算和 actions/cache 实现依赖与构建产物的高效缓存。</description><pubDate>Tue, 02 Jul 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在现代前端工程化实践中，CI/CD（持续集成/持续部署）的效率直接影响开发者的交付体验。一个典型的 React 或 Vue 项目，其流水线耗时通常集中在 &lt;code&gt;Dependency Install&lt;/code&gt; 和 &lt;code&gt;Production Build&lt;/code&gt; 两个阶段。本文将探讨如何通过 GitHub Actions 的缓存机制，大幅优化构建时间。&lt;/p&gt;
&lt;h2&gt;1. 核心机制：actions/cache 的工作原理&lt;/h2&gt;
&lt;p&gt;GitHub Actions 提供的 &lt;code&gt;actions/cache&lt;/code&gt; 插件允许我们在不同的 Workflow 运行之间持久化特定目录。其核心逻辑基于 &lt;strong&gt;Key-Value 匹配&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt;：缓存的唯一标识符。通常包含操作系统标识、工具版本和文件哈希。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restore-keys&lt;/strong&gt;：当精确匹配失败时，用于模糊匹配的备选前缀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path&lt;/strong&gt;：需要缓存的目录路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当流水线启动时，&lt;code&gt;actions/cache&lt;/code&gt; 会尝试根据 Key 下载缓存；流水线结束时，如果 Key 发生了变化，它会自动上传新的目录内容。&lt;/p&gt;
&lt;h2&gt;2. 依赖层优化：从 node_modules 到全局 Store&lt;/h2&gt;
&lt;p&gt;传统的缓存方式是直接缓存项目根目录下的 &lt;code&gt;node_modules&lt;/code&gt;。但在使用 &lt;code&gt;pnpm&lt;/code&gt; 时，这种方式并非最优。&lt;/p&gt;
&lt;h3&gt;2.1 Pnpm Store 缓存策略&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pnpm&lt;/code&gt; 使用内容寻址存储（Content-addressable store）。我们应该缓存 pnpm 的全局 store 目录，而不是每个项目的 &lt;code&gt;node_modules&lt;/code&gt;。这样即使多个项目共享同一个 Runner，也能实现最大化的复用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/ci.yml
- name: Install pnpm
  uses: pnpm/action-setup@v3
  with:
    version: 9

- name: Get pnpm store directory
  shell: bash
  run: |
    echo &quot;STORE_PATH=$(pnpm store path --silent)&quot; &amp;gt;&amp;gt; $GITHUB_ENV

- name: Setup pnpm cache
  uses: actions/cache@v4
  with:
    path: ${{ env.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles(&apos;**/pnpm-lock.yaml&apos;) }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 为什么使用 hashFiles？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;hashFiles(&apos;**/pnpm-lock.yaml&apos;)&lt;/code&gt; 会根据锁文件的内容生成唯一的哈希值。只要依赖版本没有变动，哈希值就保持不变，从而精准命中缓存。一旦开发者更新了某个包，哈希值改变，CI 会重新安装并更新缓存。&lt;/p&gt;
&lt;h2&gt;3. 构建产物优化：增量构建与 Turborepo&lt;/h2&gt;
&lt;p&gt;对于大型 Monorepo 或复杂应用，构建过程（Webpack/Vite/Tsc）往往非常耗时。我们可以利用构建工具的本地缓存能力，并将其同步到 CI 缓存中。&lt;/p&gt;
&lt;h3&gt;3.1 Turborepo 远程缓存模拟&lt;/h3&gt;
&lt;p&gt;Turborepo 默认支持将构建结果缓存到 &lt;code&gt;.turbo&lt;/code&gt; 目录。在 GitHub Actions 中，我们可以持久化这个目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache Turborepo
  uses: actions/cache@v4
  with:
    path: .turbo
    # 使用 github.sha 确保每次提交都有机会更新缓存
    # 但通过 restore-keys 继承上一次的产物
    key: ${{ runner.os }}-turbo-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-turbo-

- name: Build
  run: pnpm build --cache-dir=&quot;.turbo&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 缓存命中流转图&lt;/h3&gt;
&lt;p&gt;通过合理的配置，CI 的执行路径如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    Start[开始 CI 运行] --&amp;gt; CheckCache{检查 Cache Key}
    CheckCache --&amp;gt;|Hit 精确命中| Restore[&quot;恢复目录内容&quot;]
    CheckCache --&amp;gt;|Miss 未命中| RestorePartial[&quot;尝试 Restore-keys 模糊恢复&quot;]
    Restore --&amp;gt; Install[&quot;执行 pnpm install --frozen-lockfile&quot;]
    RestorePartial --&amp;gt; Install
    Install --&amp;gt; Build[&quot;执行 pnpm build&quot;]
    Build --&amp;gt; SaveCache[上传新缓存]
    SaveCache --&amp;gt; End[结束]
    
    subgraph Optimization
        Install -.-&amp;gt;|依赖缓存命中| Build
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：缓存雪崩与过时污染的终极解法&lt;/h2&gt;
&lt;p&gt;在真实的 CI 中，我们经常会遇到这样一种诡异的现象：&lt;strong&gt;原本 2 分钟的构建，跑了几个月后变成了 10 分钟。甚至触发了 GitHub 的 10GB 缓存上限，导致所有的流水线集体变慢。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就是经典的**“缓存污染 (Cache Pollution) 与雪崩”**。&lt;/p&gt;
&lt;h3&gt;4.1 缓存污染的元凶&lt;/h3&gt;
&lt;p&gt;当我们在用 &lt;code&gt;actions/cache&lt;/code&gt; 缓存 Webpack 或 Vite 的 &lt;code&gt;.cache&lt;/code&gt; 目录时，构建工具每次都会在里面生成带有新 Hash 的中间产物（比如 &lt;code&gt;vendor.a1b2.js.cache&lt;/code&gt;）。
GitHub Actions 的 &lt;code&gt;save&lt;/code&gt; 动作是&lt;strong&gt;累加&lt;/strong&gt;的：它会把当前目录的所有内容打包传上去。这意味着：&lt;strong&gt;旧的、再也不会用到的文件（比如上个月的旧 Hash 产物），会一直躺在这个缓存包里，跟着你的项目不断滚雪球。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最终，光是下载和解压这个庞大的垃圾缓存包，耗时就超过了重新构建一遍的时间！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解决方案：预清理 (Pre-clean) 与强一致性哈希&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在打包结束后、缓存保存前，我们必须执行一个清理脚本，或者利用构建工具自带的 prune 机制。
例如对于 &lt;code&gt;next.js&lt;/code&gt; 的缓存，我们可以用 &lt;code&gt;actions/cache&lt;/code&gt; 的官方高级插件 &lt;code&gt;setup-node&lt;/code&gt;，它底层处理得更干净；或者手动删除那些 N 天前生成的旧文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: 🧹 Clean up old cache files before saving
  run: |
    # 在上传缓存前，删除 .turbo 或 node_modules/.cache 下超过 7 天未访问的文件
    find .turbo -type f -atime +7 -delete
    find node_modules/.cache -type f -atime +7 -delete
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 并发矩阵构建 (Matrix Strategy) 的包级缓存陷阱&lt;/h3&gt;
&lt;p&gt;在 Monorepo 中，如果你为了加速，开了 4 台机器（Matrix）并行测试 4 个子包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    package: [web, admin, docs, ui]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果每台机器都在最后一步执行了 &lt;code&gt;actions/cache@v4&lt;/code&gt; 的 &lt;code&gt;save&lt;/code&gt; 操作，去覆盖同一个 &lt;code&gt;key: pnpm-store-${{ hashFiles(&apos;pnpm-lock.yaml&apos;) }}&lt;/code&gt;。
由于并发执行，这 4 台机器会&lt;strong&gt;产生竞争条件 (Race Condition)&lt;/strong&gt;，GitHub 会拒绝后 3 台机器的上传（Cache already exists），导致它们的局部构建缓存永远无法被保存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;破局思路：读写分离与后缀区分&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于并行任务，缓存的 &lt;code&gt;key&lt;/code&gt; 必须包含机器的唯一标识，或者将“依赖安装”与“业务构建”分拆到两个独立的 Job 中（如：先由一个单独的 &lt;code&gt;setup&lt;/code&gt; job 跑完 &lt;code&gt;pnpm install&lt;/code&gt; 并上传 &lt;code&gt;node_modules&lt;/code&gt; 缓存，后置的 4 个 Matrix job 全部只读（利用 &lt;code&gt;restore-keys&lt;/code&gt; 命中））。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 读写分离架构示例
jobs:
  setup-dependencies:
    runs-on: ubuntu-latest
    steps:
      # ... 安装依赖并强制存入精确 key 的缓存
      - uses: actions/cache/save@v4 # 注意这里用的是专门的 save action

  parallel-builds:
    needs: setup-dependencies
    strategy:
      matrix:
        app: [web, admin]
    steps:
      # ... 所有的并行节点，只负责 restore，绝不互相覆盖 save！
      - uses: actions/cache/restore@v4
        with:
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles(&apos;pnpm-lock.yaml&apos;) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;缓存空间限制&lt;/strong&gt;：GitHub 每个仓库的缓存上限为 10GB。超过后会按照“最久未使用”原则剔除。建议定期清理无用的分支缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨分支隔离&lt;/strong&gt;：默认情况下，子分支可以访问主分支的缓存，但主分支不能访问子分支的缓存。这保证了主干构建的稳定性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存污染&lt;/strong&gt;：如果缓存中包含了具有副作用的临时文件，可能导致构建失败。务必在 &lt;code&gt;path&lt;/code&gt; 中只包含纯粹的依赖或产物目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;通过 &lt;code&gt;actions/cache&lt;/code&gt; 配合 &lt;code&gt;pnpm&lt;/code&gt; 和 &lt;code&gt;Turborepo&lt;/code&gt;，我们可以构建出一套高效的 CI 流水线。这不仅显著提升了开发效率，还降低了 GitHub Actions 的分钟数消耗。在架构设计时，应始终遵循“哈希驱动、层级缓存、增量构建”的核心原则。&lt;/p&gt;
</content:encoded></item><item><title>Zion CRDT 重构：从传统 Diff 到基于 json-joy 的实时协同架构演进</title><link>https://nollieleo.github.io/posts/zion-crdt-architecture/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zion-crdt-architecture/</guid><description>深度剖析大型无代码平台 Zion 是如何从非常消耗内存的状态化 Diff Server，重构演进为基于 json-joy 的无状态 CRDT 实时协同架构。揭秘在缺乏官方库支持的情况下，如何自主实现基于差异的反向补丁 (Undo/Redo) 引擎与业务级冲突消解防火墙。</description><pubDate>Thu, 20 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 背景与痛点：传统 Diff 架构的“核心瓶颈”&lt;/h2&gt;
&lt;p&gt;在无代码编辑器（如 Zion）中，多人实时协同编辑是一项基础且至关重要的能力。在重构之前，我们依赖于一个传统的、基于 Java 编写的 &lt;strong&gt;状态化 (Stateful) Diff Server&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个旧架构在初期支撑了业务发展，但随着平台内项目数量的激增和画布组件树的无限膨胀，它很快暴露出几个主要的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;服务端内存溢出 (OOM)&lt;/strong&gt;：为了能够比对和计算两个客户端传来的 Diff，Java 服务端必须在内存中全量缓存所有活跃项目的 &lt;code&gt;AppSchema&lt;/code&gt;（JSON 树）。项目一多，内存消耗极大，频繁导致 OOM 崩溃，且完全无法通过简单的横向扩容（Scale-out）来解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间态缓存失效&lt;/strong&gt;：由于缓存数量有限，一旦项目被挤出缓存，重新加载和计算 Diff 需要极长的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发同步冲突&lt;/strong&gt;：在高频的多人协同编辑场景下，高度依赖 Server 端串行处理并推送同步更新。一旦推送延迟，客户端经常会弹出版本冲突（&lt;code&gt;old_value_mismatch&lt;/code&gt;），甚至导致用户丢失大批量的编辑数据，无法保存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决“服务端性能瓶颈”和“客户端合并冲突”这核心挑战，我们决定引入 &lt;strong&gt;CRDT (无冲突复制数据类型)&lt;/strong&gt;，将核心计算下放，开启一场颠覆性的架构重构。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 技术选型：为什么是 CRDT 与 &lt;code&gt;json-joy&lt;/code&gt;？&lt;/h2&gt;
&lt;p&gt;在协同领域，主要分为 OT (Operational Transformation) 和 CRDT 两大流派。我们选择了 CRDT，因为它通过数学上的&lt;strong&gt;交换律和结合律&lt;/strong&gt;，保证了在去中心化或网络延迟的情况下，各个客户端最终合并出来的数据必然是一致的（Eventual Consistency），从而&lt;strong&gt;彻底消灭了需要人工干预的合并冲突&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在具体的库选型上，目前业界最火的是 &lt;code&gt;Yjs&lt;/code&gt; 和 &lt;code&gt;Automerge&lt;/code&gt;。但经过深思熟虑，我们最终选择了 &lt;strong&gt;&lt;code&gt;json-joy/lib/json-crdt&lt;/code&gt;&lt;/strong&gt;，原因非常现实且底层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完美契合底层数据结构&lt;/strong&gt;：Zion 编辑器的底层配置 (&lt;code&gt;AppSchema&lt;/code&gt;) 是非常复杂的 JSON 树。&lt;code&gt;json-joy&lt;/code&gt; 的 CRDT 节点类型原生就抽象成了 &lt;code&gt;con&lt;/code&gt; (常量)、&lt;code&gt;val&lt;/code&gt; (值引用)、&lt;code&gt;obj&lt;/code&gt; (对象)、&lt;code&gt;arr&lt;/code&gt; (数组) 等结构，这种原汁原味的 JSON 语义，极大降低了我们从旧 Schema 迁移的成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极致的传输体积&lt;/strong&gt;：它提供了极简的二进制编码协议，能把庞大的 &lt;code&gt;Patch&lt;/code&gt; (补丁) 压到极小，非常适合我们动辄几百 K 的无代码组件属性同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 架构演进：Diff Server 的“无状态化”改造&lt;/h2&gt;
&lt;p&gt;重构的最核心目标，就是让 Server 端“失忆”。既然 CRDT 能保证多端数据的自动一致，服务端就不再需要在内存里硬扛那棵庞大的 JSON 树了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph OldArchitecture [&quot;旧架构: Stateful Diff Server&quot;]
        direction LR
        A_Old[&quot;Client A&quot;] -- &quot;提交 JSON&quot; --&amp;gt; B_Old[&quot;Java Server&quot;]
        C_Old[&quot;Client B&quot;] -- &quot;提交 JSON&quot; --&amp;gt; B_Old
        B_Old -- &quot;缓存全量树 Calculate Diff&quot; --&amp;gt; B_Old
        B_Old -- &quot;推送 Patch&quot; --&amp;gt; A_Old &amp;amp; C_Old
    end

    subgraph NewArchitecture [&quot;新架构: Stateless CRDT Architecture&quot;]
        direction LR
        A_New[&quot;Client A&quot;] -- &quot;Push Diff (Patch)&quot; --&amp;gt; E_New[&quot;Node.js Gateway (Stateless)&quot;]
        F_New[&quot;Client B&quot;] -- &quot;Push Diff (Patch)&quot; --&amp;gt; E_New
        E_New -- &quot;Route Patch (WebSocket)&quot; --&amp;gt; A_New &amp;amp; F_New
        E_New -- &quot;Async Persistence&quot; --&amp;gt; G_New[(&quot;OSS / DB&quot;)]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在全新的架构下，后端的角色从“计算中心”退化成了一个**“补丁路由器 (Patch Router)”和“持久化存储器”**：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：客户端打开编辑器时，直接从 OSS (&lt;code&gt;crdtModelUrl&lt;/code&gt;) 下载打好的基础模型二进制快照，再从 DB 拉取增量 Patch，在&lt;strong&gt;本地内存中重建&lt;/strong&gt; CRDT Model。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时协同&lt;/strong&gt;：所有人的本地操作都被转换成二进制 Patch。服务器收到 Patch 后，无需做任何校验与合并，直接通过 WebSocket 广播给房间内的其他人。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务端从此再无 OOM 之忧，理论上可以支持无限的横向扩容。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 底层技术挑战剖析 (The Core)&lt;/h2&gt;
&lt;p&gt;虽然架构很美好，但在落地过程中，我们遭遇了几个 &lt;code&gt;json-joy&lt;/code&gt; 早期版本（乃至整个协同领域）的深水区挑战。&lt;/p&gt;
&lt;h3&gt;挑战一：完善 &lt;code&gt;json-joy&lt;/code&gt;，自主实现 Undo/Redo 引擎&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：对于一个专业的低代码编辑器，撤销/重做（Undo/Redo）是绝对的刚需。但在当时，&lt;strong&gt;&lt;code&gt;json-joy&lt;/code&gt; 原生版本根本不支持 Undo/Redo 功能！&lt;/strong&gt;
在协同环境下，如果你只是单纯地把整个树的状态替换为一分钟前的快照，那不仅会撤销你自己的操作，还会把你同事在这过去一分钟里写的心血全部覆盖掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：基于差异的反向补丁生成器 (&lt;code&gt;reverseLocalPatch.ts&lt;/code&gt;)&lt;/strong&gt;
既然官方不支持，我们就自己造！我们扩展了原生的操作，设计了一个包含了非常详尽上下文的自定义 &lt;code&gt;Diff&lt;/code&gt; 接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 我们自定义的 Diff 数据结构，包含了逆向操作所需的全部元信息
interface DiffItem {
  operation: &apos;add&apos; | &apos;update&apos; | &apos;delete&apos; | &apos;move&apos; | &apos;copy&apos;;
  newValue: any;
  oldValue?: any;
  pathComponents: PathComponent[]; // 精准的目标路径，如 [&apos;components&apos;, &apos;button_1&apos;, &apos;style&apos;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当用户按下 &lt;code&gt;Cmd+Z&lt;/code&gt; 触发 Undo 时，我们不会退回旧状态，而是&lt;strong&gt;在当前最新状态下，动态计算并派发一个“反向补丁 (Reverse Patch)”&lt;/strong&gt;。在 &lt;code&gt;reverseLocalPatch.ts&lt;/code&gt; 的深层逻辑中，每一个操作都有绝对的数学逆运算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 非常简化的核心逆向路由逻辑演示
function reverseDiffItem(item: DiffItem): DiffItem {
  switch (item.operation) {
    case &apos;add&apos;:
      // 别人/自己加的内容，逆向操作就是把它删掉
      return { ...item, operation: &apos;delete&apos;, newValue: undefined, oldValue: item.newValue };
    case &apos;delete&apos;:
      // 删掉的内容，利用缓存的 oldValue 原样复原
      return { ...item, operation: &apos;add&apos;, newValue: item.oldValue, oldValue: undefined };
    case &apos;update&apos;:
      // 修改属性，直接将新旧值互换
      return { ...item, operation: &apos;update&apos;, newValue: item.oldValue, oldValue: item.newValue };
    // ... move 与 copy 的非常复杂的逆向拓扑运算
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;高光时刻&lt;/strong&gt;：在真正执行反转应用之前，为了防止别人刚刚把这个容器删了，你现在却要“撤销修改里面按钮的颜色”引发底层报错，我们还加入了一个 &lt;strong&gt;沙盒模拟校验 (Dry-run)&lt;/strong&gt;。
底层调用了 &lt;code&gt;executeCrdtNodeUpdateOptimized&lt;/code&gt;，在当前被别人改得“发生变更”的 CRDT 树上&lt;strong&gt;虚拟执行&lt;/strong&gt;一次反向操作，如果抛出校验异常，说明该历史操作在当前时空已经不合法，系统会直接丢弃这个 Undo 动作；如果校验通过，逆向 Diff 最终会被转化为正常的 CRDT Patch 广播出去，完美攻克了多人时序下的状态回溯难题。&lt;/p&gt;
&lt;h3&gt;挑战二：业务逻辑正确性拦截 (The Firewall)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：&lt;code&gt;json-joy&lt;/code&gt; (或者任何 CRDT) 只能保证数学意义上 JSON 数据结构的&lt;strong&gt;最终一致性&lt;/strong&gt;，但它&lt;strong&gt;完全不懂你的业务正确性&lt;/strong&gt;。
举个极端的例子：用户 A 往 &lt;code&gt;List&lt;/code&gt; 容器里放了一个 &lt;code&gt;Button&lt;/code&gt; 组件，触发了 &lt;code&gt;Button&lt;/code&gt; 创建的 Patch；与此同时，用户 B 把整个 &lt;code&gt;List&lt;/code&gt; 容器给删除了。
CRDT 完美合并了这两个操作，最终结果是：数据库里多出了一个幽灵 &lt;code&gt;Button&lt;/code&gt;，但它根本没有可以挂载的父容器！这种脏数据一旦渲染，直接导致 React 运行时白屏崩溃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：合并后的 &lt;code&gt;ztype&lt;/code&gt; 业务防火墙&lt;/strong&gt;
我们在每次接收并应用远端 Patch 后，强制接入了底层业务类型校验器（基于 &lt;code&gt;ztype&lt;/code&gt;），像一道防火墙一样严格拦截渲染层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Receive[&quot;接收其他客户端的 Patch&quot;] --&amp;gt; Merge[&quot;json-joy 底层合并 (无脑合并)&quot;]
    Merge --&amp;gt; Extract[&quot;提取合并后的 Schema 关联数据&quot;]
    Extract --&amp;gt; Validate{&quot;ztype 业务正确性校验\n(如: 容器存在吗? 循环嵌套了吗?)&quot;}
    
    Validate --&amp;gt;|&quot;校验通过&quot;| UpdateUI[&quot;向下流转: 派发 Diff 刷新 MobX 与 UI&quot;]
    Validate --&amp;gt;|&quot;校验失败 (产生脏数据)&quot;| Revert[&quot;触发静默回退 (Silent Revert)&quot;]
    Revert --&amp;gt; GenerateReverse[&quot;将错误节点回退，生成修正 Patch&quot;]
    GenerateReverse --&amp;gt; SendBack[&quot;将修正 Patch 广播给所有人，强行修复该错误&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在核心同步逻辑中，任何数据进入画布渲染前，都会进行拓扑图关联检查。如果发现合并出来的 JSON 违反了低代码业务的逻辑约束，系统会进行&lt;strong&gt;静默回退 (Silent Revert)&lt;/strong&gt;。我们宁可让那个非法操作“无效”，也绝对不允许脏数据污染并打断当前用户的编辑心流。&lt;/p&gt;
&lt;h3&gt;挑战三：双状态树的平滑过渡 (Dual-Store Architecture)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：面对有着数百万行遗留代码的大型编辑器，我们不可能停下业务发版，花半年时间把所有依赖 &lt;code&gt;MobX&lt;/code&gt; 状态响应式的组件全部重写为订阅 &lt;code&gt;json-joy&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：核心模型与台前镜像&lt;/strong&gt;
为了在不重构老组件的前提下接入协同，我们构建了一个名为“双状态树（Dual-Store）”的过渡期架构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant UI as React UI (依赖 MobX)
    participant CrdtHelper as @crdt-helper (同步网关)
    participant CRDT as jsonJoyModel (Single Source of Truth)
    participant Server as WebSocket 服务端
    
    %% 本地操作管线
    Note over UI,Server: 本地数据变异 (Local Action)
    UI-&amp;gt;&amp;gt;CrdtHelper: 1. 操作触发产生本地 Diff
    CrdtHelper-&amp;gt;&amp;gt;CRDT: 2. applyLocalCrdtDiff (校验后打入核心模型)
    CRDT--&amp;gt;&amp;gt;CrdtHelper: 3. 产出二进制 Patch
    CrdtHelper-&amp;gt;&amp;gt;Server: 4. 发送 Patch 给其他客户端
    CrdtHelper-&amp;gt;&amp;gt;UI: 5. updateMobxStore (同步回 UI 镜像)
    
    %% 远端同步管线
    Note over UI,Server: 远端补丁应用 (Remote Patch)
    Server-&amp;gt;&amp;gt;CrdtHelper: A. 收到别人发来的大型 Patch
    CrdtHelper-&amp;gt;&amp;gt;CRDT: B. 优先静默合并到 CRDT 模型
    CRDT--&amp;gt;&amp;gt;CrdtHelper: C. generateDiff: 深度对比新旧 Schema
    CrdtHelper-&amp;gt;&amp;gt;UI: D. 仅将极细粒度 Diff 派发给 MobX
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Single Source of Truth&lt;/strong&gt;：&lt;code&gt;jsonJoyModel&lt;/code&gt; 成为了真正的幕后单一数据源，所有本地操作、网络同步、冲突消解，全都在它内部原子化完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MobX 作为投影镜像&lt;/strong&gt;：原有的 &lt;code&gt;MobX Store&lt;/code&gt; 被降级为视图层的一个镜像。当收到远端全量的 CRDT 更新时，为了防止 MobX 触发全屏海量 Re-render 卡死，底层会先执行 &lt;code&gt;generateDiff&lt;/code&gt; 计算出极小粒度的差异（例如仅仅是一个子节点的 &lt;code&gt;insert&lt;/code&gt;），然后只将这些细微的变动派发给 MobX 增量更新。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;旧组件完全感知不到底层换了引擎，依然欢快地吃着 MobX 的响应式福利，实现了架构切换的“零阵痛”。&lt;/p&gt;
&lt;h3&gt;挑战四：抽象与封装——&lt;code&gt;@functorz/crdt-helper&lt;/code&gt; 的诞生与实战&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：&lt;code&gt;json-joy&lt;/code&gt; 提供的 API 非常底层（如 &lt;code&gt;api.find()&lt;/code&gt;, &lt;code&gt;ObjNode&lt;/code&gt;, &lt;code&gt;ArrNode&lt;/code&gt;, &lt;code&gt;api.flush()&lt;/code&gt;），如果让 &lt;code&gt;zed&lt;/code&gt;（前端业务项目）直接操作这些底层 API，会导致业务逻辑与 CRDT 引擎深度耦合。一旦未来需要更换协同引擎，或者在 Node.js 网关层复用逻辑，重构成本将是灾难性的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：独立封装 &lt;code&gt;@functorz/crdt-helper&lt;/code&gt; 桥接层&lt;/strong&gt;
为了实现业务与底层协同引擎的解耦，我们抽离出了一个独立的 npm 包 &lt;code&gt;@functorz/crdt-helper&lt;/code&gt;。它扮演了“适配层”和“协调者”的角色：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;API 升维&lt;/strong&gt;：将 &lt;code&gt;json-joy&lt;/code&gt; 晦涩的节点操作，封装为面向业务的 &lt;code&gt;executeCrdtNodeUpdateAndTransformDiff&lt;/code&gt; 等高级方法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双向数据流同步&lt;/strong&gt;：它不仅负责把业务的 &lt;code&gt;Diff&lt;/code&gt; 写入 CRDT 模型，还负责将 CRDT 的变化反向解析，并通过回调（如 &lt;code&gt;updateMobxStore&lt;/code&gt;）精准更新 MobX 状态树。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多端复用&lt;/strong&gt;：作为纯逻辑包，它同时运行在 &lt;code&gt;zed&lt;/code&gt; 前端和 Node.js 协同网关中，保证了双端对 Patch 解析逻辑的绝对一致。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;真实场景追踪：在 &lt;code&gt;zed&lt;/code&gt; 中修改一个按钮的颜色&lt;/h4&gt;
&lt;p&gt;为了让你更直观地感受到这套架构的运转，我们来追踪一个最常见的操作：&lt;strong&gt;用户在右侧属性面板，将一个 Button 的颜色改为了红色 (&lt;code&gt;#FF0000&lt;/code&gt;)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：UI 触发与本地 Diff 生成&lt;/strong&gt;
当用户修改颜色时，&lt;code&gt;zed&lt;/code&gt; 的视图层并不会直接修改 MobX，而是生成一个标准的业务 &lt;code&gt;DiffItem&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const diff = {
  __typename: &apos;DiffItem&apos;,
  operation: &apos;update&apos;,
  pathComponents: [&apos;components&apos;, &apos;button_1&apos;, &apos;style&apos;, &apos;color&apos;],
  newValue: &apos;#FF0000&apos;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二步：进入本地应用管线 (&lt;code&gt;applyLocalCrdtDiff.ts&lt;/code&gt;)&lt;/strong&gt;
&lt;code&gt;zed&lt;/code&gt; 会调用核心管线 &lt;code&gt;applyLocalCrdtDiff&lt;/code&gt;。在这里，系统首先会通过 &lt;code&gt;typeSystemStore.genIncrementalInfo&lt;/code&gt; 进行严格的业务类型校验。校验通过后，正式呼叫 &lt;code&gt;@functorz/crdt-helper&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/src/zed/views/Collaboration/utils/applyLocalCrdtDiff.ts
const result = Crdt.executeCrdtNodeUpdateAndTransformDiff(
  [diff], // 业务 Diff
  schemaStore.jsonJoyModel, // 核心模型：CRDT 实例
  coreStore, // 旧的 Schema 状态
  updateMobxStore, // 非常关键的回调：用于同步 MobX
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三步：&lt;code&gt;crdt-helper&lt;/code&gt; 的核心逻辑 (&lt;code&gt;NodeUpdate/index.ts&lt;/code&gt;)&lt;/strong&gt;
在 &lt;code&gt;crdt-helper&lt;/code&gt; 内部，它会精准找到 &lt;code&gt;json-joy&lt;/code&gt; 树上的对应节点并执行修改，随后生成用于网络传输的二进制 Patch：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// @functorz/crdt-helper 内部逻辑简写
export const executeCrdtNodeUpdateAndTransformDiff = (...) =&amp;gt; {
  // 1. 寻址并修改底层 CRDT 节点
  const operableNode = model.api.find([&apos;components&apos;, &apos;button_1&apos;, &apos;style&apos;]);
  operableNode.set({ color: &apos;#FF0000&apos; }); 

  // 2. 触发回调，将极细粒度的变更同步给 MobX 镜像
  const crdtDiffs = transformLocalDiffsToCrdtFormat([item], model, oldSchema, updateMobxStore);

  // 3. 冲刷出二进制补丁，并转为 Base64 准备广播
  const patch = model.api.flush();
  return { patchBase64: convertPatchToBase64(patch), diffsFromCrdt: crdtDiffs };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第四步：MobX 镜像更新与网络广播&lt;/strong&gt;
在上述步骤中，传入的 &lt;code&gt;updateMobxStore&lt;/code&gt; 回调被触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// zed/src/zed/views/Collaboration/utils/updateMobxStore.ts
export function updateMobxStore(node: any, diff: DiffItem) {
  const lastKey = diff.pathComponents.at(-1)?.key; // &apos;color&apos;
  node[lastKey] = diff.newValue; // 直接修改 MobX 对象，触发 React 局部重渲染
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，UI 瞬间变成了红色。同时，&lt;code&gt;crdt-helper&lt;/code&gt; 返回的 &lt;code&gt;patchBase64&lt;/code&gt; 会被推入 &lt;code&gt;schemaStore.addWaitingUploadPatches&lt;/code&gt; 队列，通过 WebSocket 传输至服务器，并最终被房间内的其他客户端通过 &lt;code&gt;useApplyNetworkPatches&lt;/code&gt; 接收并静默合并。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;4. 性能优化策略：选择性 CRDT 实例化 (Constant Node Pruning)&lt;/h4&gt;
&lt;p&gt;在将庞大的 JSON Schema 转换为 CRDT 模型时，如果为深层嵌套的每一个属性都创建 CRDT 节点（每个节点都需要维护逻辑时钟、Tombstone 等元数据），会导致灾难性的内存开销和极慢的初始化速度。&lt;/p&gt;
&lt;p&gt;Zion 在 &lt;code&gt;@functorz/crdt-helper&lt;/code&gt; 中引入了一个非常底层的启发式剪枝引擎（&lt;code&gt;ALL_NODE_MATCH&lt;/code&gt;）。它会在递归构建 CRDT 树时，精准识别出那些&lt;strong&gt;不需要细粒度协同&lt;/strong&gt;或&lt;strong&gt;总是被整体替换&lt;/strong&gt;的子树（例如 &lt;code&gt;componentFrame&lt;/code&gt;、&lt;code&gt;tableMetadata&lt;/code&gt;、&lt;code&gt;dataBinding&lt;/code&gt; 等）。&lt;/p&gt;
&lt;p&gt;对于这些子树，引擎会停止递归，直接使用 &lt;code&gt;json-joy&lt;/code&gt; 的常量节点 &lt;code&gt;s.con(json)&lt;/code&gt; 进行包裹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// crdthelper/packages/crdt/utils/BuildSchema/index.ts
export const buildSchema = (json: any, pathComponents: DiffPathComponent[]): any =&amp;gt; {
  for (const nodeMatch of ALL_NODE_MATCH) {
    if (isFullMatch(nodeMatch)) {
      if (nodeMatch.valueMatch(json)) {
        return s.con(json); // 👈 核心优化：直接作为常量节点，停止递归！
      }
    } else if (isPartialMatch(nodeMatch)) {
      if (nodeMatch.pathMatch(pathComponents.slice(0, -1))) {
        const lastKey = pathComponents.at(-1)?.key ?? &apos;&apos;;
        if (nodeMatch.isConstKey(lastKey)) {
          return s.con(json); // 👈 核心优化：特定 Key 直接作为常量节点
        }
      }
    }
  }
  
  // ... 否则继续递归构建 s.arr 或 s.str
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么这个设计很绝？&lt;/strong&gt;
这意味着 CRDT 引擎将这些庞大的对象视为&lt;strong&gt;不透明的、不可变的值&lt;/strong&gt;。当用户修改它们时，触发的是整个对象的替换（基于 LWW 机制），而不是逐个属性的 Merge。这在保留了核心业务细粒度协同能力的同时，&lt;strong&gt;砍掉了极高比例的不必要 CRDT 元数据开销&lt;/strong&gt;，极大地降低了内存占用并提升了首屏加载速度。&lt;/p&gt;
&lt;h3&gt;挑战五：CRDT 时代的时光机难题 (Undo/Redo 隔离)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;痛点&lt;/strong&gt;：传统的单机编辑器中，撤销/重做（Undo/Redo）非常简单：只要维护一个状态快照（Snapshot）数组，按下 &lt;code&gt;Cmd+Z&lt;/code&gt; 时指针后退一步即可。
但在实时协同的 CRDT 环境下，这种“粗暴的回退”是灾难性的。假设当前状态是 &lt;code&gt;S1&lt;/code&gt;，用户 A 把按钮改成了红色（状态变为 &lt;code&gt;S2&lt;/code&gt;），紧接着用户 B 把标题改成了“你好”（状态变为 &lt;code&gt;S3&lt;/code&gt;）。
如果此时用户 A 按下撤销，他期望的是**“按钮变回原来的颜色”**，但如果简单地把全局状态回退到 &lt;code&gt;S1&lt;/code&gt;，用户 B 辛辛苦苦敲的“你好”也会跟着一起被无辜抹除！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何实现只撤销“自己”的操作？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：Session 隔离与反向 Patch 的精准对冲&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在基于 json-joy 的新架构中，我们放弃了“状态快照栈”，转而实现了一个**“操作语义栈 (Action Stack)”**。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Session ID 染色&lt;/strong&gt;：
每个客户端在初始化时都会生成一个独一无二的 &lt;code&gt;SessionID&lt;/code&gt;（通常是一个随机的 UUID）。当用户在本地进行任何操作（如修改颜色）生成 Patch 时，底层引擎会自动将这个 &lt;code&gt;SessionID&lt;/code&gt; 注入到该操作的元数据（Metadata）中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;本地历史栈隔离&lt;/strong&gt;：
客户端的 Undo 栈里，不再存全局的 JSON 树，而是只存&lt;strong&gt;带有自己 &lt;code&gt;SessionID&lt;/code&gt; 的反向补丁 (Reverse Patch)&lt;/strong&gt;。当用户 A 把颜色从蓝色改成红色时，他的本地历史栈会被推入一个记录：“将目标路径的颜色改回蓝色”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;时空对冲 (Chronological Hedging)&lt;/strong&gt;：
当用户 A 按下 &lt;code&gt;Cmd+Z&lt;/code&gt; 时，系统从本地历史栈弹出那个“改回蓝色”的反向补丁，并作为一个&lt;strong&gt;全新的、发生在当前时间点的前进操作 (Forward Action)&lt;/strong&gt; 执行。&lt;/p&gt;
&lt;p&gt;最绝妙的地方在于，CRDT 引擎（json-joy）在应用这个反向补丁时，依然遵循 LWW（Last-Write-Wins，最后写入胜出）或树的拓扑合并规则。它会去检查目标节点当前的逻辑时钟（Logical Clock）。如果这个按钮在用户 A 改红之后，又被用户 C 改成了绿色，那么用户 A 的“撤销为蓝色”补丁在应用时，会基于时间戳或因果关系进行合并，而不会破坏整个数据结构的完整性，也绝对不会波及到用户 B 修改的标题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这种“以进为退”的对冲机制，我们在没有修改 json-joy 底层核心算法的前提下，完美实现了协同场景下&lt;strong&gt;极其精准的、属于用户自己的非线性 Undo/Redo&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;5. 总结与展望&lt;/h2&gt;
&lt;p&gt;这场“底层重构”式的重构，彻底拔掉了压在 Zion 基础设施头上的 OOM 问题。&lt;/p&gt;
&lt;p&gt;从中心化的 &lt;strong&gt;Stateful Diff Server&lt;/strong&gt; 演进到 &lt;strong&gt;Stateless CRDT Architecture&lt;/strong&gt;，我们不仅优化了服务器的内存占用，更为用户带来的是&lt;strong&gt;真正的 Local-first (本地优先)&lt;/strong&gt; 编辑体验——网络再差、甚至断网离线，用户依然可以毫无延迟地高频拖拽组件，等网络恢复时，CRDT 会用抹平分歧。&lt;/p&gt;
&lt;p&gt;虽然当前我们为了向后兼容，依然保留了 MobX 作为渲染代理。但在在未来，我们是要全量废弃mobx，让 React 组件直接响应原生的 CRDT 变更事件。&lt;/p&gt;
</content:encoded></item><item><title>浏览器渲染管线与 CSS 硬件加速原理</title><link>https://nollieleo.github.io/posts/browser-rendering-pipeline-gpu/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/browser-rendering-pipeline-gpu/</guid><description>解析浏览器从 DOM 树解析到像素渲染的流水线，探讨 Transform 和 Opacity 能够实现 60FPS 动画的底层原理。</description><pubDate>Sat, 08 Jun 2024 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;深入理解浏览器渲染管线，是进行前端性能调优和动画优化的核心前提。在现代 Web 应用中，实现 60FPS（每秒 60 帧）的动画已成为基本标准。本文将从底层架构出发，深度解析浏览器如何将 HTML/CSS 转化为屏幕上的像素，并探讨 CSS 硬件加速的本质。&lt;/p&gt;
&lt;h2&gt;1. 渲染管线 (Rendering Pipeline) 的核心阶段&lt;/h2&gt;
&lt;p&gt;当浏览器接收到网络层传回的资源后，渲染引擎（如 Chrome 的 Blink）会启动一条复杂的流水线。这条管线主要由以下五个核心阶段组成：&lt;/p&gt;
&lt;h3&gt;1.1 解析与 DOM/CSSOM 构建 (Parsing)&lt;/h3&gt;
&lt;p&gt;浏览器首先解析 HTML 字符串，构建 &lt;strong&gt;DOM 树&lt;/strong&gt;（Document Object Model）。与此同时，解析 CSS 文件或 style 标签，构建 &lt;strong&gt;CSSOM 树&lt;/strong&gt;（CSS Object Model）。这两个过程是并行的，但 DOM 的构建可能会被同步执行的 JavaScript 阻塞。&lt;/p&gt;
&lt;h3&gt;1.2 样式计算 (Style Calculation)&lt;/h3&gt;
&lt;p&gt;渲染引擎将 DOM 树和 CSSOM 树合并，计算出每个 DOM 节点的最终样式。这一步需要处理选择器匹配、层叠规则（Cascading）以及继承逻辑。最终生成的结果是 &lt;strong&gt;Render Tree&lt;/strong&gt;（渲染树），它只包含需要显示的节点（例如 &lt;code&gt;display: none&lt;/code&gt; 的节点会被剔除）。&lt;/p&gt;
&lt;h3&gt;1.3 布局 (Layout / Reflow)&lt;/h3&gt;
&lt;p&gt;布局阶段的任务是计算渲染树中每个节点的几何信息：位置（x, y）和尺寸（width, height）。浏览器从根节点开始遍历，根据盒模型规则确定每个元素在视口内的确切坐标。&lt;/p&gt;
&lt;h3&gt;1.4 绘制 (Paint)&lt;/h3&gt;
&lt;p&gt;绘制阶段并非直接输出像素，而是生成一系列&lt;strong&gt;绘制指令&lt;/strong&gt;（如“在坐标 (10, 10) 处画一个半径为 5 的圆”）。浏览器会将页面拆分为多个图层（Layers），并为每个图层生成各自的指令列表。&lt;/p&gt;
&lt;h3&gt;1.5 合成 (Composite)&lt;/h3&gt;
&lt;p&gt;这是现代浏览器性能优化的关键。合成线程（Compositor Thread）将页面拆分成的图层发送给 GPU。GPU 根据这些图层及其属性（如位移、透明度），将它们“拼”在一起显示在屏幕上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    HTML[&quot;HTML&quot;] --&amp;gt; DOM[&quot;DOM Tree&quot;]
    CSS[&quot;CSS&quot;] --&amp;gt; CSSOM[&quot;CSSOM Tree&quot;]
    DOM --&amp;gt; RenderTree[&quot;Render Tree&quot;]
    CSSOM --&amp;gt; RenderTree
    RenderTree --&amp;gt; Layout[&quot;Layout (几何计算)&quot;]
    Layout --&amp;gt; Paint[&quot;Paint (绘制指令)&quot;]
    Paint --&amp;gt; Layerize[&quot;Layerize (分层)&quot;]
    Layerize --&amp;gt; Composite[&quot;Composite (GPU 合成)&quot;]
    
    subgraph MainThread [&quot;主线程 (Main Thread)&quot;]
        DOM
        CSSOM
        RenderTree
        Layout
        Paint
    end
    
    subgraph GPU [&quot;GPU 进程&quot;]
        Composite
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 回流 (Reflow) 与重绘 (Repaint) 的性能瓶颈&lt;/h2&gt;
&lt;p&gt;在交互过程中，修改 DOM 或 CSS 会导致管线重新执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;回流 (Reflow)&lt;/strong&gt;：当修改了影响几何属性的样式（如 &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;border&lt;/code&gt;）或读取某些属性（如 &lt;code&gt;offsetWidth&lt;/code&gt;, &lt;code&gt;getComputedStyle&lt;/code&gt;）时，浏览器必须重新经历 Layout -&amp;gt; Paint -&amp;gt; Composite。这是代价最高的操作，因为它可能引发整个文档的重新计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重绘 (Repaint)&lt;/strong&gt;：当仅修改外观属性（如 &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;background-color&lt;/code&gt;, &lt;code&gt;visibility&lt;/code&gt;）而不影响几何位置时，浏览器跳过 Layout，直接进入 Paint 阶段。虽然比回流快，但在复杂页面中依然存在明显的 CPU 开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. CSS 硬件加速：绕过主线程&lt;/h2&gt;
&lt;p&gt;为了实现极佳的动画性能，我们需要避开主线程的 Layout 和 Paint。这就是 &lt;strong&gt;CSS 硬件加速&lt;/strong&gt;（GPU Acceleration）的意义所在。&lt;/p&gt;
&lt;h3&gt;3.1 合成层 (Compositing Layers)&lt;/h3&gt;
&lt;p&gt;当一个元素被提升为“合成层”时，它拥有独立的图形上下文。在动画过程中，如果只改变该层的合成属性（如 &lt;code&gt;transform&lt;/code&gt; 或 &lt;code&gt;opacity&lt;/code&gt;），浏览器只需要在合成线程中通知 GPU 重新组合这些层，而无需重新布局或重新绘制。&lt;/p&gt;
&lt;h3&gt;3.2 触发合成层提升的条件&lt;/h3&gt;
&lt;p&gt;以下属性会触发浏览器为元素创建独立的合成层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 3D 变换：&lt;code&gt;transform: translateZ(0)&lt;/code&gt;, &lt;code&gt;rotate3d()&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;will-change&lt;/code&gt; 属性：显式告知浏览器该元素即将发生变化。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;opacity&lt;/code&gt;, &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;backdrop-filter&lt;/code&gt; 应用 &lt;code&gt;transition&lt;/code&gt; 或 &lt;code&gt;animation&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.3 为什么 Transform 和 Opacity 如此高效？&lt;/h3&gt;
&lt;p&gt;与其他属性不同，&lt;code&gt;transform&lt;/code&gt; 和 &lt;code&gt;opacity&lt;/code&gt; 的处理完全发生在合成线程和 GPU 中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transform&lt;/strong&gt;：GPU 只需要对已有的纹理（Texture）进行矩阵变换（Matrix Transform），这在硬件层面是极快的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opacity&lt;/strong&gt;：GPU 只需要在合成时调整图层的 Alpha 通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/* 低效方案：触发回流 */
.box-slow {
  position: absolute;
  left: 0;
  transition: left 0.3s ease;
}
.box-slow:hover {
  left: 100px; 
}

/* 高效方案：仅触发 Composite */
.box-fast {
  transform: translateX(0);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  /* 提前提升为合成层，避免动态提升带来的闪烁 */
  will-change: transform; 
}
.box-fast:hover {
  transform: translateX(100px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 进阶机制：分块 (Tiling) 与栅格化 (Rasterization)&lt;/h2&gt;
&lt;p&gt;在合成阶段之前，还有一个关键步骤：&lt;strong&gt;栅格化&lt;/strong&gt;。
由于页面可能非常长，浏览器不会一次性绘制整个页面。合成线程会将图层划分为多个&lt;strong&gt;分块 (Tiles)&lt;/strong&gt;（通常是 256x256 或 512x512 像素）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;栅格化&lt;/strong&gt;：将绘制指令转化为位图（Bitmap）的过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU 栅格化&lt;/strong&gt;：现代浏览器利用 GPU 的并行计算能力来加速位图的生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分块加载&lt;/strong&gt;：浏览器优先栅格化视口（Viewport）附近的块，从而实现快速首屏响应。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 业务踩坑：图层爆炸 (Layer Explosion)&lt;/h2&gt;
&lt;p&gt;很多前端开发者在学习了 CSS 硬件加速后，喜欢给所有带动画的元素加上 &lt;code&gt;transform: translateZ(0)&lt;/code&gt; 或者 &lt;code&gt;will-change: transform&lt;/code&gt;。这种“大力出奇迹”的做法在复杂页面中会导致灾难性的&lt;strong&gt;图层爆炸&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;5.1 隐式合成 (Implicit Compositing) 的陷阱&lt;/h3&gt;
&lt;p&gt;当浏览器决定渲染层级时，有一个严格的规则：&lt;strong&gt;如果元素 B 的 z-index 高于元素 A，且 A 是一个独立的 GPU 合成层，那么为了保证 B 依然能盖在 A 上面，浏览器会被迫把 B 也提升为一个独立的 GPU 合成层。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设想一个常见的业务场景：
你给页面底部的一个绝对定位的背景动画加了 &lt;code&gt;will-change: transform&lt;/code&gt;（创建了层 A）。
在这个背景之上，有 1000 个普通的商品列表节点（属于层 B）。
因为这 1000 个节点在 z 轴上高于背景层 A，浏览器不得不为这 1000 个节点创建 1000 个独立的 GPU 纹理！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;极高的 GPU 显存占用（手机端极易引发 OOM 闪退）。&lt;/li&gt;
&lt;li&gt;每次滚动时，合成线程需要处理成百上千个图层的计算，掉帧严重。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 如何排查图层爆炸？&lt;/h3&gt;
&lt;p&gt;千万不要盲目猜测。在 Chrome DevTools 中，按下 &lt;code&gt;Cmd + Shift + P&lt;/code&gt; (Mac) / &lt;code&gt;Ctrl + Shift + P&lt;/code&gt; (Win)，搜索并打开 &lt;strong&gt;&quot;Show Layers&quot;&lt;/strong&gt; 面板。
这里会 3D 可视化地展示当前页面的所有 GPU 图层。点击任意图层，右侧的 &lt;code&gt;Details&lt;/code&gt; 会明确告诉你&lt;strong&gt;它被提升为合成层的原因&lt;/strong&gt;（例如：&lt;code&gt;Compositing reason: Assumed to overlap a layer with a composited animation&lt;/code&gt; —— 这就是典型的隐式合成受害者）。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;strong&gt;给那个需要硬件加速的底层元素设置一个极高的 &lt;code&gt;z-index&lt;/code&gt;（如果视觉上允许），或者将其与其他正常文档流的元素在空间上剥离开来。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;6. 性能优化最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;区分主线程动画与合成器动画&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;margin-left&lt;/code&gt; 动画：在主线程运行，如果 JS 在执行复杂计算（如 &lt;code&gt;while(true)&lt;/code&gt;），动画会立刻卡死。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transform: translateX&lt;/code&gt; 动画：在 GPU 合成线程运行，即使主线程被 JS 完全阻塞，动画依然丝滑（这也是为什么很多页面的 Loading 圈用 transform 写，页面卡死了圈还在转）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合理使用 &lt;code&gt;will-change&lt;/code&gt;&lt;/strong&gt;：只在动画发生前（如 hover 或 JS 触发前一刻）添加，动画结束后立刻通过 JS 移除。不要写死在 CSS 类中作为常驻属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免强制同步布局 (Forced Synchronous Layout)&lt;/strong&gt;：在 JS 的一个 tick 中，如果先修改了 DOM，立刻又去读取 &lt;code&gt;offsetHeight&lt;/code&gt; 等几何属性，浏览器会被迫立刻中止当前任务去执行 Layout，引发严重的卡顿。读写操作一定要分离（如使用 FastDOM 库或 &lt;code&gt;requestAnimationFrame&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过深入理解渲染管线，开发者可以从“凭感觉写代码”转向“基于原理的精准调优”，真正掌控 Web 性能的命脉。&lt;/p&gt;
</content:encoded></item><item><title>IndexedDB 高阶应用：前端海量数据的本地存储方案</title><link>https://nollieleo.github.io/posts/indexeddb-frontend-storage/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/indexeddb-frontend-storage/</guid><description>探讨在 LocalStorage 容量瓶颈下，如何使用 IndexedDB 构建基于事务的异步存储，实现 Local-first 架构。</description><pubDate>Sun, 12 May 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在构建现代复杂 Web 应用（如离线文档编辑器、大型 CRM 系统）时，LocalStorage 的 5MB 容量限制和同步阻塞特性已无法满足需求。IndexedDB 作为浏览器内置的非关系型数据库，提供了海量存储（通常为磁盘剩余空间的 80%）和异步事务支持，是实现 Local-first 架构的核心基石。&lt;/p&gt;
&lt;h2&gt;1. IndexedDB 核心架构与设计模式&lt;/h2&gt;
&lt;p&gt;IndexedDB 的设计理念更接近于 NoSQL 数据库，其核心组件包括对象仓库 (Object Stores)、索引 (Indexes) 和事务 (Transactions)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    DB[(IndexedDB)] --&amp;gt; OS1[Object Store: Users]
    DB --&amp;gt; OS2[Object Store: Messages]
    OS1 --&amp;gt; IX1[Index: email]
    OS1 --&amp;gt; IX2[Index: lastLogin]
    T[Transaction] -- 原子性操作 --&amp;gt; OS1
    T -- 并发控制 --&amp;gt; OS2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 使用 Dexie.js 提升工程效率&lt;/h2&gt;
&lt;p&gt;原生 API 的繁琐（基于请求/响应事件）使得代码难以维护。Dexie.js 提供了优雅的 Promise 封装和强大的查询能力。&lt;/p&gt;
&lt;h3&gt;数据库定义与版本管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import Dexie, { type Table } from &apos;dexie&apos;;

export interface Message {
  id?: number;
  content: string;
  senderId: string;
  timestamp: number;
  status: &apos;sent&apos; | &apos;pending&apos; | &apos;failed&apos;;
}

class ChatDatabase extends Dexie {
  messages!: Table&amp;lt;Message&amp;gt;;

  constructor() {
    super(&apos;ChatAppDB&apos;);
    // 定义版本。注意：索引字段必须在此声明
    this.version(1).stores({
      messages: &apos;++id, senderId, timestamp, status&apos;
    });
  }
}

export const db = new ChatDatabase();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 高性能读写策略：批量操作与事务&lt;/h2&gt;
&lt;p&gt;在处理海量数据时，频繁开启小事务会导致严重的性能损耗。正确的做法是利用 &lt;strong&gt;批量操作 (Bulk Operations)&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function syncMessagesFromServer(newMessages: Message[]) {
  try {
    await db.transaction(&apos;rw&apos;, db.messages, async () =&amp;gt; {
      // 1. 批量写入，比循环 put 快一个数量级
      await db.messages.bulkPut(newMessages);

      // 2. 清理过期数据（如只保留最近 1000 条）
      const count = await db.messages.count();
      if (count &amp;gt; 1000) {
        const oldestToKeep = await db.messages
          .orderBy(&apos;timestamp&apos;)
          .reverse()
          .offset(1000)
          .first();
        
        if (oldestToKeep) {
          await db.messages
            .where(&apos;timestamp&apos;)
            .below(oldestToKeep.timestamp)
            .delete();
        }
      }
    });
  } catch (error) {
    console.error(&apos;Transaction failed:&apos;, error);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Web Worker 中的离屏存储 (Offscreen Storage)&lt;/h2&gt;
&lt;p&gt;虽然 IndexedDB 是异步的，但在主线程进行大规模数据的序列化/反序列化（Structured Clone）仍可能导致掉帧。将数据库逻辑移至 Web Worker 是保证极致性能的重要手段。&lt;/p&gt;
&lt;h3&gt;Worker 线程逻辑&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// db.worker.ts
import { db } from &apos;./db&apos;;

self.onmessage = async (e) =&amp;gt; {
  const { type, payload } = e.data;
  if (type === &apos;QUERY_MESSAGES&apos;) {
    const results = await db.messages
      .where(&apos;senderId&apos;)
      .equals(payload.userId)
      .toArray();
    self.postMessage({ type: &apos;QUERY_RESULT&apos;, results });
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 业务踩坑：多 Tab 并发写入与事务阻塞的隐性 Bug&lt;/h2&gt;
&lt;p&gt;在真实的复杂 Web 应用（比如 Notion 这种多 Tab 页打开同一个工作区的工具）中，如果你的用户同时打开了 3 个 Tab，并且同时触发了自动保存逻辑，IndexedDB 的锁机制就会引发一场灾难。&lt;/p&gt;
&lt;h3&gt;5.1 隐蔽的事务挂起 (Transaction Suspending)&lt;/h3&gt;
&lt;p&gt;IndexedDB 采用了极其严格的&lt;strong&gt;读写锁机制&lt;/strong&gt;。如果你对表 &lt;code&gt;users&lt;/code&gt; 开启了一个 &lt;code&gt;readwrite&lt;/code&gt;（读写）事务，整个浏览器里其他所有想要写 &lt;code&gt;users&lt;/code&gt; 的事务都会被阻塞（排队等待）。&lt;/p&gt;
&lt;p&gt;很多开发者喜欢在事务里夹带私货（比如网络请求），这就触发了致命错误：&lt;strong&gt;事务挂起与隐式提交&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 灾难性的写法：事务内部发生跨 Tick 的微任务/宏任务
await db.transaction(&apos;rw&apos;, db.users, async () =&amp;gt; {
  const user = await db.users.get(1);
  
  // 致命错误：这会导致当前事务在这个 await 期间被隐式提交！
  // 等 fetch 结束，后面的 put 就会抛出 &quot;TransactionInactiveError&quot;
  const response = await fetch(&apos;/api/user/sync&apos;); 
  
  await db.users.put({ ...user, synced: true });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;底层原理：&lt;/strong&gt;
IndexedDB 的事务是与浏览器的 &lt;strong&gt;Event Loop&lt;/strong&gt; 绑定的。一旦事务回调执行完毕，当前宏任务（或微任务队列空了）结束，浏览器会自动帮你提交事务。
如果你在里面插入了 &lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;setTimeout&lt;/code&gt; 等产生新 Event Loop Tick 的异步操作，事务就会失效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：分离 I/O 与 DB 操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ 正确做法：只读事务 -&amp;gt; 网络请求 -&amp;gt; 写事务
const user = await db.transaction(&apos;r&apos;, db.users, () =&amp;gt; db.users.get(1)); // 事务 1

const response = await fetch(&apos;/api/user/sync&apos;); // DB 自由态，不占用锁

// 重新开启短平快的事务 2
await db.transaction(&apos;rw&apos;, db.users, () =&amp;gt; db.users.put({ ...user, synced: true }));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 QuotaExceededError 与不可靠的持久化&lt;/h3&gt;
&lt;p&gt;IndexedDB 并不是“安全”的避风港。当用户的 C 盘快满了，或者浏览器分配给该域名的 Quota 被用尽时，会发生两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写入报错：抛出 &lt;code&gt;QuotaExceededError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静默驱逐 (Silent Eviction)&lt;/strong&gt;：更可怕的是，浏览器会在硬盘紧张时，&lt;strong&gt;一声不吭地把你的整个 IndexedDB 删掉！&lt;/strong&gt;（基于 LRU 策略，按域名清理）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你是用 IndexedDB 存“草稿”，这种驱逐是毁灭性的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：申请持久化存储 (Persistent Storage)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在极其关键的离线应用中，你必须主动向浏览器申请将存储标记为持久化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function requestPersistentStorage() {
  if (navigator.storage &amp;amp;&amp;amp; navigator.storage.persist) {
    const isPersisted = await navigator.storage.persisted();
    if (!isPersisted) {
      // 浏览器可能会弹窗询问用户“是否允许该网站在本地永久存储数据”
      const granted = await navigator.storage.persist();
      console.log(&apos;Persistent storage granted:&apos;, granted);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被标记为 persisted 的站点数据，除非用户手动去设置里清理缓存，否则浏览器宁可把其他网站的缓存全删光，也不会动你的数据分毫。&lt;/p&gt;
&lt;h2&gt;6. Local-first 架构下的同步挑战&lt;/h2&gt;
&lt;p&gt;使用 IndexedDB 不仅仅是为了存储，更是为了实现“本地优先”的体验。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;乐观更新&lt;/strong&gt;: 交互时先写入 IndexedDB，再异步同步至服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冲突解决&lt;/strong&gt;: 引入 CRDT (Conflict-free Replicated Data Types) 或简单的 LWW (Last Write Wins) 策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增量同步&lt;/strong&gt;: 利用 &lt;code&gt;updatedAt&lt;/code&gt; 索引，仅拉取自上次同步以来的变更。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;IndexedDB 为前端应用提供了接近原生应用的持久化能力。通过 Dexie.js 的抽象、事务的合理利用以及 Web Worker 的隔离，我们可以构建出在海量数据下依然保持流畅加载和响应的现代 Web 应用。&lt;/p&gt;
</content:encoded></item><item><title>现代网页核心性能指标 (Core Web Vitals) 优化指南</title><link>https://nollieleo.github.io/posts/core-web-vitals-optimization/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/core-web-vitals-optimization/</guid><description>系统性讲解 LCP, CLS 以及取代 FID 的 INP 性能指标，提供针对主线程阻塞和渲染管线的具体优化策略。</description><pubDate>Sat, 20 Apr 2024 10:15:00 GMT</pubDate><content:encoded>&lt;p&gt;Google 提出的核心网页指标 (Core Web Vitals) 已成为衡量用户体验的工业标准。随着 2024 年 INP (Interaction to Next Paint) 正式取代 FID，性能优化的重心已从单纯的“加载快”转向“交互稳”和“响应灵”。&lt;/p&gt;
&lt;h2&gt;1. Largest Contentful Paint (LCP) 深度优化&lt;/h2&gt;
&lt;p&gt;LCP 衡量页面视口内最大内容元素的渲染时间。优化 LCP 的核心在于缩短 &lt;strong&gt;关键路径延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;资源优先级调度&lt;/h3&gt;
&lt;p&gt;利用 &lt;code&gt;Fetch Priority API&lt;/code&gt; 可以显著改变浏览器的下载决策。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 提升首屏 Hero Image 的优先级 --&amp;gt;
&amp;lt;img src=&quot;/assets/hero.webp&quot; fetchpriority=&quot;high&quot; alt=&quot;Main Banner&quot; /&amp;gt;

&amp;lt;!-- 降低非关键资源的优先级 --&amp;gt;
&amp;lt;script src=&quot;/js/analytics.js&quot; fetchpriority=&quot;low&quot; async&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键路径优化策略&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;消除渲染阻塞&lt;/strong&gt;: 将非关键 CSS 异步加载，或使用 &lt;code&gt;Critical CSS&lt;/code&gt; 提取首屏样式内联。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预连接与预加载&lt;/strong&gt;: 针对第三方 CDN 域名使用 &lt;code&gt;preconnect&lt;/code&gt;，针对关键字体使用 &lt;code&gt;preload&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端响应时间 (TTFB)&lt;/strong&gt;: 引入边缘计算 (Edge Functions) 或流式渲染 (Streaming SSR) 减少首字节等待。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. Cumulative Layout Shift (CLS) 视觉稳定性&lt;/h2&gt;
&lt;p&gt;CLS 衡量页面生命周期内发生的意外布局偏移。&lt;/p&gt;
&lt;h3&gt;布局稳定性保障&lt;/h3&gt;
&lt;p&gt;最常见的偏移源于未定义尺寸的媒体资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 使用 aspect-ratio 提前占位 */
.video-wrapper {
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: #f0f0f0; /* 骨架屏底色 */
}

/* 字体加载优化 */
@font-face {
  font-family: &apos;CustomFont&apos;;
  src: url(&apos;...&apos;);
  font-display: swap; /* 避免不可见文本闪烁 FOIT */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Interaction to Next Paint (INP) 响应性革命&lt;/h2&gt;
&lt;p&gt;INP 是目前最具挑战性的指标，它衡量用户在页面上进行的所有交互（点击、按键）到下一帧渲染的延迟。&lt;/p&gt;
&lt;h3&gt;长任务切片 (Task Yielding)&lt;/h3&gt;
&lt;p&gt;当主线程被超过 50ms 的 JavaScript 任务占据时，用户交互将被挂起。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant U as 用户输入
    participant M as 主线程 (阻塞)
    participant R as 渲染管线
    U-&amp;gt;&amp;gt;M: 点击按钮
    Note over M: 执行长任务 (300ms)
    M--&amp;gt;&amp;gt;R: 无法渲染下一帧
    Note over M: 任务结束
    M-&amp;gt;&amp;gt;R: 触发 Paint (INP 延迟高)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;优化方案：主动让出控制权&lt;/h3&gt;
&lt;p&gt;利用 &lt;code&gt;scheduler.yield&lt;/code&gt; (现代浏览器) 或 &lt;code&gt;setTimeout&lt;/code&gt; 拆分长任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function heavyDataProcessing(data) {
  const chunks = splitIntoChunks(data);
  
  for (const chunk of chunks) {
    process(chunk);
    
    // 检查是否需要让出主线程
    if (shouldYield()) {
      // 现代浏览器优先使用 scheduler.yield()
      if (&apos;scheduler&apos; in window &amp;amp;&amp;amp; &apos;yield&apos; in scheduler) {
        await scheduler.yield();
      } else {
        await new Promise(resolve =&amp;gt; setTimeout(resolve, 0));
      }
    }
  }
}

function shouldYield() {
  // 利用 isInputPending API 探测是否有挂起的输入事件
  return navigator.scheduling?.isInputPending() || Date.now() - lastYieldTime &amp;gt; 50;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：BFCache 穿透与隐藏的 LCP 杀手&lt;/h2&gt;
&lt;p&gt;在做 Core Web Vitals 优化时，很多前端会忽略一个能瞬间把 LCP 和整体加载体验提升 10 倍的神器：&lt;strong&gt;BFCache (Back/forward cache)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当用户点击浏览器“后退”或“前进”按钮时，如果页面是从 BFCache 中恢复的，它的加载时间几乎是 &lt;strong&gt;0 毫秒&lt;/strong&gt;，LCP 直接拉满。然而，在实际业务代码中，我们常常无意间破坏了 BFCache，导致用户每次后退都要重新经历漫长的网络请求和渲染。&lt;/p&gt;
&lt;h3&gt;4.1 什么会破坏 BFCache？&lt;/h3&gt;
&lt;p&gt;最常见的元凶是：&lt;strong&gt;在 &lt;code&gt;window&lt;/code&gt; 上绑定了 &lt;code&gt;unload&lt;/code&gt; 事件监听器。&lt;/strong&gt;
在早期的前端代码中，我们习惯用 &lt;code&gt;unload&lt;/code&gt; 来发送用户离开页面的埋点日志，但这会直接告诉浏览器：“这个页面离开时要执行清理逻辑，不要把它放进 BFCache！”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正确的做法：改用 &lt;code&gt;pagehide&lt;/code&gt; 或 &lt;code&gt;visibilitychange&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 错误：彻底封杀了 BFCache
window.addEventListener(&apos;unload&apos;, () =&amp;gt; {
  navigator.sendBeacon(&apos;/log&apos;, data);
});

// ✅ 正确：在页面隐藏时发送日志，保留 BFCache
window.addEventListener(&apos;visibilitychange&apos;, () =&amp;gt; {
  if (document.visibilityState === &apos;hidden&apos;) {
    navigator.sendBeacon(&apos;/log&apos;, data);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 React/Vue 单页应用 (SPA) 中的状态恢复陷阱&lt;/h3&gt;
&lt;p&gt;如果你使用了 WebSocket 或保持了某个长连接（如 SSE），当页面进入 BFCache 时，这些连接会被挂起。当用户按“后退”键回到页面时，连接可能已经超时断开。&lt;/p&gt;
&lt;p&gt;此时，你不能依赖常规的 &lt;code&gt;useEffect&lt;/code&gt; 挂载逻辑，而是需要监听 &lt;code&gt;pageshow&lt;/code&gt; 事件的 &lt;code&gt;persisted&lt;/code&gt; 属性，来判断页面是否是从 BFCache 复活的，如果是，则必须手动重连！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;pageshow&apos;, (event) =&amp;gt; {
  if (event.persisted) {
    console.log(&apos;页面从 BFCache 瞬间恢复了！&apos;);
    // 1. 重新建立 WebSocket 连接
    // 2. 重新拉取极具时效性的数据（如股票价格）
    reconnectWebSocket();
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;排查工具&lt;/strong&gt;：
Chrome DevTools 的 &lt;code&gt;Application&lt;/code&gt; -&amp;gt; &lt;code&gt;Back/forward cache&lt;/code&gt; 面板提供了一键测试功能。点击 &lt;code&gt;Test BFCache&lt;/code&gt;，它会明确指出你的代码中到底是哪个 API（如未关闭的 IndexedDB 事务、特定的 Cache-Control 头等）阻碍了页面的瞬间恢复。&lt;/p&gt;
&lt;h2&gt;5. 性能监控与持续集成&lt;/h2&gt;
&lt;p&gt;性能优化并不是一劳永逸的，需要建立闭环的监控体系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RUM (Real User Monitoring)&lt;/strong&gt;: 使用 &lt;code&gt;web-vitals&lt;/code&gt; 库收集真实用户的性能数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lab Testing&lt;/strong&gt;: 在 CI/CD 流程中集成 Lighthouse CI，设置性能预算 (Performance Budgets)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析工具&lt;/strong&gt;: 利用 Chrome DevTools 的 &lt;code&gt;Performance&lt;/code&gt; 面板定位渲染管线中的 &lt;code&gt;Long Animation Frames (LoAF)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;现代网页性能优化已进入“精细化调度”时代。通过合理利用 &lt;code&gt;Fetch Priority&lt;/code&gt;、&lt;code&gt;Task Yielding&lt;/code&gt; 以及 &lt;code&gt;Local-first&lt;/code&gt; 等架构模式，我们可以构建出既能快速呈现内容，又能即时响应用户操作的高质量 Web 应用。&lt;/p&gt;
</content:encoded></item><item><title>自定义 Vite 插件开发实战与原理</title><link>https://nollieleo.github.io/posts/custom-vite-plugin-development/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/custom-vite-plugin-development/</guid><description>从 Rollup 钩子兼容性到 Vite 专有 API，详细讲解如何编写处理特定业务流的 Vite 插件。</description><pubDate>Thu, 28 Mar 2024 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vite 的构建系统基于 Rollup，其插件体系不仅兼容了大部分 Rollup 钩子，还扩展了特定于开发服务器的专有钩子。理解 Vite 插件的工作原理，对于构建高效的自动化工具链至关重要。&lt;/p&gt;
&lt;h2&gt;1. Vite 插件生命周期全景&lt;/h2&gt;
&lt;p&gt;Vite 插件的生命周期可以分为三个阶段：服务器启动阶段、请求处理阶段和服务器关闭阶段。在生产构建模式下，Vite 则完全遵循 Rollup 的钩子流程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph DevServer [&quot;开发环境 (Dev Server)&quot;]
        A[config] --&amp;gt; B[configResolved]
        B --&amp;gt; C[configureServer]
        C --&amp;gt; D[transformIndexHtml]
        D --&amp;gt; E[handleHotUpdate]
    end
    subgraph RollupHooks [&quot;通用钩子 (Rollup 兼容)&quot;]
        F[options] --&amp;gt; G[buildStart]
        G --&amp;gt; H[resolveId]
        H --&amp;gt; I[load]
        I --&amp;gt; J[transform]
        J --&amp;gt; K[buildEnd]
    end
    E -.-&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心钩子解析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;config&lt;/code&gt;&lt;/strong&gt;: 在解析 Vite 配置前调用，可以返回一个将被深度合并到现有配置中的对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;configureServer&lt;/code&gt;&lt;/strong&gt;: 用于配置开发服务器，常用于添加自定义中间件（connect middleware）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;transform&lt;/code&gt;&lt;/strong&gt;: 最核心的钩子，用于转换模块内容。支持返回 SourceMap 以保证调试体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 虚拟模块 (Virtual Modules) 的深度实现&lt;/h2&gt;
&lt;p&gt;虚拟模块是 Vite 插件开发中非常核心的模式。它允许我们在内存中动态生成代码，而无需在磁盘上创建物理文件。这在注入全局常量、动态路由生成或构建时元数据注入等场景中非常有用。&lt;/p&gt;
&lt;h3&gt;实现机制&lt;/h3&gt;
&lt;p&gt;为了防止虚拟模块 ID 被其他插件误处理，通常使用 &lt;code&gt;\0&lt;/code&gt; 前缀作为内部标识符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Plugin } from &apos;vite&apos;;

export default function virtualModulePlugin(): Plugin {
  const virtualModuleId = &apos;virtual:env-info&apos;;
  const resolvedVirtualModuleId = &apos;\0&apos; + virtualModuleId;

  return {
    name: &apos;vite-plugin-env-info&apos;,
    
    // 1. 拦截解析请求
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    
    // 2. 加载模块内容
    load(id) {
      if (id === resolvedVirtualModuleId) {
        const info = {
          version: process.env.npm_package_version,
          timestamp: new Date().getTime(),
          nodeVersion: process.version
        };
        return `export const envInfo = ${JSON.stringify(info)};`;
      }
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 基于 AST 的代码转换拦截&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;transform&lt;/code&gt; 钩子中，简单的正则替换往往难以处理复杂的语法结构。推荐使用 &lt;code&gt;magic-string&lt;/code&gt; 配合 AST 工具（如 &lt;code&gt;estree-walker&lt;/code&gt; 或 &lt;code&gt;babel&lt;/code&gt;）进行精准转换。&lt;/p&gt;
&lt;h3&gt;实战：自动注入调试元数据&lt;/h3&gt;
&lt;p&gt;假设我们需要在每个函数的开头注入执行耗时统计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { parse } from &apos;@babel/parser&apos;;
import traverse from &apos;@babel/traverse&apos;;
import MagicString from &apos;magic-string&apos;;

export default function autoTracePlugin(): Plugin {
  return {
    name: &apos;vite-plugin-auto-trace&apos;,
    transform(code, id) {
      if (!/\.(t|j)sx?$/.test(id) || id.includes(&apos;node_modules&apos;)) return;

      const s = new MagicString(code);
      const ast = parse(code, {
        sourceType: &apos;module&apos;,
        plugins: [&apos;typescript&apos;, &apos;jsx&apos;]
      });

      traverse(ast, {
        FunctionDeclaration(path) {
          const { start } = path.node.body;
          // 在函数体开始处插入代码
          s.appendLeft(start + 1, `\n  console.time(&apos;${path.node.id.name}&apos;);`);
          
          // 在函数结束前插入结束统计（简化逻辑）
          const { end } = path.node.body;
          s.appendRight(end - 1, `\n  console.timeEnd(&apos;${path.node.id.name}&apos;);`);
        }
      });

      return {
        code: s.toString(),
        map: s.generateMap({ hires: true })
      };
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vite 插件可以通过 &lt;code&gt;handleHotUpdate&lt;/code&gt; 钩子自定义热更新行为。例如，当某个非 JS 配置文件（如 &lt;code&gt;.yaml&lt;/code&gt;）发生变化时，我们可以手动触发相关模块的更新，而不是刷新整个页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 业务踩坑：基于 Module Graph 的精准 HMR 控制&lt;/h2&gt;
&lt;p&gt;Vite 的神级开发体验来源于它的 HMR（热更新）。但在写插件处理自定义文件后缀（比如 &lt;code&gt;.mdx&lt;/code&gt; 或自己发明的 &lt;code&gt;.json5&lt;/code&gt;）时，最容易踩的坑就是：&lt;strong&gt;一改文件，整个浏览器就全页刷新（Full Reload），HMR 完全失效。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;4.1 为什么会触发 Full Reload？&lt;/h3&gt;
&lt;p&gt;当你修改了 &lt;code&gt;foo.mdx&lt;/code&gt;，Vite 的文件监听器（Chokidar）会捕获到变更。然后 Vite 会在它的 &lt;strong&gt;模块关系图（Module Graph）&lt;/strong&gt; 里寻找谁导入了 &lt;code&gt;foo.mdx&lt;/code&gt;。
如果这个 &lt;code&gt;.mdx&lt;/code&gt; 文件在 &lt;code&gt;transform&lt;/code&gt; 钩子里被你转换成了一段普通的 JS 代码，但你&lt;strong&gt;没有在这个代码里注入 HMR 的接收代码（&lt;code&gt;import.meta.hot.accept&lt;/code&gt;）&lt;/strong&gt;，Vite 就会认为：“这个模块不知道怎么热更新自己，为了安全起见，我只能把整个页面刷新了。”&lt;/p&gt;
&lt;h3&gt;4.2 工业级解法：handleHotUpdate 与 Module Graph 强干预&lt;/h3&gt;
&lt;p&gt;有时候，我们不希望在转换后的代码里硬塞 &lt;code&gt;import.meta.hot&lt;/code&gt; 逻辑，而是希望在插件层直接拦截变更，并手动告诉 Vite 应该去更新哪个组件。&lt;/p&gt;
&lt;p&gt;这就需要用到极其核心的 &lt;code&gt;handleHotUpdate&lt;/code&gt; 钩子和 &lt;code&gt;server.moduleGraph&lt;/code&gt; API。&lt;/p&gt;
&lt;p&gt;假设你写了一个插件，读取 &lt;code&gt;locales/zh-CN.json&lt;/code&gt;，并把它变成一个供 React 组件导入的字典对象。
当产品经理修改了翻译文件时，我们希望页面能瞬间更新文字，而不是全页白屏刷新。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function i18nHmrPlugin() {
  return {
    name: &apos;vite-plugin-i18n-hmr&apos;,
    
    // 拦截热更新事件
    async handleHotUpdate({ file, server, read }) {
      // 只拦截我们关心的 json 文件
      if (file.endsWith(&apos;zh-CN.json&apos;)) {
        console.log(&apos;[HMR] 检测到语言包更新&apos;);
        
        // 1. 获取这个 JSON 文件在 Vite 内存图谱中的模块节点
        const jsonModule = server.moduleGraph.getModuleById(file);
        
        if (jsonModule) {
          // 2. 核心：手动使这个模块的缓存失效！
          // 否则下次浏览器请求这个文件时，Vite 还是会返回旧的内存缓存
          server.moduleGraph.invalidateModule(jsonModule);
          
          // 3. 通过 WebSocket 向客户端发送自定义事件，通知 React 去重新拉取语言包
          // 客户端需要监听 import.meta.hot.on(&apos;i18n-update&apos;, ...)
          server.ws.send({
            type: &apos;custom&apos;,
            event: &apos;i18n-update&apos;,
            data: { 
              file, 
              newContent: JSON.parse(await read()) 
            }
          });
          
          // 4. 返回一个空数组！
          // 这句话极其关键：它告诉 Vite &quot;这个热更新我已经处理完毕了，你不要再去顺藤摸瓜触发 Full Reload 了！&quot;
          return [];
        }
      }
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;踩坑提醒&lt;/strong&gt;：
很多人在 &lt;code&gt;handleHotUpdate&lt;/code&gt; 里返回了空数组阻止了刷新，但忘记调用 &lt;code&gt;server.moduleGraph.invalidateModule(jsonModule)&lt;/code&gt;。结果就是客户端收到了 WebSocket 消息去重新 &lt;code&gt;import()&lt;/code&gt; 这个文件，但拿到的依然是 Vite 内存里的旧代码，导致热更新看起来“没生效”。&lt;/p&gt;
&lt;h2&gt;5. 性能优化建议&lt;/h2&gt;
&lt;p&gt;编写 Vite 插件时，应遵循以下原则以避免拖慢构建速度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路径过滤&lt;/strong&gt;: 始终在钩子开始处通过 &lt;code&gt;id&lt;/code&gt; 过滤掉不需要处理的文件（如 &lt;code&gt;node_modules&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存计算&lt;/strong&gt;: 对于复杂的 AST 转换，考虑使用缓存机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步处理&lt;/strong&gt;: 尽量使用异步 API，避免阻塞 Vite 的并行扫描过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过深入掌握这些核心机制，开发者可以构建出高度定制化的构建流，显著提升团队的开发效率与工程质量。&lt;/p&gt;
</content:encoded></item><item><title>Zion无代码画布核心架构技术总结</title><link>https://nollieleo.github.io/posts/zion%E6%97%A0%E4%BB%A3%E7%A0%81%E7%94%BB%E5%B8%83%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zion%E6%97%A0%E4%BB%A3%E7%A0%81%E7%94%BB%E5%B8%83%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/</guid><description>个人独立自研高性能无代码画布架构 CanvasPro 的深度剖析，包含状态管理、渲染引擎与性能优化实践。</description><pubDate>Wed, 20 Mar 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Zion无代码画布核心架构技术总结文档&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;线上体验地址&lt;/strong&gt;: &lt;a href=&quot;https://zion.functorz.com&quot;&gt;https://zion.functorz.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Zion无代码画布&lt;/strong&gt; 是由我个人独立从零设计并研发的一套高性能无代码画布架构，致力于解决复杂组件树的渲染性能、复杂的拖拽交互、以及画布无限缩放/平移等一系列无代码/低代码平台的核心难题。&lt;/p&gt;
&lt;p&gt;为了让您对画布有一个直观的体感，以下是我独立设计研发的画布整体界面（支持多层级嵌套、自由拖拽、悬浮辅助线与响应式属性配置）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./hero.png&quot; alt=&quot;画布全局一览&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本文档将从整体架构、核心模块设计、关键链路实现以及重点与难点（Performance &amp;amp; Architecture Challenges）四个维度，深度剖析 Zion无代码 的架构设计。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 整体架构概览&lt;/h2&gt;
&lt;p&gt;CanvasPro 的整体架构在设计上遵循 &lt;strong&gt;状态与视图解耦&lt;/strong&gt;、&lt;strong&gt;按需渲染&lt;/strong&gt;、&lt;strong&gt;事件驱动分层&lt;/strong&gt; 的理念。&lt;/p&gt;
&lt;p&gt;为了更清晰地展示庞大的架构体系，以下分别呈现“状态-渲染核心流”与“外围辅助与控制流”两张图解：&lt;/p&gt;
&lt;h3&gt;1.1 核心数据与渲染管道&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph State_Management [&quot;数据层 (State Management)&quot;]
        direction TB
        A[&quot;CanvasStore (MobX)&quot;] --&amp;gt;|&quot;扁平化存储&quot;| B(&quot;metas: Record&amp;lt;ID, Meta&amp;gt;&quot;)
        A --&amp;gt;|&quot;路由状态&quot;| C(&quot;boardsRecord 路由栈&quot;)
        A --&amp;gt;|&quot;派生与变异&quot;| D(&quot;CanvasAction 接口&quot;)
    end

    subgraph Render_Engine [&quot;渲染引擎 (Render Engine)&quot;]
        direction TB
        E[&quot;MetaRenderTree&quot;] --&amp;gt;|&quot;递归入口&quot;| F{&quot;按需解析&quot;}
        F --&amp;gt;|&quot;编辑态&quot;| G[&quot;MetaEditableRenderTree&quot;]
        F --&amp;gt;|&quot;预览态&quot;| H[&quot;MetaPreviewRenderTree&quot;]
        G -.精确订阅.-&amp;gt; B
        G --&amp;gt;|&quot;异常隔离&quot;| J[&quot;CanvasErrorBoundary&quot;]
    end

    A -.- E
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 视口事件与拖拽碰撞体系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph TB
    subgraph Viewport_Event [&quot;无限画布与事件层 (Viewport)&quot;]
        direction LR
        K[&quot;react-infinite-viewer&quot;] --&amp;gt; L(&quot;Transform 偏移矩阵&quot;)
        M[&quot;CanvasEventEmitter&quot;] --&amp;gt;|&quot;无状态分发&quot;| L
        O[&quot;GlobalEventMonitor&quot;] --&amp;gt;|&quot;统一拦截&quot;| M
    end

    subgraph DnD_Engine [&quot;拖拽引擎 (DnD Engine)&quot;]
        direction LR
        Q[&quot;DndContextWrapper&quot;] --&amp;gt; R(&quot;碰撞检测 (Collision Detection)&quot;)
        R --&amp;gt; S(&quot;useInsertTarget 计算落点&quot;)
        S --&amp;gt; T[&quot;触发 DOM 高亮占位&quot;]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心状态驱动 (State Management)&lt;/strong&gt;：基于 &lt;strong&gt;MobX&lt;/strong&gt; (&lt;code&gt;mobx-react&lt;/code&gt;) 构建核心状态库 &lt;code&gt;CanvasStore&lt;/code&gt;。所有页面组件数据（&lt;code&gt;metas&lt;/code&gt;）、画板路由（&lt;code&gt;boards&lt;/code&gt;）、选中状态、断点信息等均收敛于此。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染引擎 (Render Engine)&lt;/strong&gt;：基于 React 递归渲染，通过 &lt;code&gt;MetaRenderTree&lt;/code&gt; 动态解析元数据并生成真实的 DOM 节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拖拽引擎 (DnD Engine)&lt;/strong&gt;：基于 &lt;code&gt;@dnd-kit/core&lt;/code&gt; 二次封装，提供了组件级别的拖拽节点映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无限画布 (Infinite Viewport)&lt;/strong&gt;：基于 &lt;code&gt;react-infinite-viewer&lt;/code&gt;，结合自定义的全局事件总线，实现画布丝滑的平移与缩放。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心模块与设计细节&lt;/h2&gt;
&lt;p&gt;为了支撑无代码平台的极高复杂度，我在 CanvasPro 的底层架构中拆分了大量的独立子系统与 Hooks。以下是核心模块的深入解析：&lt;/p&gt;
&lt;h3&gt;2.1 状态管理：平面化与响应式 (CanvasStore)&lt;/h3&gt;
&lt;p&gt;在无代码应用中，组件树的层级往往极其深邃。若采用传统的 React Context 或纯层级 State，任何叶子节点的修改都会引发自顶向下的重渲染。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构解耦的强类型分层&lt;/strong&gt;：在 &lt;code&gt;CanvasStore/types/index.ts&lt;/code&gt; 中，Store 被严格划分为 &lt;code&gt;CanvasState&lt;/code&gt; (核心源数据)、&lt;code&gt;CanvasComputedState&lt;/code&gt; (派生与缓存数据，如 &lt;code&gt;metaByComponentId&lt;/code&gt; 索引字典) 以及 &lt;code&gt;CanvasAction&lt;/code&gt; (状态突变接口)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平面化存储 (Flattening)&lt;/strong&gt;：组件树被扁平化，以 &lt;code&gt;id&lt;/code&gt; 为键存放在 &lt;code&gt;Record&amp;lt;Meta[&apos;id&apos;], Meta&amp;gt;&lt;/code&gt; (&lt;code&gt;metas&lt;/code&gt;) 中，彻底避免了深度嵌套引发的更新困难。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Before vs After: 状态降维的视觉直观对比&lt;/strong&gt;
如果不做扁平化，一个组件树的更新是灾难性的（需要层层递归寻找节点）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 传统树形 State (深层嵌套，更新困难)
{
  &quot;id&quot;: &quot;root&quot;,
  &quot;children&quot;: [
    {
      &quot;id&quot;: &quot;container_1&quot;,
      &quot;children&quot;: [ { &quot;id&quot;: &quot;button_1&quot;, &quot;text&quot;: &quot;Click&quot; } ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但在 Zion 的 &lt;code&gt;CanvasStore&lt;/code&gt; 中，我们将其彻底拍平，所有的节点无论层级多深，都变成了 O(1) 复杂度的字典寻址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ✅ Zion 的扁平化 State (O(1) 更新与读取)
{
  &quot;metas&quot;: {
    &quot;root&quot;: { &quot;id&quot;: &quot;root&quot;, &quot;childIds&quot;: [&quot;container_1&quot;] },
    &quot;container_1&quot;: { &quot;id&quot;: &quot;container_1&quot;, &quot;childIds&quot;: [&quot;button_1&quot;], &quot;parentId&quot;: &quot;root&quot; },
    &quot;button_1&quot;: { &quot;id&quot;: &quot;button_1&quot;, &quot;text&quot;: &quot;Click&quot;, &quot;parentId&quot;: &quot;container_1&quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;精确粒度订阅 (Granular Reactivity)&lt;/strong&gt;：结合底层通用的 &lt;code&gt;memoWithObserver&lt;/code&gt; 高阶组件。通过闭包按需读取具体的 Meta 数据，只有当该 Meta 对应的数据变更时，才会触发对应 DOM 的 Re-render。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;平面化存储缓存字典示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface CanvasComputedState {
  metaKeys: Array&amp;lt;Meta[&apos;id&apos;]&amp;gt;;
  // O(1) 复杂度的组件反查字典
  metaByComponentId: Record&amp;lt;Component[&apos;id&apos;], Record&amp;lt;string, Meta&amp;gt;&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 渲染树引擎 (MetaRenderTree &amp;amp; Hooks)&lt;/h3&gt;
&lt;p&gt;渲染树是画布的“血肉”。入口通过传入 &lt;code&gt;rootId&lt;/code&gt; 开始，进行组件级别的递归渲染。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬核踩坑：React Render 的性能黑洞与 Immutable 优化&lt;/strong&gt;
在画布最初的版本中，由于 &lt;code&gt;MetaRenderTree&lt;/code&gt; 是一个巨大的递归组件，当用户在画布顶部拖动一个按钮时，顶层 &lt;code&gt;root&lt;/code&gt; 的 state 发生改变，导致整个画布（包含几千个嵌套节点）瞬间触发自顶向下的 React Render，整个浏览器直接卡死。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工业级解法：细粒度订阅 + React.memo + ID 指针传递&lt;/strong&gt;
为了彻底阻断无意义的渲染，我们对 &lt;code&gt;MetaRenderTree&lt;/code&gt; 进行了极其严苛的优化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;父组件只传递 ID，绝不传递 Object&lt;/strong&gt;：父节点在渲染子节点时，只传 &lt;code&gt;childId&lt;/code&gt; 字符串，而不是完整的 &lt;code&gt;childMeta&lt;/code&gt; 对象。这样即使父节点的属性变了，由于子组件接收的 &lt;code&gt;id&lt;/code&gt; 没变，配合 &lt;code&gt;React.memo&lt;/code&gt;，子树的渲染被完美拦截。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子组件按需自订阅 (Self-Subscription)&lt;/strong&gt;：子组件拿到 &lt;code&gt;childId&lt;/code&gt; 后，利用 MobX 的 &lt;code&gt;observer&lt;/code&gt; 在自己内部去 &lt;code&gt;CanvasStore&lt;/code&gt; 里精确订阅自己的状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 灾难写法：每次父级更新，整个子树全部重渲染
function RenderTree({ meta }) {
  return (
    &amp;lt;div&amp;gt;
      {meta.children.map(childMeta =&amp;gt; &amp;lt;RenderTree key={childMeta.id} meta={childMeta} /&amp;gt;)}
    &amp;lt;/div&amp;gt;
  );
}

// ✅ 极致优化写法：只传 ID，阻断 Render 瀑布流
const RenderTree = observer(({ metaId }) =&amp;gt; {
  // 子组件只精确订阅自己的数据
  const meta = canvasStore.metas[metaId];
  
  return (
    &amp;lt;div style={meta.style}&amp;gt;
      {meta.childIds.map(id =&amp;gt; &amp;lt;RenderTree key={id} metaId={id} /&amp;gt;)}
    &amp;lt;/div&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套机制，画布内无论拖拽、变色、修改文字，&lt;strong&gt;真正触发 React 重渲染的永远只有被修改的那一个具体 DOM 节点&lt;/strong&gt;，渲染性能提升了成百上千倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引擎层渲染策略与可视区裁剪（虚拟化）伪代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const childTrees = useMemo(() =&amp;gt; {
  if (!childMetas || !childMetas.length) {
    return &amp;lt;MetaPlaceholder meta={meta} /&amp;gt;;
  }
  return map(childMetas, (childMeta) =&amp;gt; (
    &amp;lt;MetaEditableRenderTree meta={childMeta} key={childMeta.id} /&amp;gt;
  ));
}, [childMetas, meta]);

// 核心：基于可视区进行裁剪
const showShadow = useMemo(() =&amp;gt; {
  if (isForceVisible) return false;
  return isOutOfView &amp;amp;&amp;amp; type !== CONDITIONAL_VIEW; // 如果越界则显示 Shadow 占位
}, [isOutOfView, isForceVisible]);

if (showShadow) {
  // 退化渲染以保证滚动条高度不塌陷
  return &amp;lt;MetaShadow size={sizeRef.current} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 无限视口与高性能事件 (Viewport &amp;amp; EventEmitter)&lt;/h3&gt;
&lt;p&gt;画布场景下，缩放和平移是最高频的操作。如果在这一层频繁修改 React State，会导致整个画布（几千个节点）进入 Diff 流程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完全绕过 VDOM 的高性能处理方案&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 触发缩放时，不调用 setState，而是通过全局单例的 EventBus 抛出
const onZoomIn = useCallback(() =&amp;gt; {
  const newZoom = getViewportLegalZoom(curZoom + rate);
  // 这里直接操作单例事件总线，原生修改 DOM Transform，彻底切断 React Render 管线！
  CanvasEventEmitter.emit(CanvasEmitterEventType.VIEWPORT_SET_ZOOM, newZoom);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;CanvasEventEmitter.emit&lt;/code&gt; 分发事件，单例闭包直接截获并修改底层节点 Transform 矩阵，保障了 60fps 的交互体验。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 独立的高亮与辅助线层 (Tools Overlay)&lt;/h3&gt;
&lt;p&gt;为了防止业务组件被画布的编辑器状态污染，所有的选中高亮（Select）、悬浮边框（Hover）、以及拖拽插入的辅助线（InsertGuideLine），均被抽离至 &lt;code&gt;Tools/views&lt;/code&gt; 下独立渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[用户操作] --&amp;gt; B(CanvasStore 更新 selectedMetaIds)
    B --&amp;gt; C[Tools Overlay]
    C --&amp;gt;|计算绝对坐标| D(绘制高亮边框/辅助线)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 事件监控与快捷键系统 (Shortcut Monitor)&lt;/h3&gt;
&lt;p&gt;利用 &lt;code&gt;ShortcutMonitor&lt;/code&gt; 在全局顶层拦截键盘事件。无论是复制（Cmd+C）、撤销重做（Cmd+Z），都在该系统内统一转化为 &lt;code&gt;CanvasAction&lt;/code&gt;，防止浏览器的默认行为与画布内部逻辑发生冲突。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 统一监听与事件代理
useEventEmitterSubscription(CanvasEmitterEventType.SHORTCUT_TRIGGERED, (param) =&amp;gt; {
  const { event, type, target } = param;
  event.preventDefault(); 
  executeShortcutAction(type); // 派发至专门的处理管线
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 组件元数据转译管道 (Transpile Pipeline)&lt;/h3&gt;
&lt;p&gt;不同类型的组件拥有不同的 Schema 结构。在 &lt;code&gt;useTranspile&lt;/code&gt; 系列钩子中，服务端下发的基础 JSON 会根据当前的视口断点（Breakpoint）被动态转译成标准的 &lt;code&gt;Meta&lt;/code&gt; 数据结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[服务端 Component JSON] --&amp;gt;|匹配 Breakpoint| B(useTranspile 管道)
    B --&amp;gt;|动态挂载特定属性| C(规范化 Meta 结构)
    C --&amp;gt; D[存入 CanvasStore]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.7 多层级画板路由栈 (Boards &amp;amp; Routing)&lt;/h3&gt;
&lt;p&gt;无代码搭建经常需要“下钻”编辑（例如双击一个列表组件，进入列表项的独立编辑环境）。通过维护 &lt;code&gt;boardsRecord&lt;/code&gt; 路由栈，画布支持无缝的上下文切换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 记录进入下钻模式前的状态
export interface BoardConfig {
  type: BoardConfigType;
  componentId: string;
  viewConfig: BoardViewConfig; // 包含此层级记录的视角和缩放比例
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8 动态上下文菜单体系 (Context Menu System)&lt;/h3&gt;
&lt;p&gt;封装了 &lt;code&gt;MetaContextMenu&lt;/code&gt; 与 &lt;code&gt;PageContextMenu&lt;/code&gt;，在用户右键点击不同节点时，通过计算触发位置的 DOM Dataset 信息，动态唤起不同的菜单项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 右键菜单按需判定策略
const isIllegalType = includes(
  [ComponentRefactorComponentType.TAB_VIEW, ComponentRefactorComponentType.CONDITIONAL_VIEW],
  meta.type,
);
if (isIllegalType) return false;
// 根据不同类型的 meta 唤起对应的操作组（组合、分离、绑定数据源等）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.9 异步组件与渲染沙箱 (Canvas Error Boundary)&lt;/h3&gt;
&lt;p&gt;无代码平台中的配置数据常常存在脏数据。通过在树节点的特定层级包裹 &lt;code&gt;CanvasErrorBoundary&lt;/code&gt;，一旦某个子组件因为异常参数崩溃，错误边界会将其捕获并渲染为错误提示块（ErrorTips）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;沙箱隔离机制:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CanvasErrorBoundary fallbackRender={({ error }) =&amp;gt; &amp;lt;ErrorTips error={error} /&amp;gt;}&amp;gt;
  &amp;lt;MetaEditableRenderTree meta={childMeta} /&amp;gt;
&amp;lt;/CanvasErrorBoundary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.10 响应式与变体样式合成 (Responsive &amp;amp; Variants)&lt;/h3&gt;
&lt;p&gt;通过 &lt;code&gt;useComputedMetaStyles&lt;/code&gt; Hook，在渲染管线中实时对基础样式（Styles）和变体样式（Variants）进行深度合并。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 核心：基于当前环境动态混合样式表
let targetStyles = toJS(styles);
if (variantStyles) {
  // 当命中变体时（如断点/状态变化），将变体样式覆盖至基础样式之上
  targetStyles = mergeDeep(targetStyles, variantStyles);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.11 只读模式与沙箱拦截 (Readonly &amp;amp; Permission)&lt;/h3&gt;
&lt;p&gt;通过底层的 &lt;code&gt;ReadonlyMonitor&lt;/code&gt; 与状态树的 &lt;code&gt;readonly&lt;/code&gt; 标识，可以在查看历史版本、无权限访问时，彻底锁死拖拽引擎（设置 Dnd 为 disabled）、拦截双击以及右键菜单，实现查看与编辑态的同构复用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只读态锁定示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const draggableConfig = useMemo&amp;lt;UseDraggableArguments&amp;gt;(
  () =&amp;gt; ({
    id,
    disabled: readonly || !isDraggable(id), // 核心鉴权拦截
    data: draggableData,
  }),
  [id, readonly, isDraggable, draggableData],
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.12 拖拽物料抽象与起源判定 (Drag Origin Calculation)&lt;/h3&gt;
&lt;p&gt;并非所有的拖拽行为都是相同的。在 &lt;code&gt;useDraggableConfig&lt;/code&gt; 中，系统会根据组件的 CSS 定位属性预计算出 &lt;code&gt;DragOrigin&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const draggableOrigin = useMemo(() =&amp;gt; {
  if (recordedShortcut?.mode === RecordedShortcutType.CROSS_LEVEL) {
    return DragOrigin.CROSS_LEVEL; // 跨层级降维打击拖拽
  }
  const isWrapperAbsOrFixed = isAbsoluteOrFixed(wrapperStyle);
  // 如果是绝对定位，其拖拽逻辑为单纯移动；如果是文档流，则判定为弹性流式排序
  return isWrapperAbsOrFixed ? DragOrigin.ABS_OR_FIXED_MOVE : DragOrigin.FLEX_SORT;
}, [wrapperStyle, recordedShortcut?.mode]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.13 批量选取与框选机制 (Canvas Selecto)&lt;/h3&gt;
&lt;p&gt;深度整合了 &lt;code&gt;Selecto.js&lt;/code&gt;，用户在画布空白处拖动鼠标可拉出多选框。通过几何交集算法匹配节点坐标，将多选的 ID 批量写入 &lt;code&gt;selectedMetaConfigs&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;框选算法代理事件:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Selecto
  dragContainer={`#${VIEWPORT_ID}`}
  selectableTargets={selectedTargets}
  hitRate={0}
  selectByClick={false}
  {...selectoEvents} // 将框选边界事件代理回 Store
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 重点与难点突破 (Key Challenges &amp;amp; Solutions)&lt;/h2&gt;
&lt;p&gt;在开发过程中，我成功攻克了以下几个业界公认的“无代码画布难题”：&lt;/p&gt;
&lt;h3&gt;难点 1：巨型 DOM 树下的渲染性能 (Visibility Optimization)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：当用户搭建了上千个组件，画布渲染将变得极其卡顿，拖拽也会存在巨大的延迟。
&lt;strong&gt;解法&lt;/strong&gt;：&lt;strong&gt;视口外节点裁剪（虚拟化画布）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在无代码编辑器的无限画板中，节点并不是排列成一维数组的，无法使用常规的虚拟列表。因此，我引入了基于 &lt;code&gt;IntersectionObserver&lt;/code&gt; 和 &lt;code&gt;MetaShadow&lt;/code&gt; (影子占位符) 的递归剔除算法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph Browser_Window [&quot;用户可见视窗 (Viewport)&quot;]
        A[&quot;[真实的 React 节点] Container&quot;] --&amp;gt; B[&quot;[真实的 React 节点] Image&quot;]
        A --&amp;gt; C[&quot;[真实的 React 节点] List&quot;]
    end
    
    subgraph Out_Of_View [&quot;视窗之外的不可见区域&quot;]
        C -.-&amp;gt;|&quot;斩断递归树，卸载真实 DOM&quot;| D(&quot;👻 MetaShadow 占位符&quot;)
        D -.-&amp;gt;|&quot;保留 sizeRef (width/height)&quot;| E(&quot;防止父级容器高度塌陷&quot;)
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;MetaEditableRenderTree&lt;/code&gt; 中，深度应用了 &lt;code&gt;useMetaVisibilityState&lt;/code&gt; 技术。&lt;/li&gt;
&lt;li&gt;采用 &lt;code&gt;startTransition&lt;/code&gt; 降级判断优先级。基于底层的 &lt;code&gt;IntersectionObserver&lt;/code&gt; 监控元素的交叉状态。如果为 &lt;code&gt;isOutOfView&lt;/code&gt;（不在可视区），就立刻&lt;strong&gt;斩断子树的递归渲染&lt;/strong&gt;，这意味着藏在某个超长列表底部的成百上千个复杂组件，在滚出视口的一瞬间就会被彻底卸载。&lt;/li&gt;
&lt;li&gt;为了防止剔除渲染导致父容器塌陷，利用闭包中的 &lt;code&gt;sizeRef&lt;/code&gt; 记录退出可视区前的宽高，并用轻量级的 &lt;code&gt;MetaShadow&lt;/code&gt; 幽灵组件去撑开原本的物理骨架尺寸。这使得滚动条、绝对定位和 Flex 布局等都完全不会因为 DOM 树的卸载而错乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;难点 2：极其复杂的协同与撤销重做 (Diff Apply)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：多人协同编辑或 Undo/Redo 时，全量替换 Meta 树代价极其高昂，会导致焦点丢失。
&lt;strong&gt;解法&lt;/strong&gt;：&lt;strong&gt;细粒度 Diff 补丁分发机制 (&lt;code&gt;useDiffApply&lt;/code&gt;)&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[协同事件 / Undo指令] --&amp;gt; B(解析 Diff 快照 operation: &apos;add&apos;)
    B --&amp;gt; C[useAddOrDeleteComponentDiffApply]
    C --&amp;gt; D[useTranspile 转译为 Meta]
    D --&amp;gt; E[MobX Transaction 开启]
    E --&amp;gt; F[定点更新 CanvasStore.metas]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;单独抽象了 &lt;code&gt;useDiffApply&lt;/code&gt; 机制作为增量构建引擎。服务端传入的并非整棵树，而是属性操作快照（如 &lt;code&gt;operation: &apos;add&apos;&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基于 MobX 事务的无闪烁补丁机制&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// useAddOrDeleteComponentDiffApply.ts 中处理 Add 补丁
case &apos;add&apos;: {
  // 1. 获取最新的组件描述信息
  const targetComponent = getComponent(id);
  // 2. Transpile转译器：将原始 Schema 翻译为 Meta 对象
  const pendingTasks = map(canvasStore.breakpoints, (bp) =&amp;gt; transpile(targetComponent, bp));
  const metas = await Promise.all(pendingTasks);
  
  // 3. 通过 transaction 将生成的 metas 直接打入 Store 字典中
  return async () =&amp;gt; {
    transaction(() =&amp;gt; {
      forEach(metas, (newMeta) =&amp;gt; {
        canvasStore.addMeta(newMeta);
      });
    });
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过底层事务同步打入状态字典，彻底杜绝了状态不同步引发的白屏与闪烁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;难点 3：多维度灾难恢复机制 (Crash Recovery)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：浏览器内存溢出（OOM）或网络异常退出，会导致用户数小时心血付之东流。
&lt;strong&gt;解法&lt;/strong&gt;：&lt;strong&gt;低廉成本的本地快照容灾&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A(编辑画布中) -.静默.-&amp;gt; B[useRecoveryRecord 定时防抖备份]
    B --&amp;gt; C[序列化关键状态 metas/viewport]
    C --&amp;gt; D[(写入 IndexedDB / LocalStorage)]
    E[异常崩溃 / 重新打开页面] --&amp;gt; F[RecoveryMonitor 挂载]
    F --&amp;gt; G{检测到新于服务端的快照碎片?}
    G -- Yes --&amp;gt; H[弹窗提示用户无缝重载恢复]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;采用了 &lt;code&gt;useRecoveryRecord&lt;/code&gt; 机制，这套代码深埋在核心链路外。&lt;/li&gt;
&lt;li&gt;以极低的性能损耗，通过 &lt;code&gt;ahooks&lt;/code&gt; 的 &lt;code&gt;useLocalStorageState&lt;/code&gt; 定期对关键状态（Metas、Viewport等）序列化并写入 LocalStorage。&lt;/li&gt;
&lt;li&gt;重启时平台通过 &lt;code&gt;RecoveryMonitor&lt;/code&gt; 捕获上次遗留的数据碎片，并无缝重载恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;定时快照备份示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [localRecord, setLocalRecord] = useLocalStorageState(&apos;CANVAS_RECOVERY_DATA&apos;, { defaultValue: null });

useDebounceEffect(() =&amp;gt; {
  if (!isCanvasDirty) return;
  // 在帧空闲时执行深度序列化，避免阻塞主线程交互
  const snapshot = serializeCanvasState(canvasStore);
  setLocalRecord({ timestamp: Date.now(), data: snapshot });
}, [canvasStore.metas], { wait: 5000 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;难点 4：拖拽的自由度与严谨性 (DnD Collision &amp;amp; Insert Target)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：组件间嵌套规则复杂（如模态框内不允许插入页面组件），需要灵活处理 Drop 边界与精准高亮框。
&lt;strong&gt;解法&lt;/strong&gt;：&lt;strong&gt;极致解耦的碰撞算法引擎&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A[拖拽移动 DragMove] --&amp;gt; B{DnD Context 碰撞检测}
    B --&amp;gt;|收集指针下方所有相交的 Dataset ID| C(过滤非法父级白名单)
    C --&amp;gt; D(获取合法容器 containerId)
    D --&amp;gt; E[useInsertTarget 落点算法计算]
    E --&amp;gt; F{判定位置坐标区间}
    F -- 靠上/左 --&amp;gt; G[INSERT_BEFORE]
    F -- 居中 --&amp;gt; H[INSERT_INNER]
    F -- 靠下/右 --&amp;gt; I[INSERT_AFTER]
    G &amp;amp; H &amp;amp; I --&amp;gt; J[更新 Store dispatch 并渲染辅助线]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;拖拽事件 &lt;code&gt;Listeners&lt;/code&gt; 完全与业务组件解耦。组件仅仅透传自身的 &lt;code&gt;id&lt;/code&gt;，真正的碰撞检测（Collision Detection）由顶层的 &lt;code&gt;DndContextWrapper&lt;/code&gt; 统一代理结算。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useInsertTarget&lt;/code&gt; 预计算落点（基于鼠标指针矩阵的偏移量计算出上插、下插、内嵌），实时派发事件渲染 &lt;code&gt;DropHighlight&lt;/code&gt; 或特定的虚线占位符。这保证了底层组件依然纯净无副作用，拖拽体感极其丝滑严谨。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;碰撞检测与锚点挂载代码:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. Meta.ts: 生成锚点信息用于碰撞反查
export const genMetaDataSet = (metaId: string, type: Meta[&apos;type&apos;]) =&amp;gt; {
  return {
    &apos;data-meta-id&apos;: metaId,
    &apos;data-meta-type&apos;: type,
  };
};

// 2. 算法核心：结合当前指针位置与容器盒模型计算精准落点
export function calculateInsertTarget(pointerY: number, containerRect: DOMRect) {
  const { top, height } = containerRect;
  // 如果处于容器上部20%区域 -&amp;gt; 前插
  if (pointerY &amp;lt; top + height * 0.2) return InsertPosition.BEFORE;
  // 处于中间60%区域 -&amp;gt; 内嵌
  if (pointerY &amp;lt; top + height * 0.8) return InsertPosition.INNER;
  // 处于下部20%区域 -&amp;gt; 后插
  return InsertPosition.AFTER;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;CanvasPro 是一套&lt;strong&gt;工程化成熟、深思熟虑度极高&lt;/strong&gt;的前端基石架构。它通过 &lt;strong&gt;MobX 扁平化数据&lt;/strong&gt; 解决了状态层级深的痛点，通过 &lt;strong&gt;视区不可见裁剪 (Out-of-view Pruning)&lt;/strong&gt; 解决了巨量组件渲染的性能瓶颈，再结合 &lt;strong&gt;基于事件总线的矩阵操作&lt;/strong&gt; 与 &lt;strong&gt;细粒度的 Diff 同步机制&lt;/strong&gt;，最终打造出了一个具备极客级协同能力、无限拓展视野且交互极致顺滑的无代码基座。&lt;/p&gt;
</content:encoded></item><item><title>微前端架构演进：Module Federation 2.0 实践</title><link>https://nollieleo.github.io/posts/module-federation-2-practice/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/module-federation-2-practice/</guid><description>对比早期的微前端方案，解析 Module Federation 2.0 在运行时动态加载、依赖共享协商和类型推导上的架构优化。</description><pubDate>Tue, 05 Mar 2024 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;随着业务应用体量的不断膨胀，单体架构（Monolith）在代码组织、构建速度和跨团队协同上经常遇到瓶颈。微前端（Micro-frontends）架构应运而生。&lt;/p&gt;
&lt;p&gt;从早期的 iframe 隔离，到基于路由分发的 single-spa，微前端一直在寻找在**运行时加载（Runtime Integration）&lt;strong&gt;与&lt;/strong&gt;依赖复用（Dependency Sharing）**之间的最佳平衡点。Webpack 5 引入的 Module Federation (MF) 彻底改变了游戏规则。如今，Module Federation 2.0 更是将这一架构推向了标准化的新高度。&lt;/p&gt;
&lt;p&gt;本文将深入探讨 Module Federation 的核心运行机制，以及 2.0 版本带来的重大架构升级。&lt;/p&gt;
&lt;h2&gt;1. 核心机制：共享作用域与模块动态加载&lt;/h2&gt;
&lt;p&gt;传统的微前端方案通常需要在主应用（Host）中硬编码子应用（Remote）的资源入口，且难以解决子应用之间重复加载相同依赖（如 React、Lodash）的性能问题。&lt;/p&gt;
&lt;p&gt;Module Federation 的底层创新在于引入了 &lt;strong&gt;共享作用域 (Shared Scope)&lt;/strong&gt; 和 &lt;strong&gt;容器引用 (Container Reference)&lt;/strong&gt; 的概念。在构建时，Webpack 会将 Host 和 Remote 的依赖信息收集起来，在运行时通过一个全局的 &lt;code&gt;__webpack_share_scopes__&lt;/code&gt; 对象进行版本协商和单例控制。&lt;/p&gt;
&lt;h3&gt;1.1 依赖协商与复用&lt;/h3&gt;
&lt;p&gt;当我们在 &lt;code&gt;webpack.config.js&lt;/code&gt; 中配置 &lt;code&gt;ModuleFederationPlugin&lt;/code&gt; 时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js (Host 宿主应用配置)
const { ModuleFederationPlugin } = require(&apos;webpack&apos;).container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: &apos;host_app&apos;, // 宿主名称
      remotes: {
        // 定义远程子应用的访问路径
        app_remote: &apos;app_remote@http://localhost:3001/remoteEntry.js&apos;,
      },
      shared: {
        // 声明共享依赖
        react: { singleton: true, requiredVersion: &apos;^18.2.0&apos; },
        &apos;react-dom&apos;: { singleton: true, requiredVersion: &apos;^18.2.0&apos; },
      },
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述配置中，&lt;code&gt;singleton: true&lt;/code&gt; 是一项极其重要的设置。如果不配置单例模式，Host 和 Remote 可能会分别在浏览器的内存中加载两份独立的 React 实例。由于 React Hooks（如 &lt;code&gt;useState&lt;/code&gt;、&lt;code&gt;useContext&lt;/code&gt;）强依赖于内部全局的 Dispatcher 单例，存在多个 React 实例会导致应用在调用 Hook 时抛出经典的 &lt;code&gt;Invalid hook call&lt;/code&gt; 错误。&lt;/p&gt;
&lt;p&gt;通过声明 &lt;code&gt;singleton: true&lt;/code&gt;，MF 引擎在运行时会比对 Host 和 Remote 中 React 的版本（&lt;code&gt;requiredVersion&lt;/code&gt;）。只要版本兼容（遵循 Semantic Versioning），Remote 应用将直接使用 Host 应用提供的 React 实例，从而避免了多实例冲突，并显著降低了微前端环境下的内存消耗。&lt;/p&gt;
&lt;h3&gt;1.2 动态加载子应用模块&lt;/h3&gt;
&lt;p&gt;在 Host 应用中，我们可以像使用普通的 React 组件一样，利用 &lt;code&gt;React.lazy&lt;/code&gt; 动态异步加载 Remote 的暴露模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Suspense } from &apos;react&apos;;

// 懒加载来自 app_remote 暴露的 Button 组件
const RemoteButton = React.lazy(() =&amp;gt; import(&apos;app_remote/Button&apos;));

function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Host Application&amp;lt;/h1&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading Remote Button...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;RemoteButton onClick={() =&amp;gt; alert(&apos;Hello from Remote!&apos;)} /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;底层通过 &lt;code&gt;__webpack_init_sharing__&lt;/code&gt; 接口，Webpack 会动态加载 &lt;code&gt;http://localhost:3001/remoteEntry.js&lt;/code&gt; 这个包含了依赖映射图（Manifest）的文件，随后再按需加载真实的业务代码 Chunk。&lt;/p&gt;
&lt;h2&gt;2. Module Federation 2.0 的架构优化&lt;/h2&gt;
&lt;p&gt;虽然 MF 1.0 提供了强大的能力，但它也存在一些工程上的局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置硬编码&lt;/strong&gt;：&lt;code&gt;remoteEntry.js&lt;/code&gt; 的 URL 必须在 Webpack 配置中写死，不利于多环境（Dev/Test/Prod）的灵活切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型推导缺失&lt;/strong&gt;：在 TypeScript 环境中，&lt;code&gt;import(&apos;app_remote/Button&apos;)&lt;/code&gt; 会报错，因为该模块在本地文件系统中不存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺乏标准化生态&lt;/strong&gt;：它与 Webpack 深度绑定，难以在 Vite、Rsbuild 等其他现代打包工具中使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MF 2.0（或称为 &lt;code&gt;@module-federation/enhanced&lt;/code&gt;）针对这些痛点进行了系统性的重构。&lt;/p&gt;
&lt;h3&gt;2.1 运行时动态注册 (Dynamic Remotes)&lt;/h3&gt;
&lt;p&gt;MF 2.0 引入了独立的 Runtime API，将微前端的加载和解析逻辑从 Webpack 的内部封装中剥离。这允许开发者在应用运行时动态注入 Remote Entry，实现极度灵活的按需加载策略和版本管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 运行时动态注册 Remote 模块
import { init, loadRemote } from &apos;@module-federation/runtime&apos;;

// 1. 初始化联邦引擎
init({
  name: &apos;host_app&apos;,
  remotes: [
    {
      name: &apos;app_remote&apos;,
      // 这个 entry 路径可以在运行时从接口或环境变量中获取
      entry: &apos;http://localhost:3001/remoteEntry.js&apos;, 
    },
  ],
  shared: {
    react: { version: &apos;18.2.0&apos;, scope: &apos;default&apos;, lib: () =&amp;gt; React },
  }
});

// 2. 动态加载模块
async function loadDynamicComponent() {
  // 手动触发并拉取模块，不依赖构建时的 import 静态分析
  const RemoteComponentModule = await loadRemote(&apos;app_remote/Button&apos;);
  const RemoteButton = RemoteComponentModule.default;
  return RemoteButton;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种架构允许我们将子应用的加载逻辑完全接入后端的配置中心（如 Zephyr Cloud），实现真正的微前端灰度发布和动态热更新。&lt;/p&gt;
&lt;h3&gt;2.2 类型推导与 TypeScript 支持&lt;/h3&gt;
&lt;p&gt;为了解决类型安全问题，MF 2.0 提供了一套内置的类型同步机制。
在构建时，通过专门的 TypeScript 插件（&lt;code&gt;@module-federation/typescript&lt;/code&gt;），Remote 应用能够将其导出的模块类型自动聚合为一个 &lt;code&gt;.d.ts&lt;/code&gt; 文件。Host 应用在开发时可以直接拉取这些类型定义，实现完美的智能提示与静态类型检查。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rsbuild.config.ts (使用 Rsbuild 结合 MF 2.0)
import { pluginModuleFederation } from &apos;@module-federation/rsbuild-plugin&apos;;

export default {
  plugins: [
    pluginModuleFederation({
      name: &apos;host_app&apos;,
      remotes: {
        app_remote: &apos;app_remote@http://localhost:3001/remoteEntry.js&apos;,
      },
      // 开启自动类型生成与下载
      dts: true, 
      shared: [&apos;react&apos;, &apos;react-dom&apos;],
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启 &lt;code&gt;dts: true&lt;/code&gt; 后，当我们在宿主环境修改远端模块的属性名时，VSCode 会直接报出类型错误，大幅提升了跨团队协同开发时的代码健壮性。&lt;/p&gt;
&lt;h2&gt;3. 业务踩坑：共享依赖的“单例崩溃”陷阱 (Singleton Crash)&lt;/h2&gt;
&lt;p&gt;在微前端落地中最让人崩溃的 Bug，莫过于 Host 和 Remote 分别正常运行，但拼在一起时突然全盘白屏，控制台赫然写着：&lt;code&gt;Invalid hook call. Hooks can only be called inside of the body of a function component.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3.1 为什么会 Invalid Hook Call？&lt;/h3&gt;
&lt;p&gt;这是典型的&lt;strong&gt;多实例冲突&lt;/strong&gt;。React 内部维护了一个全局的 &lt;code&gt;__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED&lt;/code&gt; 对象来追踪当前的 Dispatcher（也就是你调用的 useState 到底归谁管）。
如果 Host 加载了 &lt;code&gt;React@18.2.0&lt;/code&gt;，Remote 也打包了一份 &lt;code&gt;React@18.2.0&lt;/code&gt;，内存里就会有两个独立的作用域。当 Remote 的组件调用 &lt;code&gt;useState&lt;/code&gt; 时，它访问的是自己闭包里的 React，但此时 React 的渲染上下文却在 Host 的 React 实例中。状态机割裂，直接 Crash。&lt;/p&gt;
&lt;p&gt;同样的问题也会发生在 &lt;code&gt;Zustand&lt;/code&gt;、&lt;code&gt;React Router&lt;/code&gt;、&lt;code&gt;Styled-components&lt;/code&gt; 等重度依赖 Context 或全局单例的库上。&lt;/p&gt;
&lt;h3&gt;3.2 工业级解法：严格的单例依赖墙&lt;/h3&gt;
&lt;p&gt;为了彻底杜绝这种问题，在配置 &lt;code&gt;shared&lt;/code&gt; 时，我们必须建立极其严格的约束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shared: {
  react: { 
    singleton: true, // 强制要求内存中只能有一个 React 实例
    requiredVersion: &apos;^18.0.0&apos;, // 声明自己需要的版本范围
    strictVersion: true, // 极其关键：如果版本不匹配，宁可报错也不要静默加载多份！
    eager: true // 允许在应用的 entry chunk 中直接打包这份依赖，避免异步加载带来的时序问题
  },
  &apos;react-dom&apos;: { singleton: true, requiredVersion: &apos;^18.0.0&apos;, strictVersion: true, eager: true },
  zustand: { singleton: true, strictVersion: true }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;strictVersion: true&lt;/code&gt; 的意义：&lt;/strong&gt;
如果不加这个参数，当 Host 是 React 17，Remote 强行要求 React 18 时，Webpack 为了让 Remote 能跑起来，会“好心”地再加载一份 React 18。结果就是上文提到的 &lt;code&gt;Invalid Hook Call&lt;/code&gt;。
加上这个参数后，构建或运行时会直接抛出明确的 &lt;code&gt;Version Conflict Error&lt;/code&gt;，逼迫前端架构师去对齐各个子应用的基础库版本，从物理上消灭多实例 Bug。&lt;/p&gt;
&lt;h2&gt;4. Fallback 与错误边界 (Error Boundaries) 机制&lt;/h2&gt;
&lt;p&gt;在微前端架构中，最严重的风险在于：如果某个非核心的 Remote 应用宕机或加载超时，它不应当导致整个 Host 应用白屏崩溃。&lt;/p&gt;
&lt;p&gt;除了使用 &lt;code&gt;React.Suspense&lt;/code&gt; 处理加载中的占位，我们必须引入错误边界（Error Boundaries）机制来拦截底层模块解析失败的异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component, Suspense } from &apos;react&apos;;

// 标准的 React Error Boundary 组件
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 检测到错误，更新状态以渲染降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 将错误信息上报至前端监控平台（如 Sentry）
    console.error(&quot;Remote module failed to load:&quot;, error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || &amp;lt;h2&amp;gt;Failed to load component.&amp;lt;/h2&amp;gt;;
    }
    return this.props.children;
  }
}

// 封装高阶的 Remote 加载器
const SafeRemoteComponent = ({ moduleName, fallback }) =&amp;gt; {
  const ComponentToLoad = React.lazy(() =&amp;gt; import(moduleName));
  
  return (
    &amp;lt;ErrorBoundary fallback={fallback}&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading Remote...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;ComponentToLoad /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/ErrorBoundary&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这套隔离机制，即使 &lt;code&gt;app_remote&lt;/code&gt; 的服务器发生网络故障无法返回 &lt;code&gt;remoteEntry.js&lt;/code&gt;，Host 应用依然能够正常渲染其他的核心业务模块，保证了微前端架构的高可用性。&lt;/p&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;Module Federation 是一项里程碑式的工程化创新。相比早期的将微应用打包为独立容器的方案，MF 在&lt;strong&gt;运行时整合（Runtime Integration）&lt;strong&gt;和&lt;/strong&gt;共享依赖降重&lt;/strong&gt;上取得了完美的平衡。&lt;/p&gt;
&lt;p&gt;随着 MF 2.0 的发布，它通过脱离 Webpack 底层 API 的束缚，构建了标准化的运行时插件系统（Runtime Plugin System），并极大地提升了跨项目类型推导的能力。这种演进方向使得基于 Module Federation 构建大型分布式的现代 Web 应用变得更加高效、可靠，且具备了长期的可维护性。&lt;/p&gt;
</content:encoded></item><item><title>Zustand 状态管理库的设计原理与源码解析</title><link>https://nollieleo.github.io/posts/zustand-architecture-design/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/zustand-architecture-design/</guid><description>深入剖析 Zustand 的核心实现原理。探讨其基于闭包和发布订阅模式的独立状态机设计，以及通过 useSyncExternalStore 解决 React Concurrent Mode 下渲染撕裂问题的架构策略。</description><pubDate>Sat, 10 Feb 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 React 生态系统中，状态管理经历了从早期的 Redux、MobX，到 Context API，再到如今的原子化（Jotai/Recoil）和极简闭包状态机（Zustand）。Zustand 凭借其极其轻量的包体积、灵活的中间件机制和零样板代码（Boilerplate）的特性，成为了现代 React 项目中极具竞争力的选择。&lt;/p&gt;
&lt;p&gt;本文将从 Zustand 的核心原理出发，剖析其底层实现以及它在并发模式（Concurrent Mode）下的状态一致性保障。&lt;/p&gt;
&lt;h2&gt;1. 架构理念：独立于 React 的状态机&lt;/h2&gt;
&lt;p&gt;传统基于 React Context 的状态库，其状态（State）被强绑定在 React 组件树的生命周期内。这意味着如果顶层 Provider 的状态发生任何改变，无论底层组件是否使用了该状态，都可能触发整棵子树的重渲染（Re-render）。&lt;/p&gt;
&lt;p&gt;Zustand 采取了截然不同的架构：&lt;strong&gt;它的 Store 是一个完全游离于 React 之外的纯 JavaScript 对象闭包&lt;/strong&gt;。状态的变化直接在外部发生，通过订阅机制按需通知 React 组件更新。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph ZustandStore [&quot;Zustand Store (外部闭包)&quot;]
        State[&quot;内部状态 (State)&quot;]
        Listeners[&quot;监听器集合 (Set)&quot;]
        SetState[&quot;setState 突变接口&quot;]
    end

    subgraph ReactTree [&quot;React 组件树&quot;]
        CompA[&quot;组件 A (订阅 state.count)&quot;]
        CompB[&quot;组件 B (订阅 state.user)&quot;]
        CompC[&quot;组件 C (触发 setState)&quot;]
    end

    SetState --&amp;gt;|更新| State
    State --&amp;gt;|派发更新| Listeners
    Listeners -.-&amp;gt;|精准通知| CompA
    Listeners -.-&amp;gt;|精准通知| CompB
    CompC --&amp;gt;|直接调用| SetState
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.1 Vanilla Store 的核心实现&lt;/h3&gt;
&lt;p&gt;为了理解其运行机制，我们可以自己实现一个简化版的 Zustand 核心，它不依赖于任何框架。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Zustand vanilla store 核心原理简化版
const createStoreImpl = (createState) =&amp;gt; {
  let state;
  // 维护一个 Set 用于存放所有订阅了该 Store 的回调函数
  const listeners = new Set();

  const setState = (partial, replace) =&amp;gt; {
    // 允许传入函数或对象进行更新
    const nextState = typeof partial === &quot;function&quot; ? partial(state) : partial;

    // 如果状态没有发生实质性改变，直接返回，避免不必要的派发
    if (!Object.is(nextState, state)) {
      const previousState = state;
      // 支持全量替换 (replace) 或部分合并 (assign)
      state = replace ? nextState : Object.assign({}, state, nextState);
      // 遍历通知所有监听器
      listeners.forEach((listener) =&amp;gt; listener(state, previousState));
    }
  };

  const getState = () =&amp;gt; state;

  const subscribe = (listener) =&amp;gt; {
    listeners.add(listener);
    // 返回一个取消订阅的函数，供组件卸载时调用
    return () =&amp;gt; listeners.delete(listener);
  };

  // 提供操作接口，并将其实例化
  const api = { setState, getState, subscribe };
  state = createState(setState, getState, api);
  return api;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这段纯粹的 JavaScript 代码，Zustand 在脱离了 React 环境后依然可以正常运行，这极大地方便了在 Web Worker、普通 JS 类或者 Vue 等其他框架中复用逻辑。&lt;/p&gt;
&lt;h2&gt;2. 跨层同步与 React 18 Concurrent Mode 挑战&lt;/h2&gt;
&lt;p&gt;在 React 18 引入的并发渲染（Concurrent Rendering）机制下，由于渲染过程可以被中断和恢复，如果外部的 Store 在 React 执行耗时较长的渲染树遍历时发生了更新，会导致树中已经渲染的组件和尚未渲染的组件读取到不一致的状态，这就是著名的&lt;strong&gt;渲染撕裂 (Tearing)&lt;/strong&gt; 问题。&lt;/p&gt;
&lt;h3&gt;2.1 useSyncExternalStore 的引入&lt;/h3&gt;
&lt;p&gt;为了安全地将外部状态同步到 React 内部，React 官方在 v18 中提供了 &lt;code&gt;useSyncExternalStore&lt;/code&gt; Hook。Zustand 在其 React 绑定层中深度集成了该钩子，它强制在状态发生突变时，让当前的渲染任务失效并立即同步最新状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useSyncExternalStoreWithSelector } from &quot;use-sync-external-store/shim/with-selector&quot;;

// Zustand 在 React 层的绑定逻辑
export function useStore(api, selector = api.getState, equalityFn) {
  // slice 是组件最终获取到的局部状态
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe, // 外部 Store 提供的订阅函数
    api.getState, // 外部 Store 提供的获取全量状态函数
    api.getServerState || api.getState, // SSR 环境下的初始状态获取
    selector, // 局部状态选择器 (例如 state =&amp;gt; state.bears)
    equalityFn, // 自定义比较函数 (例如 shallow)
  );
  return slice;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;useSyncExternalStore&lt;/code&gt; 的封装，Zustand 在并发模式下提供了与原生 React State 同等的安全保障。&lt;/p&gt;
&lt;h3&gt;2.2 精准控制重渲染 (Selector &amp;amp; Equality)&lt;/h3&gt;
&lt;p&gt;Zustand 的核心优势在于其细粒度的订阅控制。&lt;/p&gt;
&lt;p&gt;如果组件使用 &lt;code&gt;const bears = useStore(state =&amp;gt; state.bears)&lt;/code&gt;，当 &lt;code&gt;state.nuts&lt;/code&gt; 发生改变时，即使外部状态树变了，由于 &lt;code&gt;state.bears&lt;/code&gt; 这个切片（Slice）没有变化，&lt;code&gt;useSyncExternalStoreWithSelector&lt;/code&gt; 的浅比较机制会拦截这次更新，当前组件便不会触发重渲染。&lt;/p&gt;
&lt;p&gt;对于返回对象或数组的 Selector，我们可以结合 Zustand 提供的 &lt;code&gt;shallow&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &apos;zustand&apos;;
import { shallow } from &apos;zustand/shallow&apos;;

const useBearStore = create((set) =&amp;gt; ({
  bears: 0,
  nuts: 0,
  increasePopulation: () =&amp;gt; set((state) =&amp;gt; ({ bears: state.bears + 1 })),
}));

// 组件只会在 bears 或 nuts 发生改变时渲染，不会受到其他状态影响
function BearCounter() {
  const { bears, nuts } = useBearStore(
    (state) =&amp;gt; ({ bears: state.bears, nuts: state.nuts }),
    shallow
  );

  return &amp;lt;h1&amp;gt;{bears} bears, {nuts} nuts&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 中间件架构 (Middleware Pattern)&lt;/h2&gt;
&lt;p&gt;Zustand 的 API 设计极其灵活，它允许开发者通过高阶函数（Higher-Order Functions）实现洋葱圈模型（Onion Architecture）的中间件拦截。&lt;/p&gt;
&lt;h3&gt;3.1 柯里化与状态扩展&lt;/h3&gt;
&lt;p&gt;中间件的核心本质是一个接收底层 &lt;code&gt;createState&lt;/code&gt; 并返回新的 &lt;code&gt;createState&lt;/code&gt; 函数的工厂函数。&lt;/p&gt;
&lt;p&gt;例如，实现一个简单的日志中间件，它会在每次 &lt;code&gt;set&lt;/code&gt; 操作前打印当前状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 自定义日志中间件
const logMiddleware = (config) =&amp;gt; (set, get, api) =&amp;gt;
  config(
    (args) =&amp;gt; {
      console.log(&quot;  applying&quot;, args);
      // 调用底层原始的 set 方法
      set(args);
      console.log(&quot;  new state&quot;, get());
    },
    get,
    api,
  );

// 使用中间件
const useStore = create(
  logMiddleware((set) =&amp;gt; ({
    count: 0,
    inc: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count + 1 })),
  })),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 Immer 与持久化&lt;/h3&gt;
&lt;p&gt;在处理深层嵌套状态时，直接修改对象往往会导致意外的浅拷贝丢失。通过结合 Immer 中间件，开发者可以直接“修改”状态树，底层会自动生成结构共享的新对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

const useDeepStore = create(
  immer((set) =&amp;gt; ({
    deep: { nested: { obj: { count: 0 } } },
    inc: () =&amp;gt;
      set((state) =&amp;gt; {
        // 利用 Immer 直接修改属性，无需层层展开拷贝
        state.deep.nested.obj.count += 1;
      }),
  })),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，Zustand 提供了内置的 &lt;code&gt;persist&lt;/code&gt; 中间件，自动将状态序列化至 &lt;code&gt;localStorage&lt;/code&gt; 或 &lt;code&gt;sessionStorage&lt;/code&gt;，大大简化了表单草稿箱或用户偏好设置等场景的数据落盘工作。&lt;/p&gt;
&lt;h2&gt;4. 企业级开发的高阶玩法：Zustand + React Context&lt;/h2&gt;
&lt;p&gt;虽然 Zustand 主打的是“脱离 Context 的全局状态”，但在&lt;strong&gt;组件库开发&lt;/strong&gt;或&lt;strong&gt;复杂微前端/多实例页面&lt;/strong&gt;场景中，全局单例往往是灾难。&lt;/p&gt;
&lt;p&gt;例如：一个页面里渲染了三个独立的 &lt;code&gt;&amp;lt;VideoPlayer /&amp;gt;&lt;/code&gt; 组件，如果它们共用一个 &lt;code&gt;useVideoStore&lt;/code&gt;，状态就会互相覆盖。&lt;/p&gt;
&lt;p&gt;最佳实践是：&lt;strong&gt;将 Zustand Store 实例作为值，放入 React Context 中。&lt;/strong&gt; 这样既实现了多实例隔离，又保留了 Zustand 的细粒度重渲染优势。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createContext, useContext, useRef } from &apos;react&apos;;
import { createStore, useStore } from &apos;zustand&apos;;

// 1. 定义 Store 的类型与工厂函数，而不是直接 create()
interface PlayerProps {
  isPlaying: boolean;
  volume: number;
}
interface PlayerState extends PlayerProps {
  play: () =&amp;gt; void;
  pause: () =&amp;gt; void;
}

type PlayerStore = ReturnType&amp;lt;typeof createPlayerStore&amp;gt;;
const createPlayerStore = (initProps?: Partial&amp;lt;PlayerProps&amp;gt;) =&amp;gt; {
  return createStore&amp;lt;PlayerState&amp;gt;()((set) =&amp;gt; ({
    isPlaying: false,
    volume: 50,
    ...initProps,
    play: () =&amp;gt; set({ isPlaying: true }),
    pause: () =&amp;gt; set({ isPlaying: false }),
  }))
}

// 2. 创建 Context (Context 的值是一个 Store 实例，不是状态数据本身)
const PlayerContext = createContext&amp;lt;PlayerStore | null&amp;gt;(null);

// 3. Provider 组件
export function PlayerProvider({ children, ...props }: React.PropsWithChildren&amp;lt;Partial&amp;lt;PlayerProps&amp;gt;&amp;gt;) {
  const storeRef = useRef&amp;lt;PlayerStore&amp;gt;();
  if (!storeRef.current) {
    storeRef.current = createPlayerStore(props); // 初始化单次实例
  }
  return (
    &amp;lt;PlayerContext.Provider value={storeRef.current}&amp;gt;
      {children}
    &amp;lt;/PlayerContext.Provider&amp;gt;
  )
}

// 4. 自定义 Hook: 结合 useContext 和 Zustand 的 useStore
export function usePlayerContext&amp;lt;T&amp;gt;(selector: (state: PlayerState) =&amp;gt; T): T {
  const store = useContext(PlayerContext);
  if (!store) throw new Error(&apos;Missing PlayerProvider&apos;);
  return useStore(store, selector);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种模式，当 &lt;code&gt;isPlaying&lt;/code&gt; 变化时，&lt;strong&gt;Context 的值（即 store 实例本身的引用）并未改变&lt;/strong&gt;，因此不会触发所有子组件重渲染。只有那些通过 &lt;code&gt;usePlayerContext(s =&amp;gt; s.isPlaying)&lt;/code&gt; 订阅了该切片的子组件才会更新。这是目前 React 中构建复杂、高性能状态多实例隔离的最佳方案之一。&lt;/p&gt;
&lt;h2&gt;5. API 演进：useShallow 的引入&lt;/h2&gt;
&lt;p&gt;在 Zustand v4 及早期版本中，我们通常将 &lt;code&gt;shallow&lt;/code&gt; 作为 &lt;code&gt;useStore&lt;/code&gt; 的第三个参数传入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 老写法 (v4 之前)
const { nuts, bears } = useStore(
  (state) =&amp;gt; ({ nuts: state.nuts, bears: state.bears }), 
  shallow
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Zustand v5 (及 v4 的后期次要版本) 中，官方引入了更符合 Hooks 语义的 &lt;code&gt;useShallow&lt;/code&gt; 辅助函数。它直接包裹 Selector，将比较逻辑内聚：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useShallow } from &apos;zustand/react/shallow&apos;

// 新写法 (推荐)
const { nuts, bears } = useStore(
  useShallow((state) =&amp;gt; ({ nuts: state.nuts, bears: state.bears }))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种改变不仅让 TypeScript 的类型推导更加稳健，而且通过提前浅比较，进一步减少了内部的不必要计算。&lt;/p&gt;
&lt;h2&gt;6. 总结与权衡&lt;/h2&gt;
&lt;p&gt;Zustand 通过剥离状态容器与视图层，在单向数据流的基础上，提供了与 Redux 相当的可控性，却省去了所有复杂的样板代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对比 Context&lt;/strong&gt;：Zustand 利用外部闭包结合 Selector 实现了极细粒度的组件渲染控制，有效规避了 Context 导致的不必要渲染开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比 Jotai/Recoil&lt;/strong&gt;：原子化状态管理更适合那些状态之间存在复杂派生关系、或状态频繁创建销毁的网格应用（如电子表格）。而 Zustand 依然遵循中心化存储（Centralized Store）的范式，更适合管理结构相对稳定的全局状态（如用户信息、主题配置、大型表单数据）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作为架构师，在选择状态管理方案时，理解库底层的运行机制往往比盲目追求流行度更为重要。Zustand 这套精巧的 &lt;code&gt;Vanilla Store + useSyncExternalStore&lt;/code&gt; 架构，为我们提供了一个性能与易用性平衡的绝佳参考。&lt;/p&gt;
</content:encoded></item><item><title>深入理解 React Server Components (RSC) 的运行机制</title><link>https://nollieleo.github.io/posts/react-server-components-internals/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react-server-components-internals/</guid><description>剖析 React Server Components 的底层渲染原理，对比传统 SSR 与 RSC 的差异，探讨服务端组件在现代前端架构中的应用与边界。</description><pubDate>Mon, 15 Jan 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;React Server Components (RSC) 代表了 React 渲染模型的根本性演进。本文将深入分析其架构差异与底层实现。&lt;/p&gt;
&lt;h2&gt;1. 渲染架构差异：SSR vs RSC&lt;/h2&gt;
&lt;p&gt;传统的 SSR (Server-Side Rendering) 仅仅是在服务端生成初始的 HTML 字符串，到达客户端后，仍然需要下载全量的 React 组件代码并执行 Hydration（水合）以绑定事件。&lt;/p&gt;
&lt;p&gt;RSC 引入了真正的“服务端组件”。这些组件仅在服务端执行，产出的是一种被称为 React Flight 的序列化数据格式，而非原生 HTML。客户端接收到 Flight 数据后，直接将其协调（Reconcile）到现有的组件树中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph TraditionalSSR [&quot;Traditional SSR&quot;]
        A1[Server: Render to HTML] --&amp;gt; B1[Client: Download HTML]
        B1 --&amp;gt; C1[Client: Download JS Bundle]
        C1 --&amp;gt; D1[Client: Hydration]
    end

    subgraph RSCArchitecture [&quot;RSC Architecture&quot;]
        A2[Server: Execute Server Components] --&amp;gt; B2[Server: Serialize to Flight Data]
        B2 --&amp;gt; C2[Client: Receive Flight Data Stream]
        C2 --&amp;gt; D2[Client: Reconcile Tree without JS Bundle]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 状态边界与 &quot;use client&quot;&lt;/h2&gt;
&lt;p&gt;在 RSC 中，服务端组件无法访问浏览器 API，也无法使用 &lt;code&gt;useState&lt;/code&gt; 或 &lt;code&gt;useEffect&lt;/code&gt;。所有的交互与状态管理必须委托给客户端组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ServerComponent.tsx (默认在服务端执行)
import db from &apos;@/lib/db&apos;;
import ClientInteractiveButton from &apos;./ClientButton&apos;;

export default async function ProductDetails({ id }) {
  // 直接在组件内访问数据库
  const product = await db.query(&apos;SELECT * FROM products WHERE id = ?&apos;, [id]);
  
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{product.name}&amp;lt;/h1&amp;gt;
      {/* 将数据作为 props 传递给客户端组件 */}
      &amp;lt;ClientInteractiveButton productId={product.id} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

// ClientButton.tsx
&apos;use client&apos;; // 明确声明边界
import { useState } from &apos;react&apos;;

export default function ClientInteractiveButton({ productId }) {
  const [count, setCount] = useState(0);
  return &amp;lt;button onClick={() =&amp;gt; setCount(c =&amp;gt; c + 1)}&amp;gt;Add {count}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. RSC Payload (Flight 数据格式) 探秘&lt;/h2&gt;
&lt;p&gt;RSC 与传统 SSR 最大的区别在于数据传输层。RSC 返回的不是 HTML 字符串，也不是纯 JSON，而是一种被称为 &lt;strong&gt;React Flight&lt;/strong&gt; 的流式序列化协议。&lt;/p&gt;
&lt;p&gt;让我们看一个真实的 RSC Payload 示例。假设我们有以下服务端组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ServerComponent.tsx
import ClientComponent from &apos;./ClientComponent&apos;;

export default async function Page() {
  const data = await fetch(&apos;https://api.example.com/data&apos;).then(res =&amp;gt; res.json());
  return (
    &amp;lt;div className=&quot;container&quot;&amp;gt;
      &amp;lt;h1&amp;gt;{data.title}&amp;lt;/h1&amp;gt;
      &amp;lt;ClientComponent user={data.user} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当浏览器请求这个组件时，它接收到的 Flight 流大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0:[&quot;$&quot;,&quot;div&quot;,null,{&quot;className&quot;:&quot;container&quot;,&quot;children&quot;:[
  [&quot;$&quot;,&quot;h1&quot;,null,{&quot;children&quot;:&quot;Hello RSC&quot;}],
  [&quot;$&quot;,&quot;@1&quot;,null,{&quot;user&quot;:{&quot;name&quot;:&quot;Alice&quot;,&quot;id&quot;:123}}]
]}]
1:I{&quot;id&quot;:&quot;./src/ClientComponent.tsx&quot;,&quot;chunks&quot;:[&quot;client-bundle&quot;],&quot;name&quot;:&quot;default&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解析：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0:&lt;/code&gt; 代表渲染树的根节点结构。注意它用特殊的占位符 &lt;code&gt;@1&lt;/code&gt; 表示这里有一个客户端组件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1:I&lt;/code&gt; 代表客户端组件的模块引用（Module Reference）。它告诉 React：&quot;当你解析到 &lt;code&gt;@1&lt;/code&gt; 时，请去下载 &lt;code&gt;client-bundle.js&lt;/code&gt; 并实例化里面的 &lt;code&gt;default&lt;/code&gt; 导出&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种格式支持&lt;strong&gt;流式传输 (Streaming)&lt;/strong&gt;。只要服务端获取到部分数据，就可以立刻下发对应的行，React 在客户端可以即刻协调（Reconcile）并渲染出可见部分，这在 Suspense 边界下表现得尤为强大。&lt;/p&gt;
&lt;h2&gt;4. RSC 与 SSR 的协同工作流&lt;/h2&gt;
&lt;p&gt;许多人容易混淆 RSC 和 SSR。实际上，在如 Next.js App Router 的架构中，它们是&lt;strong&gt;协同工作&lt;/strong&gt;的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;首屏加载 (Initial Load)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端首先运行 RSC，生成 Flight 树。&lt;/li&gt;
&lt;li&gt;然后，&lt;strong&gt;服务端 SSR 引擎&lt;/strong&gt;会接管这棵 Flight 树，并在服务端直接将其渲染成完整的 HTML 字符串下发给浏览器（为了 SEO 和首屏 FCP）。&lt;/li&gt;
&lt;li&gt;浏览器展示 HTML，下载客户端组件的 JS 代码，并进行 Hydration。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;路由导航 (Client Navigation)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当用户点击 &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; 跳转时，浏览器不再请求整个页面的 HTML。&lt;/li&gt;
&lt;li&gt;Next.js 会向服务端发起一个特殊的请求（带有 &lt;code&gt;RSC: 1&lt;/code&gt; 请求头）。&lt;/li&gt;
&lt;li&gt;服务端仅运行目标页面的 RSC，并只返回 &lt;strong&gt;Flight 数据流&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;客户端 React 接收流数据，精准地更新 DOM，整个过程没有任何全局刷新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. Server Actions：打通读写的最后一公里&lt;/h2&gt;
&lt;p&gt;RSC 完美解决了**“服务端读取数据”&lt;strong&gt;的问题，但如何将用户操作&lt;/strong&gt;“写回”**服务端呢？在传统的 SPA 中，我们需要手动编写 API 路由，然后通过 &lt;code&gt;fetch&lt;/code&gt; 提交数据。&lt;/p&gt;
&lt;p&gt;React 19 引入的 &lt;strong&gt;Server Actions&lt;/strong&gt; 彻底改变了这一范式。它允许你在客户端组件中直接调用定义在服务端的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// actions.ts (服务端执行)
&apos;use server&apos;

export async function updateProfile(formData: FormData) {
  const name = formData.get(&apos;name&apos;);
  await db.updateUser({ name });
  // 触发当前路径的重新验证，拉取最新的 RSC payload
  // revalidatePath(&apos;/profile&apos;); 
}

// ProfileForm.tsx (客户端执行)
&apos;use client&apos;
import { updateProfile } from &apos;./actions&apos;;

export default function ProfileForm() {
  // 这里的 updateProfile 实际上是一个被编译器转换过的 RPC 调用
  return (
    &amp;lt;form action={updateProfile}&amp;gt;
      &amp;lt;input name=&quot;name&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Save&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在底层，构建工具会将 &lt;code&gt;&apos;use server&apos;&lt;/code&gt; 的函数转换成一个隐藏的 HTTP POST 接口。当表单提交时，React 会自动序列化参数，发起 RPC 请求。这使得我们可以完全抛弃胶水 API 层的编写，实现了真正的全栈类型安全与逻辑闭环。&lt;/p&gt;
&lt;h2&gt;6. 架构优势与边界权衡&lt;/h2&gt;
&lt;p&gt;RSC 的主要优势在于显著减小了客户端的 JavaScript Bundle 体积，因为依赖的大型库（如 Markdown 解析器、日期处理库）可以仅保留在服务端。
然而，这也引入了新的复杂性：开发者必须清晰地划分组件的执行环境，并在 Server 和 Client 之间处理好序列化数据的传递边界。&lt;/p&gt;
</content:encoded></item><item><title>手写叠词的处理</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8F%A0%E8%AF%8D%E7%9A%84%E5%A4%84%E7%90%86/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8F%A0%E8%AF%8D%E7%9A%84%E5%A4%84%E7%90%86/</guid><description>...</description><pubDate>Fri, 03 Mar 2023 00:38:46 GMT</pubDate><content:encoded/></item><item><title>手写获取url上的query参数</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E8%8E%B7%E5%8F%96url%E4%B8%8A%E7%9A%84query%E5%8F%82%E6%95%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E8%8E%B7%E5%8F%96url%E4%B8%8A%E7%9A%84query%E5%8F%82%E6%95%B0/</guid><description>就如下:  js function urlHandler(url) {   const idx = url.indexOf(&quot;?&quot;);    let result = {};    if (idx  -1) {     const params = url.slice(idx + 1).split(...</description><pubDate>Thu, 02 Mar 2023 21:49:11 GMT</pubDate><content:encoded>&lt;p&gt;就如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function urlHandler(url) {
  const idx = url.indexOf(&quot;?&quot;);

  let result = {};

  if (idx &amp;gt; -1) {
    const params = url.slice(idx + 1).split(&quot;&amp;amp;&quot;);

    params.forEach((paramQuery) =&amp;gt; {
      const [name, value] = paramQuery.split(&quot;=&quot;);
      const decodeVal = decodeURIComponent(value);
      if (result[name] !== undefined) {
        if (Array.isArray(result[name])) {
          result[name].push(decodeVal);
        } else {
          result[name] = [result[name], decodeVal];
        }
      } else {
        result[name] = decodeVal;
      }
    });
  }

  return result;
}

console.log(
  urlHandler(
    &quot;https://juejin.cn/post/6979775184911728671?name=%E6%88%91%E6%98%AF%E4%BD%A0%E7%88%B8%E7%88%B8&quot;
  )
);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写用js切换字符串大小写</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E7%94%A8js%E5%88%87%E6%8D%A2%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%A4%A7%E5%B0%8F%E5%86%99/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E7%94%A8js%E5%88%87%E6%8D%A2%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%A4%A7%E5%B0%8F%E5%86%99/</guid><description>如题 用 js 实现英文字母大小写的切换，也就是将字符串中的大写字母变成小写，小写字母变成大小。 示例：&apos;123aBc&apos; = &apos;123AbC&apos;  直接用 ASCII 码来做  js  function switchLetterCase2(s) {   let res = &quot;&quot;;    s.split...</description><pubDate>Mon, 27 Feb 2023 21:46:31 GMT</pubDate><content:encoded>&lt;p&gt;如题
用 js 实现英文字母大小写的切换，也就是将字符串中的大写字母变成小写，小写字母变成大小。
示例：&apos;123aBc&apos; =&amp;gt; &apos;123AbC&apos;&lt;/p&gt;
&lt;p&gt;直接用 ASCII 码来做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
function switchLetterCase2(s) {
  let res = &quot;&quot;;

  s.split(&apos;&apos;).forEach((letter) =&amp;gt; {
    const asciiCode = letter.charCodeAt(0);
    if ((asciiCode &amp;gt; 64) &amp;amp; (asciiCode &amp;lt; 91)) {
      res += letter.toLowerCase();
    } else if ((asciiCode &amp;gt; 96) &amp;amp; (asciiCode &amp;lt; 123)) {
      res += letter.toUpperCase();
    } else {
      res += letter;
    }
  });

  return res;
}

console.log(switchLetterCase2(&apos;ASCII&apos;))


```js
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写版本号排序</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E7%89%88%E6%9C%AC%E5%8F%B7%E6%8E%92%E5%BA%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E7%89%88%E6%9C%AC%E5%8F%B7%E6%8E%92%E5%BA%8F/</guid><description>对，就如下  js function versionSort(arr) {   return arr.sort((pre, next) = {     let i = 0;      pre = pre.split(&quot;.&quot;);     next = next.split(&quot;.&quot;);      whi...</description><pubDate>Sun, 26 Feb 2023 01:23:58 GMT</pubDate><content:encoded>&lt;p&gt;对，就如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function versionSort(arr) {
  return arr.sort((pre, next) =&amp;gt; {
    let i = 0;

    pre = pre.split(&quot;.&quot;);
    next = next.split(&quot;.&quot;);

    while (true) {
      const preCur = pre[i];
      const nextCur = next[i];

      i++;

      if (preCur === undefined || nextCur === undefined) {
        return next.length - pre.length;
      }

      if (preCur === nextCur) continue;

      return nextCur - preCur;
    }
  });
}

console.log(
  versionSort([&quot;2.1.0&quot;, &quot;2.1.0.1&quot;, &quot;0.402.1&quot;, &quot;10.2.1&quot;, &quot;5.1.2&quot;, &quot;1.0.4.5&quot;])
);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写千分位分割</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8D%83%E5%88%86%E4%BD%8D%E5%88%86%E5%89%B2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8D%83%E5%88%86%E4%BD%8D%E5%88%86%E5%89%B2/</guid><description>就是把 123456789.1234 分割成 123,654,789.1234  js function format(num) {   let [int, fraction] = String(num).split(&quot;.&quot;);    int = int.split(&quot;&quot;);    let intS...</description><pubDate>Sun, 26 Feb 2023 00:06:09 GMT</pubDate><content:encoded>&lt;p&gt;就是把 123456789.1234 分割成 123,654,789.1234&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function format(num) {
  let [int, fraction] = String(num).split(&quot;.&quot;);

  int = int.split(&quot;&quot;);

  let intStr = &quot;&quot;;

  // 反转数组，从逆序开始，才能算准确千位
  int.reverse().forEach((str, idx) =&amp;gt; {
    if (idx !== 0 &amp;amp;&amp;amp; idx % 3 === 0) {
      // 要从前加字符串，生成的字符串才是正序的
      intStr = str + &quot;,&quot; + intStr;
    } else {
      // 要从前加字符串，生成的字符串才是正序的
      intStr = str + intStr;
    }
  });

  if (fraction.length) {
    return intStr + &quot;.&quot; + fraction;
  }
  return intStr;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之快速排序</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</guid><description>快速排序就是:  1. 选取基准元素 2. 比基准元素小的元素放到左边，大的放右边 3. 在左右子数组中重复步骤一二，直到数组只剩下一个元素 4. 向上逐级合并数组   基本实现  js function quickSort(arr = []) {   if (arr.length &lt;= 1) re...</description><pubDate>Sat, 25 Feb 2023 19:01:37 GMT</pubDate><content:encoded>&lt;p&gt;快速排序就是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选取基准元素&lt;/li&gt;
&lt;li&gt;比基准元素小的元素放到左边，大的放右边&lt;/li&gt;
&lt;li&gt;在左右子数组中重复步骤一二，直到数组只剩下一个元素&lt;/li&gt;
&lt;li&gt;向上逐级合并数组&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;基本实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function quickSort(arr = []) {
  if (arr.length &amp;lt;= 1) return arr;

  const middleIdx = Math.floor(arr.length / 2) || 0;

  const middle = arr.splice(middleIdx, 1)[0];

  const leftArr = [];

  const rightArr = [];

  arr.forEach((item) =&amp;gt; {
    item &amp;gt; middle ? rightArr.push(item) : leftArr.push(item);
  });

  return [...quickSort(leftArr), middle, ...quickSort(rightArr)];
}

console.log(quickSort([2, 9, 7, 8, 1, 4, 6, 5]));

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优化（重写算了，但思路一样）&lt;/h2&gt;
&lt;p&gt;上边这个快排只是找找感觉，我们不能这样写快排，如果每次都开两个数组，会消耗很多内存空间，数据量大时可能造成内存溢出，我们要避免开新的内存空间，即原地完成排序&lt;/p&gt;
&lt;p&gt;我们可以用元素交换来取代开新数组，在每一次分区的时候直接在原数组上交换元素，&lt;strong&gt;将小于基准数的元素挪到数组开头&lt;/strong&gt;，以&lt;code&gt;[5,1,4,2,3]&lt;/code&gt;为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/5/17148debd6e97be5~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们定义一个pos指针, 标识等待置换的元素的位置, 然后逐一遍历数组元素, 遇到比基准数小的就和arr[pos]交换位置, 然后pos++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function quickSortPro(arr) {
  function swap(first, next) {
    let temp = arr[first];
    arr[first] = arr[next];
    arr[next] = temp;
  }
  function quicker(left, right) {
    if (left &amp;lt; right) {
      const last = arr[right];
      let pos = left - 1;
      for (let i = left; i &amp;lt;= right; i++) {
        if (arr[i] &amp;lt;= last) {
          pos++;
          swap(i, pos);
        }
      }
      quicker(left, pos - 1);
      quicker(pos + 1, right);
    }
  }
  quicker(0, arr.length - 1);
  return arr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个交换的过程还是需要一些时间理解消化的，详细分析可以看这篇：&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000017814119&quot;&gt;js算法-快速排序(Quicksort)&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>算法之冒泡排序</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/</guid><description>- VisuAlgo](https://visualgo.net/en/sorting))   手写冒泡排序  毋庸置疑，就是把每一项和下一项做对比，从小到大排序，那么最大的肯定放最后面，需要经过两轮循环  1. 从区间0&lt;= i &lt;=arr.length-1的范围中去每一次生成 区间 [0, j]...</description><pubDate>Sat, 25 Feb 2023 16:55:05 GMT</pubDate><content:encoded>&lt;p&gt;[冒泡排序动画](&lt;a href=&quot;https://visualgo.net/en/sorting&quot;&gt;Sorting (Bubble, Selection, Insertion, Merge, Quick, Counting, Radix) - VisuAlgo&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;手写冒泡排序&lt;/h2&gt;
&lt;p&gt;毋庸置疑，就是把每一项和下一项做对比，从小到大排序，那么最大的肯定放最后面，需要经过两轮循环&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从区间0&amp;lt;= i &amp;lt;=arr.length-1的范围中去每一次生成 区间 [0, j]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每一次从区间[0, j]中找到这个区间最大的，因此根据“每一项和下一项做对比”来做比对&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中j的范围为0&amp;lt;=j&amp;lt;arr.length-1-i，算上上一层已经排序后的，就不需要比对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function bubbleSort(arr) {
  for (let i = 0; i &amp;lt; arr.length; i++) {
    for (let j = 0; j &amp;lt; arr.length - i - 1; j++) {
      const current = arr[j];
      const next = arr[j + 1];
      if (current &amp;gt; next) {
        arr[j] = next;
        arr[j + 1] = current;
      }
    }
  }
  return arr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优化&lt;/h2&gt;
&lt;p&gt;其实步骤1中，每次都去生成一个区间，如果这个区间[0,j]都是排过序的，那么说明，可以退出了&lt;/p&gt;
&lt;p&gt;因此加一个flag用来表示不需要排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function bubbleSort(arr) {
  for (let i = 0; i &amp;lt; arr.length; i++) {
    let flag = true;
    for (let j = 0; j &amp;lt; arr.length - i - 1; j++) {
      const current = arr[j];
      const next = arr[j + 1];
      if (current &amp;gt; next) {
        flag = false;
        arr[j] = next;
        arr[j + 1] = current;
      }
    }
    if (flag) {
      break;
    }
  }
  return arr;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写LRU算法实现</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99lru%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99lru%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0/</guid><description>1.什么是 LRU？  LRU 英文全称是 Least Recently Used，英译过来就是”最近最少使用“的意思。 它是页面置换算法中的一种，我们先来看一段百度百科的解释。   百度百科：   LRU 是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段，...</description><pubDate>Sat, 25 Feb 2023 15:34:21 GMT</pubDate><content:encoded>&lt;h2&gt;1.什么是 LRU？&lt;/h2&gt;
&lt;p&gt;LRU 英文全称是 Least Recently Used，英译过来就是”最近最少使用“的意思。 它是页面置换算法中的一种，我们先来看一段百度百科的解释。&lt;/p&gt;
&lt;h3&gt;百度百科：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;LRU 是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段，用来记录一个页面自上次被访问以来所经历的时间 t，当须淘汰一个页面时，选择现有页面中其 t 值最大的，即最近最少使用的页面予以淘汰。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;百度百科解释的比较窄，它这里只使用了页面来举例，我们通俗点来说就是：假如我们最近访问了很多个页面，内存把我们最近访问的页面都缓存了起来，但是随着时间推移，我们还在不停的访问新页面，这个时候为了减少内存占用，我们有必要删除一些页面，而删除哪些页面呢？我们可以通过访问页面的时间来决定，或者说是一个标准：在最近时间内，最久未访问的页面把它删掉。&lt;/p&gt;
&lt;h3&gt;通俗的解释：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;假如我们有一块内存，专门用来缓存我们最近发访问的网页，访问一个新网页，我们就会往内存中添加一个网页地址，随着网页的不断增加，内存存满了，这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址，然后把它删掉。
这一整个过程就可以称之为 LRU 算法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然上面的解释比较好懂了，但是我们还有很多地方没有考虑到，比如如下几点：&lt;/p&gt;
&lt;p&gt;当我们访问内存中已经存在了的网址，那么该网址是否需要更新在内存中的存储顺序。
当我们内存中还没有数据的时候，是否需要执行删除操作。&lt;/p&gt;
&lt;p&gt;最后上一张图，就更容易理解了&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 1677311955835]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;上图就很好的解释了 &lt;code&gt;LRU&lt;/code&gt; 算法在干嘛了，其实非常简单，无非就是我们往内存里面添加或者删除元素的时候，遵循&lt;strong&gt;最近最少使用原则&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;2. 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class LRUCache {
  constructor(length) {
    this.maxSize = length;
    this.cacheMap = new Map();
  }

  /** 存储数据，通过键值对的方式 */
  set(key, value) {
    if (this.cacheMap.has(key)) {
      this.cacheMap.delete(key);
    }

    this.cacheMap.set(key, value);

    if (this.cacheMap.size &amp;gt; this.maxSize) {
      const deleteKey = this.cacheMap.keys().next().value;

      this.cacheMap.delete(deleteKey);
    }
  }

  /** 获取数据, 可以理解为再访问了一遍，所以要把之前的删了，然后重新放到最前 */
  get(key) {
    if (!this.cacheMap.has(key)) {
      return null;
    }

    /** 暂存值 */
    const current = this.cacheMap.get(key);

    // 先删除这个值
    this.cacheMap.delete(key);

    // 再重新插入, 能保证当前key在最前面
    this.cacheMap.set(key, current);
  }
}

const LRUList = new LRUCache(5);

LRUList.set(&quot;weng&quot;, 1);

LRUList.set(&quot;kai&quot;, 2);

LRUList.set(&quot;min&quot;, 3);

LRUList.set(&quot;ha&quot;, 4);

LRUList.set(&quot;oi&quot;, 5);

LRUList.set(&quot;o121i&quot;, 6);

LRUList.get(&quot;weng&quot;);

console.log(LRUList.data);

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>数组转树形结构</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E6%95%B0%E7%BB%84%E8%BD%AC%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E6%95%B0%E7%BB%84%E8%BD%AC%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/</guid><description>这是一道面试题  要求如下，把 data 转换成树形结构  js const data = [   { id: &quot;01&quot;, name: &quot;张大大&quot;, pid: &quot;&quot;, job: &quot;项目经理&quot; },   { id: &quot;02&quot;, name: &quot;小亮&quot;, pid: &quot;01&quot;, job: &quot;产品leader...</description><pubDate>Fri, 24 Feb 2023 22:54:32 GMT</pubDate><content:encoded>&lt;p&gt;这是一道面试题&lt;/p&gt;
&lt;p&gt;要求如下，把 data 转换成树形结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = [
  { id: &quot;01&quot;, name: &quot;张大大&quot;, pid: &quot;&quot;, job: &quot;项目经理&quot; },
  { id: &quot;02&quot;, name: &quot;小亮&quot;, pid: &quot;01&quot;, job: &quot;产品leader&quot; },
  { id: &quot;03&quot;, name: &quot;小美&quot;, pid: &quot;01&quot;, job: &quot;UIleader&quot; },
  { id: &quot;04&quot;, name: &quot;老马&quot;, pid: &quot;01&quot;, job: &quot;技术leader&quot; },
  { id: &quot;05&quot;, name: &quot;老王&quot;, pid: &quot;01&quot;, job: &quot;测试leader&quot; },
  { id: &quot;06&quot;, name: &quot;老李&quot;, pid: &quot;01&quot;, job: &quot;运维leader&quot; },
  { id: &quot;07&quot;, name: &quot;小丽&quot;, pid: &quot;02&quot;, job: &quot;产品经理&quot; },
  { id: &quot;08&quot;, name: &quot;大光&quot;, pid: &quot;02&quot;, job: &quot;产品经理&quot; },
  { id: &quot;09&quot;, name: &quot;小高&quot;, pid: &quot;03&quot;, job: &quot;UI设计师&quot; },
  { id: &quot;10&quot;, name: &quot;小刘&quot;, pid: &quot;04&quot;, job: &quot;前端工程师&quot; },
  { id: &quot;11&quot;, name: &quot;小华&quot;, pid: &quot;04&quot;, job: &quot;后端工程师&quot; },
  { id: &quot;12&quot;, name: &quot;小李&quot;, pid: &quot;04&quot;, job: &quot;后端工程师&quot; },
  { id: &quot;13&quot;, name: &quot;小赵&quot;, pid: &quot;05&quot;, job: &quot;测试工程师&quot; },
  { id: &quot;14&quot;, name: &quot;小强&quot;, pid: &quot;05&quot;, job: &quot;测试工程师&quot; },
  { id: &quot;15&quot;, name: &quot;小涛&quot;, pid: &quot;06&quot;, job: &quot;运维工程师&quot; },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方法一（性能不是那么好）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function transformArrToTree(arr) {
  /** 存储所有id对应项 */
  const idMap = {};

  const res = [];

  arr.forEach((item) =&amp;gt; {
    const { id } = item;
    idMap[id] = item;
  });

  arr.forEach((item) =&amp;gt; {
    const { pid } = item;

    if (!pid) {
      res.push(item);
    } else {
      const parent = idMap[pid];
      if (parent.children) {
        parent.children.push(item);
      } else {
        parent.children = [item];
      }
    }
  });

  return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;计算时间: 0.235107421875 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方法二（好啊）&lt;/h2&gt;
&lt;p&gt;维护一个 json 的 map，每一个 id 都有自己的 children 和本身的数据， 把属于这个 id 的 pid 项都存入 children 数组，因为 json 的 map 都是对象，浅拷贝下， 只要是属于这个对象的 children 数组都会是同一个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function transformArrToTree(arr) {
  const idMap = {};

  const res = [];

  arr.forEach((item) =&amp;gt; {
    const { pid, id } = item;

    idMap[id] = {
      ...item,
      children: !idMap[id] ? [] : idMap[id].children,
    };

    if (!pid) {
      res.push(idMap[id]);
    } else {
      const parent = idMap[pid];
      if (!parent) {
        idMap[pid] = {
          children: [idMap[id]],
        };
      } else {
        parent.children.push(idMap[id]);
      }
    }
  });

  return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;计算时间: 0.193115234375 ms
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写sleep函数</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99sleep%E5%87%BD%E6%95%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99sleep%E5%87%BD%E6%95%B0/</guid><description>手动实现一个 sleep 函数   通过 while 实现  js function sleep(time) {   const startTime = Date.now();    while (Date.now() - startTime &lt; time) {     continue;   } ...</description><pubDate>Fri, 24 Feb 2023 21:32:29 GMT</pubDate><content:encoded>&lt;p&gt;手动实现一个 sleep 函数&lt;/p&gt;
&lt;h2&gt;通过 while 实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function sleep(time) {
  const startTime = Date.now();

  while (Date.now() - startTime &amp;lt; time) {
    continue;
  }
}

console.log(&quot;start&quot;);
sleep(2000);
console.log(&quot;end&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;async await&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function sleep(time) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve();
    }, time);
  });
}

async function test() {
  console.log(&quot;start&quot;);
  await sleep(2000);
  console.log(&quot;end&quot;);
}

test();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>输入URL到页面呈现发生了什么</title><link>https://nollieleo.github.io/posts/%E8%BE%93%E5%85%A5url%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%91%88%E7%8E%B0%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%BE%93%E5%85%A5url%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%91%88%E7%8E%B0%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88/</guid><description>https://juejin.cn/post/6844904021308735502heading-24...</description><pubDate>Fri, 24 Feb 2023 21:30:11 GMT</pubDate><content:encoded>&lt;p&gt;https://juejin.cn/post/6844904021308735502#heading-24&lt;/p&gt;
</content:encoded></item><item><title>图片懒加载的三种实现</title><link>https://nollieleo.github.io/posts/%E5%9B%BE%E7%89%87%E6%87%92%E5%8A%A0%E8%BD%BD%E7%9A%84%E4%B8%89%E7%A7%8D%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9B%BE%E7%89%87%E6%87%92%E5%8A%A0%E8%BD%BD%E7%9A%84%E4%B8%89%E7%A7%8D%E5%AE%9E%E7%8E%B0/</guid><description>三种测试全是用于以下的 html 代码  html &lt;!DOCTYPE html &lt;html lang=&quot;en&quot;   &lt;head     &lt;meta charset=&quot;UTF-8&quot; /     &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; ...</description><pubDate>Fri, 24 Feb 2023 21:04:43 GMT</pubDate><content:encoded>&lt;p&gt;三种测试全是用于以下的 html 代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;图片懒加载&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot; id=&quot;container&quot;&amp;gt;
      *[Image missing: u=3202947311,1179654885&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=800&amp;amp;h=500]*
      *[Image missing: u=3156137851,1307209439&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=889&amp;amp;h=500]*
      *[Image missing: u=1474625213,1040099858&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=800&amp;amp;h=500]*
      *[Image missing: u=3973060071,1632243021&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=890&amp;amp;h=500]*
      *[Image missing: u=1428075551,3971081578&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=750&amp;amp;h=500]*
      *[Image missing: u=3573056321,2239143646&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=889&amp;amp;h=500]*
      *[Image missing: u=1003272215,1878948666&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=120&amp;amp;f=JPEG?w=1280&amp;amp;h=800]*
      *[Image missing: u=345670089,3951600800&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=889&amp;amp;h=500]*
      *[Image missing: u=617579813,2960860841&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=120&amp;amp;f=JPEG?w=1280&amp;amp;h=800]*
      *[Image missing: u=45841977,3664621913&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=889&amp;amp;h=500]*
      *[Image missing: u=1273517628,1100314156&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=500&amp;amp;h=281]*
      *[Image missing: u=861863691,2776527252&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=800&amp;amp;h=500]*
      *[Image missing: u=413643897,2296924942&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=138&amp;amp;f=JPEG?w=800&amp;amp;h=500]*
      *[Image missing: u=3151377466,4172354467&amp;amp;fm=253&amp;amp;fmt=auto&amp;amp;app=120&amp;amp;f=JPEG?w=1280&amp;amp;h=800]*
    &amp;lt;/div&amp;gt;
    &amp;lt;style&amp;gt;
      .container {
        height: 100%;
        overflow: auto;
        width: 500px;
      }

      .container img {
        width: 100%;
        display: block;
        height: 300px;
      }
    &amp;lt;/style&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;./index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用视口高度和 offsetHeight，scrollTop&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import throttle from &quot;../throttle/throttle.js&quot;;

let imgs = document.querySelectorAll(&quot;img&quot;);

let length = imgs.length;

let count = 0;

function lazyLoadImg() {
  let viewH = document.documentElement.clientHeight;
  const scrollHeight = document.documentElement.scrollTop;

  for (let i = count; i &amp;lt; length; i++) {
    const dom = imgs[i];

    const domOffsetTop = dom.offsetTop;

    if (domOffsetTop &amp;lt;= viewH + scrollHeight) {
      const imgSrc = dom.getAttribute(&quot;data-src&quot;);

      dom.setAttribute(&quot;src&quot;, imgSrc);
      count++;
    }
  }
}
lazyLoadImg();

document.addEventListener(&quot;scroll&quot;, throttle(lazyLoadImg, 200));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;getBoundingClientRect&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import throttle from &quot;../throttle/throttle.js&quot;;

let imgs = document.querySelectorAll(&quot;img&quot;);

let length = imgs.length;

let count = 0;

function lazyLoadImg() {
  let viewH = document.documentElement.clientHeight;
  for (let i = count; i &amp;lt; length; i++) {
    const dom = imgs[i];

    const { top } = dom.getBoundingClientRect();

    if (top &amp;lt;= viewH) {
      const imgSrc = dom.getAttribute(&quot;data-src&quot;);

      dom.setAttribute(&quot;src&quot;, imgSrc);
      count++;
    }
  }
}
lazyLoadImg();

document.addEventListener(&quot;scroll&quot;, throttle(lazyLoadImg, 200));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IntersectionObserver&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;let imgs = document.querySelectorAll(&quot;img&quot;);

let length = imgs.length;

const observer = new IntersectionObserver((changes) =&amp;gt; {
  changes.forEach((change) =&amp;gt; {
    const { isIntersecting, target } = change;

    if (isIntersecting) {
      console.log(&quot;进来了&quot;);
      const dataSrc = target.getAttribute(&quot;data-src&quot;);

      target.setAttribute(&quot;src&quot;, dataSrc);

      observer.unobserve(target);
    }
  });
});

Array.from(imgs).forEach((dom) =&amp;gt; {
  observer.observe(dom);
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写自动重试n次</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E8%87%AA%E5%8A%A8%E9%87%8D%E8%AF%95n%E6%AC%A1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E8%87%AA%E5%8A%A8%E9%87%8D%E8%AF%95n%E6%AC%A1/</guid><description>请求失败的时候自动重试，这里默认2次重试  js function retry(promiseFn, times = 2) {   let count = 0;    function handleRetry(res, rej) {     return new Promise((resolve =...</description><pubDate>Fri, 24 Feb 2023 17:55:50 GMT</pubDate><content:encoded>&lt;p&gt;请求失败的时候自动重试，这里默认2次重试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function retry(promiseFn, times = 2) {
  let count = 0;

  function handleRetry(res, rej) {
    return new Promise((resolve = res, reject = rej) =&amp;gt; {
      promiseFn()
        .then((data) =&amp;gt; {
          resolve(data);
        })
        .catch((err) =&amp;gt; {
          if (count === times || !times) {
            reject(err);
          } else {
            count++;
            handleRetry(resolve, reject);
          }
        });
    });
  }

  return handleRetry();
}

let mark = 0;

function test() {
  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      mark++;
      if (mark === 10) {
        resolve(&quot;成功resolve&quot;);
        console.log(&quot;成功了&quot;);
      } else {
        reject(&quot;error&quot;);
        console.log(&quot;失败了&quot;);
      }
    }, 500);
  });
}

retry(test, 5)
  .then((data) =&amp;gt; {
    console.log(data);
  })
  .catch((err) =&amp;gt; {
    console.log(err);
  });
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>微任务缓冲队列</title><link>https://nollieleo.github.io/posts/%E5%BE%AE%E4%BB%BB%E5%8A%A1%E7%BC%93%E5%86%B2%E9%98%9F%E5%88%97/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%BE%AE%E4%BB%BB%E5%8A%A1%E7%BC%93%E5%86%B2%E9%98%9F%E5%88%97/</guid><description>微任务的缓冲队列，就是为了解决很多重复执行的场景，避免某个任务的重复执行造成性能的开销问题，包括 Vue 中的副作用更新任务，都是采用微任务队列进行缓冲  js // 任务缓存队列，用Set数据结构表示 // 这样可以保证同一个任务执行多次的时候，Set中存入的永远都是唯一的 cont queue ...</description><pubDate>Thu, 02 Feb 2023 09:40:08 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;微任务的缓冲队列，就是为了解决很多重复执行的场景，避免某个任务的重复执行造成性能的开销问题，包括 Vue 中的副作用更新任务，都是采用微任务队列进行缓冲&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 任务缓存队列，用Set数据结构表示
// 这样可以保证同一个任务执行多次的时候，Set中存入的永远都是唯一的
cont queue = new Set();
// 标记，表示是否正在刷新队列
let isFlushing = false;
// 创建一个立即resolve的Promise实例
const p = Promise.resolve();

// 添加任务的函数，在Vue中可以作为effect的调度器
function queueJob(job){
    // 将job添加到任务队列的queue上
    queue.add(job)
    // 如果还没有开始刷新队列，则刷新之
    if(!isFlushing){
        // 将此设置为true
        isFlushing = true
        p.then(()=&amp;gt;{
            try{
                // 执行任务队列中的任务
                queue.forEach((queueJob)=&amp;gt; queueJob())
            } finally {
                // 重制状态
                isFlushing = false;
                queue.length = 0
            }
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>diff算法原理和总结</title><link>https://nollieleo.github.io/posts/diff%E7%AE%97%E6%B3%95%E5%8E%9F%E7%90%86%E5%92%8C%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/diff%E7%AE%97%E6%B3%95%E5%8E%9F%E7%90%86%E5%92%8C%E6%80%BB%E7%BB%93/</guid><description>这里举例采用 vue 的框架设计进行解释 diff   diff 算法的核心其实是都是只关心新旧的虚拟节点都存在一组子节点的情况。目的都是达到减少性能的开销，其中有比较直观的就是--减少 dom 节点的操作  在中 当涉及到两组子节点的更新的时候，我们采用的是全量卸载并且重新挂载的形式去进行子节点的...</description><pubDate>Sat, 28 Jan 2023 13:45:24 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这里举例采用 vue 的框架设计进行解释 diff&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;diff 算法的核心其实是都是只关心新旧的虚拟节点都存在一组子节点的情况。目的都是达到减少性能的开销，其中有比较直观的就是--&lt;strong&gt;减少 dom 节点的操作&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在&lt;a href=&quot;https://nollieleo.github.io/2022/12/03/vue3%E6%B8%B2%E6%9F%93%E5%99%A8%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%80%BB%E7%BB%93/#2-%E6%96%B0%E5%AD%90%E8%8A%82%E7%82%B9%E6%98%AF%E4%B8%80%E7%BB%84&quot;&gt;Vue3 渲染器设计与总结&lt;/a&gt;中 当涉及到两组子节点的更新的时候，我们采用的是全量卸载并且重新挂载的形式去进行子节点的更新的，这样是非常不合理。&lt;/p&gt;
&lt;p&gt;如果只从 “挂载”和“卸载”的两个简单角度出发，我们可以很容易实现一个减少 dom 开销的更新逻辑，如下几图所示&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况一&lt;/strong&gt;：数量相同的情况&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-00-17-49-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况二&lt;/strong&gt;：旧节点多于新节点&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-00-19-24-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况三&lt;/strong&gt;：旧节点少于新节点&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-00-20-45-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;综上：数量相同的节点我们先进行 patch 从而进行更新操作，卸载和挂载从剩余的节点出发做处理。&lt;/p&gt;
&lt;p&gt;因此我们可以改造 patchChildren 的函数简单实现这套逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;
    /** 旧节点长度 */
    const oldL = oldChildren.length;
    /** 新节点长度 */
    const newL = newChildren.length;
    // 比较两者长度，取最短的，然后进行pacth操作
    const minL = Math.min(oldL, newL);
    // 遍历，目的就是为了达到减少dom的操作
    for (let i = 0; i &amp;lt; minL; i++) {
      patch(oldChildren[i], newChildren[i]);
    }
    // 如果有新增节点，及newL &amp;gt; oldL, 说明有节点要进行挂载
    if (newL &amp;gt; oldL) {
      for (let i = minL; i &amp;lt; newL; i++) {
        patch(null, newChildren[i], container);
      }
    } else if (oldL &amp;gt; newL) {
      for (let i = minL; i &amp;lt; oldL; i++) {
        unmount(oldChildren[i]);
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这种方式仍然不是最优的解法，当我们考虑以下这种情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 旧节点：
[
    {type:&apos;p&apos;},
    {type:&apos;span&apos;},
    {type:&apos;div&apos;}
]
// 新节点
[

    {type:&apos;div&apos;},
    {type:&apos;p&apos;},
    {type:&apos;span&apos;},
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他们只是简单的进行了位置的移动，却要进行 3 次的卸载和 3 次的挂载 dom 的操作。因此我们可以从 dom 的&lt;strong&gt;复用角度&lt;/strong&gt;出发，从而引出 diff 算法实现更优解&lt;/p&gt;
&lt;h2&gt;简单 diff 算法&lt;/h2&gt;
&lt;p&gt;要想知道前后的节点进行了移动从而去复用，必须要有标识，在 react 和 vue 中都是采用 key 的形式去给虚拟 dom 做标识的；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &quot;type&quot;: &quot;p&quot;,
    &quot;key&quot;: &quot;123&quot;,
    &quot;children&quot;: &quot;123&quot;
  },
  {
    &quot;type&quot;: &quot;span&quot;,
    &quot;key&quot;: &quot;234&quot;,
    &quot;children&quot;: &quot;234&quot;
  },
  {
    &quot;type&quot;: &quot;div&quot;,
    &quot;key&quot;: &quot;345&quot;,
    &quot;children&quot;: &quot;345&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打补丁&lt;/h3&gt;
&lt;p&gt;在我们有了标识了就可以知道前后节点对应的位置，但是 key 值的前后不变，不代表节点不需要更新，因为前后的 children 或者相关的属性也会发生变化，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 旧 */
const oldVNode = {
  type: &quot;div&quot;,
  children: [
    { type: &quot;p&quot;, children: &quot;1&quot;, key: &quot;1&quot; },
    { type: &quot;p&quot;, children: &quot;2&quot;, key: &quot;2&quot; },
    { type: &quot;p&quot;, children: &quot;weng&quot;, key: &quot;3&quot; },
  ],
};
/** 新 */
const newVNode = {
  type: &quot;div&quot;,
  children: [
    { type: &quot;p&quot;, children: &quot;kaimin&quot;, key: &quot;3&quot; },
    { type: &quot;p&quot;, children: &quot;1&quot;, key: &quot;1&quot; },
    { type: &quot;p&quot;, children: &quot;2&quot;, key: &quot;2&quot; },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们从新的节点中找到对应 key 值的旧节点从而先进行打补丁的一个操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      const newVNode = array[i];
      for (let j = 0; j &amp;lt; oldChildren.length; j++) {
        const oldVNode = array[j];
        // 如果找到了前后想等key的vnode，那么先进行打补丁操作
        if (newVNode.key === oldVNode.key) {
          patch(oldVNode, newVNode, container);
          break; // 找到了直接退出循环
        }
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打完补丁之后，前后节点的顺序仍然是不同的，所以我们要进行移动可复用节点&lt;/p&gt;
&lt;h3&gt;找到可移动的元素&lt;/h3&gt;
&lt;p&gt;判断一个节点是否需要移动，则就是判断更新前后的节点&lt;strong&gt;顺序&lt;/strong&gt;有无发生变化&lt;/p&gt;
&lt;p&gt;前后节点的 key 值没有发生变化，对应的索引更新后也是 0，1，2 和旧 children 的索引保持一个一个递增的状态，因此这种情况是不需要节点移动的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;p-[key]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-10-37-47-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果节点前后顺序改变了&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-10-43-49-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;更新后，节点的索引为，2，0，1，因此我们可以得出，此次更新打破了原有的索引递增规则，原始节点索引为 0 和 1 的节点需要进行移动，且真实的 dom 元素需要移动到 p-3 dom 节点的后头&lt;/p&gt;
&lt;p&gt;其实可以将 p-3 在旧 children 中对应的索引定义为：&lt;strong&gt;在旧 children 中寻找相同 key 值节点的过程中，遇到的最大索引值&lt;/strong&gt;，如果后续在寻找过程中发现节点索引值比这个还要小，说明需要进行移动&lt;/p&gt;
&lt;p&gt;因此我们改动代码，用一个 “遇到的最大索引” -- &lt;code&gt;lastIndex&lt;/code&gt;来做标记&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    /** 寻找旧数组相同key值节点的过程中，遇到的最大索引 */
    let lastIndex = 0; // ➕

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      const newVNode = array[i];
      for (let j = 0; j &amp;lt; oldChildren.length; j++) {
        const oldVNode = array[j];
        // 如果找到了前后想等key的vnode，那么先进行打补丁操作
        if (newVNode.key === oldVNode.key) {
          patch(oldVNode, newVNode, container);
          if (j &amp;lt; lastIndex) {
            // ➕
            // 说明需要移动节点了
          } else {
            // 表明j比lastIndex还大，需要更新
            lastIndex = j; // ➕
          }
          break; // 找到了直接退出循环
        }
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就找到了需要移动的节点，只要这个节点的旧节点的索引，小于 lastIndex，则就需要移动，接下来说如何移动他&lt;/p&gt;
&lt;h3&gt;移动节点&lt;/h3&gt;
&lt;p&gt;如上的例子，移动完 p-1 之后 vNode 和真实 dom 的对应关系，我们知道新 children 的顺序其实就是更新后的真实 dom 的顺序，所以这里我们把 p-1 移动到 p-3 对应的真实 dom 后面，p-2 同理，p-3 在这个过程是保持不动的。所以我们根据这种情况对 dom 顺序进行操作&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-11-09-30-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;因此我们加入移动真实 dom 的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    /** 寻找旧数组相同key值节点的过程中，遇到的最大索引 */
    let lastIndex = 0;

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      const newVNode = array[i];
      for (let j = 0; j &amp;lt; oldChildren.length; j++) {
        const oldVNode = array[j];
        // 如果找到了前后想等key的vnode，那么先进行打补丁操作
        if (newVNode.key === oldVNode.key) {
          patch(oldVNode, newVNode, container);
          if (j &amp;lt; lastIndex) {
            // 说明需要移动节点了
            /** 先获取newVNode的前一个Vnode */
            const prevVnode = newChildren[i - 1];
            // 如果prevVnode不存在，则说明当前newVNode是第一个节点，他不需要进行移动
            if (prevVnode) {
              // 由于需要将newVNode所对应的真实DOM移到prevVnode所对应的真实DOM的后面
              // 所以需要获取prevVNode所对应的真实DOM的下一个兄弟节点，将其作为锚点
              const anchor = prevVnode.el.nextSibling;
              // 调用inser方法将其插入到anchor锚点的前面，也就是prevVNode对应真实DOM的后面
              insert(newVNode.el, container, anchor);
            }
          } else {
            // 表明j比lastIndex还大，需要更新
            lastIndex = j;
          }
          break; // 找到了直接退出循环
        }
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加新节点&lt;/h3&gt;
&lt;p&gt;以上说的都是前后节点数量相同的情况，现在说新节点数大于旧节点数的情况；&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-16-24-46-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;出发点就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找到需要添加的新节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;想办法弄进去&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;找到需要添加的节点&lt;/h4&gt;
&lt;p&gt;找到需要添加的节点，这层好理解，也就是我们在外层（newChildren）遍历去找内层的（oldChildren）key 相同节点的这个过程中，没有找见，说明这个节点就是一个新增的节点。&lt;/p&gt;
&lt;p&gt;在内层遍历的时候，这里采用一个&lt;code&gt;find&lt;/code&gt;的字段去表示是否找到了节点是 key 相同的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    /** 寻找旧数组相同key值节点的过程中，遇到的最大索引 */
    let lastIndex = 0;
    /** 表明是否在oldChildren中找到了可复用节点 */
    let find = false; // ➕

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      const newVNode = array[i];
      for (let j = 0; j &amp;lt; oldChildren.length; j++) {
        const oldVNode = array[j];
        // 如果找到了前后想等key的vnode，那么先进行打补丁操作
        if (newVNode.key === oldVNode.key) {
          // 找到了复用的节点，设置为true
          find = true; // ➕
          patch(oldVNode, newVNode, container);
          if (j &amp;lt; lastIndex) {
            // 说明需要移动节点了
            /** 先获取newVNode的前一个Vnode */
            const prevVnode = newChildren[i - 1];
            // 如果prevVnode不存在，则说明当前newVNode是第一个节点，他不需要进行移动
            if (prevVnode) {
              // 由于需要将newVNode所对应的真实DOM移到prevVnode所对应的真实DOM的后面，所以需要获取prevVNode所对应的真实DOM的下一个兄弟节点，将其作为锚点
              const anchor = prevVnode.el.nextSibling;
              // 调用inser方法将其插入到anchor锚点的前面，也就是prevVNode对应真实DOM的后面
              insert(newVNode.el, container, anchor);
            }
          } else {
            // 表明j比lastIndex还大，需要更新
            lastIndex = j;
          }
          break; // 找到了直接退出循环
        }
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;正确添加节点&lt;/h4&gt;
&lt;p&gt;通过 find 字段，我们可以增加逻辑，当没有找到相关的复用节点的时候，就可以执行插入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;插入的节点必须插入在 newVNode 的前一个节点的后头，因此需要找到前面一个节点锚点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有前一个节点的情况，那说明，这个 newVNode 所对应的 DOM 是整个 dom 树的第一个节点&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此改造逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // 当新节点为文本节点的时候，如果旧节点是一组子的节点，我们需要逐个去卸载，其他情况啥也不做
    if (Array.isArray(n1.children)) {
      n1.children.forEach((node) =&amp;gt; unmount(node));
    }
    // 最后设置新的节点内容
    setElementText(container, n2.children);
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    /** 寻找旧数组相同key值节点的过程中，遇到的最大索引 */
    let lastIndex = 0;
    /** 表明是否在oldChildren中找到了可复用节点 */
    let find = false;

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      const newVNode = array[i];
      for (let j = 0; j &amp;lt; oldChildren.length; j++) {
        const oldVNode = array[j];
        // 如果找到了前后想等key的vnode，那么先进行打补丁操作
        if (newVNode.key === oldVNode.key) {
          // 找到了复用的节点，设置为true
          find = true;
          patch(oldVNode, newVNode, container);
          if (j &amp;lt; lastIndex) {
            // 说明需要移动节点了
            /** 先获取newVNode的前一个Vnode */
            const prevVnode = newChildren[i - 1];
            // 如果prevVnode不存在，则说明当前newVNode是第一个节点，他不需要进行移动
            if (prevVnode) {
              // 由于需要将newVNode所对应的真实DOM移到prevVnode所对应的真实DOM的后面，所以需要获取prevVNode所对应的真实DOM的下一个兄弟节点，将其作为锚点
              const anchor = prevVnode.el.nextSibling;
              // 调用inser方法将其插入到anchor锚点的前面，也就是prevVNode对应真实DOM的后面
              insert(newVNode.el, container, anchor);
            }
          } else {
            // 表明j比lastIndex还大，需要更新
            lastIndex = j;
          }
          break; // 找到了直接退出循环
        }
      }

      // 如果走到这里find仍然为false，说明这个newNode是新节点，oldChildren中没找到可复用的节点
      if (!find) {
        /** 先获取newVNode的前一个Vnode */
        const prevVnode = newChildren[i - 1];
        /** 锚点 */
        let anchor = null;
        if (prevVnode) {
          // 如果有前一个vnode节点，那么就赋值给他的下一个兄弟节点
          anchor = prevVnode.el.nextSibling;
        } else {
          // 说没有前一个节点，那么当前的节点就是整个容器的第一个节点
          anchor = container.firstChild;
        }
        // 执行新节点的挂载操作
        patch(null, newVNode, container, anchor);
      }
    }
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;移除节点&lt;/h3&gt;
&lt;p&gt;如下讨论节点被删除的情况&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-20-00-16-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如图 p-2 节点在更新之后被删除了，按照原有新节点遍历遍历之后，我们会发现，旧的数组中还有残余，因此还需要增加处理老节点中被删除的节点处理逻辑&lt;/p&gt;
&lt;p&gt;对于老节点，我们增加如下逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;遍历老节点，从中找到新节点中不存在的节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;卸载老节点&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    //...
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children;

    /** 寻找旧数组相同key值节点的过程中，遇到的最大索引 */
    let lastIndex = 0;
    /** 表明是否在oldChildren中找到了可复用节点 */
    let find = false;

    // 遍历新children
    for (let i = 0; i &amp;lt; newChildren.length; i++) {
      // ...
    }

    // 遍历老children
    for (let i = 0; i &amp;lt; oldChildren.length; i++) {
      const oldVNode = oldChildren[i];
      // 拿旧子节点oldVNode去新的一组中寻找相同key值的节点
      const has = newChildren.find((vnode) =&amp;gt; vnode.key === oldVNode.key);
      // 如果发现新节点中不存在，择要删掉
      if (!has) {
        unmount(oldVNode);
      }
    }
  } else {
    //...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单 diff 算法就到这里。&lt;/p&gt;
&lt;h2&gt;双端 diff 算法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;双端 diff 算法指的就是 - 同时对&lt;strong&gt;新旧两组子节点&lt;/strong&gt;的&lt;strong&gt;两个端点&lt;/strong&gt;进行比较的算法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相较于简单 diff 算法，双端 diff 有个优势，通过 4 个指针去进行节点比较找到可复用的节点，比如涉及到简单 diff 中的一个例子&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-29-10-43-49-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;简单 diff 要进行 2 次的 dom 节点的位置改变&lt;/p&gt;
&lt;p&gt;而双端 diff 在这种情况下只需要 1 次 dom 操作（迁移 p-3 节点到 p-1 的前面）&lt;/p&gt;
&lt;h3&gt;四个端点的表示&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-09-47-11-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;以下使用代码表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;比较方式&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-10-32-38-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;主要分为 4 个步骤，按照如图 1，2，3，4 的步骤进行比较&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  if (oldStartVNode.key === newStartVNode.key) {
    // 第一步：比较oldStartVNode 和newStartVNode
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 第二步：比较oldEndVNode 和 newEndVNode
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 第三步：比较oldStartVNode和 newEndVNode
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 第四步：比较oldEndVNode和 newStartVNode
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如 在步骤 1 或者 2 中找到对应复用的节点，说明他们的位置相对而言没有改变，所以不需要进行移动，这时候只需要进行节点的打补丁操作即可，之后将对应的索引同时上移或者是下移。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  if (oldStartVNode.key === newStartVNode.key) {
    // 第一步：比较oldStartVNode 和newStartVNode
    // 打补丁
    patch(oldStartVNode, newStartVNode, container);
    // 更新索引
    oldStartVNode = oldChildren[++oldStartIdx];
    newStartVNode = newChildren[++newStartIdx];
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 第二步：比较oldEndVNode 和 newEndVNode
    // 打补丁
    patch(oldEndVNode, newEndVNode, container);
    // 更新索引
    oldEndVNode = oldChildren[--oldEndIdx];
    newEndVNode = newChildren[--newEndIdx];
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 第三步：比较oldStartVNode和 newEndVNode
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 第四步：比较oldEndVNode和 newStartVNode
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如是步骤 4 命中，说明原本在 oldChildren 中是最后一个节点，但是在新顺序中，变成了第一个节点，对应到代码中的逻辑就是：&lt;strong&gt;将索引 oldEndIdx 指向的真实 dom 节点，移动到索引 oldStartIdx 指向的虚拟节点所对应的真实 dom 前面&lt;/strong&gt;，同时更新索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  if (oldStartVNode.key === newStartVNode.key) {
    // 第一步：比较oldStartVNode 和newStartVNode
    // 打补丁
    patch(oldStartVNode, newStartVNode, container);
    // 更新索引
    oldStartVNode = oldChildren[++oldStartIdx];
    newStartVNode = newChildren[++newStartIdx];
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 第二步：比较oldEndVNode 和 newEndVNode
    // 打补丁
    patch(oldEndVNode, newEndVNode, container);
    // 更新索引和头尾部节点变量
    oldEndVNode = oldChildren[--oldEndIdx];
    newEndVNode = newChildren[--newEndIdx];
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 第三步：比较oldStartVNode和 newEndVNode
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 第四步：比较oldEndVNode和 newStartVNode
    // 打补丁
    patch(oldEndVNode, newStartVNode, container);
    // 移动dom
    insert(oldEndVNode.el, container, newStartVNode.el);
    // 移动完dom之后，更新索引
    oldEndVNode = oldChildren[--oldEndIdx];
    newStartVNode = newChildren[++newStartIdx];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如是步骤 3 命中，说明原本在 oldChildren 中是第一个节点，但是在新顺序中，变成了最后一个节点，对应到代码中的逻辑是：&lt;strong&gt;将索引 oldStartIdx 指向的真实 dom 节点，移动到索引 oldEndIdx 指向的虚拟节点对应的真实 dom 的后面&lt;/strong&gt;，同时更新索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  if (oldStartVNode.key === newStartVNode.key) {
    // 第一步：比较oldStartVNode 和newStartVNode
    // 打补丁
    patch(oldStartVNode, newStartVNode, container);
    // 更新索引
    oldStartVNode = oldChildren[++oldStartIdx];
    newStartVNode = newChildren[++newStartIdx];
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 第二步：比较oldEndVNode 和 newEndVNode
    // 打补丁
    patch(oldEndVNode, newEndVNode, container);
    // 更新索引和头尾部节点变量
    oldEndVNode = oldChildren[--oldEndIdx];
    newEndVNode = newChildren[--newEndIdx];
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 第三步：比较oldStartVNode和 newEndVNode
    // 打补丁
    patch(oldStartVNode, newEndVNode, container);
    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling);
    // 更新下一个索引
    oldStartVNode = oldChildren[++oldStartIdx];
    newEndVNode = newChildren[--newEndIdx];
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 第四步：比较oldEndVNode和 newStartVNode
    // 打补丁
    patch(oldEndVNode, newStartVNode, container);
    // 移动dom
    insert(oldEndVNode.el, container, newStartVNode.el);
    // 移动完dom之后，更新索引
    oldEndVNode = oldChildren[--oldEndIdx];
    newStartVNode = newChildren[++newStartIdx];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再将更新的逻辑封装到一个 while 循环中，由于每一轮的循环，都会更新相对应的索引值，所以循环的条件：&lt;strong&gt;头部的索引值要小于等于尾部索引值&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    if (oldStartVNode.key === newStartVNode.key) {
      // 第一步：比较oldStartVNode 和newStartVNode
      // 打补丁
      patch(oldStartVNode, newStartVNode, container);
      // 更新索引
      oldStartVNode = oldChildren[++oldStartIdx];
      newStartVNode = newChildren[++newStartIdx];
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步：比较oldEndVNode 和 newEndVNode
      // 打补丁
      patch(oldEndVNode, newEndVNode, container);
      // 更新索引和头尾部节点变量
      oldEndVNode = oldChildren[--oldEndIdx];
      newEndVNode = newChildren[--newEndIdx];
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步：比较oldStartVNode和 newEndVNode
      // 打补丁
      patch(oldStartVNode, newEndVNode, container);
      insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling);
      // 更新下一个索引
      oldStartVNode = oldChildren[++oldStartIdx];
      newEndVNode = newChildren[--newEndIdx];
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步：比较oldEndVNode和 newStartVNode
      // 打补丁
      patch(oldEndVNode, newStartVNode, container);
      // 移动dom
      insert(oldEndVNode.el, container, newStartVNode.el);
      // 移动完dom之后，更新索引
      oldEndVNode = oldChildren[--oldEndIdx];
      newStartVNode = newChildren[++newStartIdx];
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双端 diff 的优势&lt;/h3&gt;
&lt;p&gt;比如简单 diff 中的所说的一个例子&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-21-14-52-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在简单的 diff 算法中，我们需要将 p-1 和 p-2 的节点移动到 p-3 的节点后面，进行两次的 dom 移动操作。&lt;/p&gt;
&lt;p&gt;双端 diff 针对这种情况流程如下：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-21-25-43-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;第一轮循环&lt;/p&gt;
&lt;p&gt;1~3 步骤：比较，两者 key 不同，不能复用&lt;/p&gt;
&lt;p&gt;4 步骤：比较发现新的子节点头部节点和旧子节点的尾部节点 key 相同，因此我们执行第四个 else if 执行逻辑，旧的尾部节点移动到 oldStartVNode 对应的真实 dom 前面&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-22-54-23-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;接下来开始新的一轮循环&lt;/p&gt;
&lt;p&gt;1 步骤：比较新旧子节点的头部节点 key，发现相等，都在头部所以只需要打补丁即可，不需要移动，并且更新索引值&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-22-58-39-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;再开始新的一轮循环&lt;/p&gt;
&lt;p&gt;1 步骤直接匹配，同上，只需要打补丁和更新索引值就好了。到这一步不满足循环条件就直接跳出了&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-23-09-48-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;非理想状态情况考虑&lt;/h3&gt;
&lt;p&gt;上面的几种情况都是比较理想的情况，在每一轮的循环中，都可以匹配到相等的 key 值，但是非理想的状态下只能通过增加额外的处理方式来进行。&lt;/p&gt;
&lt;p&gt;比如如下的这种情况：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-23-13-44-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这种情况，无论是哪一轮，都不能匹配上，如果我们用之前的代码，循环直接就死了&lt;/p&gt;
&lt;p&gt;这里有个骚操作，就是拿新子节点的头部节点，去旧子节点中寻找与之相同的 key 的节点。&lt;/p&gt;
&lt;p&gt;在这里我们用 p-2 去找旧子节点中的匹配节点，发现下标是 1，而新的位置下标是 0，比他大，说明 p-2 节点对应的真实 dom 应该移动到 oldStartVNode 对应 dom 位置之前，因此我们追加逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    if (oldStartVNode.key === newStartVNode.key) {
      // ...
    } else if (oldEndVNode.key === newEndVNode.key) {
      // ...
    } else if (oldStartVNode.key === newEndVNode.key) {
      // ...
    } else if (oldEndVNode.key === newStartVNode.key) {
      // ...
    } else {
      // 遍历旧的子节点组，找到与newStartIdx拥有相同的key值的元素
      const idxInOld = oldChildren.findIndex(
        (node) =&amp;gt; node.key === newStartVNode.key
      );
      // idxInOld大于0，说明找到了可复用的节点，并且需要将其对应的真实dom移动到头部
      if (idxInOld &amp;gt; 0) {
        const vnodeToMove = oldChildren[idxInOld];
        // 先打补丁
        patch(vnodeToMove, newStartVNode, container);
        // 将vnodeToMove.el移动到oldStartVnode.el之前，用其做锚点
        insert(vnodeToMove.el, container, oldStartVNode.el);
        // 由于位置idxInOld处的节点所对应的真实dom已经移到了别处，所以这里我们直接设置这个vnode为undefined
        oldChildren[idxInOld] = undefined;
        // 最后更新newStartIdx到下一个位置
        newStartIdx = newChildren[++newStartIdx];
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移动之后的状态图如下：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-23-50-11-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;之后再进行 2 轮的循环，最终索引会匹配到我们上面特殊处理后被置为 undefined 的节点，到了这一步我们也需要对 undefined 的情况做特殊处理，此时节点的状态如下&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-30-23-57-50-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;再循环的时候，我们需要绕过 undefined 的这一个已经处理过的节点，可能涉及的情况就是，头部节点和尾部节点都不存在的情况，因此加入判断逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    if (!oldStartVNode) {
      // ➕
      oldStartVNode = oldChildren[++oldStartIdx]; // ➕
    } else if (!oldEndVNode) {
      // ➕
      oldEndVNode = oldChildren[--oldEndIdx]; // ➕
    } else if (oldStartVNode.key === newStartVNode.key) {
      // ...
    } else if (oldEndVNode.key === newEndVNode.key) {
      // ...
    } else if (oldStartVNode.key === newEndVNode.key) {
      // ...
    } else if (oldEndVNode.key === newStartVNode.key) {
      // ...
    } else {
      // 遍历旧的子节点组，找到与newStartIdx拥有相同的key值的元素
      const idxInOld = oldChildren.findIndex(
        (node) =&amp;gt; node.key === newStartVNode.key
      );
      // idxInOld大于0，说明找到了可复用的节点，并且需要将其对应的真实dom移动到头部
      if (idxInOld &amp;gt; 0) {
        const vnodeToMove = oldChildren[idxInOld];
        // 先打补丁
        patch(vnodeToMove, newStartVNode, container);
        // 将vnodeToMove.el移动到oldStartVnode.el之前，用其做锚点
        insert(vnodeToMove.el, container, oldStartVNode.el);
        // 由于位置idxInOld处的节点所对应的真实dom已经移到了别处，所以这里我们直接设置这个vnode为undefined
        oldChildren[idxInOld] = undefined;
        // 最后更新newStartIdx到下一个位置
        newStartIdx = newChildren[++newStartIdx];
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后的流程就可以正常的进行了。&lt;/p&gt;
&lt;h3&gt;添加新元素情况&lt;/h3&gt;
&lt;p&gt;如下这种情况&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-31-10-30-26-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;经过一轮的循环会发现，1 2 3 4 步骤全部没有命中，那么我们会执行特殊的逻辑，以新节点的头部去旧子节点中去寻找与之 key 相等的，发现也没有命中。这里就需要补充逻辑，去弥补没有查找到的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    if (!oldStartVNode) {
      // ...
    } else if (!oldEndVNode) {
      // ...
    } else if (oldStartVNode.key === newStartVNode.key) {
      // ...
    } else if (oldEndVNode.key === newEndVNode.key) {
      // ...
      newEndVNode = newChildren[--newEndIdx];
    } else if (oldStartVNode.key === newEndVNode.key) {
      // ...
    } else if (oldEndVNode.key === newStartVNode.key) {
      // ...
    } else {
      // 遍历旧的子节点组，找到与newStartIdx拥有相同的key值的元素
      const idxInOld = oldChildren.findIndex(
        (node) =&amp;gt; node.key === newStartVNode.key
      );
      // idxInOld大于0，说明找到了可复用的节点，并且需要将其对应的真实dom移动到头部
      if (idxInOld &amp;gt; 0) {
        const vnodeToMove = oldChildren[idxInOld];
        // 先打补丁
        patch(vnodeToMove, newStartVNode, container);
        // 将vnodeToMove.el移动到oldStartVnode.el之前，用其做锚点
        insert(vnodeToMove.el, container, oldStartVNode.el);
        // 由于位置idxInOld处的节点所对应的真实dom已经移到了别处，所以这里我们直接设置这个vnode为undefined
        oldChildren[idxInOld] = undefined;
      } else {
        // 没有命中说明，是新节点在newChildren中的头部，因此需要将此新节点挂载到oldStartVNode对应dom的前面
        patch(null, newStartVNode, container, oldStartVNode.el);
      }
      // 最后更新newStartIdx到下一个位置
      newStartVNode = newChildren[++newStartIdx];
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上是在 1 2 3 4 轮中，都没有命中的情况才会考虑到新节点的情况，如果其中一轮命中了，那么新节点会被忽略掉，如下这种情况&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-31-14-47-06-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在第 2 轮的比较中，能够匹配到相对应的 key 值，并且在后续的循环中都能一一对应找到节点，直至以下这种状态&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-31-15-50-48-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这种状态已经表明，索引越界，循环跳出了，但是此时的 p-4 新增的这个几点也被忽略了，&lt;/p&gt;
&lt;p&gt;因此我们还需要增加逻辑，去处理遗漏的新增节点。&lt;/p&gt;
&lt;p&gt;遗漏的节点位于的区间就在于 newStartIdx 和 newEndIdx 之间，所以我们去循环遍历挨个挂载，挂载时候的锚点仍然是处于 oldStartVNode 对应 dom&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    // ...
  }

  // 循环结束后检查索引的情况，避免需要新增的节点被遗漏
  if (oldEndIdx &amp;lt; oldStartIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    // 如果满足条件，我们去遍历遗漏节点，逐个挂载
    for (let i = newStartIdx; i &amp;lt;= newEndIdx; i++) {
      // 以oldStartVNode对应的dom为锚点
      patch(null, newChildren[i], container, oldStartVNode.el);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;移除不存在的元素&lt;/h3&gt;
&lt;p&gt;移除和新增的类似，都是在最后一步检查索引值，如下这种情况：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-31-16-04-50-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;通过几次循环之后直到以下这种情况，此时的变量 newStartIdx 的值大于变量 newEndIdx 的值，停止循环，但是旧的一组节点中还存在未被处理的节点，所以应该将其移除，同理，此时是旧节点剩余，所以应该卸载在区间 oldStartIdx 和 oldEndIdx 之间的节点&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2023-01-31-16-25-31-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 双端diff算法 */
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;
  // 四个索引
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;
  // 四个索引对应的vNode
  let oldStartVNode = oldChildren[oldStartIdx];
  let oldEndVNode = oldChildren[oldEndIdx];
  let newStartVNode = newChildren[newStartIdx];
  let newEndVNode = newChildren[newEndIdx];

  while (oldStartIdx &amp;lt;= oldEndIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    // ...
  }

  // 循环结束后检查索引的情况，避免需要新增的节点被遗漏
  if (oldEndIdx &amp;lt; oldStartIdx &amp;amp;&amp;amp; newStartIdx &amp;lt;= newEndIdx) {
    // 如果满足条件，我们去遍历遗漏节点，逐个挂载
    for (let i = newStartIdx; i &amp;lt;= newEndIdx; i++) {
      patch(null, newChildren[i], container, oldStartVNode.el);
    }
  } else if (newEndIdx &amp;lt; newStartIdx &amp;amp;&amp;amp; oldStartIdx &amp;lt;= oldEndIdx) {
    for (let i = oldStartIdx; i &amp;lt;= oldEndIdx; i++) {
      unmount(oldChildren[i]);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里就都处理完了&lt;/p&gt;
&lt;h2&gt;快速diff算法&lt;/h2&gt;
</content:encoded></item><item><title>vue3渲染器设计与总结</title><link>https://nollieleo.github.io/posts/vue3%E6%B8%B2%E6%9F%93%E5%99%A8%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/vue3%E6%B8%B2%E6%9F%93%E5%99%A8%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%80%BB%E7%BB%93/</guid><description>前置概念   虚拟 dom  vdom，虚拟 dom 就是用来表示真实的 dom 元素的属性或者特点的一套数据结构，和真实 dom 一样具有树形结构，具有许多树形节点 vnode  可以简要的表示  js const vnode = {   type: &quot;h1&quot;,   children: [    ...</description><pubDate>Sat, 03 Dec 2022 19:39:37 GMT</pubDate><content:encoded>&lt;h1&gt;前置概念&lt;/h1&gt;
&lt;h2&gt;虚拟 dom&lt;/h2&gt;
&lt;p&gt;vdom，虚拟 dom 就是用来表示真实的 dom 元素的属性或者特点的一套数据结构，和真实 dom 一样具有树形结构，具有许多树形节点 vnode&lt;/p&gt;
&lt;p&gt;可以简要的表示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vnode = {
  type: &quot;h1&quot;,
  children: [
    {
      type: &quot;h2&quot;,
      children: &quot;我是h2&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;渲染器&lt;/h2&gt;
&lt;p&gt;渲染器的基本作用就是把虚拟 dom 渲染为平台上面的真实 元素，浏览器上就是真实的 dom 元素。&lt;/p&gt;
&lt;h2&gt;挂载&lt;/h2&gt;
&lt;p&gt;mounted，意思就是渲染器读取解析 vnode/vdom 属性之后，使用真实的 dom 形式并且表现在页面上/或者是具体的页面某个位置上&lt;/p&gt;
&lt;h1&gt;实现渲染器&lt;/h1&gt;
&lt;h2&gt;简单的渲染原理&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;renderer&lt;/strong&gt;: 渲染函数&lt;/p&gt;
&lt;p&gt;就是通过 innerHTML 的形式将第一个参数 domString 插入到对应容器当中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function renderer(domString, container) {
  container.innerHTML = domString;
}

renderer(&quot;&amp;lt;h1&amp;gt;vue3 renderer&amp;lt;/h1&amp;gt;&quot;, document.getElementById(&quot;app&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-12-03-20-32-21-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;结合 reactivity&lt;/h3&gt;
&lt;p&gt;引入 vue3 的 reactivity 包，他会在全局暴露一个 VueReactivity 变量&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn.jsdelivr.net/npm/@vue/reactivity@3.2.45/dist/reactivity.global.min.js&quot;&gt;cdn 地址&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/@vue/reactivity@3.2.45/dist/reactivity.global.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;module&quot;&amp;gt;
  import { renderer } from &quot;./render.js&quot;;

  const { effect, ref } = VueReactivity;

  const count = ref(1);

  effect(() =&amp;gt; {
    renderer(
      `&amp;lt;h1&amp;gt;vue3 renderer times: ${count.value}&amp;lt;/h1&amp;gt;`,
      document.getElementById(&quot;app&quot;)
    );
  });

  setTimeout(() =&amp;gt; {
    count.value++;
  }, 2000);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过引入 vue3 的 reactivity，我们能够实现一个动态渲染的基本逻辑&lt;/p&gt;
&lt;h2&gt;自定义渲染器&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;vue3 中的渲染器，是设计为通用可配置的，即可实现渲染到任意目标的平台上，我们目前说的目标平台，先指定浏览器；后续可以将一些可抽象的 API 抽离，使得渲染器的核心不依赖与浏览器的 api&lt;/p&gt;
&lt;p&gt;这也就是 vue 的核心之一，将相关浏览器的 api 封装到了&lt;strong&gt;runtime-dom&lt;/strong&gt;的一个包，提供了很多针对浏览器的 dom api，属性以及事件处理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;工厂函数&lt;/h3&gt;
&lt;p&gt;首先我们创建一个 createRenderer 的工厂函数用于创建一个渲染器, 并且抛出许多的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 创建一个渲染器 */
function createRenderer() {
  const render = (vnode, container) =&amp;gt; {
    // ...
  };

  // 后续会有各种方法

  return { render };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后进行相关的渲染调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const renderer = createRenderer();
renderer.render(vNode, document.getElementById(&quot;app&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;加入更新的概念&lt;/h3&gt;
&lt;p&gt;第一次调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;renderer.render(vNode, document.getElementById(&quot;app&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二次调用渲染的时候还是在同一个 container 上调用的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;renderer.render(newVNode, document.getElementById(&quot;app&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于首次的渲染已经将对应的 dom 渲染到了 container 内部了，所以再次调用 render 函数的时候，渲染一个新的虚拟 dom，就单单是做一个简单的挂载的动作了，而是要进行更新对比，找出变动的节点，这个过程就叫做 - &lt;strong&gt;打补丁（更新）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此我们可以相关的改造一下代码：&lt;/p&gt;
&lt;p&gt;引入一个更新的概念处理的逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 创建一个渲染器 */
function createRenderer() {
  const render = (vnode, container) =&amp;gt; {
    if (vnode) {
      // 新的node存在的情况，将其旧的vnode一起传递给patch函数进行补丁的更新
      patch(container._vnode, vnode, container);
    } else {
      if (container._vnode) {
        // 旧的vnode存在，新的vnode不存在的情况，说明是一个 unmount（卸载）的操作
        // 这里只需要将container内dom清空就可以, 目前暂时这样清空
        container.innerHTML = &quot;&quot;;
      }
    }
    // 每一次都需要保存上一次的vnode，存储在container下
    container._vnode = vnode; // 暂时使用这段代码清空，后续会完善
  };

  return { render };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;patch 简单实现&lt;/h4&gt;
&lt;p&gt;其中 patch 的函数，是最最重要的一个渲染器的核心，主要是做初始化和相关的 diff 操作&lt;/p&gt;
&lt;p&gt;目前进行简单实现，后续着重说&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新对比 */
function patch(n1, n2, container) {
  // 如果不存在
  if (!n1) {
    mountElement(n2, container);
  } else {
    // n1存在的情况下面，我们进行更新（打补丁）
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;n1: 老的 vnode&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;n2: 新的 vnode&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;container: 容器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果不存在旧的 vnode 的情况下，说明只是需要进行元素的挂载即可&lt;/p&gt;
&lt;h4&gt;实现 mountElement 挂载&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = document.createElement(vnode.type);
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    el.textContent = vnode.children;
  }
  // 将元素添加到容器中
  container.appendChild(el);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 vnode 的 type 标签名称创建一个新的 dom 元素，接着处理 children，如果是字符串的类型，说明是文本的 child 节点，直接设置 textContent 就可以了，之后 appendChild 插入容器当中&lt;/p&gt;
&lt;h3&gt;配置项形式抽离&lt;/h3&gt;
&lt;p&gt;因为我们要设计一个相当于是不依赖于平台的一个通用渲染器，所以，需要将上述所用到的所有依赖于浏览器的 api 都给抽离出来，实现独立封装的配置项&lt;/p&gt;
&lt;p&gt;例如，我们抽离&lt;strong&gt;mountElement&lt;/strong&gt;函数使用到的一些浏览器方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 浏览器端的相关api */
const BROWSER_APIS = {
  // 用于创建元素
  createElement(tag) {
    return document.createElement(tag);
  },

  /** 用于设置元素的文本节点 */
  setElementText(el, text) {
    el.textContent = text;
  },

  /** 给特定的parent下添加指定的元素 */
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor);
  },
};

export default BROWSER_APIS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后我们改造&lt;strong&gt;createRenderer&lt;/strong&gt;函数&lt;/p&gt;
&lt;p&gt;将相关的 api 以 options 的形式传入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 创建一个渲染器 */
function createRenderer(options) {
  const { createElement, insert, setElementText } = options;

  /** 挂载 */
  function mountElement(vnode, container) {
    // ...
  }

  /** 更新对比 */
  function patch(n1, n2, container) {
    // ...
  }

  /** 渲染方法 */
  const render = (vnode, container) =&amp;gt; {
    // ...
  };

  return { render };
}

export { createRenderer };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后我们就可以通过传递进来的可配置的 apis 去实现相关的渲染器操作&lt;/p&gt;
&lt;p&gt;例如，我们可以改造&lt;strong&gt;mountElement&lt;/strong&gt;并且使用到相关的 apis&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type); // ➕
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(e, vnode.children); // ➕
  }
  // 将元素添加到容器中
  insert(el, container); // ➕
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;通过了以上配置之后，渲染器将不仅仅可以在浏览器端进行使用，我们也可以根据不同的平台，传入不同的自定义的相关 api 参数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;挂载和更新&lt;/h1&gt;
&lt;h2&gt;子节点挂载&lt;/h2&gt;
&lt;p&gt;上述我们只考虑到了一个 vnode 的 children 为 string 的情况下的挂载，使用 &lt;strong&gt;setElementText&lt;/strong&gt; 对元素进行挂载，但是 children 可能存在多个 vnode 非 string 类型的情况&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例如&lt;/strong&gt;：以下的 vnode，有两个子节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;div&quot;,
  children: [
    {
      type: &quot;p&quot;,
      children: &quot;111&quot;,
    },
    {
      type: &quot;p&quot;,
      children: &quot;222&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此需要改造挂载函数，使其具有挂载子节点的能力。这里加入一层 children 节点的判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    // ➕
    // 如果children是数组，则便遍历每一个字节点，然后调用patch的方法
    vnode.children.forEach((child) =&amp;gt; {
      patch(null, child, el);
    });
  }
  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;挂载节点的属性&lt;/h2&gt;
&lt;p&gt;一个元素可以用多个属性来进行描述，当然映射到虚拟 dom 上的话，用一个 props 的属性进行表示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;div&quot;,
  props: {
    id: &quot;foo&quot;,
  },
  children: [
    {
      type: &quot;p&quot;,
      children: &quot;111&quot;,
    },
    {
      type: &quot;p&quot;,
      children: &quot;222&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们在挂载元素的时候，也需要将这些属性值渲染到对应的元素上面&lt;/p&gt;
&lt;p&gt;改造挂载函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    // 如果children是数组，则便遍历每一个字节点，然后调用patch的方法
    vnode.children.forEach((child) =&amp;gt; {
      patch(null, child, el);
    });
  }

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // ➕
    // 遍历
    for (const key in vnode.props) {
      if (key in el) {
        const prop = vnode.props[key];
        // 调用setAttribute将属性设置到元素上
        el.setAttribute(key, prop); // 🌟
      }
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在标记“🌟“的地方，也可以使用 &lt;code&gt;el[key] = vnode.props[key]&lt;/code&gt;，但是由于这都是属于直接操作 dom 对象的行为，所以都会存在缺陷，因此，我们需要想办法如何的正确设置元素的属性&lt;/p&gt;
&lt;h3&gt;🌟HTML attribute 和 DOM properties 区别&lt;/h3&gt;
&lt;p&gt;HTML attribute 指的就是定义在 HMTL 标签上的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input id=&quot;my-input&quot; type=&quot;text&quot; value=&quot;foo&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;document 文档解析之后，会生成一个与之相符的 dom 元素对象，这个对象上面包含了很多的属性&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-12-05-00-37-36-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这些就是所谓的 dom property。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两者的区分&lt;/strong&gt;大致如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;很多的 HTMl attribute 在 DOM 对象上面都有与之同名的 DOM properties，但是命名规则却不一样&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例如&lt;/strong&gt;：HTML attribute 的 &lt;strong&gt;class&lt;/strong&gt; 对应的 dom property 就是 &lt;strong&gt;className&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两者存在关联：例如上述设置了 HTML attribute 的 id 为‘foo’，那么对应的 DOM properties 当中存在相同属性名称为 id 也为 foo，&lt;strong&gt;两者可以当作直接映射的关系&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并不是都存在直接直接映射关系：例如 value 属性，上述 input 设置了 value 值，但是在 DOM properties 对应的值不仅仅是 value，还有 defaultValue 值；如果后续在 input 框中输入了其他的 value 值 bar&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-12-05-00-50-37-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;然后我们再去读取其相关的 HTML Attribute 和 DOM properties 会发现&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-12-05-00-53-03-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;其实 HTML Attribute 只是做了一个&lt;strong&gt;初始值的赋值&lt;/strong&gt;，但是却是对其他的 DOM property 也有相关影响&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非法属性值会被浏览器校验处理掉：例如我们在 &lt;strong&gt;input&lt;/strong&gt; 的 HTML attribute 上面设置了一个 &lt;strong&gt;type&lt;/strong&gt; 等于一个‘foo‘的属性，那么会被浏览器自动处理掉。最终我们读取 DOM property 的时候，是被矫正之后的值 ‘&lt;strong&gt;text&lt;/strong&gt;’&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;HTMl Atrribute 的作用就是设置与之对应的 DOM properties 的初始值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以按照上述的 HTML attribute 和 DOM props 的区别，我们可以改造相关的 mountElement 的操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  console.log(el);
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    // 如果children是数组，则便遍历每一个字节点，然后调用patch的方法
    vnode.children.forEach((child) =&amp;gt; {
      patch(null, child, el);
    });
  }

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // 遍历
    for (const key in vnode.props) {
      const value = vnode.props[key]; // 获取props对应key的value
      // dom properties存在的情况
      if (key in el) {
        el[key] = value;
      } else {
        // 设置的属性没有对应的DOM properties的情况，调用setAttribute将属性设置到元素上
        el.setAttribute(key, value);
      }
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;正确的设置节点的属性&lt;/h3&gt;
&lt;p&gt;&lt;a&gt;浏览&lt;/a&gt;器会自动为我们解析 HTML 文件中的 dom 元素，以及相关的 props 的属性设置操作。&lt;/p&gt;
&lt;p&gt;但是在 vue 中，因为用到了自身的模板文件，所以在解析相关的节点的时候需要自身处理这些属性的挂载操作&lt;/p&gt;
&lt;h4&gt;布尔类型属性处理&lt;/h4&gt;
&lt;p&gt;例如下面这段 html 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button disabled /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中会将其属性 &lt;strong&gt;disabled&lt;/strong&gt; 自动矫正为 disabled=true&lt;/p&gt;
&lt;p&gt;在目前的我们渲染器中，类似与等价于如下 vnode&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;button&quot;,
  props: {
    disabled: &quot;&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终在挂载的时候，会调用设置方法将空字符串 设置到 dom 属性上面。&lt;/p&gt;
&lt;p&gt;类似于：&lt;code&gt;el.disabled=&apos;&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是由于浏览器的自动矫正功能，会将我们的空字符串，自动矫正为 false，这就不符合用户的本意了。&lt;/p&gt;
&lt;p&gt;因此我们要对这种 DOM attribute 布尔类型的属性，在赋值的时候加入一层判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  // ... 省略代码

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // 遍历
    for (const key in vnode.props) {
      const value = vnode.props[key]; // 获取props对应key的value
      // dom properties存在的情况
      if (key in el) {
        // 获取这个DOM properties元素的属性类型
        const type = typeof el[key]; // ➕
        // 如果原生属性类型为布尔类型，并且value是空的字符串的话，给他值矫正为true
        if (type === &quot;boolean&quot; &amp;amp;&amp;amp; value === &quot;&quot;) {
          // ➕
          el[key] = true;
        } else {
          // 其他情况
          el[key] = value;
        }
      } else {
        // 设置的属性没有对应的DOM properties的情况，调用setAttribute将属性设置到元素上
        el.setAttribute(key, value);
      }
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;只读类型属性处理&lt;/h4&gt;
&lt;p&gt;DOM properties 当中还存在很多只读的属性，例如：form&lt;/p&gt;
&lt;p&gt;如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form id=&quot;form1&quot;&amp;gt;&amp;lt;/form&amp;gt;
&amp;lt;input form=&quot;form1&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似 &lt;code&gt;form&lt;/code&gt; 这种 DOM properties 在所有的 form 控件上都是，只读的属性的，我们只能通过 &lt;code&gt;setAttribute&lt;/code&gt;来设置他的属性，所以这时候还得要修改我们的逻辑，判断当前的属性在浏览器中是否是只读的，如果是匹配上这种只读的情况的属性，使用&lt;code&gt;setAttribute&lt;/code&gt;来进行赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 判断是否应该作为DOM properties的设置
function shouldSetAsProps(el, key, value) {
  // 对特殊只读属性的处理
  if (key === &quot;form&quot; &amp;amp;&amp;amp; el.tagName === &quot;INPUT&quot;) return false;

  return key in el;
}

/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  // ... 省略代码

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // 遍历
    for (const key in vnode.props) {
      const value = vnode.props[key]; // 获取props对应key的value
      // dom properties存在的情况
      if (shouldSetAsProps(el, key, value)) {
        // 获取这个DOM properties元素的属性类型
        const type = typeof el[key];
        // 如果原生属性类型为布尔类型，并且value是空的字符串的话，给他值矫正为true
        if (type === &quot;boolean&quot; &amp;amp;&amp;amp; value === &quot;&quot;) {
          el[key] = true;
        } else {
          el[key] = value;
        }
      } else {
        // 设置的属性没有对应的DOM properties的情况，调用setAttribute将属性设置到元素上
        el.setAttribute(key, value);
      }
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;提取平台无关代码&lt;/h4&gt;
&lt;p&gt;我们将遍历 props 时候的 判断设值的代码，提取到 BROWSE_APIS 当中，作为浏览器端的方法，取名为&lt;code&gt;patchProps&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 将属性设置相关的操作封装到patchProps的函数中，并作为渲染器选项传递 */
  patchProps(el, key, preValue, nextValue) {
    // 判断是否应该作为DOM properties的设置
    function shouldSetAsProps(el, key, value) {
      // 对特殊只读属性的处理
      if (key === &quot;form&quot; &amp;amp;&amp;amp; el.tagName === &quot;INPUT&quot;) return false;
      // ...还有更多特殊处理情况todo

      return Object.hasOwnProperty.call(vnode.props, key);
    }

    // dom properties存在的情况
    if (shouldSetAsProps(el, key, nextValue)) {
      // 获取这个DOM properties元素的属性类型
      const type = typeof el[key];
      // 如果原生属性类型为布尔类型，并且value是空的字符串的话，给他值矫正为true
      if (type === &quot;boolean&quot; &amp;amp;&amp;amp; nextValue === &quot;&quot;) {
        el[key] = true;
      } else {
        el[key] = nextValue;
      }
    } else {
      // 设置的属性没有对应的DOM properties的情况，调用setAttribute将属性设置到元素上
      el.setAttribute(key, nextValue);
    }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终在 mountElement 中就这样调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  console.log(el);
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    // 如果children是数组，则便遍历每一个字节点，然后调用patch的方法
    vnode.children.forEach((child) =&amp;gt; {
      patch(null, child, el);
    });
  }

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // 遍历
    for (const key in vnode.props) {
      patchProps(el, key, null, vnode.props[key]); // ➕
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;class 和 style 的处理&lt;/h3&gt;
&lt;p&gt;在 vue 中，我们可以用 3 种方式去传递一个 class 的值&lt;/p&gt;
&lt;p&gt;字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p class=&quot;foo bar&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p :class=&quot;{ foo:true,  bar:false }&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p :class=&quot;[&apos;foo bar&apos;, {foo:true,  bar:false }]&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论是何种结构，我们最终转化到 vNode 上面都是要成为一个字符串的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;p&quot;,
  props: {
    class: &quot;foo bar&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此在生成 vnode 的过程，我们需要调用 normalizeClass 的方法去转换 class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;p&quot;,
  props: {
    class: normalizeClass([
      &quot;foo&quot;,
      {
        foo: false,
        bar: true,
      },
    ]),
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;实现 normalizeClass&lt;/h4&gt;
&lt;p&gt;normalizeClass&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 格式化class */
function normalizeClass(value) {
  let res = &quot;&quot;;
  if (isString(value)) {
    res = value;
  } else if (isArray(value)) {
    // 类似数组 join(&apos; &apos;)
    for (let i = 0; i &amp;lt; value.length; i++) {
      const normalized = normalizeClass(value[i]);
      if (normalized) {
        res += normalized + &quot; &quot;;
      }
    }
  } else if (isObject(value)) {
    for (const name in value) {
      if (value[name]) {
        res += name + &quot; &quot;;
      }
    }
  }
  return res.trim();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;设置 class&lt;/h4&gt;
&lt;p&gt;设置 dom class 属性的方式有多种，&lt;code&gt;setAttribute&lt;/code&gt;, &lt;code&gt;el.className&lt;/code&gt;, &lt;code&gt;el.classList&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;el.className&lt;/code&gt;的性能最好
因此我们改之我们的&lt;code&gt;patchProps&lt;/code&gt;方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 将属性设置相关的操作封装到patchProps的函数中，并作为渲染器选项传递 */
  patchProps(el, key, preValue, nextValue) {
    // 判断是否应该作为DOM properties的设置
    function shouldSetAsProps(el, key, value) {
      // ...
    }

    if (key === &quot;class&quot;) { // ➕
      el.className = nextValue || &quot;&quot;;  // ➕
    } else if (shouldSetAsProps(el, key, nextValue)) {
      // ...
    } else {
      // ...
    }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然 style 也是类似的处理方案，只不过在 vue 中调用的是&lt;code&gt;normalizeStyle&lt;/code&gt;的方法&lt;/p&gt;
&lt;h2&gt;卸载操作&lt;/h2&gt;
&lt;p&gt;上述说到，我们在 render 函数中使用了 &lt;code&gt;container.innerHTML = &apos;&apos;&lt;/code&gt;的方式去清空卸载;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 渲染方法 */
const render = (vnode, container) =&amp;gt; {
  if (vnode) {
    // 新的node存在的情况，将其旧的vnode一起传递给patch函数进行补丁的更新
    patch(container._vnode, vnode, container);
  } else {
    if (container._vnode) {
      // 旧的vnode存在，新的vnode不存在的情况，说明是一个 unmount（卸载）的操作
      // 这里只需要将container内dom清空就可以
      container.innerHTML = &quot;&quot;; // ✨
    }
  }
  // 每一次都需要保存上一次的vnode，存储在container下
  container._vnode = vnode;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么做不严谨，原因如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容器的内容可能是某个组件或者多个渲染的，在卸载的时候应该去触发组件相关的卸载生命周期函数&lt;/li&gt;
&lt;li&gt;自定义指令，没办法正确执行&lt;/li&gt;
&lt;li&gt;dom 身上绑定的一些事件，不会进行移除&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确的卸载方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据 vnode 对象获取其关联的真实 dom 元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用原生的 dom 移除操作将其移除&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;建立 dom 与 vnode 联系&lt;/h3&gt;
&lt;p&gt;要想根据 vnode 对象获取其关联的真实 dom 元素，首先必须要先建立联系&lt;/p&gt;
&lt;p&gt;在挂载阶段我们利用 &lt;code&gt;createElement&lt;/code&gt;创建了真实的 dom，之后将其绑定在 vnode 上，这样就可以将 vNode 和真实 dom 之间的联系建立起来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  console.log(el);
  vnode.el = el; // 将其创建出来的dom添加到vnode上，建立联系 ➕
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(vnode.children)) {
    // ...
  }

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // ...
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;改造 render 函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/** 渲染方法 */
const render = (vnode, container) =&amp;gt; {
  if (vnode) {
    // 新的node存在的情况，将其旧的vnode一起传递给patch函数进行补丁的更新
    patch(container._vnode, vnode, container);
  } else {
    if (container._vnode) {
      // 旧的vnode存在，新的vnode不存在的情况，说明是一个 unmount（卸载）的操作
      // 根据vnode获取要卸载的真实dom元素
      const el = container._vnode.el; // ➕
      // 获取el的父级元素
      const parent = el.parentNode; // ➕
      // 调用removeChild删除元素
      if (parent) parent.removeChild(el); // ➕
    }
  }
  // 每一次都需要保存上一次的vnode，存储在container下
  container._vnode = vnode;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 container._vnode 代表的就是旧的 vnode（即将要被卸载的），由于我们之前绑定上了相关的 dom 在 vnode 上，就可以调用其父级的移除元素的操作&lt;/p&gt;
&lt;h3&gt;封装 unmount&lt;/h3&gt;
&lt;p&gt;将上述的卸载操作封装成一个 unmount 的函数，方便后续功能增加&lt;/p&gt;
&lt;p&gt;unmount&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 卸载操作 */
function unmount(vnode) {
  // 根据vnode获取要卸载的真实dom元素
  // 获取el的父级元素
  const parent = vnode.el.parentNode;
  if (parent) {
    parent.removeChild(vnode.el);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;render&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const render = (vnode, container) =&amp;gt; {
  if (vnode) {
    // 新的node存在的情况，将其旧的vnode一起传递给patch函数进行补丁的更新
    patch(container._vnode, vnode, container);
  } else {
    if (container._vnode) {
      // 旧的vnode存在，新的vnode不存在的情况，说明是一个 unmount（卸载）的操作
      unmount(container._vnode); // ➕
    }
  }
  // 每一次都需要保存上一次的vnode，存储在container下
  container._vnode = vnode;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;区分 vnode 类型&lt;/h2&gt;
&lt;p&gt;在 render 函数中，vnode 存在的情况下，会将老的 vnode 和新的 vnode 都传递给 patch 函数去做一个更新，当然是我们的 node 的类型都是相同的情况下，我们才有去做比较的意义。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vnode = { type: &quot;p&quot; }; //第一次渲染：
const vnode = { type: &quot;input&quot; }; // 第二次渲染
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况就没有对比更新的一个必要了。&lt;/p&gt;
&lt;p&gt;这种情况下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先去卸载 p 元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再将 input 挂载到容器中&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此我们需要改造 patch 的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新对比, 并且做挂载相关的功能 */
function patch(n1, n2, container) {
  // n1老节点存在，对比n1和n2的类型
  if (n1 &amp;amp;&amp;amp; n1.type !== n2.type) {
    // ➕
    // 如果新旧vnode的类型不同，则直接将旧的vnode卸载
    unmount(n1); // ➕
    n1 = null; // ➕
  }
  // 如果不存在
  if (!n1) {
    mountElement(n2, container);
  } else {
    // n1存在的情况下面，我们进行更新（打补丁）
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况都是 vnode 为普通标签元素的类型情况下，我们也可以稍微改造一些对组件类型等等的支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新对比, 并且做挂载相关的功能 */
function patch(n1, n2, container) {
  // n1老节点存在，对比n1和n2的类型
  if (n1 &amp;amp;&amp;amp; n1.type !== n2.type) {
    // 如果新旧vnode的类型不同，则直接将旧的vnode卸载
    unmount(n1);
    n1 = null;
  }

  const { type } = n2;

  if (typeof type === &quot;string&quot;) {
    // 说明是普通的标签元素
    if (!n1) {
      // 如果不存在, 就进行挂载
      mountElement(n2, container);
    } else {
      // n1存在的情况下面，我们进行更新（打补丁）
      patchElement(n1, n2);
    }
  } else if (typeof type === &quot;object&quot;) {
    // 组件
  } else {
    // 其他
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事件处理&lt;/h2&gt;
&lt;p&gt;首先我们要在 vnode 中去描述事件，假定一个规则，以字符串 on 开头的都视作事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vnode = {
  type: &quot;p&quot;,
  props: {
    onClick: () =&amp;gt; {},
  },
  children: &quot;text&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绑定事件和更新&lt;/h3&gt;
&lt;p&gt;事件绑定和更新我们就需要在 patchProps 函数中做相关的处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /** 将属性设置相关的操作封装到patchProps的函数中，并作为渲染器选项传递 */
  patchProps(el, key, preValue, nextValue) {
    // 匹配事件，以on开头
    if (/^on/.test(key)) { // ➕
      // 根据属性名称得到对应的事件名称
      const name = key.slice(2).toLowerCase(); // ➕
      // 移除上一次绑定的事件处理函数
      prevValue &amp;amp;&amp;amp; el.removeEventListener(name, prevValue); // ➕
      // 绑定事件, nextvalue为事件函数
      el.addEventListener(name, nextValue); // ➕
    } else if (key === &quot;class&quot;) {
      // ...
    } else if (shouldSetAsProps(el, key, nextValue)) {
       // ...
    } else {
      // ...
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按如上的逻辑，就是相当于每次更新都要去移除之前绑定的函数，然后再对新的值重新进行监听。但是这么做性能并不是最优的。&lt;/p&gt;
&lt;p&gt;我们可以绑定一个伪造的事件处理函数 invoker，然后把真正的事件处理函数设置为 invoker.value，后续更新值的时候，我们只需要更新 invoker.value 值就可以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 将属性设置相关的操作封装到patchProps的函数中，并作为渲染器选项传递 */
function patchProps(el, key, prevValue, nextValue) {
  // 匹配事件，以on开头
  if (/^on/.test(key)) {
    // 定义el.vei为一个对象，存在事件名称到事件处理函数的映射
    const invokers = el._vei || (el._vei = {});

    // 获取该元素伪造的事件处理函数, 根据key
    let invoker = invokers[key];
    // 根据属性名称得到对应的事件名称
    const name = key.slice(2).toLowerCase();

    if (nextValue) {
      if (!invoker) {
        // 如果没有invoker，为首次监听，则去伪造一个缓存到el.vei中
        invoker = el._vei[key] = (e) =&amp;gt; {
          // 如果invoker是数组的情况，需要遍历执行
          if (Array.isArray(invoker.value)) {
            invoker.value.forEach((fn) =&amp;gt; fn(e));
          } else {
            // 这里才是处理真正的事件函数
            invoker.value(e);
          }
        };
        // 赋值事件处理函数到invoker的value上
        invoker.value = nextValue;
        // 绑定invoker
        el.addEventListener(name, invoker);
      } else {
        // 存在invoker说明是更新，只需要更新invoker.value值就行
        invoker.value = nextValue;
      }
    } else if (invoker) {
      // 新的事件函数不存在，需要销毁invoker
      el.removeEventListener(name, invoker);
    }
  } else if (key === &quot;class&quot;) {
    // ...
  } else if (shouldSetAsProps(el, key, nextValue)) {
    // ....
  } else {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;invokers：存事件名称与对应函数的映射&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el._vei：vue event invoker，在 el 上缓存 invoker&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;更新属性以及子节点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更新必定涉及到整个 vnode 上面的属性的变化，包括节点的属性以及节点的子节点的变化&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;元素的挂载是由 mountElement 触发的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 挂载函数调用 */
function mountElement(vnode, container) {
  // 创建dom元素
  const el = createElement(vnode.type);
  console.log(el);
  vnode.el = el; // 将其创建出来的dom添加到vnode上，建立联系
  //处理子节点, 如果子节点是字符串，代表元素具有文本节点
  if (typeof vnode.children === &quot;string&quot;) {
    // 只需要设置元素的textContent的属性即可
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    // 如果children是数组，则便遍历每一个字节点，然后调用patch的方法
    vnode.children.forEach((child) =&amp;gt; {
      patch(null, child, el);
    });
  }

  // 处理props, 存在的情况下进行处理
  if (vnode.props) {
    // 遍历
    for (const key in vnode.props) {
      patchProps(el, key, null, vnode.props[key]);
    }
  }

  // 将元素添加到容器中
  insert(el, container);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在挂载子节点的时候，首先有两种的类型的区分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字符串：具有文本的子节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组：多个子节点&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，子节点分为 3 种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;没有子节点的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vnode = {
  type: &quot;div&quot;,
  children: null,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vnode = {
  type: &quot;div&quot;,
  children: &quot;222&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vnode = {
  type: &quot;div&quot;,
  children: [&quot;111&quot;, { type: &quot;p&quot; }],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对应到更新的时候，我们对应的新旧节点都分别是 3 种情况&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 2022-12-09-22-45-02-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;接下来我们去实现 &lt;strong&gt;patchElement&lt;/strong&gt;函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;n1: 旧 vnode&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;n2: 新 vnode&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实现步骤&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先我们去更新他们的 &lt;strong&gt;props&lt;/strong&gt; 的变化，调用之前封装好的 &lt;strong&gt;patchProps&lt;/strong&gt; 函数做更新变化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后去更新他们的 &lt;strong&gt;children&lt;/strong&gt;，这里要对以上 &lt;strong&gt;9&lt;/strong&gt; 种的情况进行覆盖。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实现 props 变化更新&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;实现 props 的更新&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从新的 props 参数中找出旧的 props 中与之对应的 key value，调用 pacthProps 方法对 dom 元素进行对比更新&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从旧的 props 中找出不存在新 props 中的属性，调用 pacthProps 的方法进行 dom 属性的卸载&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/** 更新子节点 */
function pacthElement(n1, n2) {
  n2.el = n1.el;
  const el = n2.el;
  const { props: oldProps } = n1;
  const { props: newProps } = n2;
  // step 1 更新props
  for (const key in newProps) {
    if (newProps[key] !== oldProps[key]) {
      patchProps(el, key, oldProps[key], newProps[key]);
    }
  }
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patchProps(el, key, oldProps[key], null);
    }
  }

  // step 2：更新children
  pacthChildren(n1, n2, el);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;顺手改造 patchProps 函数&lt;/h4&gt;
&lt;p&gt;我们依据 patchProps 的各个分支，去相对应封装我们的更新方法&lt;/p&gt;
&lt;p&gt;最终的改造如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 比对props做更新
const patchProp = (el, key, prevValue, nextValue) =&amp;gt; {
  if (key === &quot;class&quot;) {
    // class的处理
    patchClass(el, nextValue);
  } else if (key === &quot;style&quot;) {
    // style的处理
    patchStyle(el, prevValue, nextValue);
  } else if (/^on[^a-z]/.test(key)) {
    // 事件的处理
    patchEvent(el, key, nextValue);
  } else {
    // 其他属性的处理
    patchAttr(el, key, nextValue);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;操作 class 更新&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/** 比对class属性 */
function patchClass(el, value) {
  // 根据最新值设置类名
  if (value == null) {
    el.removeAttribute(&quot;class&quot;);
  } else {
    el.className = value;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;操作样式的更新&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/** 比对class属性 */
function patchStyle(el, prev, next) {
  // 更新style
  const style = el.style;
  for (const key in next) {
    // 用最新的直接覆盖
    style[key] = next[key];
  }
  if (prev) {
    for (const key in prev) {
      // 老的有新的没有删除
      if (next[key] == null) {
        style[key] = null;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;操作事件的更新&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/** 创建一个invoker */
function createInvoker(initialValue) {
  const invoker = (e) =&amp;gt; invoker.value(e);
  invoker.value = initialValue;
  return invoker;
}
/** 比对事件更新 */
function patchEvent(el, rawName, nextValue) {
  // 更新事件
  const invokers = el._vei || (el._vei = {});
  const exisitingInvoker = invokers[rawName]; // 是否缓存过

  if (nextValue &amp;amp;&amp;amp; exisitingInvoker) {
    exisitingInvoker.value = nextValue;
  } else {
    const name = rawName.slice(2).toLowerCase(); // 转化事件是小写的
    if (nextValue) {
      // 缓存函数
      const invoker = (invokers[rawName] = createInvoker(nextValue));
      el.addEventListener(name, invoker);
    } else if (exisitingInvoker) {
      el.removeEventListener(name, exisitingInvoker);
      invokers[rawName] = undefined;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;操作属性的更新&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/** 比对dom properties或者是html attributes */
function patchAttr(el, key, value) {
    // 更新属性
    if (value == null) {
      // 如果值不存在，说明是卸载props的操作
      el.removeAttribute(key);
    } else {
      if (shouldSetAsProps(el, key, nextValue)) {
        // dom properties存在的情况
        // 获取这个DOM properties元素的属性类型
        const type = typeof el[key];
        // 如果原生属性类型为布尔类型，并且value是空的字符串的话，给他值矫正为true
        if (type === &quot;boolean&quot; &amp;amp;&amp;amp; nextValue === &quot;&quot;) {
          el[key] = true;
        } else {
          el[key] = nextValue;
        }
      } else {
        el.setAttribute(key, value);
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更新 children 节点&lt;/h3&gt;
&lt;p&gt;对照以上 3*3 的节点情况&lt;/p&gt;
&lt;h4&gt;1. 新子节点是文本节点&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // 当新节点为文本节点的时候，如果旧节点是一组子的节点，我们需要逐个去卸载，其他情况啥也不做
    if (Array.isArray(n1.children)) {
      n1.children.forEach((node) =&amp;gt; unmount(node));
    }
    // 最后设置新的节点内容
    setElementText(container, n2.children);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 新子节点是一组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // ...
  } else if (Array.isArray(n2.children)) {
    // 当新的子节点是一组
    // 我们判断旧的子节点是否也是一组
    if (Array.isArray(n1.children)) {
      // diff算法 todo
    } else {
      // 到这里，存在两种情况，要么是文本节点要么无
      // 什么情况下都去清空，然后将一组新的子节点添加进来
      setElementText(container, &quot;&quot;);
      n2.children.forEach((node) =&amp;gt; patch(null, node, container));
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;diff 算法，目前先用傻瓜逻辑实现，全部卸载然后再全部挂载&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n1.children.forEach((node) =&amp;gt; unmount(node));
n2.children.forEach((node) =&amp;gt; patch(null, node, container));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 新子节点啥也没有&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 更新children */
function pacthChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === &quot;string&quot;) {
    // 当新节点为文本节点的时候，如果旧节点是一组子的节点，我们需要逐个去卸载，其他情况啥也不做
    if (Array.isArray(n1.children)) {
      n1.children.forEach((node) =&amp;gt; unmount(node));
    }
    // 最后设置新的节点内容
    setElementText(container, n2.children);
  } else if (Array.isArray(n2.children)) {
    // ....
  } else {
    // 到这里，说明新的子节点不存在
    // 如果旧的节点是一组子节点，只需要逐个卸载就可以
    if (Array.isArray(n1.children)) {
      n1.children.forEach((node) =&amp;gt; unmount(node));
    } else if (typeof n1.children === &quot;string&quot;) {
      // 旧节点是文本，清空
      setElementText(container, &quot;&quot;);
    }
    // 其他情况不用管
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;特殊节点类型&lt;/h2&gt;
&lt;p&gt;一般我们用 vnode 描述节点，都是用 type 去描述节点类型&lt;/p&gt;
&lt;p&gt;但是 像 文本节点 注释节点 以及 vue3 的 fragment，都较为特殊&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Fragment&amp;gt;
  &amp;lt;!-- 注释节点 --&amp;gt;
  文本节点
&amp;lt;/Fragment&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文本节点和注释节点处理&lt;/h3&gt;
&lt;p&gt;这两者对于普通标签节点来说，不具备标签名称，所以需要框架认为的去创造一些唯一的标识，并且将其作为注释节点和文本节点的 type&lt;/p&gt;
&lt;p&gt;描述文本和注释节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 加入人为的文本节点标识
const Text = Symbol();
const newVnode = {
  type: Text,
  children: &quot;文本节点&quot;,
};
// 加入人为的注释节点标识
const Comment = Symbol();
const newVnode = {
  type: Comment,
  children: &quot;注释的节点&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入两者之后，我们需要在 patch 中去做相关的改造&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新对比, 并且做挂载相关的功能 */
  function patch(n1, n2, container) {
    // n1老节点存在，对比n1和n2的类型
    if (n1 &amp;amp;&amp;amp; n1.type !== n2.type) {
      // 如果新旧vnode的类型不同，则直接将旧的vnode卸载
      unmount(n1);
      n1 = null;
    }

    const { type } = n2;

    if (typeof type === &quot;string&quot;) {
      // ....
    } else if (typeof type === &quot;object&quot;) {
      // 组件
    } else if (type === Text) {
      // 文本标签
      if (!n1) {
        // 使用原生createTextNode去创建文本节点
        const el = (n2.el = document.createTextNode(n2.children));
        // 将文本节点插入到容器中
        insert(el, container);
      } else {
        // 如果旧的vnode存在，只需要使用心得文本节点的文本内容更新旧文本节点就可以了
        const el = (n2.el = n1.el);
        if (n2.children !== n1.children) {
          el.nodeValue = n2.children;
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然上述用到了相关浏览器的 api 我们需要给他提取出来&lt;code&gt;document.createTextNode&lt;/code&gt;以及&lt;code&gt;el.nodeValue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 浏览器端的相关api */
const BROWSER_APIS = {
  // 用于创建元素
  createElement(tag) {
    // ...
  },

  /** 用于设置元素的文本节点 */
  setElementText(el, text) {
    // ...
  },

  /** 给特定的parent下添加指定的元素 */
  insert(el, parent, anchor = null) {
    // ...
  },

  /** 创建文本节点 */
  createText(text) {
    // ➕
    return document.createTextNode(text);
  },

  /** 设置文本值 */
  setText(el, text) {
    // ➕
    el.nodeValue = text;
  },

  /** 将属性设置相关的操作封装到patchProps的函数中，并作为渲染器选项传递 */
  patchProps(el, key, prevValue, nextValue) {
    // ...
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理注释的节点和其类似，调用原生的 &lt;code&gt;document.createComment&lt;/code&gt;函数来创建即可&lt;/p&gt;
&lt;h3&gt;Fragment&lt;/h3&gt;
&lt;p&gt;Fragment 和 tex 以及注释类似，type 也需要人为加入标识&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Fragment = Symbol();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目中经常会有类似这样的 vNode，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const vNode = {
  type: &quot;ul&quot;,
  children: [
    {
      type: Fragment,
      children: [
        {
          type: &quot;li&quot;,
          children: &quot;1&quot;,
        },
        {
          type: &quot;li&quot;,
          children: &quot;2&quot;,
        },
      ],
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Fragment 本身是不属于一个真正的节点的，所以在做渲染的时候，我们只需要渲染它的子节点就可以了&lt;/p&gt;
&lt;p&gt;因此我们改造 patch 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 更新对比, 并且做挂载相关的功能 */
function patch(n1, n2, container) {
  // n1老节点存在，对比n1和n2的类型
  if (n1 &amp;amp;&amp;amp; n1.type !== n2.type) {
    // 如果新旧vnode的类型不同，则直接将旧的vnode卸载
    unmount(n1);
    n1 = null;
  }

  const { type } = n2;

  if (typeof type === &quot;string&quot;) {
    // ...
  } else if (typeof type === &quot;object&quot;) {
    // 组件
  } else if (type === Text) {
    // ...
  } else if (type === Fragment) {
    // 如果是 片段节点
    if (!n1) {
      // 如果不存在旧节点的话，只需要将Fragment的children逐个挂载就可以
      n2.children.forEach((node) =&amp;gt; patch(null, node, container));
    } else {
      // 如果旧的vnode存在的话， 则只需要更新fragment的children就可以
      pacthChildren(n1, n2, container);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且对于我们在卸载的时候，也要做相关的处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 卸载操作 */
function unmount(vnode) {
  // 在卸载的时候，如果是卸载的vnode类型为Fragment， 则需要卸载他的children
  if (vnode.type === Fragment) {
    vnode.children.forEach((node) =&amp;gt; unmount(node));
    return;
  }
  // 根据vnode获取要卸载的真实dom元素
  // 获取el的父级元素
  const parent = vnode.el.parentNode;
  if (parent) {
    parent.removeChild(vnode.el);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;后面再说 diff&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;diff 算法&lt;/h2&gt;
&lt;p&gt;// todo....&lt;/p&gt;
&lt;h3&gt;简单的 diff 算法&lt;/h3&gt;
&lt;h3&gt;双端 diff 算法&lt;/h3&gt;
&lt;h3&gt;快速 diff 算法&lt;/h3&gt;
</content:encoded></item><item><title>双击事件和单机事件冲突解决方案</title><link>https://nollieleo.github.io/posts/%E5%8F%8C%E5%87%BB%E4%BA%8B%E4%BB%B6%E5%92%8C%E5%8D%95%E6%9C%BA%E4%BA%8B%E4%BB%B6%E5%86%B2%E7%AA%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%8F%8C%E5%87%BB%E4%BA%8B%E4%BB%B6%E5%92%8C%E5%8D%95%E6%9C%BA%E4%BA%8B%E4%BB%B6%E5%86%B2%E7%AA%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</guid><description>双击事件（dblclick）时，不触发单击事件(click) 事件绑定中，执行双击事件(dblclick)时能触发两次单击事件(click)。即一个标签元素（如button等），如果元素同时绑定了单击事件(click)和双击事件(dblclick),那么执行单击事件(click)时，不会触发双击事件...</description><pubDate>Wed, 09 Nov 2022 14:37:20 GMT</pubDate><content:encoded>&lt;h1&gt;双击事件（dblclick）时，不触发单击事件(click)&lt;/h1&gt;
&lt;p&gt;事件绑定中，执行双击事件(dblclick)时能触发两次单击事件(click)。即一个标签元素（如button等），如果元素同时绑定了单击事件(click)和双击事件(dblclick),那么执行单击事件(click)时，不会触发双击事件(dblclick)， 执行双击事件(dblclick)时却会触发两次单击事件(click)。&lt;/p&gt;
&lt;h2&gt;执行顺序&lt;/h2&gt;
&lt;p&gt;单击(click)：mousedown，mouseout，click；
双击(dblclick)：mousedown，mouseout，click ， mousedown，mouseout，click，dblclick；&lt;/p&gt;
&lt;p&gt;在单击的时候不会执行双击，但是双击的时候会执行两次单击再执行双击事件。
解决的思路：使用定时器清除掉两个单击事件，留下一个双击事件。&lt;/p&gt;
&lt;h2&gt;setTimeout&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./code.jpg&quot; alt=&quot;code&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后现在，单击按钮打印“单击”，双击按钮打印“双击”。&lt;/p&gt;
&lt;p&gt;关于 time=200，大家知道js的事件循环机制，点击事件会添加一个任务队列。time=0， 也会添加一个任务队列。那么time=0与time=200有什么区别呢？&lt;/p&gt;
&lt;p&gt;因为第一次单击事件后，主线程没有任何任务，就会立马执行这个单击事件的任务。待第二次单击的时候，假设距离第一次单击事件是150ms, 如果你的定时器小于150ms, 那么第一次的任务队列就会执行完。
要想不执行第一次的任务队列，那么定时器时间间隔就必须大于两次单击的时间间隔了。所以，这个200是酌情值，大于间隔就行。&lt;/p&gt;
&lt;p&gt;第一次单击任务不执行了，是被定时器延时，然后第二次点击的时候给清除了。那么第二次点击事件呢？
在两次单击之后，会立马执行一个双击事件，双击事件的一开头就把这个第二次点击事件给清除了。&lt;/p&gt;
&lt;p&gt;这就是双击事件的大概过程。&lt;/p&gt;
</content:encoded></item><item><title>vite学习记录</title><link>https://nollieleo.github.io/posts/vite%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/vite%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</guid><description>vite前置知识  - no-bundle：利用浏览器原生 ES 模块的支持，实现开发阶段的 Dev Server，进行模块的按需加载，而不是先整体打包再进行加载。 - import：vite中的一个 import 语句代表一个 HTTP 请求   初始化模板   这里我们采用pnpm和vite，当...</description><pubDate>Thu, 03 Nov 2022 10:37:51 GMT</pubDate><content:encoded>&lt;h1&gt;vite前置知识&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-bundle&lt;/code&gt;：利用浏览器原生 ES 模块的支持，实现开发阶段的 Dev Server，进行模块的按需加载，而不是先整体打包再进行加载。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import&lt;/code&gt;：vite中的一个 import 语句代表一个 HTTP 请求&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;初始化模板&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我们采用pnpm和vite，当然需要全局安装一下。
学习记录中使用的是react + ts技术栈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pnpm create vite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化之后的文件格式是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── index.html
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── favicon.svg
│   ├── index.css
│   ├── logo.svg
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;index.html：入口文件，vite会默认把根目录下的index.html作为打包入口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Vite + React + TS&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码代表了我们脚本文件的入口，使用type=&quot;module&quot;的ES模块加载的模式，当我们访问项目的时候会去通过本地的去请求main.tsx文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;main.tsx: 入口文件引用的脚本文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;修改入口文件位置&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一般来说项目的入口文件，可能随着项目的变化而改动&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如我们将入口文件改到 src的根下；&lt;/p&gt;
&lt;p&gt;要注意两个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相对路径&lt;/li&gt;
&lt;li&gt;path包要注意ts类型报错
&lt;ol&gt;
&lt;li&gt;你需要通过 &lt;code&gt;pnpm i @types/node -D&lt;/code&gt; 安装类型&lt;/li&gt;
&lt;li&gt;tsconfig.node.json 中设置 &lt;code&gt;allowSyntheticDefaultImports: true&lt;/code&gt;，以允许下面的 default 导入方式&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;// vite.config.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;
// 引入 path 包注意两点:
// 1. 为避免类型报错，你需要通过 `pnpm i @types/node -D` 安装类型
// 2. tsconfig.node.json 中设置 `allowSyntheticDefaultImports: true`，以允许下面的 default 导入方式
import path from &apos;path&apos;
import react from &apos;@vitejs/plugin-react&apos;

export default defineConfig({
  // 手动指定项目根目录位置
  root: path.join(__dirname, &apos;src&apos;)
  plugins: [react()]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;// tsconfig.node.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;composite&quot;: true,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;Node&quot;,
    // 以允许 node相关的默认 导入方式
    &quot;allowSyntheticDefaultImports&quot;: true
  },
  &quot;include&quot;: [&quot;vite.config.ts&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;默认指令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  // 开发阶段启动 Vite Dev Server
  &quot;dev&quot;: &quot;vite&quot;,
  // 生产环境打包
  &quot;build&quot;: &quot;tsc &amp;amp;&amp;amp; vite build&quot;,
  // 生产环境打包完预览产物
  &quot;preview&quot;: &quot;vite preview&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&quot;dev&quot;: 通过启动vite的Dev Server，在开发阶段实现不打包的特性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;build&quot;: 因为当前初始化模板选用的是ts，所以在真正的使用vite打生产包的时候，要编译 TypeScript 代码并进行类型检查&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此从用到的时候tsc，是TypeScript 的官方编译命令。在tsconfig.ts文件中其实有这样的配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;compilerOptions&quot;: {
        // 省略其他配置
        // 1. noEmit 表示只做类型检查，而不会输出产物文件
        // 2. 这行配置与 tsc --noEmit 命令等效
        &quot;noEmit&quot;: true,
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在vue的项目中，我们就得使用到 &lt;code&gt;vue-tsc&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;preview&quot;: 预览打包产物的执行效果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;css相关改造点&lt;/h1&gt;
&lt;h2&gt;样式方案的意义&lt;/h2&gt;
&lt;p&gt;原生 CSS 开发的各种问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;开发体验欠佳。比如原生 CSS 不支持选择器的嵌套:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 选择器只能平铺，不能嵌套
.container .header .nav .title .text {
 color: blue;
}

.container .header .nav .box {
 color: blue;
 border: 1px solid grey;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;样式污染问题。如果出现同样的类名，很容易造成不同的样式互相覆盖和污染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// a.css
.container {
  color: red;
}

// b.css
// 很有可能覆盖 a.css 的样式！
.container {
  color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器兼容问题。为了兼容不同的浏览器，我们需要对一些属性(如transition)加上不同的浏览器前缀，比如 -webkit-、-moz-、-ms-、-o-，意味着开发者要针对同一个样式属性写很多的冗余代码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打包后的代码体积问题。如果不用任何的 CSS 工程化方案，所有的 CSS 代码都将打包到产物中，即使有部分样式并没有在代码中使用，导致产物体积过大。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;针对如上原生 CSS 的痛点，社区中诞生了不少解决方案，常见的有 5 类。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS 预处理器&lt;/strong&gt;：主流的包括Sass/Scss、Less和Stylus。这些方案各自定义了一套语法，让 CSS 也能使用嵌套规则，甚至能像编程语言一样定义变量、写条件判断和循环语句，大大增强了样式语言的灵活性，解决原生 CSS 的开发体验问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS Modules&lt;/strong&gt;：能将 CSS 类名处理成哈希值，这样就可以避免同名的情况下样式污染的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS 后处理器PostCSS&lt;/strong&gt;，用来解析和处理 CSS 代码，可以实现的功能非常丰富，比如将 px 转换为 rem、根据目标浏览器情况自动加上类似于--moz--、-o-的属性前缀等等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS in JS 方案&lt;/strong&gt;，主流的包括emotion、styled-components等等，顾名思义，这类方案可以实现直接在 JS 中写样式代码，基本包含CSS 预处理器和 CSS Modules 的各项优点，非常灵活，解决了开发体验和全局样式污染的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS 原子化框架&lt;/strong&gt;，如Tailwind CSS、Windi CSS，通过类名来指定样式，大大简化了样式写法，提高了样式开发的效率，主要解决了原生 CSS 开发体验的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不过，各种方案没有孰优孰劣，各自解决的方案有重叠的部分，但也有一定的差异&lt;/p&gt;
&lt;h2&gt;css预处理器&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Vite 本身对 CSS 各种预处理器语言(Sass/Scss、Less和Stylus)做了内置支持。也就是说，即使你不经过任何的配置也可以直接使用各种 CSS 预处理器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;但是由于 Vite 底层会调用 CSS 预处理器的官方库进行编译，而 Vite 为了实现按需加载，并没有内置这些工具库，而是让用户根据需要安装&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如我们要使用 sass预处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install sass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后不需要任何配置就可以使用scss了&lt;/p&gt;
&lt;h3&gt;全局变量的引入&lt;/h3&gt;
&lt;p&gt;比如我们在全局有许多的样式变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$theme-color: #17c2c2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在其他scss文件中引入的时候，常常会需要先导入这个样式变量的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@import &apos;./variable.scss&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vite中有可配置参数，能够配置全局引入的文件&lt;/p&gt;
&lt;p&gt;// vite.config.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts

import { normalizePath } from &apos;vite&apos;;

// 如果类型报错，需要安装 @types/node: pnpm i @types/node -D

import path from &apos;path&apos;;

// 全局 scss 文件的路径

// 用 normalizePath 解决 window 下的路径问题

const variablePath = normalizePath(path.resolve(&apos;./xxxxx&apos;));

export default defineConfig({

// css 相关的配置

css: {

    preprocessorOptions: {

        scss: {

        // additionalData 的内容会在每个 scss 文件的开头自动注入

        additionalData: `@import &quot;${variablePath}&quot;;`

        }

    }

   }

})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;css modules&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;CSS Modules 在 Vite 也是一个开箱即用的能力，Vite 会对后缀带有&lt;code&gt;.module&lt;/code&gt;的样式文件自动应用 CSS Modules。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接将相关的样式文件改为，[name].module.scss&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/madyankin/postcss-modules&quot;&gt;CSS Modules 配置&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;例如一个Header组件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;改造样式文件名称为index.module.scss&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.page-header {
    display: flex;
    color: #fff;
    background-color: $theme-color;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在tsx中引入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

import style from  &quot;./index.module.scss&quot;;

function PageHeader() {
  const prefixCls = &quot;w-page-header&quot;;

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

  return (
    &amp;lt;div className={style[&apos;page-header&apos;]}&amp;gt;
      page header
      &amp;lt;button onClick={() =&amp;gt; setCount(1)}&amp;gt;add&amp;lt;/button&amp;gt;
      {count}
    &amp;lt;/div&amp;gt;
  );
}

export default PageHeader;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改配置，生成的hash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig, normalizePath } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;
import path from &apos;path&apos;;

const variablePath = normalizePath(path.resolve(&apos;./src/assets/style/variable.scss&apos;));

// https://vitejs.dev/config/
export default defineConfig({
  root: path.join(__dirname, &apos;src&apos;),
  plugins: [react()],
  css: {
    modules: {
      // 一般我们可以通过 generateScopedName 属性来对生成的类名进行自定义
      // 其中，name 表示当前文件名，local 表示类名
      generateScopedName: &quot;[name]__[local]___[hash:base64:5]&quot;
    },
    preprocessorOptions: {
      scss: {
        // additionalData 的内容会在每个 scss 文件的开头自动注入
        additionalData: `@import &quot;${variablePath}&quot;;`
      }

    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以看到生成的类名为&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-11-06-15-55-54-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;postcss&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一般你可以通过 &lt;code&gt;postcss.config.js&lt;/code&gt; 来配置 postcss ，不过在 Vite 配置文件中已经提供了 PostCSS 的配置入口，我们可以直接在 Vite 配置文件中进行操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;autoprefixer&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这个插件主要用来自动为不同的目标浏览器添加样式前缀，解决的是浏览器兼容性的问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;装上这个插件（不再需要手动安装postcss了，只需要装对应插件）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i autoprefixer -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置vite.config.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;
import autoprefixer from &apos;autoprefixer&apos;;


// https://vitejs.dev/config/
export default defineConfig({
  root: path.join(__dirname, &apos;src&apos;),
  plugins: [react()],
  css: {
   .....
    postcss: {
      plugins: [
        autoprefixer({
          // 指定目标浏览器
          overrideBrowserslist: [
            &apos;Android 4.1&apos;,
            &apos;iOS 7.1&apos;,
            &apos;Chrome &amp;gt; 31&apos;,
            &apos;ff &amp;gt; 31&apos;,
            &apos;ie &amp;gt;= 8&apos;,
            &apos;&amp;gt; 1%&apos;,]
        })
      ]
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后打包生成的文件中就能自动添加前缀了&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-11-06-16-20-23-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;其他插件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐一个站点：&lt;a href=&quot;https://www.postcss.parts/&quot;&gt;www.postcss.parts/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cuth/postcss-pxtorem&quot;&gt;postcss-pxtorem&lt;/a&gt;： 用来将 px 转换为 rem 单位，在适配移动端的场景下很常用。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/csstools/postcss-preset-env&quot;&gt;postcss-preset-env&lt;/a&gt;: 通过它，你可以编写最新的 CSS 语法，不用担心兼容性问题。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cssnano/cssnano&quot;&gt;cssnano&lt;/a&gt;: 主要用来压缩 CSS 代码，跟常规的代码压缩工具不一样，它能做得更加智能，比如提取一些公共样式进行复用、缩短一些常见的属性值等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;css in js&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;社区中有两款主流的&lt;code&gt;CSS In JS&lt;/code&gt; 方案: &lt;code&gt;styled-components&lt;/code&gt;和&lt;code&gt;emotion&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于css in js的方案，我们要考虑多个问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;选择器命名问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DCE&lt;/code&gt;(Dead Code Elimination 即无用代码删除)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;代码压缩&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;生成 SourceMap&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;服务端渲染(SSR)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而&lt;code&gt;styled-components&lt;/code&gt;和&lt;code&gt;emotion&lt;/code&gt;已经提供了对应的 babel 插件来解决这些问题，我们在 Vite 中要做的就是集成这些 babel 插件。&lt;/p&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({
      babel: {
        // 加入 babel 插件
        // 以下插件包都需要提前安装
        // 当然，通过这个配置你也可以添加其它的 Babel 插件
        plugins: [
          // 适配 styled-component
          &quot;babel-plugin-styled-components&quot;
          // 适配 emotion
          &quot;@emotion/babel-plugin&quot;
        ]
      },
      // 注意: 对于 emotion，需要单独加上这个配置
      // 通过 `@emotion/react` 包编译 emotion 中的特殊 jsx 语法
      jsxImportSource: &quot;@emotion/react&quot;
    })
  ]
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>理解Proxy和Reflect</title><link>https://nollieleo.github.io/posts/%E7%90%86%E8%A7%A3proxy%E5%92%8Creflect/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%90%86%E8%A7%A3proxy%E5%92%8Creflect/</guid><description>理解Proxy   Vue3中利用的是Proxy以及Reflect去代理对象的  我们知道Proxy是只能代理对象类型的，非对象类型不可以进行代理  所谓代理：   指的是对一个 对象 的 基本语义 的代理   何为基本语义  比如我们对对象的一堆简单操作  js console.log(obj.a...</description><pubDate>Sat, 03 Sep 2022 10:43:45 GMT</pubDate><content:encoded>&lt;h1&gt;理解Proxy&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Vue3中利用的是Proxy以及Reflect去代理对象的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们知道Proxy是只能代理对象类型的，非对象类型不可以进行代理&lt;/p&gt;
&lt;p&gt;所谓代理：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;指的是对一个 对象 的 &lt;strong&gt;基本语义&lt;/strong&gt; 的代理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;何为基本语义&lt;/h2&gt;
&lt;p&gt;比如我们对对象的一堆简单操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(obj.a); // 读取属性操作
obj.a++; // 设置属性值操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似这种读取，设置属性值的操作，就是属于基本的语义操作 ---- &lt;strong&gt;基本操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;类似这种的基本操作就可以用Proxy进行代理拦截&lt;/p&gt;
&lt;p&gt;基本操作的基本用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = new Proxy(obj, {
  // 拦截读取属性操作
  get(){ .... },
  // 拦截设置属性操作
  set(){  .... }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如函数：我们也可以使用apply对函数进行拦截&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fn = ()=&amp;gt;{
  console.log(&apos;wengkaimin&apos;)
}

const proFn = new Proxy(fn, {
  apply(target, thisArg, argArray){
    target.call(thisArg, ...argArray)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复合操作&lt;/h2&gt;
&lt;p&gt;既然有基本操作，可以也有非基本操作，在js里头，我们叫他复合操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.fn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个显而易见，是又多个语义构成的（调用一个对象的一个函数属性）、&lt;/p&gt;
&lt;p&gt;两个语义是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先通过get获取到obj的fn属性&lt;/li&gt;
&lt;li&gt;通过获取到的fn进行调用&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;理解Reflect&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Reflect是一个全局对象，其中存在和Proxy的拦截器很多名字相同的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如下的等价操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = { a: &apos;wengkaimin&apos; };
console.log(obj.a);
console.log(Reflect.get(obj, &apos;a&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是Reflect它能够传入第三个参数 reveiver&lt;/p&gt;
&lt;p&gt;就相当于函数执行过程中，指向的this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Reflect.get(obj, &apos;a&apos;, { a: &apos;kaimin&apos; })); // kaimin
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在vue3响应式学习 整理的那篇文章中，记录了实现 响应式代理的代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    // 返回函数属性
    // 这里没有用Reflect.get实现读取数据
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在Proxy中无论设置get还是set拦截，都是直接用的原始对象target来进行读取或者赋值&lt;/p&gt;
&lt;p&gt;假如目前的obj为，返回了this.foo的值。&lt;/p&gt;
&lt;p&gt;接着我们在effect副作用函数中通过代理对象data读取b的值。&lt;/p&gt;
&lt;p&gt;之后我们修改了a的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
  a:1,
  get b(){
    return this.a + 1;
  }
}

const data = new Proxy(obj, {
  get(target, key) {
    track(target, key);
    // 返回函数属性
    // 这里没有用Reflect.get实现读取数据
    return target[key];
  },
  set(target, key, newVal) {
    // 这里没有用Reflect.set
    target[key] = newVal;
    trigger(target, key);
  },
})

effect(()=&amp;gt;{
  console.log(data.b) // 2
})

data.a++
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改了a的值之后，并不会相对应的触发副作用函数的重新执行&lt;/p&gt;
&lt;p&gt;梳理下读取步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先我们在副作用函数中读取了data.b的值&lt;/li&gt;
&lt;li&gt;会触发data代理对象的get拦截器，在get拦截器中，通过target[key]读取;&lt;/li&gt;
&lt;li&gt;此时target就指的是 obj 原始对象，key就是 &apos;b&apos;，所以相当直接读了obj.b&lt;/li&gt;
&lt;li&gt;访问obj.b的时候，其实是一个getter函数，这个getter的this指向了obj，最终实质上是访问了 obj.a 并且给他加了个1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，在副作用函数effect当中相当于，直接读取了原生对象obj的属性，虽然看上去走了代理，但不多。所以这肯定是没有追踪到的，建立不起相应的联系&lt;/p&gt;
&lt;p&gt;就类似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
  console.log(obj.a + 1) // 2
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种场景下Reflect的第三个参数receiver就派上用场了&lt;/p&gt;
&lt;p&gt;使用Reflect改造完get拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  get(target, key, receiver) {
    track(target, key);
    // 返回函数属性
    return Reflect.get(target, key, receiver)
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用了Relfect之后，this指向就转为了data代理对象，就可以成功的建立联系了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reflect的作用不仅于此&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>typescript刷题记录</title><link>https://nollieleo.github.io/posts/typescript%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/typescript%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%95/</guid><description>typescript刷题记录   题目一   解决如下错误  ts type User = {     id: number;     kind: string; }; function makeCustomer&lt;T extends User(u: T): T {   // Error（TS 编译器...</description><pubDate>Tue, 02 Aug 2022 10:44:13 GMT</pubDate><content:encoded>&lt;h1&gt;typescript刷题记录&lt;/h1&gt;
&lt;h2&gt;题目一&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;解决如下错误&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
    id: number;
    kind: string;
};
function makeCustomer&amp;lt;T extends User&amp;gt;(u: T): T {
  // Error（TS 编译器版本：v4.4.2）
  // Type &apos;{ id: number; kind: string; }&apos; is not assignable to type &apos;T&apos;.
  // &apos;{ id: number; kind: string; }&apos; is assignable to the constraint of type &apos;T&apos;, 
  // but &apos;T&apos; could be instantiated with a different subtype of constraint &apos;User&apos;.
  return {
    id: u.id,
    kind: &apos;customer&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为T受User类型的约束，但是也有可能有其他情况，比如&lt;code&gt;{id:1, kind:&apos;2&apos;,name:&apos;test&apos;}&lt;/code&gt;，所以返回值中缺少了name&lt;/p&gt;
&lt;h3&gt;方法一&lt;/h3&gt;
&lt;p&gt;直接把剩余的u的key返回出去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
    id: number;
    kind: string;
};
function makeCustomer&amp;lt;T extends User&amp;gt;(u: T): T {
  return {
    ...u,
    id: u.id,
    kind: &apos;customer&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法二&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function makeCustomer&amp;lt;T extends User&amp;gt;(u: T): ReturnMake&amp;lt;T, User&amp;gt; {
// Error（TS 编译器版本：v4.4.2）
// Type &apos;{ id: number; kind: string; }&apos; is not assignable to type &apos;T&apos;.
// &apos;{ id: number; kind: string; }&apos; is assignable to the constraint of type &apos;T&apos;, 
// but &apos;T&apos; could be instantiated with a different subtype of constraint &apos;User&apos;.
    return {
        id: u.id,
        kind: &apos;customer&apos;
    }
}

type ReturnMake&amp;lt;T extends User, U&amp;gt; = {
    [K in keyof R as K extends keyof T ? K: never]: U[K]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遍历 &lt;code&gt;User &lt;/code&gt;中的 key ，并使用 &lt;code&gt;as&lt;/code&gt; 断言，如果&lt;code&gt;K&lt;/code&gt;（也就是 User 类型的 key），约束于 泛型类型的 key 就返回 &lt;code&gt;K&lt;/code&gt;，否侧返回 &lt;code&gt;never&lt;/code&gt;，&lt;code&gt;U[K]&lt;/code&gt; 取键值。&lt;/p&gt;
&lt;h2&gt;题目二（考察函数重载）&lt;/h2&gt;
&lt;p&gt;本道题我们希望参数 a 和 b 的类型都是一致的，即 a 和 b 同时为 number 或 string 类型。当它们的类型不一致的值，TS 类型检查器能自动提示对应的错误信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function f(a: string | number, b: string | number) {
  if (typeof a === &apos;string&apos;) {
    return a + &apos;:&apos; + b; // no error but b can be number!
  } else {
    // error: 运算符“+”不能应用于类型“number”和“string | number”。ts(2365)
    return a + b; // error as b can be number | string
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案：函数重载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function f(a:string, b:string ):string
function f(a:number, b:number):number;
function f(a: string | number, b: string | number) {
    if (typeof a === &apos;string&apos; || typeof b === &apos;string&apos;) {
      return a + &apos;:&apos; + b; // no error but b can be number!
    } else {
      return a + b; // error as b can be number | string
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目三（SetOptional || SetRequired）&lt;/h2&gt;
&lt;p&gt;如何定义一个 &lt;code&gt;SetOptional&lt;/code&gt; 工具类型，支持把给定的&lt;code&gt;keys&lt;/code&gt;对应的属性变成可选的？对应的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = {
	a: number;
	b?: string;
	c: boolean;
}

// 测试用例
type SomeOptional = SetOptional&amp;lt;Foo, &apos;a&apos; | &apos;b&apos;&amp;gt;;

// type SomeOptional = {
// 	a?: number; // 该属性已变成可选的
// 	b?: string; // 保持不变
// 	c: boolean; 
// }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Foo = {
	a: number;
	b?: string;
	c: boolean;
}

// 测试用例
type SomeOptional = SetOptional&amp;lt;Foo, &apos;a&apos; | &apos;b&apos;&amp;gt;;

// type SomeOptional = {
// 	a?: number; // 该属性已变成可选的
// 	b?: string; // 保持不变
// 	c: boolean; 
// }

type SetOptional&amp;lt;T, R extends keyof T&amp;gt; = Omit&amp;lt;T,R&amp;gt; &amp;amp; Partial&amp;lt;Pick&amp;lt;T,R&amp;gt;&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;setRequried也是一样的如下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;type SetRequired&amp;lt;T, K extends keyof T&amp;gt; = Omit&amp;lt;T, K&amp;gt; &amp;amp; Required&amp;lt;Pick&amp;lt;T, K&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目四（ConditionalPick）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Pick&amp;lt;T, K extends keyof T&amp;gt;&lt;/code&gt;的作用是将某个类型中的子属性挑出来，变成包含这个类型部分属性的子类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick&amp;lt;Todo, &quot;title&quot; | &quot;completed&quot;&amp;gt;;

const todo: TodoPreview = {
  title: &quot;Clean room&quot;,
  completed: false
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么如何定义一个 &lt;code&gt;ConditionalPick&lt;/code&gt;工具类型，支持根据指定的 Condition 条件来生成新的类型，对应的使用示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Example {
	a: string;
	b: string | number;
	c: () =&amp;gt; void;
	d: {};
}

// 测试用例：
type StringKeysOnly = ConditionalPick&amp;lt;Example, string&amp;gt;;
//=&amp;gt; {a: string}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;p&gt;1、&lt;code&gt;in keyof&lt;/code&gt;遍历 &lt;code&gt;V&lt;/code&gt; 泛型；&lt;/p&gt;
&lt;p&gt;2、通过类型断言判断 &lt;code&gt;V[K]&lt;/code&gt; 对应键值是否约束于传入的 &lt;code&gt;string&lt;/code&gt;如果是 &lt;code&gt;true &lt;/code&gt;那么断言成返回遍历的当前 &lt;code&gt;K&lt;/code&gt;，否则为 &lt;code&gt;never&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;返回 &lt;code&gt;never &lt;/code&gt;在 TypeScript 编译器中，会默认认为这是个用不存在的类型，也相当于没有这个 &lt;code&gt;K &lt;/code&gt;会被过滤，对应值则是 &lt;code&gt;V[K]&lt;/code&gt; 获取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
interface Example {
	a: string;
	b: string | number;
	c: () =&amp;gt; void;
	d: {};
}

// 测试用例：
type StringKeysOnly = ConditionalPick&amp;lt;Example, string&amp;gt;;
//=&amp;gt; {a: string}

type ConditionalPick&amp;lt;T,R&amp;gt; = {
    [K in keyof T as T[K] extends R ? K : never ]: T[K]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目五（追加args）&lt;/h2&gt;
&lt;p&gt;定义一个工具类型 &lt;code&gt;AppendArgument&lt;/code&gt;，为已有的函数类型增加指定类型的参数，新增的参数名是&lt;code&gt;x&lt;/code&gt;，将作为新函数类型的第一个参数。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Fn = (a: number, b: string) =&amp;gt; number
type AppendArgument&amp;lt;F, A&amp;gt; = // 你的实现代码

type FinalFn = AppendArgument&amp;lt;Fn, boolean&amp;gt; 
// (x: boolean, a: number, b: string) =&amp;gt; number
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案1 （使用泛型工具）&lt;/h3&gt;
&lt;p&gt;使用泛型工具&lt;code&gt;Parameters&lt;/code&gt;以及&lt;code&gt;ReturnType&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Fn = (a: number, b: string) =&amp;gt; number

type FinalFn = AppendArgument&amp;lt;Fn, boolean&amp;gt; 
// (x: boolean, a: number, b: string) =&amp;gt; number

type AppendArgument&amp;lt;F extends (...args:any[]) =&amp;gt; any, A&amp;gt; = (arg1:A , ...args2:Parameters&amp;lt;F&amp;gt;) =&amp;gt; ReturnType&amp;lt;F&amp;gt; // 你的实现代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案2 （使用infer）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;infer&lt;/code&gt;推导拿到参数类型&lt;code&gt;T&lt;/code&gt;返回值类型为&lt;code&gt;R&lt;/code&gt;，再从新返回一个新函数&lt;code&gt;arg1&lt;/code&gt;参数为&lt;code&gt;A&lt;/code&gt;，&lt;code&gt;...args&lt;/code&gt;参数类型为前面推导保留的&lt;code&gt;T&lt;/code&gt;，返回值即&lt;code&gt;R&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type AppendArgument&amp;lt;F extends (...args:any[]) =&amp;gt; any, A&amp;gt; = F extends (...args:infer T)=&amp;gt; infer R ? (agr1:A, ...args:T)=&amp;gt;R : never
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目五（数组类型拍平）&lt;/h2&gt;
&lt;p&gt;定义一个&lt;code&gt;NativeFlat&lt;/code&gt;工具类型，支持把数组类型拍平（扁平化）。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type NaiveFlat&amp;lt;T extends any[]&amp;gt; = // 你的实现代码

// 测试用例：
type NaiveResult = NaiveFlat&amp;lt;[[&apos;a&apos;], [[&apos;b&apos;, &apos;c&apos;]], [&apos;d&apos;]]&amp;gt;
  
// NaiveResult的结果： &quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案（递归）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type NaiveFlat&amp;lt;T extends any[]&amp;gt; = T extends (infer R)[] ? (R extends any[] ? NaiveFlat&amp;lt;R&amp;gt;: R ): never// 你的实现代码

// 测试用例：
type NaiveResult = NaiveFlat&amp;lt;[[&apos;a&apos;], [[&apos;b&apos;, &apos;c&apos;]], [&apos;d&apos;]]&amp;gt;
  
// NaiveResult的结果： &quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、首先需要在约束条件中使用&lt;code&gt;infer&lt;/code&gt;关键字推导出 &lt;code&gt;T&lt;/code&gt; 传入的数组类型，并用 &lt;code&gt;P&lt;/code&gt; 保存数组类型。&lt;/p&gt;
&lt;p&gt;2、三元嵌套判断&lt;code&gt;R&lt;/code&gt;类型是否约束于类型&lt;code&gt;any[]&lt;/code&gt;如果还是是数组继续递归遍历调用&lt;code&gt;NaiveFlat&amp;lt;R&amp;gt;&lt;/code&gt;并传入&lt;code&gt;R&lt;/code&gt;，放 &lt;code&gt;R&lt;/code&gt;类型不满足 &lt;code&gt;any[]&lt;/code&gt;，返回最后的扁平完成&lt;code&gt;R&lt;/code&gt;类型所以得到最终联合类型&lt;code&gt;&quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot;&lt;/code&gt; 。&lt;/p&gt;
&lt;h2&gt;题目六 (数组不为空)&lt;/h2&gt;
&lt;p&gt;定义&lt;code&gt;NonEmptyArray&lt;/code&gt;工具类型，用于确保数据非空数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type NonEmptyArray&amp;lt;T&amp;gt; = // 你的实现代码
const a: NonEmptyArray&amp;lt;string&amp;gt; = [] // 将出现编译错误
const b: NonEmptyArray&amp;lt;string&amp;gt; = [&apos;Hello TS&apos;] // 非空数据，正常使用
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const a: NonEmptyArray&amp;lt;string&amp;gt; = [] // 将出现编译错误
const b: NonEmptyArray&amp;lt;string&amp;gt; = [&apos;Hello TS&apos;] // 非空数据，正常使用

type NonEmptyArray&amp;lt;T&amp;gt; = [T, ...T[]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[T, ...T[]]&lt;/code&gt;确保第一项一定是&lt;code&gt;T&lt;/code&gt;，&lt;code&gt;[...T[]]&lt;/code&gt;，为剩余数组类型。&lt;/p&gt;
&lt;h2&gt;题目七&lt;/h2&gt;
&lt;p&gt;定义一个&lt;code&gt;JoinStrArray&lt;/code&gt;工具类型，用于根据指定的&lt;code&gt;Separator&lt;/code&gt;分隔符，对字符串数组类型进行拼接。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type JoinStrArray&amp;lt;Arr extends string[], Separator extends string&amp;gt; = // 你的实现代码

// 测试用例
type Names = [&quot;Sem&quot;, &quot;Lolo&quot;, &quot;Kaquko&quot;]
type NamesComma = JoinStrArray&amp;lt;Names, &quot;,&quot;&amp;gt; // &quot;Sem,Lolo,Kaquko&quot;
type NamesSpace = JoinStrArray&amp;lt;Names, &quot; &quot;&amp;gt; // &quot;Sem Lolo Kaquko&quot;
type NamesStars = JoinStrArray&amp;lt;Names, &quot;⭐️&quot;&amp;gt; // &quot;Sem⭐️Lolo⭐️Kaquko&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type Names = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
type NamesComma = JoinStrArray&amp;lt;Names, &quot;,&quot;&amp;gt; // &quot;Sem,Lolo,Kaquko&quot;
type NamesSpace = JoinStrArray&amp;lt;Names, &quot; &quot;&amp;gt; // &quot;Sem Lolo Kaquko&quot;
type NamesStars = JoinStrArray&amp;lt;Names, &quot;⭐️&quot;&amp;gt; // &quot;Sem⭐️Lolo⭐️Kaquko&quot;

type JoinStrArray&amp;lt;Arr extends string[], Separator extends string&amp;gt; = Arr extends [infer A,...infer B] ? (`${A extends string ? A : &apos;&apos;}${B extends [string, ...string[]]
    ? `${Separator}${JoinStrArray&amp;lt;B, Separator&amp;gt;}`
    : &apos;&apos;}`):&apos;&apos; // 你的实现代码

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;JoinStrArray&lt;/code&gt;工具方法，&lt;code&gt;Arr&lt;/code&gt;泛型必须约束于&lt;code&gt;string[]&lt;/code&gt;类型，&lt;code&gt;Separator&lt;/code&gt;为分隔符，也必须约束于&lt;code&gt;string&lt;/code&gt;类型；&lt;/p&gt;
&lt;p&gt;1、首先&lt;code&gt;Arr&lt;/code&gt;约束于后面&lt;code&gt;[infer A, ...infer B]&lt;/code&gt;并通过&lt;code&gt;infer&lt;/code&gt;关键字推导拿到第一个索引&lt;code&gt;A&lt;/code&gt;的类型，以及剩余（rest）数组的类型为&lt;code&gt;B&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;2、如果满足约束，则连接字符，连接字符使用模板变量，先判断&lt;code&gt;A&lt;/code&gt;（也就是第一个索引）是否约束于&lt;code&gt;string&lt;/code&gt;类型，满足就取第一个&lt;code&gt;A&lt;/code&gt;否则直接返回空字符串；&lt;/p&gt;
&lt;p&gt;3、后面连接的&lt;code&gt;B&lt;/code&gt;（...rest）判断是否满足于&lt;code&gt;[string, ...string[]]&lt;/code&gt;，意思就是是不是还有多个索引。如果有，用分割符号，加上递归再调用&lt;code&gt;JoinStrArray&lt;/code&gt;工具类型方法，&lt;code&gt;Arr&lt;/code&gt;泛型就再为 B ，分隔符泛型&lt;code&gt;Separator&lt;/code&gt;不变。减治思想，拿出数组的每一项，直至数组为空。&lt;/p&gt;
&lt;p&gt;最开始的话，如果&lt;code&gt;Arr&lt;/code&gt;不满足约束，那么直接返回为空字符串。&lt;/p&gt;
&lt;h2&gt;题目八&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;Trim&lt;/code&gt;工具类型，用于对字符串字面量类型进行去空格处理。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Trim&amp;lt;V extends string&amp;gt; = // 你的实现代码

// 测试用例
Trim&amp;lt;&apos; semlinker &apos;&amp;gt;
//=&amp;gt; &apos;semlinker&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type TrimLeft&amp;lt;V extends string&amp;gt; = V extends ` ${infer R}` ? TrimLeft&amp;lt;R&amp;gt; : V;
type TrimRight&amp;lt;V extends string&amp;gt; = V extends `${infer R} ` ? TrimRight&amp;lt;R&amp;gt; : V;

type Trim&amp;lt;V extends string&amp;gt; = TrimLeft&amp;lt;TrimRight&amp;lt;V&amp;gt;&amp;gt;;

// 测试用例
type Result = Trim&amp;lt;&apos; semlinker &apos;&amp;gt;
//=&amp;gt; &apos;semlinker&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目九&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;IsEqual&lt;/code&gt;工具类型，用于比较两个类型是否相等。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type IsEqual&amp;lt;A, B&amp;gt; = // 你的实现代码

// 测试用例
type E0 = IsEqual&amp;lt;1, 2&amp;gt;; // false
type E1 = IsEqual&amp;lt;{ a: 1 }, { a: 1 }&amp;gt; // true
type E2 = IsEqual&amp;lt;[1], []&amp;gt;; // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type E0 = IsEqual&amp;lt;1, 2&amp;gt;; // false
type E1 = IsEqual&amp;lt;{ a: 1 }, { a: 1 }&amp;gt; // true
type E2 = IsEqual&amp;lt;[1], []&amp;gt;; // false
type IsEqual&amp;lt;A, B&amp;gt; = [A] extends [B]? ([B] extends [A]? true:false ): false // 你的实现代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目十&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下两者都是相类似的类型推倒&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实现一个获取数组第一个位置的类型 &lt;code&gt;HEAD&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type H0 = Head&amp;lt;[]&amp;gt; // never
type H1 = Head&amp;lt;[1]&amp;gt; // 1
type H2 = Head&amp;lt;[3, 2]&amp;gt; // 3
type Head&amp;lt;T extends Array&amp;lt;any&amp;gt;&amp;gt; =  T extends [infer A, ...infer B] ? A :never// 你的实现代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现一个获取数组最后一个位置的类型&lt;code&gt;TAIL&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type T0 = Tail&amp;lt;[]&amp;gt; // []
type T1 = Tail&amp;lt;[1, 2]&amp;gt; // [2]
type T2 = Tail&amp;lt;[1, 2, 3, 4, 5]&amp;gt; // [2, 3, 4, 5]
type Tail&amp;lt;T extends Array&amp;lt;any&amp;gt;&amp;gt; = T extends [...any, infer A] ? A  : never  // 你的实现代码

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目十一&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;Unshift&lt;/code&gt;工具类型，用于把指定类型&lt;code&gt;E&lt;/code&gt;作为第一个元素添加到 T 数组类型中。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Unshift&amp;lt;T extends any[], E&amp;gt; =  // 你的实现代码

// 测试用例
type Arr0 = Unshift&amp;lt;[], 1&amp;gt;; // [1]
type Arr1 = Unshift&amp;lt;[1, 2, 3], 0&amp;gt;; // [0, 1, 2, 3]
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Unshift&amp;lt;T extends any[], E&amp;gt; = [E, ...T];

// 测试用例
type Arr0 = Unshift&amp;lt;[], never&amp;gt;; // [1]
type Arr1 = Unshift&amp;lt;[1, 2, 3], 0&amp;gt;; // [0, 1, 2, 3]
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建一个数组，第一项类型为&lt;code&gt;E&lt;/code&gt;，剩余使用&lt;code&gt;...T&lt;/code&gt;连接。&lt;/p&gt;
&lt;h2&gt;题目十二&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;Shift&lt;/code&gt;工具类型，用于移除&lt;code&gt;T&lt;/code&gt;数组类型中的第一个类型。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Shift&amp;lt;T extends any[]&amp;gt; = // 你的实现代码

// 测试用例
type S0 = Shift&amp;lt;[1, 2, 3]&amp;gt; 
type S1 = Shift&amp;lt;[string,number,boolean]&amp;gt; 

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type S0 = Shift&amp;lt;[1, 2, 3]&amp;gt; 
type S1 = Shift&amp;lt;[string,number,boolean]&amp;gt; 
type Shift&amp;lt;T extends any[]&amp;gt; = T extends [infer A, ...infer B] ? B : []  // 你的实现代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;...infer B&lt;/code&gt;去除第一项之后的集合，使用变量&lt;code&gt;B&lt;/code&gt;保存该类型。如果满足约束，返回剩余参数类型，也就是&lt;code&gt;B&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;题目十三&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;Push&lt;/code&gt;工具类型，用于把指定类型&lt;code&gt;E&lt;/code&gt;作为第最后一个元素添加到&lt;code&gt;T&lt;/code&gt;数组类型中。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Push&amp;lt;T extends any[], V&amp;gt; = // 你的实现代码

// 测试用例
type Arr0 = Push&amp;lt;[], 1&amp;gt; // [1]
type Arr1 = Push&amp;lt;[1, 2, 3], 4&amp;gt; // [1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type Arr0 = Push&amp;lt;[], 1&amp;gt; // [1]
type Arr1 = Push&amp;lt;[1, 2, 3], 4&amp;gt; // [1, 2, 3, 4]
type Push&amp;lt;T extends any[], E&amp;gt; = [...T, E]  // 你的实现代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目十四&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;Includes&lt;/code&gt;工具类型，用于判断指定的类型&lt;code&gt;E&lt;/code&gt;是否包含在&lt;code&gt;T&lt;/code&gt;数组类型中。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Includes&amp;lt;T extends Array&amp;lt;any&amp;gt;, E&amp;gt; = // 你的实现代码
type I0 = Includes&amp;lt;[], 1&amp;gt; // false
type I1 = Includes&amp;lt;[2, 2, 3, 1], 2&amp;gt; // true
type I2 = Includes&amp;lt;[2, 3, 3, 1], 1&amp;gt; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type I0 = Includes&amp;lt;[], 1&amp;gt; // false
type I1 = Includes&amp;lt;[2, 2, 3, 1], 2&amp;gt; // true
type I2 = Includes&amp;lt;[2, 3, 3, 1], 1&amp;gt; // true

type Includes&amp;lt;T extends Array&amp;lt;any&amp;gt;, E&amp;gt; =  E extends T[number] ? true :false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;T[number]&lt;/code&gt;可以理解返回&lt;code&gt;T&lt;/code&gt;数组元素的类型，比如传入的泛型&lt;code&gt;T&lt;/code&gt;为&lt;code&gt;[2, 2, 3, 1]&lt;/code&gt;，那么&lt;code&gt;T[number]&lt;/code&gt;被解析为：&lt;code&gt;2 | 2 | 3 | 1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;*题目十五&lt;/h2&gt;
&lt;p&gt;实现一个&lt;code&gt;UnionToIntersection&lt;/code&gt;工具类型，用于把联合类型转换为交叉类型。具体的使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type UnionToIntersection&amp;lt;U&amp;gt; = // 你的实现代码

// 测试用例
type U0 = UnionToIntersection&amp;lt;string | number&amp;gt; // never
type U1 = UnionToIntersection&amp;lt;{ name: string } | { age: number }&amp;gt; // { name: string; } &amp;amp; { age: number; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 测试用例
type U0 = UnionToIntersection&amp;lt;string | number&amp;gt; // never
type U1 = UnionToIntersection&amp;lt;{ name: string } | { age: number }&amp;gt; // { name: string; } &amp;amp; { age: number; }

type UnionToIntersection&amp;lt;U&amp;gt; = (
    U extends unknown ? (props: U)=&amp;gt;void : never
) extends (props2: infer P) =&amp;gt; void ? P : never
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、&lt;code&gt;extends unknown&lt;/code&gt;始终为true，默认进入到分发情况&lt;/p&gt;
&lt;p&gt;2、会声明一个以&lt;code&gt;U&lt;/code&gt;为入参类型的函数类型&lt;code&gt;A&lt;/code&gt;，即&lt;code&gt;(props: U) =&amp;gt; void&lt;/code&gt;，该函数约束于以&lt;code&gt;props2&lt;/code&gt;类型为入参的函数类型&lt;code&gt;B&lt;/code&gt;，即&lt;code&gt;(props2: infer P) =&amp;gt; void&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;3、如果函数&lt;code&gt;A&lt;/code&gt;能继承函数&lt;code&gt;B&lt;/code&gt;则 返回&lt;code&gt;infer P&lt;/code&gt;声明的&lt;code&gt;P&lt;/code&gt;，否则返回&lt;code&gt;never&lt;/code&gt;，再利用&lt;strong&gt;函数参数类型逆变&lt;/strong&gt;，从而实现得到的结果从联合类型到交叉类型的转变。&lt;/p&gt;
&lt;p&gt;这里是也设计到一个知识点：**分布式条件类型，**条件类型的特性：分布式条件类型。在结合联合类型使用时（&lt;strong&gt;只针对*&lt;em&gt;**extends**&lt;/em&gt;*左边的联合类型&lt;/strong&gt;），分布式条件类型会被自动分发成联合类型。例如，&lt;code&gt;T extends U ? X : Y&lt;/code&gt;，&lt;code&gt;T&lt;/code&gt;的类型为&lt;code&gt;A | B | C&lt;/code&gt;，会被解析为&lt;code&gt;(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;都知道&lt;code&gt;infer&lt;/code&gt;声明都是只能出现在&lt;code&gt;extends&lt;/code&gt;子语句中。但是，在协变的位置上，同一类型变量的多个候选类型会被推断为联合类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo&amp;lt;T&amp;gt; = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo&amp;lt;{ a: string, b: string }&amp;gt;;  // string
type T11 = Foo&amp;lt;{ a: string, b: number }&amp;gt;;  // string | number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在逆变的位置上，同一个类型多个候选类型会被推断为交叉类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Bar&amp;lt;T&amp;gt; = T extends { a: (x: infer U) =&amp;gt; void, b: (x: infer U) =&amp;gt; void } ? U : never;
type T20 = Bar&amp;lt;{ a: (x: string) =&amp;gt; void, b: (x: string) =&amp;gt; void }&amp;gt;;  // string
type T21 = Bar&amp;lt;{ a: (x: string) =&amp;gt; void, b: (x: number) =&amp;gt; void }&amp;gt;;  // string &amp;amp; number
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目十六（元组转化为对象）&lt;/h2&gt;
&lt;h1&gt;ts内置方法实现&lt;/h1&gt;
&lt;h2&gt;Exclude &lt;strong&gt;排除类型&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Dir=&apos;1&apos;|&apos;2&apos;|&apos;3&apos;|&apos;4&apos;

// type dir1 = &quot;3&quot; | &quot;4&quot;
type dir1=Exclude&amp;lt;Dir,&apos;1&apos;|&apos;2&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyExclude&amp;lt;T, R&amp;gt; = T extends R ? never :T
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pick &lt;strong&gt;获取 key 类型&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyPick&amp;lt;T, R extends keyof T&amp;gt; = {
    [Key in R]: T[Key]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Omit &lt;strong&gt;排除 key 类型&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyOmit&amp;lt;T, R extends keyof T&amp;gt; = MyPick&amp;lt;T, MyExclude&amp;lt;keyof T, R&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Readonly 属性只读&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyReadonly&amp;lt;T&amp;gt; = {
    readonly [Key in keyof T]: T[Key]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Required 属性必选&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyRequired&amp;lt;T&amp;gt; = {
    [Key in keyof T]-?:T[Key]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Partial 属性可选&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyPartial&amp;lt;T&amp;gt; = {
    [Key in keyof T]?: T[Key]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ReturnType 获取函数返回类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyReturnType&amp;lt;T extends (...args:any[])=&amp;gt;any&amp;gt; = T extends (...args:any[]) =&amp;gt; infer R ? R :never
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Parameters 获取函数参数类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyParameters&amp;lt;T extends (...args:any[])=&amp;gt;any&amp;gt; = T extends (...args:infer B) =&amp;gt; any? B:never
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;ts 实用范型工具&lt;/h1&gt;
&lt;h2&gt;OptionalRequired 部分属性必选&lt;/h2&gt;
&lt;h3&gt;实现一&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @Description: 部分属性必选
 */

export type OptionalRequired&amp;lt;T,R extends keyof T&amp;gt; = {
    [key in R]-?: T[key]
} &amp;amp; {
    [key in Exclude&amp;lt;keyof T, R&amp;gt;]: T[key]
}

type TestObj = {
    a?:string,
    b:number,
    c?:object
}

type OptionRequiredObjType = OptionalRequired&amp;lt;TestObj,&apos;a&apos; | &apos;c&apos;&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现二&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export type OptionRequired&amp;lt;T,R extends keyof T&amp;gt; = Required&amp;lt;T&amp;gt; &amp;amp; Omit&amp;lt;T, R&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OptionalReadonly 部分属性只读&lt;/h2&gt;
&lt;h3&gt;实现一&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @Description: 部分属性只读
 */
export type OptionalReadonly&amp;lt;T,R extends keyof T&amp;gt; = {
    readonly [key in R]: T[key]
} &amp;amp; {
    [key in Exclude&amp;lt;keyof T, R&amp;gt;]: T[key]
}

type TestObj = {
    a?:string,
    b:number,
    c?:object
}

type OptionReadonlyObjType = OptionalReadonly&amp;lt;TestObj,&apos;a&apos; | &apos;c&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现二&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export type OptionalReadonly&amp;lt;T,R extends keyof T&amp;gt; = Omit&amp;lt;T,R&amp;gt; &amp;amp; Readonly&amp;lt;Pick&amp;lt;T,R&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OptionalPartial 部分属性可选&lt;/h2&gt;
&lt;h3&gt;实现一&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @Description: 部分可选
 */

type OptionalPartial&amp;lt;T, R extends keyof T&amp;gt; = {
    [key in R]?: T[key]
} &amp;amp; {
    [key in Exclude&amp;lt;keyof T, R&amp;gt;]: T[key]
}

type TestObj = {
    a:string,
    b:number,
    c:object
}

type OptionPartialObjType = OptionalPartial&amp;lt;TestObj,&apos;a&apos; | &apos;c&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现二&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type OptionalPartial&amp;lt;T, R extends keyof T&amp;gt; =  Omit&amp;lt;T, R&amp;gt; &amp;amp; Partial&amp;lt;Pick&amp;lt;T,R&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>ts之协变和逆变</title><link>https://nollieleo.github.io/posts/ts%E4%B9%8B%E5%8D%8F%E5%8F%98%E5%92%8C%E9%80%86%E5%8F%98/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/ts%E4%B9%8B%E5%8D%8F%E5%8F%98%E5%92%8C%E9%80%86%E5%8F%98/</guid><description>协变和逆变  协变和逆变是编程理论中一个很重要的话题。用于表达父类子类在安全类型转换后的兼容性（或者说继承关系）。定义为：如果A，B代表两个类型；f()表示类型转换；A - B表示A是B的子类。  - 当f()是协变时：若 A - B，则f(A) - f(B) - 当f()是逆变时：若 A - B，...</description><pubDate>Sun, 05 Jun 2022 16:53:07 GMT</pubDate><content:encoded>&lt;h1&gt;协变和逆变&lt;/h1&gt;
&lt;p&gt;协变和逆变是编程理论中一个很重要的话题。用于表达父类子类在安全类型转换后的兼容性（或者说继承关系）。定义为：如果&lt;code&gt;A&lt;/code&gt;，&lt;code&gt;B&lt;/code&gt;代表两个类型；&lt;code&gt;f()&lt;/code&gt;表示类型转换；&lt;code&gt;A -&amp;gt; B&lt;/code&gt;表示&lt;code&gt;A&lt;/code&gt;是&lt;code&gt;B&lt;/code&gt;的子类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当&lt;code&gt;f()&lt;/code&gt;是协变时：若 &lt;code&gt;A -&amp;gt; B&lt;/code&gt;，则&lt;code&gt;f(A) -&amp;gt; f(B)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;f()&lt;/code&gt;是逆变时：若 &lt;code&gt;A -&amp;gt; B&lt;/code&gt;，则&lt;code&gt;f(B) -&amp;gt; f(A)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;f()&lt;/code&gt;是双变时：若 &lt;code&gt;A -&amp;gt; B&lt;/code&gt;，则以上均成立&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;f()&lt;/code&gt;是不变时：若 &lt;code&gt;A -&amp;gt; B&lt;/code&gt;，则以上均不成立，没有兼容关系&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
  move(){
    console.log(&quot;animal is moving&quot;);
  }
}

class Cat extends Animal {
  purr() {
    console.log(&quot;cat is purring&quot;);
  }
}

class WhiteCat extends Cat {
  showoffColor() {
    console.log(&quot;see my hair color&quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们有名为&lt;code&gt;Animial&lt;/code&gt;的父类，&lt;code&gt;Cat&lt;/code&gt;是&lt;code&gt;Animal&lt;/code&gt;的子类。&lt;code&gt;WhiteCat&lt;/code&gt;是&lt;code&gt;Cat&lt;/code&gt;的子类， 即&lt;code&gt;WhiteCat -&amp;gt; Cat -&amp;gt; Animal&lt;/code&gt;。根据父类兼容子类的原则可知：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let animal: Animal;
let cat: Cat;
let whiteCat: WhiteCat;

animal = cat;
animal = whiteCat;
cat = whiteCat;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;抛出问题&lt;/h2&gt;
&lt;p&gt;假如现在有一个函数，类型为&lt;code&gt;(param: Cat) =&amp;gt; Cat&lt;/code&gt;。那么它的兼容类型是什么呢？&lt;/p&gt;
&lt;p&gt;我们可以把这个问题分解成两个部分参数兼容性和返回值兼容性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(param: Cat) =&amp;gt; void&lt;/code&gt;的兼容类型是什么？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;() =&amp;gt; Cat&lt;/code&gt;的兼容类型是什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参数兼容性&lt;/h2&gt;
&lt;p&gt;我们假设&lt;code&gt;(param: Cat) =&amp;gt; void&lt;/code&gt;为&lt;code&gt;A&lt;/code&gt;，此时有以下两种函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;B&lt;/code&gt;: &lt;code&gt;(param: WhiteCat) =&amp;gt; void&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt;：&lt;code&gt;(param: Animal) =&amp;gt; void&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么&lt;code&gt;A&lt;/code&gt;兼容哪一个函数？&lt;/p&gt;
&lt;h3&gt;假设兼容B&lt;/h3&gt;
&lt;p&gt;那么此时 &lt;code&gt;A = B&lt;/code&gt;成立：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let A: (param: Cat) =&amp;gt; void;
const B = (param: WhiteCat) =&amp;gt; {
  param.move();
  param.purr();
  param.showoffColor();
};

A = B;
A(new Cat());

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数运行到&lt;code&gt;param.showoffColor()&lt;/code&gt;会报错。那么假设不成立。&lt;/p&gt;
&lt;h3&gt;假设兼容C&lt;/h3&gt;
&lt;p&gt;那么此时 &lt;code&gt;A = C&lt;/code&gt;成立：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let A: (param: Cat) =&amp;gt; void;
const C = (param: Animal) =&amp;gt; {
  param.move();
};

A = C;
A(new Cat());

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时函数成功运行。那么假设成立。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;(param: Animal) =&amp;gt; void -&amp;gt; (param: Cat) =&amp;gt; void&lt;/code&gt; 。根据前面的定义可以看出函数参数是逆变的。&lt;/p&gt;
&lt;h2&gt;返回值兼容性&lt;/h2&gt;
&lt;p&gt;我们假设&lt;code&gt;() =&amp;gt; Cat&lt;/code&gt;为&lt;code&gt;A&lt;/code&gt;，此时有以下两种函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;B&lt;/code&gt;: &lt;code&gt;() =&amp;gt; Animal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt;：&lt;code&gt;() =&amp;gt; WhiteCat&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么&lt;code&gt;A&lt;/code&gt;兼容哪一个函数？&lt;/p&gt;
&lt;h3&gt;假设兼容B&lt;/h3&gt;
&lt;p&gt;那么此时 &lt;code&gt;A = B&lt;/code&gt;成立：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let A: () =&amp;gt; Cat;
const B = () =&amp;gt; new Animal();

A = B;
const result = A();
result.move();
result.purr();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数运行到&lt;code&gt;result.purr()&lt;/code&gt;会报错。那么假设不成立。&lt;/p&gt;
&lt;h3&gt;假设兼容C&lt;/h3&gt;
&lt;p&gt;那么此时 &lt;code&gt;A = C&lt;/code&gt;成立：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let A: () =&amp;gt; Cat;
const C = () =&amp;gt; new WhiteCat();

A = C;
const result = A();
result.move();
result.purr();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时函数成功运行。那么假设成立。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;() =&amp;gt; WhiteCat -&amp;gt; () =&amp;gt; Cat&lt;/code&gt;。根据前面的定义可以看出函数返回值是协变的。&lt;/p&gt;
&lt;h2&gt;函数参数类型的现实&lt;/h2&gt;
&lt;p&gt;在ts中，参数类型是双变的，也就是说既是协变，也是逆变。这当然不安全。所以我们可以通过开启&lt;code&gt;strictFunctionTypes&lt;/code&gt;修复这个问题，保证参数类型是逆变。&lt;/p&gt;
&lt;p&gt;那么为什么ts会让函数参数类型保留双变转换呢？下面是一个十分常见的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }

function listenEvent(eventType: EventType, handler: (n: Event) =&amp;gt; void) {
    /* ... */
}

// 虽然不安全，且编译无法通过，但是十分常见的使用方式
listenEvent(EventType.Mouse, (e: MouseEvent) =&amp;gt; console.log(e.x, e.y));

// 为了保证编译通过，只能通过以下方式
listenEvent(EventType.Mouse, (e: Event) =&amp;gt; console.log((e as MouseEvent).x, (e as MouseEvent).y));
listenEvent(EventType.Mouse, ((e: MouseEvent) =&amp;gt; console.log(e.x, e.y)) as (e: Event) =&amp;gt; void);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而如果函数参数类型是双变，那么上面第一种形式的代码也能顺利通过编译，无需使用后两种绕路的方式。&lt;/p&gt;
</content:encoded></item><item><title>vue的响应式系统设计总结</title><link>https://nollieleo.github.io/posts/vue%E7%9A%84%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/vue%E7%9A%84%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</guid><description>vue3 - 响应式学习   前置知识   副作用函数  副作用函数就是指的会产生副作用的函数  ts function effect(){   document.body.innerHTML = &apos;hello world&apos; }   effect函数会直接或者间接的影响到其他地方函数的执行，也就是说...</description><pubDate>Sun, 08 May 2022 14:50:34 GMT</pubDate><content:encoded>&lt;h1&gt;vue3 - 响应式学习&lt;/h1&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;h3&gt;副作用函数&lt;/h3&gt;
&lt;p&gt;副作用函数就是指的会产生副作用的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function effect(){
  document.body.innerHTML = &apos;hello world&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;effect函数会直接或者间接的影响到其他地方函数的执行，也就是说，比如修改一个变量，这个变量其他地方也读取到了&lt;/p&gt;
&lt;h3&gt;响应式数据&lt;/h3&gt;
&lt;p&gt;假如，有一个引用类型变量, 有一个函数读取到他的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = { a: &apos;wengkaimin&apos; };

function read(){
  document.body.innerText = obj.a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们修改了obj.a的值的时候，希望&lt;code&gt;read&lt;/code&gt;这个函数重新执行；如果&lt;code&gt;read&lt;/code&gt;函数能够因为obj.a的值变化，重新自动的执行，那么我们说obj是一个响应数据&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h2&gt;实现响应式数据&lt;/h2&gt;
&lt;p&gt;有一个data数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从两个点出发去实现：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;副作用函数执行的时候，会触发读取 data.a的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当修改了data.a的值的时候，会触发data.a的设置操作&lt;/p&gt;
&lt;p&gt;不难联想到es5的&lt;code&gt;Object.defineProperty&lt;/code&gt;和es6的&lt;code&gt;Proxy&lt;/code&gt;代理，都是用于数据的拦截操作，我们只要做好读和取的拦截操作，就能实现数据的响应&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;基本实现&lt;/h3&gt;
&lt;h4&gt;实现读取 拦截&lt;/h4&gt;
&lt;p&gt;当我们读取data.a的时候，通过拦截将副作用函数加入到一个 “桶” 的数据结构中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220807160051989.png&quot; alt=&quot;image-20220807160051989.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接着我们设置data.a的时候把副作用函数取出来&lt;/p&gt;
&lt;p&gt;接下来执行对data的代理, obj就是代理的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const buckect = new Set(); // 桶

const data = {
  a: 1,
};

const obj = new Proxy(data, {
  get(target, key) {
    buckect.add(effect);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    // 取出副作用函数effect
    buckect.forEach((fn) =&amp;gt; fn());
    return true;
  },
});

const effect = () =&amp;gt; {
  document.body.innerText = obj.a;
};

effect();

setTimeout(() =&amp;gt; {
  obj.a = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;Step1: 完善系统&lt;/h2&gt;
&lt;p&gt;上面的基本实现存在很多缺陷&lt;/p&gt;
&lt;h3&gt;硬编码了副作用函数名称 &lt;code&gt;effect&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;要想副作用函数被正确的收集到 bucket当中，就不能使用硬编码的函数名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，这里提供一个函数的注册机制，用于注册副作用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let activeEffect;

// 注册函数
function effect(fn){
    activeEffect = fn;
    fn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用effect包裹 读去了obj的那个管他什么函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
 document.body.innerText = obj.a
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后需要在&lt;strong&gt;读&lt;/strong&gt;的拦截器中做判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
    get(target, key){
        if(activeEffect){ // ➕
            buckect.add(activeEffect); 
        }
        return target[key]
    },
    set(target, key, newVal){
        target[key] = newVal;
        // 取出副作用函数effect
        buckect.forEach(fn =&amp;gt; fn());
        return true
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;属性和副作用函数关系不明确&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;上面的桶数据结构是一个set，这肯定是不符合情理的，因为一个对象可能存在多个属性，不同的属性之间对应的副作用函数也有可能不一样&lt;/p&gt;
&lt;p&gt;这样就会导致，无论读取什么值（是否存在）都会去触发副作用函数的重新执行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const buckect = new Set(); // 桶
let activeEffect;

const data = {
    a: 1,
    b: 2,
};

const obj = new Proxy(data, {
    get(target, key) {
    if (activeEffect) {
        // new
        buckect.add(activeEffect);
    }
    return target[key];
    },
    set(target, key, newVal) {
    target[key] = newVal;
    // 取出副作用函数effect
    buckect.forEach((fn) =&amp;gt; fn());
    return true;
    },
});

// 注册函数
function effect(fn) {
    activeEffect = fn;
    fn();
}

function setDoc() {
    document.body.innerText = obj.a;
    console.log(&apos;触发了&apos;)// obj.b的修改也会引起effect重新
}

effect(setDoc);

setTimeout(() =&amp;gt; {
    obj.a = 2;
    obj.b = 3; // ➕
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;因此需要重新的设计桶结构，就不能&lt;strong&gt;简单&lt;/strong&gt;的用Set了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多个属性，多个响应式数据，要有一一的对应关系&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220809141823945.png&quot; alt=&quot;image-20220809141823945.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;target：表示不同的需要代理的引用类型数据（当然会存在很多不同的引用数据啊）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;key：表示当前的target里头不同的key（不同的key，触发的副作用函数们也不一样，这里要做区分）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;target存在不同的key，所以不同的key也有不同的副作用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;function someFn(){
    console.log(obj.a, obj.b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;effectFn：表示target下面，这个key所对应的&lt;strong&gt;所有副作用函数&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为存在，例如&lt;code&gt;obj.a&lt;/code&gt; 被A和B或者C等等其他不同引用地址的函数读取的情况&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;function someFn(){
    console.log(obj.a)
}

function someFn2(){
    console.log(obj.a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上&lt;/p&gt;
&lt;p&gt;这个bucket数据结构可以为&lt;/p&gt;
&lt;p&gt;bucket -- &amp;gt; WeakMap （weakMap由target的引用组成）&lt;/p&gt;
&lt;p&gt;target --- &amp;gt; Map （map由key组成）&lt;/p&gt;
&lt;p&gt;key --- &amp;gt; Set （Set存储了当前key对应的副作用函数）&lt;/p&gt;
&lt;h4&gt;修改get拦截器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;get(target, key){
    // 如果没有 activeEffect说明没有注册过这个副作用函数，直接return
    if(!activeEffect) return; // ➕
    // 根据targt代理引用，取得它对应的map，对应关系target --- &amp;gt; Map
    const depsMap = buckect.get(target); // ➕
    // 假如说这个map不存在，那么就new一个map，与target引用关联
    if(!depsMap) { // ➕
        buckect.set(target, (depsMap = new Map())) // ➕
    }
    // 再根据key从map中取得key对应的Set，key ---&amp;gt; Set
    // Set里头存储了这个key对应的副作用函数
    const deps = depsMap.get(key); // ➕
    if(!deps){ // ➕
        depsMap.set(key, (deps = new Set())) // ➕ 
    }
    // 最后将activeEffect加入到key对应的Set当中
    deps.add(activeEffect);// ➕

    // 返回函数属性
    return target[key]
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;修改set拦截&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; set(target, key, newVal){
    target[key] = newVal;

    // 根据target引用从桶中取得对应的Map
    const depsMap = buckect.get(target); // ➕
    if(!depsMap) return; // ➕
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);// ➕
    if(effects){
        effects.forEach(fn =&amp;gt; fn())
    }
    return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以避免修改其他key值带来的不必要的副作用执行，并且明确了代理目标和副作用函数之间的关系&lt;/p&gt;
&lt;h4&gt;优化&lt;/h4&gt;
&lt;p&gt;这一步骤提取一下get和set的里头的一些逻辑，作为封装函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;track&lt;/code&gt;：表示追踪的含义，追踪副作用函数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;trigger&lt;/code&gt;：是表示触发副作用的函数&lt;/p&gt;
&lt;p&gt;完整代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let activeEffect;

// 注册函数
function effect(fn){
    activeEffect = fn;
    fn();
}

function track(target,key){
    // 如果没有 activeEffect说明没有注册过这个副作用函数，直接return
    if(!activeEffect) return;
    // 根据targt代理引用，取得它对应的map，对应关系target --- &amp;gt; Map
    let depsMap = buckect.get(target);
    // 假如说这个map不存在，那么就new一个map，与target引用关联
    if(!depsMap) {
        buckect.set(target, (depsMap = new Map()))
    }
    // 再根据key从map中取得key对应的Set，key ---&amp;gt; Set
    // Set里头存储了这个key对应的副作用函数
    let deps = depsMap.get(key);
    if(!deps){
        depsMap.set(key, (deps = new Set()))
    }
    // 最后将activeEffect加入到key对应的Set当中
    deps.add(activeEffect);
}

function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = buckect.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);
    if(effects){
        effects.forEach(fn =&amp;gt; fn())
    }
}


const obj = new Proxy(data, {
    get(target, key){
        track(target,key)
        // 返回函数属性
        return target[key]
    },
    set(target, key, newVal){
        target[key] = newVal;
        trigger(target, key)
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为何用WeakMap：因为target的数据源是不确定的，存在数据量非常大的情况，假如用Map作为数据结构，在IIFE当中其实是很可能存在内存溢出的现象；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step2: 加强系统健壮性&lt;/h2&gt;
&lt;h3&gt;考虑切换分支情况（clean up）&lt;/h3&gt;
&lt;p&gt;在开发过程经常会有条件判断的情况，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: true, b: &apos;weng&apos; };
const obj = new Proxy(data, {....});

effect(function someFn(){
  console.log(obj.a ? obj.b : &apos;yes!&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当&lt;strong&gt;someFn&lt;/strong&gt;触发的时候，会读取obj.a和obj.b，但是obj.b只有在obj.a为true的情况下，才会打印出来，这时候someFn已经被收集到了a和b各自对应的Set当中了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220811141233280.png&quot; alt=&quot;image-20220811141233280.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假如改变obj.a 为false，会触发&lt;code&gt;someFn&lt;/code&gt;副作用重新 执行，但是由于obj.a已经是false了，所以就不再读取到obj.b了；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.a = false
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是由于第一次读取了b，已经将b的副作用函数someFn收集到了对于的key -- &amp;gt; Set当中了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这时候如果去改变obj.b的值，其实还是会触发someFn&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
a: true,
b: &quot;weng&quot;,
};

const obj = new Proxy(data, {
get(target, key) {
    track(target, key);
    // 返回函数属性
    return target[key];
},

set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
},
});

function someFn() {
console.log(obj.a ? obj.b : &quot;yes!&quot;);
console.log(&apos;触发了&apos;, bucket);
}

effect(someFn);

obj.a = false;

setTimeout(() =&amp;gt; {
obj.b = &apos;kaimin&apos;; // 仍然会触发someFn
}, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;到这里，目前这套系统还没有一种能够清理这种 &lt;strong&gt;分支切换&lt;/strong&gt; 的情况的能力&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决这种问题的思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;副作用函数执行的时候，第一步，先把它从与之关联的 key---&amp;gt;Set中删除&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220813145103685.png&quot; alt=&quot;image-20220813145103685.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;副作用函数执行完，去重新建立联系，但是在新的联系中，就不会再包含其他遗留的副作用函数了&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;改造effect - 1&lt;/h4&gt;
&lt;p&gt;要想知道这个副作用函数被哪些对象的哪些key -- &amp;gt; Set收集了，必须要知道如何去&lt;strong&gt;收集&lt;/strong&gt;以及&lt;strong&gt;标记&lt;/strong&gt;下这些 key ---&amp;gt; Set&lt;/p&gt;
&lt;p&gt;所以，我们可以先改造一下副作用函数的注册函数effect，&lt;strong&gt;目的在于收集&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册函数
function effect(fn){
    const effectFn = ()=&amp;gt;{
        activeEffect = effectFn;
        fn();
    }
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = []; // deps: Array&amp;lt;Set&amp;gt;
    effectFn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;deps就是我们所说的，用于收集 用到这个副作用函数的集合的地方，存储的将是Set的引用&lt;/p&gt;
&lt;h4&gt;改造track&lt;/h4&gt;
&lt;p&gt;有了 收集 Set的集合deps，收集的时机也很重要；&lt;/p&gt;
&lt;p&gt;看之前的track函数，是作用在get拦截器当中的，将 当前激活的副作用函数 存入Set当中，如果没有key对应的Set，则会new一个，所以在这里，收集deps的时机是最好的&lt;/p&gt;
&lt;p&gt;因此我们改造track函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function track(target,key){
    // 如果没有 activeEffect说明没有注册过这个副作用函数，直接return
    if(!activeEffect) return;
    // 根据targt代理引用，取得它对应的map，对应关系target --- &amp;gt; Map
    let depsMap = buckect.get(target);
    // 假如说这个map不存在，那么就new一个map，与target引用关联
    if(!depsMap) {
        buckect.set(target, (depsMap = new Map()))
    }
    // 再根据key从map中取得key对应的Set，key ---&amp;gt; Set
    // Set里头存储了这个key对应的副作用函数
    let deps = depsMap.get(key);
    if(!deps){
        depsMap.set(key, (deps = new Set()))
    }
    // 最后将activeEffect加入到key对应的Set当中
    deps.add(activeEffect);
    // 将 集合 Set 的引用推入 副作用函数的deps集合里头 ➕
    activeEffect.caller.deps.push(deps); // ➕
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;改造effect - 2，实现cleanup清除函数&lt;/h4&gt;
&lt;p&gt;上面提到，要在副作用函数&lt;strong&gt;执行之前&lt;/strong&gt;，将事先收集到的deps中的所有对应的Set中someFn删掉&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function cleanup(effectFn){
    for (let i = 0; i &amp;lt; effectFn.caller.deps.length; i++) {
        const deps = effectFn.deps[i];
        // 将 effectFn 从依赖集合从移除
        deps.delete(effectFn)
    }
    // 最后重置一下 deps
    effectFn.deps.length = 0
}

// 注册函数
function effect(fn){
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn)
        activeEffect = effectFn;
        fn();
    }
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    effectFn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;改造trigger函数&lt;/h4&gt;
&lt;p&gt;因为在副作用执行之前，我们调用cleanup清除包含副作用函数的所有Set，但是执行之后又收集了进去。问题就在这里，我们在trigger的时候，去遍历这个Set执行fn（使用的foreach）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;foreach在遍历Set的时候，如果，一个值虽然被访问了，但是在遍历过程中被删除了，又被重新的添加到了集合当中，如果遍历过程还没有结束的话，就会重新被访问，一直在遍历，所以需要改造一下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = bucket.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);
    if(effects){
        effects.forEach(fn =&amp;gt; fn()); // 问题所在， 遍历的时候在执行副作用，执行的时候又被收集进去到同一个Set当中了，所以我们可以使这个遍历的effects Set更换为另外一个引用
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = bucket.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);
    if(effects){
        const effectsClone = new Set(effects)  // ➕
        effectsClone.forEach(fn =&amp;gt; fn()); // ➕
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，重新的创建一个Set的集合，就可以避免无限递归的情况了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      const data = {
        a: true,
        b: &quot;weng&quot;,
      };

      const obj = new Proxy(data, {
        get(target, key) {
          track(target, key);
          // 返回函数属性
          return target[key];
        },

        set(target, key, newVal) {
          target[key] = newVal;
          trigger(target, key);
        },
      });

      function someFn() {
        console.log(obj.a ? obj.b : &quot;yes!&quot;);
        console.log(&quot;触发了&quot;, bucket);
      }

      effect(someFn);

      obj.a = false;

      setTimeout(() =&amp;gt; {
        obj.b = &quot;kaimin&quot;; // 修改b值的时候，不会再触发someFn了
      }, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;考虑嵌套执行的情况&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在MVVM或者MVC的框架中，都会存在，多层组建嵌套的情况&lt;/p&gt;
&lt;p&gt;在vue中的每一个组建，其实都是存在一个render函数的。&lt;/p&gt;
&lt;p&gt;多层组建之间的嵌套，在vue中又涉及到了响应式的数据，因此可以当作每一个组建如果使用到了响应式的数据，每一个render的函数其实都是可以看成是需要在数据变更时候重新执行的副作用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;组建之间发生了嵌套，可以看成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 组建B
const B = {
  render(){
    return {.....}
  }
}
// 组建A
const A = {
  render(){
    return &amp;lt;B /&amp;gt;
  }
}
// 由于A中嵌套了B，所以可以理解为
effect(()=&amp;gt;{
  A.render();
  effect(()=&amp;gt;{
    B.render();
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种类似的嵌套情况是非常常见的；但是我们目前的系统是不支持嵌套的&lt;/p&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a:true, b:false };

const obj = new Proxy(data, {....});

let temp1, temp2;

function innerFn() {
    console.log(&quot;run effect inner&quot;);
    temp2 = obj.b;
}

function outerFn() {
    console.log(&quot;run effect outer&quot;);
    effect(innerFn);
    temp1 = obj.a;
}

effect(outerFn);

setTimeout(() =&amp;gt; {
    obj.a = 3;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常理上我们，理想我们需要这种收集的情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220818170841267.png&quot; alt=&quot;image-20220818170841267.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;初始化执行结果是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;run effect outer
run effect inner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候，我们去改动obj.a的值&lt;/p&gt;
&lt;p&gt;输出的结果为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;run effect inner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现外层的effect函数并没有执行，而只是执行了内层的&lt;/p&gt;
&lt;p&gt;这是由于，我们的effect副作用的包裹函数的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let activeEffect;

// 注册函数
function effect(fn){
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn)
        activeEffect = effectFn;
        fn();
    }
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    effectFn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次执行副作用函数的时候之前，会将副作用函数引用赋值给全局遍历activeEffect，而且activeEffect在同一时刻有且仅有一个。 由于发生了嵌套，第二次执行的副作用函数直接覆盖了上一层赋值的activeEffect&lt;/p&gt;
&lt;p&gt;在track函数中，最终收集到的是内层的innerEffect副作用&lt;/p&gt;
&lt;h4&gt;使用副作用函数栈&lt;/h4&gt;
&lt;p&gt;因为嵌套存在多层的情况，所以，考虑用一个副作用的函数栈 进行存储之前的副作用函数，保证不会丢失上一层的副作用函数&lt;/p&gt;
&lt;p&gt;每次的执行之前，将副作用函数压入这个栈当中，等副作用函数执行完之后，再将当前执行完的这个副作用函数从栈中弹出，&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并始终将activeEffect这个全局（正被激活的副作用函数）指向这个栈顶&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 全局加入effectStack栈结构
let effectStack=[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述例子的流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220820144311868.png&quot; alt=&quot;image-20220820144311868&quot; /&gt;&lt;/p&gt;
&lt;p&gt;改造effect函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册函数
function effect(fn){
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn)
        // 当调用effect注册副作用函数时，将副作用函数复制给activeEffect
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn) // ➕
        fn();
        effectStack.pop(); // ➕
        // 执行完之后，弹出栈，并把activeEffect还原为之前的值, 指向栈顶
        activeEffect = effectStack[effectStack.length - 1] // ➕
    }
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    effectFn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改之后，我们会发现，修改了a的属性之后，会正常打印两次数据&lt;/p&gt;
&lt;h3&gt;考虑自增（自减）值的情况&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;自增就是例如obj.a++这样的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这种情况，等价于 obj.a = obj.a + 1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
   console.log((obj.a = obj.a + 1));
}

effect(someFn);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况就是既读取了obj.a的值，又设置了obj.a的值，会直接爆栈，&lt;strong&gt;原因如下&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化的时候，我们执行副作用函数（&lt;strong&gt;这里我们叫做A，表示初始化的时候执行的&lt;/strong&gt;），首先读取了obj.a的值，触发了track函数，会将当前的副作用函数收集到桶中。&lt;/li&gt;
&lt;li&gt;接着将其 + 1之后 重新设置obj.a的值的时候，被setter拦截，这时候触发了trigger函数，把副作用函数（&lt;strong&gt;这里叫做B，表示赋值造成的第二次执行&lt;/strong&gt;）从桶中取出执行&lt;/li&gt;
&lt;li&gt;但是这个时候初始化的那次副作用函数（A）还在执行，赋值操作只会再次触发被收集的副作用函数（产生了B），因此会无限的递归赋值操作到爆栈&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;增加守卫&lt;/h4&gt;
&lt;p&gt;其实，这种情况很普遍，不单单是自增的情况，也有可能副作用函数读取obj.a之后，经过一番处理进行了重新赋值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function someFn(){
  const b = obj.a + ....;
     obj.a = b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;造成上述的递归，是因为不断的调用自身的副作用函数造成的&lt;/p&gt;
&lt;p&gt;所以我们在第三步骤执行的时候，加一个守卫，如果&lt;strong&gt;当前正在执行的副作用函数和trigger触发的副作用函数相同&lt;/strong&gt;，则不执行&lt;/p&gt;
&lt;p&gt;改造trigger函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = buckect.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);

    const effectsToRun = new Set();

    if(effects){
        effects.forEach((current)=&amp;gt;{
            if(current !== activeEffect){
                // 如果当前trigger执行的，和activeEffect不一样，加入
                effectsToRun.add(current)
            }
        })
    }
    effectsToRun.forEach((fn)=&amp;gt;fn())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造之后，就可以正常的进行副作用函数的收集和执行了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
   console.log((obj.a = obj.a + 1)); // 不会爆栈了
}

effect(someFn);


setTimeout(()=&amp;gt;{
    obj.a = 3; // 正常触发收集到的副作用函数
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;考虑调度实现&lt;/h3&gt;
&lt;p&gt;响应式系统比较重要的就是，一个可调度性，可调度性可以将控制权力交接给用户。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可调度性：有能力 决定副作用函数&lt;strong&gt;执行时机&lt;/strong&gt;，执行次数，以及&lt;strong&gt;执行方式&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如修改了obj.a的值，执行顺序是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function someFn(){
  console.log(obj.a)
}

effect(someFn)


obj.a++;

console.log(&apos;我是分割&apos;);


// 1
// 2
// 我是分割
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如我想调整打印的顺序，需要在某些条件延迟去执行这个副作用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1
// 我是分割
// 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以这样设计&lt;/p&gt;
&lt;p&gt;给effect函数加上调度的参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(effectFn, options)

options = {
  scheduler(effectFn){
    ....
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，可以想到，我们在trigger函数中进行副作用函数执行的时候，如果有调度器，我们就去执行用户传递进来的调度器，将副作用函数的执行权利抛给外界控制&lt;/p&gt;
&lt;h4&gt;effect加入调度函数参数&lt;/h4&gt;
&lt;p&gt;加入options，挂载到effectFn上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册函数
function effect(fn, options){ // ➕
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn)
        // 当调用effect注册副作用函数时，将副作用函数复制给activeEffect
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn)
        fn();
        // 执行完之后，弹出栈，并把activeEffect还原为之前的值, 指向栈顶
        activeEffect = effectStack[effectStack.length - 1] // ➕
    }
    // 将调度相关的参数加入到副作用函数当中
    effectFn.options = options; // ➕
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    effectFn();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;trigger触发调度&lt;/h4&gt;
&lt;p&gt;遍历 执行&lt;strong&gt;副作用函数&lt;/strong&gt;的时候，去判定是否具有调度器，有调度器就执行调度器，并将副作用函数&lt;strong&gt;执行权力交给用户&lt;/strong&gt;，否则就默认执行副作用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = buckect.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);

    const effectsToRun = new Set();

    if(effects){
        effects.forEach((current)=&amp;gt;{
            if(current !== activeEffect){
                // 如果当前trigger执行的，和activeEffect不一样，加入
                effectsToRun.add(current)
            }
        })
    }
    effectsToRun.forEach((fn)=&amp;gt;{
        if(fn.options.scheduler){ // ➕
            fn.options.scheduler(fn) // ➕
        }else{
            fn()
        }
    })    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述的例子就可以这样实现了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
    console.log(obj.a);
}

effect(someFn, {
    scheduler(fn) {
        setTimeout(() =&amp;gt; {
            fn();
        }, 0);
    },
});

obj.a++;

console.log(&quot;我是分割&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;考虑防抖情况&lt;/h3&gt;
&lt;p&gt;在vue3中，我们多次在一次执行中，例如对obj.a做了3次赋值操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };

const obj = new Proxy(data, {...});

function someFn(){
    console.log(obj.a)
}

effect(someFn)

obj.a++;
obj.a++;
obj.a++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3次的自增操作，vue3中只会打印2次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1  // 第一次是初始化的时候打印的
4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们的系统会打印4次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
2
3
4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实我们关注的只是最后的结果，所以第2 3次的打印数据我们并不关心；&lt;/p&gt;
&lt;p&gt;因此我们可以基于调度器，接入防抖系统&lt;/p&gt;
&lt;h4&gt;微任务队列实现防抖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 定义任务队列
const jobQueue = new Set();
// 使用promise.resolve创建微任务，添加到微任务队列
const p = Promise.resolve();

// 一个标志代表是否正在刷新队列
let isFlushing = false;
function flushJob(){
    // 如果队列正在刷新，则什么都不做
    if(isFlushing) return;
    // 设置true, 代码正在刷新
    isFlushing = true;
    // 在微任务队列中刷新 jobQueue 队列
    p.then(()=&amp;gt;{
        jobQueue.forEach((fn =&amp;gt; fn()))
    }).finally(()=&amp;gt;{
        // 结束后重置isFlushing
        isFlushing = false;
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;effect(someFn, {
    scheduler(fn){
        // 每次调度时，将副作用函数添加到jobQueue队列中
        jobQueue.add(fn);

        // 调用 flushJob 刷新队列
        flushJob();
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;定一个任务队列jobQueue ---&amp;gt; Set，目的是利用Set数据结构的自动去重能力。&lt;/li&gt;
&lt;li&gt;scheduler在每次调用一个副作用函数的时候，将 副作用函数加入jobQueue，然后再调用flushJob刷新队列&lt;/li&gt;
&lt;li&gt;flushJob函数开始执行的时候，通过isFlushing字段进行限制，只有false的时候才执行；在进行的时候就是true&lt;/li&gt;
&lt;li&gt;这样可以知道，无论我们走了多少次的flushJob，每个微任务的周期内都会执行一次；之后在微任务队列里头完成对jobQueue的遍历&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;多次修改响应式的值，其实在vue中也是类似这样的实现，只是比这个更完善&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如上的例子就可以实现防抖的效果了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };

const obj = new Proxy(data, {...});

function someFn(){
    console.log(obj.a)
}

effect(someFn, {
    scheduler(fn) {
          jobQueue.add(fn);
          flushJob();
    },
})

obj.a++;
obj.a++;
obj.a++;


// 最终只会打印1 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结下来的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let activeEffect;

const effectStack = [];

const buckect = new WeakMap();


const source = { 
    a: &apos;wengkaimin&apos;,
    b: &apos;weng&apos;
}


// 定义任务队列
const jobQueue = new Set();
// 使用promise.resolve创建微任务，添加到微任务队列
const p = Promise.resolve();

// 一个标志代表是否正在刷新队列
let isFlushing = false;
function flushJob(){
    // 如果队列正在刷新，则什么都不做
    if(isFlushing) return;
    // 设置true, 代码正在刷新
    isFlushing = true;
    // 在微任务队列中刷新 jobQueue 队列
    p.then(()=&amp;gt;{
        jobQueue.forEach((fn =&amp;gt; fn()))
    }).finally(()=&amp;gt;{
        // 结束后重置isFlushing
        isFlushing = false;
    })
}

function cleanup(effectFn){
    for (let i = 0; i &amp;lt; effectFn.deps.length; i++) {
        const deps = effectFn.deps[i];
        // 将 effectFn 从依赖集合从移除
        deps.delete(effectFn)
    }
    // 最后重置一下 deps
    effectFn.deps.length = 0
}

// 注册函数
function effect(fn, options){ // ➕
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn);
        // 当调用effect注册副作用函数时，将副作用函数复制给activeEffect
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn);
        fn();
        // 执行完之后，弹出栈，并把activeEffect还原为之前的值, 指向栈顶
        activeEffect = effectStack[effectStack.length - 1];  // ➕
    }
    // 将调度相关的参数加入到副作用函数当中
    effectFn.options = options; // ➕
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    effectFn();
}


function someFn(){
    console.log(source.a)
}

function someFn2(){
    console.log(source.a)
}

effect(someFn, {
    scheduler(fn){
        // 每次调度时，将副作用函数添加到jobQueue队列中
        jobQueue.add(fn);

        // 调用 flushJob 刷新队列
        flushJob();
    }
})


function track(target,key){
    // 如果没有 activeEffect说明没有注册过这个副作用函数，直接return
    if(!activeEffect) return;
    // 根据targt代理引用，取得它对应的map，对应关系target --- &amp;gt; Map
    let depsMap = buckect.get(target);
    // 假如说这个map不存在，那么就new一个map，与target引用关联
    if(!depsMap) {
        buckect.set(target, (depsMap = new Map()))
    }
    // 再根据key从map中取得key对应的Set，key ---&amp;gt; Set
    // Set里头存储了这个key对应的副作用函数
    let deps = depsMap.get(key);
    if(!deps){
        depsMap.set(key, (deps = new Set()))
    }
    // 最后将activeEffect加入到key对应的Set当中
    deps.add(activeEffect);
    // 将 集合 Set 的引用推入 副作用函数的deps集合里头
    activeEffect.deps.push(deps);
}

function trigger(target, key){
    // 根据target引用从桶中取得对应的Map
    const depsMap = buckect.get(target);
    if(!depsMap) return;
    // 取得target 上key对应的Set数据，遍历，执行里头的副作用
    const effects = depsMap.get(key);

    const effectsToRun = new Set();

    if(effects){
        effects.forEach((current)=&amp;gt;{
            if(current !== activeEffect){
                // 如果当前trigger执行的，和activeEffect不一样，加入
                effectsToRun.add(current)
            }
        })
    }
    effectsToRun.forEach((fn)=&amp;gt;{
        if(fn.options.scheduler){ // ➕调度器
            fn.options.scheduler(fn) // ➕调度器
        }else{
            fn()
        }
    })    
}

const obj = new Proxy(data, {
    get(target, key){
        track(target,key)
        // 返回函数属性
        return target[key]
    },
    set(target, key, newVal){
        target[key] = newVal;
        trigger(target, key)
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step3: 实现lazy+computed&lt;/h2&gt;
&lt;p&gt;综上所说的，副作用函数，其实在我们目前的系统都是会立即执行的，但是其实有些情况下我们是不需要去做立刻执行我们的副作用函数的&lt;/p&gt;
&lt;p&gt;所以结合上面的options副作用函数的参数，其实能够和调度器一样去实现我们的lazy的能力。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
  console.log(obj.a)
},{
  scheduler(){....},
  lazy:true // ➕
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现lazy效果 - 改造effect函数&lt;/h3&gt;
&lt;p&gt;当我们判断lazy字段为true的时候，我们就不立即执行effect函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册函数
function effect(fn, options){
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn);
        // 当调用effect注册副作用函数时，将副作用函数复制给activeEffect
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn);
        fn();
        effectStack.pop();
        // 执行完之后，弹出栈，并把activeEffect还原为之前的值, 指向栈顶
        activeEffect = effectStack[effectStack.length - 1];  
    }
    // 将调度相关的参数加入到副作用函数当中
    effectFn.options = options;
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    // ➕ 如果options中 lazy为false，才立即执行副作用函数
    if(!options.lazy){
        effectFn();
    }
    return effectFn // ➕将副作用函数作为返回值返回出去
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是其实，作为返回值，返回副作用函数其实意义不大的，因为你还得手动执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const effectFn = effect(someFn, {
    lazy:true // ➕
})
effectFn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们引出computed&lt;/p&gt;
&lt;h3&gt;实现computed&lt;/h3&gt;
&lt;p&gt;但是假如说，我们的someFn，也就是副作用函数是一个 有返回值的 函数，类似 getter，那么我们就有意思了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function someFn(){
  return obj.a + 1;
}

const effectFn = effect(someFn, {
    lazy:true // ➕
})

const value = effectFn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们可以去除手动执行的这个逻辑，执行的时候，将执行结果返回。&lt;/p&gt;
&lt;p&gt;去实现一个computed的工厂函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function computed(getter){
  // 把getter 作为副作用函数，创建一个lazy的effect
  const effectFn = effect(getter, {
    lazy: true
  })

  const obj = {
    get value(){
      return effectFn() // 手动执行
    }
  }

  return obj 
}

const testA = computed(()=&amp;gt;{
  return obj.a + 1
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;computed函数接受一个函数，返回值是一个对象，包含一个value属性。getter函数作为参数，然后getter其实就是副作用函数，用他创建一个lazy的effectFn，然后我们定一个obj，在访问obj的时候去执行getter。&lt;/p&gt;
&lt;h4&gt;改造effect函数&lt;/h4&gt;
&lt;p&gt;&lt;a&gt;我们&lt;/a&gt;在&lt;strong&gt;effectFn&lt;/strong&gt;函数调用的时候，将执行结果返回出去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册函数
function effect(fn, options){
    const effectFn = ()=&amp;gt;{
        cleanup(effectFn);
        // 当调用effect注册副作用函数时，将副作用函数复制给activeEffect
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn);
        const res = fn(); // ➕
        effectStack.pop();

        // 执行完之后，弹出栈，并把activeEffect还原为之前的值, 指向栈顶
        activeEffect = effectStack[effectStack.length - 1];  
        return res; // ➕ 将函数执行结果返回出去，实现getter的效果
    }
    // 将调度相关的参数加入到副作用函数当中
    effectFn.options = options; 
    // 用于存储所有与该副作用函数相关的依赖合集 bucket桶中 key---&amp;gt; Set 的 Set集合
    effectFn.deps = [];
    // 如果options中 lazy为false，才立即执行副作用函数
    if(!options.lazy){
        effectFn();
    }
    return effectFn // 将副作用函数作为返回值返回出去
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结下来，就是lazy配合getter实现一套computed函数&lt;/p&gt;
&lt;p&gt;试验一下，就可以实现computed的效果了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
    console.log(&quot;执行了getter&quot;);
    return obj.a + 1;
}

const computedData = computed(someFn);

console.log(computedData.value);

setTimeout(() =&amp;gt; {
    obj.a = 3; // 在改变值得的时候也会做到同步的重新计算
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现缓存 优化&lt;/h3&gt;
&lt;p&gt;如上的例子，假如我们读取了两次computedData&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
    console.log(&quot;执行了getter&quot;);
    return obj.a + 1;
}

const computedData = computed(someFn);

console.log(computedData.value);

setTimeout(() =&amp;gt; {
    console.log(computedData.value);
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会发现，执行了两次的getter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;执行了getter
2
执行了getter
2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们并没有对a的值进行修改，而是重复读取了而已&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因如下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当我们读取 &lt;code&gt;computedData&lt;/code&gt;值的时候，才会去执行getter副作用函数。&lt;/p&gt;
&lt;p&gt;但是目前的实现，我们在每次读取    &lt;code&gt;computedData&lt;/code&gt;的时候其实都会将getter副作用函数重新执行一遍，就算是obj.a没有变化的情况下。&lt;/p&gt;
&lt;p&gt;所以，我们在obj.a没有变化的时候，我们其实并不需要去重新执行副作用函数，我们只需要将第一次读取的值缓存下来就可以了。&lt;/p&gt;
&lt;h4&gt;改造computed函数&lt;/h4&gt;
&lt;p&gt;加入缓存，我们可以再computed中，在第一次计算的时候加入缓存字段 - value，通过dirty表示判断，是否需要重新计算新的值，结合调度器参数，在依赖改变的时候，去回归dirty参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function computed(getter){
  let value; // ➕ 缓存值
  let dirty = true; // true表示需要重新计算值 ➕

  // 把getter 作为副作用函数，创建一个lazy的effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler(){ // ➕
      dirty = true; // 执行调度，trigger执行的时候，重新触发调度
    }
  })

  const obj = {
    get value(){
      if(dirty){
          value = effectFn();// 手动执行
          dirty = false; // 设置为false ➕
      }
      return value  // ➕
    }
  }

  return obj 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就能将计算值缓存起来。&lt;/p&gt;
&lt;p&gt;如下的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
    console.log(&quot;执行了getter&quot;);
    return obj.a + 1;
}

const computedData = computed(someFn);

console.log(computedData.value)

obj.a = 8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先访问了computedData，会进行一层计算，此刻得值为2&lt;/p&gt;
&lt;p&gt;之后我们修改了obj.a的值，会去走getter - 也就是someFn，因为存在调度器，所以这时候执行的是someFn的调度，将dirty设置为了true&lt;/p&gt;
&lt;p&gt;所以在下一次访问computedData.value的时候，就会重新计算；&lt;/p&gt;
&lt;p&gt;当然，如果没有去调用obj.a的赋值操作，这时候访问的computedData的时候就不会重新计算getter&lt;/p&gt;
&lt;h3&gt;嵌套读取计算属性值 缺陷&lt;/h3&gt;
&lt;p&gt;我们使用计算属性的时候，经常会在模板或者是另外一个副作用函数里头使用，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
    b: 2,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

function someFn() {
    console.log(&quot;执行了getter&quot;);
    return obj.a + obj.b;
}

const computedData = computed(someFn);

function effectOuter() {
    console.log(computedData.value);
}

effect(effectOuter);

setTimeout(() =&amp;gt; {
    obj.a++;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一个effect函数中读取了计算数据，当我们修改了obj.a的值之后，发现并不会 打印出 computedSum.value之后的值&lt;/p&gt;
&lt;p&gt;回头看computed函数，会发现，getter函数使用到了obj.a和obj.b，getter被收集到了a,b对应的Set当中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220826163739163.png&quot; alt=&quot;image-20220826163739163.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;getter是一个副作用函数，也是是被effect包裹并且处理为懒执行的函数&lt;/p&gt;
&lt;p&gt;如上的情况，就是发生了嵌套，外层的effect包裹了内层effect，而外层的effect真正读取的是computedSum.value，而这玩意并不是响应式数据，也无法收集到这个外层effect包裹的这个函数。&lt;/p&gt;
&lt;p&gt;而真正外层副作用函数读取的引用是computed内层的这个数据 &lt;code&gt;computedSum.value&lt;/code&gt; ---&amp;gt; obj.value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; const obj = {
    get value(){
      if(dirty){
          value = effectFn();// 手动执行
          dirty = false; // 设置为false ➕
      }
      return value  // ➕
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以为了能够追踪到读取计算属性的副作用函数，我们可以手动嵌入 track和trigger，用于obj的追踪和触发，追踪的是obj对象的key 为value&lt;/p&gt;
&lt;h4&gt;改造computed函数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function computed(getter){
    let value; // 
    let dirty = true; // true表示需要重新计算值 

    // 把getter 作为副作用函数，创建一个lazy的effect
    const effectFn = effect(getter, {
      lazy: true,
      scheduler(){
        dirty = true; // 执行调度，trigger执行的时候，重新触发调度
        // 当计算属性依赖的响应式数据发生变化的时候，手动的去调用trigger函数触发相应
        trigger(obj, &apos;value&apos;) // ➕
      }
    })

    const obj = {
      get value(){
        if(dirty){
            value = effectFn();// 手动执行
            dirty = false; // 设置为false 
        }
        track(obj, &apos;value&apos;); // 当读取这个value值的时候，手动调用track函数  // ➕
        return value;
      }
    }

    return obj 
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此这样就可以解决读取计算属性的问题了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1, b: 2 };
const obj = new Proxy(data, {...});

const computedData = computed(()=&amp;gt; obj.a + obj.b);

effect(function effectOuter(){
  console.log(computedData.value)
})


setTimeout(()=&amp;gt;{
    obj.a++;
}, 2000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就样可以打印出修改的之后的computed的值了，并且执行副作用函数&lt;/p&gt;
&lt;p&gt;他们之间的依赖关系就如图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220829150759429.png&quot; alt=&quot;image-20220829150759429.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Step4: 实现watch&lt;/h2&gt;
&lt;p&gt;vue3中的wacth函数用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wactch(obj, ()=&amp;gt;{
  console.log(&apos;变化数据&apos;)
})

// 修改
obj.a++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观测obj的值的改变，从而触发数据变化&lt;/p&gt;
&lt;p&gt;主要是由两个参数，第一个是观测的数据，第二个是数据变化时候触发的回调函数&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;watch的实现本质就是利用了effect函数options.scheduler调度器选项。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
  console.log(obj.a)
}, {
  scheduler(){
    // 当obj.a变化的时候，执行调度函数
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从之前的调度器属性可以看到，当存在调度器的时候，数据变化会触发调度器的执行&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先要先读取watch的那个数据。
&lt;ul&gt;
&lt;li&gt;原始值如何处理&lt;/li&gt;
&lt;li&gt;对象递归处理&lt;/li&gt;
&lt;li&gt;目前暂时不考虑其他的数据结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;watch的回调函数，可以直接加入调度器中执行，利用effect的getter骚操作进行响应式数据的track，但是最终trigger触发的副作用函数并不是getter，而是调度器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假如是如下的场景&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

watch(obj, () =&amp;gt; {
    console.log(&quot;改变了值&quot;);
});

setTimeout(() =&amp;gt; {
    obj.a = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候去改变a的值，希望能够实现和vue中的一样，直接执行这个回调函数&lt;/p&gt;
&lt;h3&gt;基础实现&lt;/h3&gt;
&lt;p&gt;scheduler + effect&lt;/p&gt;
&lt;p&gt;effect进行数据的读取追踪，scheduler作为调度器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wacth基本
function watch(source, callback){
    effect(()=&amp;gt; traverse(source),{
        scheduler(){
            callback()
        }
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先我们要对watch的数据进行读取&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;source在这里指的是 数据源&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实现traverse&lt;/h4&gt;
&lt;p&gt;封装一个通用的读取操作，能够保证source能够被完全的读取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function traverse(value, seen = new Set()){
    // 如果要读取的数据是原始值，或者已经被读取过了，那么什么都不做
    if(typeof value !== &apos;object&apos; || value === null || seen.has(value)) return

    // 将数据添加到seen中，代表遍历的时候读取过了，避免循环引用
    seen.add(value);
    // 。。。。目前不考虑数组等其他数据结构
    // 假设value是一个对象，使用for...in...读取对象的每一个值，递归调用traverse
    for (const key in value) {
        traverse(value[key], seen)
    }

    return value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;seen: 是一个Set结构， 这块防止重复读取引用类型数据造成stack overflow&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这里直接考虑vue中deep的情况，目前暂时不考虑其他的数据结构，只考虑对象类型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;函数类型兼容&lt;/h3&gt;
&lt;p&gt;在vue中，watch第一个参数还能是一个getter，在getter内部，可以指定wacth哪些响应式数据，只有数据变化的时候，才会触发回调&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;watch(()=&amp;gt;obj.a,()=&amp;gt;{
  console.log(&apos;修改了值&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此对source类型做判断，改造watch&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wacth基本
function watch(source, callback) {
  // 定义一个getter
  let getter; // ➕
  // 如果source为函数, 说明传进来了getter，所以source直接等于getter
  if (typeof source === &quot;function&quot;) { // ➕
    getter = source;
  } else {
    getter = () =&amp;gt; traverse(source);
  }
  effect(() =&amp;gt; getter(source), {
    scheduler() {
      callback();
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们在修改a值得时候就可以如愿检测到数据变化并且执行回调函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
    a: 1,
};

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        // 返回函数属性
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
    },
});

watch(obj, () =&amp;gt; {
    console.log(&quot;改变了值&quot;);
});

// or  ()=&amp;gt; obj.a

setTimeout(() =&amp;gt; {
    obj.a = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;新值和旧值&lt;/h3&gt;
&lt;p&gt;在vue中使用watch的时候，经常需要 知道新的值 和旧的值 传入回调函数之中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;watch(obj, (newVal, oldVal) =&amp;gt; {
    console.log(&quot;改变了值&quot;, newVal, oldVal);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以需要实现一下这种能力&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要知道的点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新值和旧值都是 getter函数触发最终的return结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新值和旧值需要传入我们的callback函数当中作为参数提供给外界&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要存储新值和旧值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要加入lazy属性，创建懒执行effect&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;改造watch&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// wacth基本
function watch(source, callback) {
  // 定义一个getter
  let getter;
  // 如果source为函数, 说明传进来了getter，所以source直接等于getter
  if (typeof source === &quot;function&quot;) {
    getter = source;
  } else {
    getter = () =&amp;gt; traverse(source);
  }
  let oldValue; // ➕ 旧值 
  let newValue; // ➕ 新值
  const effectFn = effect(() =&amp;gt; getter(source), {
    lazy: true, // ➕
    scheduler() {
      newValue = effectFn(); // 将执行结果返回给新值 ➕ 🐂👃
      callback(newValue, oldValue);
      oldValue = newValue; // 每次记录上一次的值 ➕
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最核心的地方就是使用到了lazy创建懒执行的effect；&lt;/p&gt;
&lt;p&gt;如下这个例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
  a: 1,
};

const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    // 返回函数属性
    return target[key];
  },

  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  },
});

watch(
  () =&amp;gt; obj.a,
  (newVal, oldVal) =&amp;gt; {
    console.log(&quot;改变了值&quot;, oldVal, &quot;---&amp;gt;&quot;, newVal);
  }
);

setTimeout(() =&amp;gt; {
  obj.a = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话就可以记录下来旧值了&lt;/p&gt;
&lt;h3&gt;立即执行&lt;/h3&gt;
&lt;p&gt;vue3中的watch能够通过 &lt;code&gt;immediate&lt;/code&gt;参数立即进行执行&lt;/p&gt;
&lt;p&gt;当immediate参数胃true的时候，回调函数就会在watch函数执行的时候，立刻执行一次回调函数&lt;/p&gt;
&lt;p&gt;所以我们可以改造下watch加入immediate配置项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wacth
function watch(source, callback, options = {}) {
  // 定义一个getter
  let getter;
  // 如果source为函数, 说明传进来了getter，所以source直接等于getter
  if (typeof source === &quot;function&quot;) {
    getter = source;
  } else {
    getter = () =&amp;gt; traverse(source);
  }

  let oldValue; // ➕ 旧值
  let newValue; // ➕ 新值

  // 提取scheduler调度函数为一个独立的job函数
  const job = () =&amp;gt; {
    newValue = effectFn(); // 将执行结果返回给新值 ➕
    callback(newValue, oldValue);
    oldValue = newValue; // 每次记录上一次的值 ➕
  };

  const effectFn = effect(() =&amp;gt; getter(source), {
    lazy: true, // ➕
    scheduler: job,
  });

  if (options.immediate) {
    job();
  } else {
    oldValue = effectFn(); // 不是immediate的时候，其实只是将getter第一次的执行结果保存为oldValue，并不执行callback
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于是立即执行，所以第一次没有oldValue是正常的&lt;/p&gt;
&lt;h3&gt;回调的时机&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;处理立即执行之外，vue中还能指定回调函数的执行时机，通过&lt;code&gt;flush&lt;/code&gt;参数进行限制&lt;/p&gt;
&lt;p&gt;flush： ’pre‘ &apos;post&apos; &apos;sync&apos;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;‘pre’的执行时机暂时先不处理，后续可以加上，因为涉及到组件的更新时机。&lt;/p&gt;
&lt;p&gt;‘sync‘的话相当于同步执行&lt;/p&gt;
&lt;p&gt;‘post’则是代表调度的函数需要将副作用函数放到一个微任务队列中，等dom更新后进行执行&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h4&gt;改造watch&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// wacth
function watch(source, callback, options = {}) {
  // 定义一个getter
  let getter;
  // 如果source为函数, 说明传进来了getter，所以source直接等于getter
  if (typeof source === &quot;function&quot;) {
    getter = source;
  } else {
    getter = () =&amp;gt; traverse(source);
  }

  let oldValue; //
  let newValue; // 

  //   提取scheduler调度函数为一个独立的job函数
  const job = () =&amp;gt; {
    newValue = effectFn(); // 将执行结果返回给新值 
    callback(newValue, oldValue);
    oldValue = newValue; // 每次记录上一次的值
  };

  const effectFn = effect(() =&amp;gt; getter(source), {
    lazy: true,
    scheduler: ()=&amp;gt; {
        if(options.flush === &apos;post&apos;){  // ➕
            const p = Promise.resolve();  // ➕
            p.then(job) // ➕
        }else{
            job()
        }
    },
  });

  if (options.immediate) {
    job();
  } else {
    oldValue = effectFn(); // 不是immediate的时候，其实只是将getter第一次的执行结果保存为oldValue，并不执行callback
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在调度器触发的时候，检测flush的类型，进而进行不一样的执行步骤&lt;/p&gt;
&lt;h3&gt;*竞态情况&lt;/h3&gt;
&lt;p&gt;例如如下的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let data;

watch(obj, async ()=&amp;gt;{
 // 发送请求网络请求
  const res = await axios.get(&apos;/getData&apos;);
  data = res
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们修改了obj的某个值，会触发getData的请求，假如，我们在短时间内连续修改了，obj的属性两次，其实会触发两次getData的请求&lt;/p&gt;
&lt;p&gt;假设第一次叫做A，第二次叫做B。因为两次请求落地的时机不相同，可能B会比A先落定，但是由于，我们第二次修改触发的请求得到的结果，才是我们真实想要的，但是由于AB落定时机不确定，我们无法明确最终是哪一次请求的值赋给了data。这种就存在了竞态的情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220831114324906.png&quot; alt=&quot;image-20220831114324906.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于B才是我们真正想要的结果，所以 我们可以当B为 “最新”，A为“过期”的副作用函数产生的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在vue中watch的回调函数有第三个参数，叫做onInvalidate，这个函数就是当前副作用函数过期的时候执行的回调&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;watch(obj, async (newValue, oldValue, onInvalidate)=&amp;gt;{
     // 定义一个标志， 代表当前副作用函数是否过期，默认为false，代表没有过期
  let expired = false;
 // 调用onInvalidate()函数注册一个过期的回调
  onInvalidate(()=&amp;gt; {
   // 当过期的时候，将expired的字段设置为true
    expired = true
  })

 // 获取server数据
  const res = await axios.get(&apos;/getData&apos;);

 // 只有当该副作用函数的执行没有过期的时候，才会执行后续的操作 
  if(!expired){
    data = res;
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过expired的变量标识，判断当前副作用是不是过期了。没有过期才采用请求结果&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;onInvalidate的原理，就是每次watch内部每次检测到变化的时候，在副作用函数执行之前，首先调用onInvalidate就可以了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此我们可以在watch函数的内部加入onInvalidtae的逻辑&lt;/p&gt;
&lt;h4&gt;改造watch&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// wacth
function watch(source, callback, options = {}) {
  // 定义一个getter
  let getter;
  // 如果source为函数, 说明传进来了getter，所以source直接等于getter
  if (typeof source === &quot;function&quot;) {
    getter = source;
  } else {
    getter = () =&amp;gt; traverse(source);
  }

  let cleanupEffect; // ➕

  let oldValue; //  旧值
  let newValue; //  新值

  function onInvalidate(fn){ // ➕
    // 将过期的回调存储到 cleanupEffect中，以方便下一次触发回调之前调用
    cleanupEffect = fn;
  }

  //   提取scheduler调度函数为一个独立的job函数
  const job = () =&amp;gt; {
    newValue = effectFn(); // 将执行结果返回给新值
    // 在调用回调函数callback之前，先调用过期的回调函数
    if(cleanupEffect){ // ➕
        cleanupEffect() // ➕
    }
    callback(newValue, oldValue, onInvalidate); // ➕
    oldValue = newValue; // 每次记录上一次的值
  };

  const effectFn = effect(() =&amp;gt; getter(source), {
    lazy: true,
    scheduler: ()=&amp;gt; {
        if(options.flush === &apos;post&apos;){
            const p = Promise.resolve();
            p.then(job)
        }else{
            job()
        }
    },
  });

  if (options.immediate) {
    job();
  } else {
    oldValue = effectFn(); // 不是immediate的时候，其实只是将getter第一次的执行结果保存为oldValue，并不执行callback
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入onInvalidate的注册函数，每次执行job的时候，都会去提前查看之前是否注册过cleanupEffect，有的话在执行之前需要去执行一遍。&lt;/p&gt;
&lt;h4&gt;方便理解的一个例子&lt;/h4&gt;
&lt;p&gt;例如如下场景&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;watch(obj, async (newValue, oldValue, onInvalidate)=&amp;gt;{
  // 定义一个标志， 代表当前副作用函数是否过期，默认为false，代表没有过期
  let expired = false;
 // 调用onInvalidate()函数注册一个过期的回调
  onInvalidate(()=&amp;gt; {
   // 当过期的时候，将expired的字段设置为true
    expired = true
  })

 // 获取server数据
  const res = await axios.get(&apos;/getData&apos;);

 // 只有当该副作用函数的执行没有过期的时候，才会执行后续的操作 
  if(!expired){
    data = res;
  }
})

// 第一次修改
obj.a++;

setTimeout(()=&amp;gt;{
  obj.a++;
},200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码我们修改了两次obj.a&lt;/p&gt;
&lt;p&gt;第一次sync立即执行，导致watch的回调执行。在回调内部，我们调用了onInvalidate，这时候就会在watch内部注册一个cleanupEffect，然后发送请求A（假设A用了2000ms才返回）&lt;/p&gt;
&lt;p&gt;第二次200ms后又修改了obj.a，又执行了watch的回调函数，这时候由于第一次已经注册了一个cleanupEffect，在job执行之前呢，其实会先去处理之前注册的回调函数，这时候之前的那个闭包变量expired就会被设置为true，之后就算是A请求落定了，也不会进行data的赋值操作&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20220831155534870.png]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Step5: 深入代理&lt;/h2&gt;
&lt;h3&gt;前置知识&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Vue3中利用的是Proxy以及Reflect去代理对象的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们知道Proxy是只能代理对象类型的，非对象类型不可以进行代理&lt;/p&gt;
&lt;p&gt;所谓代理：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;指的是对一个 对象 的 &lt;strong&gt;基本语义&lt;/strong&gt; 的代理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;何为proxy基本语义&lt;/h4&gt;
&lt;p&gt;比如我们对对象的一堆简单操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(obj.a); // 读取属性操作
obj.a++; // 设置属性值操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似这种读取，设置属性值的操作，就是属于基本的语义操作 ---- &lt;strong&gt;基本操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;类似这种的基本操作就可以用Proxy进行代理拦截&lt;/p&gt;
&lt;p&gt;基本操作的基本用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = new Proxy(obj, {
  // 拦截读取属性操作
  get(){ .... },
  // 拦截设置属性操作
  set(){  .... }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如函数：我们也可以使用apply对函数进行拦截&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fn = ()=&amp;gt;{
  console.log(&apos;wengkaimin&apos;)
}

const proFn = new Proxy(fn, {
  apply(target, thisArg, argArray){
    target.call(thisArg, ...argArray)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;proxy复合操作&lt;/h4&gt;
&lt;p&gt;既然有基本操作，可以也有非基本操作，在js里头，我们叫他复合操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.fn()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个显而易见，是又多个语义构成的（调用一个对象的一个函数属性）、&lt;/p&gt;
&lt;p&gt;两个语义是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先通过get获取到obj的fn属性&lt;/li&gt;
&lt;li&gt;通过获取到的fn进行调用&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Reflect基本改造getter&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Reflect是一个全局对象，其中存在和Proxy的拦截器很多名字相同的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如下的等价操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = { a: &apos;wengkaimin&apos; };
console.log(obj.a);
console.log(Reflect.get(obj, &apos;a&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是Reflect它能够传入第三个参数 reveiver&lt;/p&gt;
&lt;p&gt;就相当于函数执行过程中，指向的this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Reflect.get(obj, &apos;a&apos;, { a: &apos;kaimin&apos; })); // kaimin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们的响应式代码当中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    // 返回函数属性
    // 这里没有用Reflect.get实现读取数据
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在Proxy中无论设置get还是set拦截，都是直接用的原始对象target来进行读取或者赋值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假如目前的obj为，返回了this.foo的值。&lt;/p&gt;
&lt;p&gt;接着我们在effect副作用函数中通过代理对象data读取b的值。&lt;/p&gt;
&lt;p&gt;之后我们修改了a的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
  a:1,
  get b(){
    return this.a + 1;
  }
}

const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    // 返回函数属性
    // 这里没有用Reflect.get实现读取数据
    return target[key];
  },
  set(target, key, newVal) {
    // 这里没有用Reflect.set
    target[key] = newVal;
    trigger(target, key);
  },
})

effect(()=&amp;gt;{
  console.log(obj.b) // 2
})

obj.a++
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改了a的值之后，按道理应该执行读取了b属性的副作用函数，但是实际并不会相对应的触发副作用函数的重新执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;梳理下读取步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先我们在副作用函数中读取了obj.b的值&lt;/li&gt;
&lt;li&gt;会触发obj代理对象的get拦截器，在get拦截器中，通过target[key]读取;&lt;/li&gt;
&lt;li&gt;此时target就指的是 data 原始对象，key就是 &apos;b&apos;，所以相当直接读了data.b&lt;/li&gt;
&lt;li&gt;访问obj.b的时候，其实是一个getter函数，这个getter的this指向了data，最终实质上是访问了 data.a 并且给他加了个1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，在副作用函数effect当中相当于，直接读取了原生对象data的属性，虽然看上去走了代理，但不多。所以这肯定是没有追踪到的，建立不起相应的联系&lt;/p&gt;
&lt;p&gt;就类似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
  console.log(data.a + 1) // 2
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种场景下Reflect的第三个参数receiver就派上用场了&lt;/p&gt;
&lt;h4&gt;改造getter拦截&lt;/h4&gt;
&lt;p&gt;使用Reflect改造完get拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  get(target, key, receiver) {
    track(target, key);
    // 返回函数属性
    return Reflect.get(target, key, receiver) // ➕
  },
    ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用了Relfect之后，this指向就转为了obj代理对象，就可以成功的建立联系了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reflect的作用不仅于此&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;改造setter拦截&lt;/h4&gt;
&lt;p&gt;使用Reflect改造完setter拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  set(target, key, newVal, receiver) {
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 执行副作用
    trigger(target, key);

    return res
  },
  ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代理obj&lt;/h3&gt;
&lt;p&gt;使一个obj成为响应式的数据，我们必须要做的就是做好它代理。&lt;/p&gt;
&lt;p&gt;首先需要知道一个普通对象的所有可能的读取操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问属性 ob.a&lt;/li&gt;
&lt;li&gt;使用in操作符，key in obj&lt;/li&gt;
&lt;li&gt;使用for....in....访问对象属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;属性的读取操作，上面我们已经实现过了, obj.a直接使用getter拦截器来实现拦截&lt;/p&gt;
&lt;h4&gt;in操作符拦截&lt;/h4&gt;
&lt;p&gt;比如如下的副作用需要实现数据代理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;effect(()=&amp;gt;{
  &apos;a&apos; in obj;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想找拦截in操作符必须知道in操作符的原理，其实需要理解&lt;a href=&quot;https://262.ecma-international.org/13.0/#sec-relational-operators-runtime-semantics-evaluation&quot;&gt;ECMA-262&lt;/a&gt;的规范&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220905204422368.png&quot; alt=&quot;image-20220905204422368.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第六点的  &lt;a href=&quot;https://262.ecma-international.org/13.0/#sec-hasproperty&quot;&gt;HasProperty&lt;/a&gt;(rval, ? &lt;a href=&quot;https://262.ecma-international.org/13.0/#sec-topropertykey&quot;&gt;ToPropertyKey&lt;/a&gt;(lval))&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220905205830202.png&quot; alt=&quot;image-20220905205830202.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这块的 &lt;code&gt;Return ? O.[[HasProperty]](P).&lt;/code&gt; 的意思就是，调用原生对象的has方法去判断，是否有key值，因此我们可以理解为，in操作符的读取操作我们需要调用[[HasProperty]]函数槽对应的has方法，去做拦截&lt;/p&gt;
&lt;p&gt;因此我们可以修改我们的拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  has(target, key){ // ➕
    track(target, key); // ➕
    return Reflect.has(target, key) // ➕
  }
  ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;拦截for...in...&lt;/h4&gt;
&lt;p&gt;for...in...会在我们为响应式对象添加新的属性的时候，重新触发副作用函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a:1, b:2 };
const obj = new Proxy(data, {....});

effect(()=&amp;gt;{
  // for...in...
  for(const key in obj){
    console.log(key)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们查看ECMA规范14.7.5.6&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220905215218547.png&quot; alt=&quot;image-20220905215218547&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第六步的c. Let iterator be &lt;a href=&quot;https://262.ecma-international.org/13.0/#sec-enumerate-object-properties&quot;&gt;EnumerateObjectProperties&lt;/a&gt;(obj).&lt;/p&gt;
&lt;p&gt;如下是一个抽象方法，EnumerateObjectProperties该方法返回一个迭代器对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220905215335311.png&quot; alt=&quot;image-20220905215335311.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实际上，可以看出，如果的obj就是被for...in...循环遍历的对象，其关键点在与 使用到了 &lt;code&gt;Reflect.ownKeys()&lt;/code&gt;来获取只属于对象自身拥有的键。&lt;/p&gt;
&lt;p&gt;因此我们可以使用Proxy的ownKeys来进行拦截。&lt;/p&gt;
&lt;h5&gt;改造代理，加入ownKeys拦截&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const ITERATE_KEY = Symbol()

const obj = new Proxy(data, {
     .....
  // 拦截for...in..., 获取所有对象所有key
  ownKeys(target) {
    // 将副作用函数与ITERATE_KEY关联起来
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target)
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;将&lt;code&gt;ITERATE_KEY&lt;/code&gt;作为track追踪的key&lt;/strong&gt;：因为ownKeys拦截函数和get/set不同，无法具体到某个key值，ownKeys代表的时候拿到所有对象属于自己的key值，因此是无法具体的对应到哪个key的&lt;/li&gt;
&lt;li&gt;既然追踪的是 &lt;code&gt;ITERATE_KEY&lt;/code&gt;，在触发响应的时候也应该触发 &lt;code&gt;trigger(target, ITERATE_KEY)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;改造trigger&lt;/h5&gt;
&lt;p&gt;上面的for循环的副作用函数，会与&lt;code&gt;ITERATE_KEY&lt;/code&gt;建立联系，这时候，我们去新增obj的key，会触发setter拦截&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.c=3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按道理来说是能够触发上面的for循环的副作用函数。但是实际却不能正常触发。&lt;/p&gt;
&lt;p&gt;那是因为，我们是将 obj作为一个元素对应的&lt;code&gt;ITERATE_KEY&lt;/code&gt;做关联&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220906221150671.png&quot; alt=&quot;image-20220906221150671.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其他的Key的Set副作用函数集合并没有将这个for...in...对应的副作用函数收起起来&lt;/p&gt;
&lt;p&gt;所以在设置新的属性 c 的时候，for...in...对应的副作用函数 和 c 完全没有任何关系&lt;/p&gt;
&lt;p&gt;所以，我们可以这么做，我们在trigger的时候，将那些与&lt;code&gt;ITERATE_KEY&lt;/code&gt;相关副作用函数耶取出来执行就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key) {
  // 根据target引用从桶中取得对应的Map
  const depsMap = buckect.get(target);
  if (!depsMap) return;
  // 取得target 上key对应的Set数据，遍历，执行里头的副作用
  const effects = depsMap.get(key);

  // 取得for...in... 也就是ITERATE_KEY相关的副作用函数
  const iterateEffects = depsMap.get(ITERATE_KEY); // ➕

  const effectsToRun = new Set();

  if (effects) {
    effects.forEach((current) =&amp;gt; {
      if (current !== activeEffect) {
        // 如果当前trigger执行的，和activeEffect不一样，加入
        effectsToRun.add(current);
      }
    });
  }
  // 将与ITERATE_KEY挂钩的副作用函数也加入到effectsToRun ➕
  if(iterateEffects){ // ➕
    iterateEffects.forEach((current) =&amp;gt; { // ➕
      if (current !== activeEffect) { // ➕
        // 如果当前trigger执行的，和activeEffect不一样，加入 // ➕
        effectsToRun.add(current); // ➕
      }
    });
  }
  effectsToRun.forEach((fn) =&amp;gt; {
    if (fn.options.scheduler) {
      // 调度器
      fn.options.scheduler(fn); // 调度器
    } else {
      fn();
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造之后，我们去新增新的属性值，就会重新触发for...in...的那个副作用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
  a: 1,
};

const obj = new Proxy(data, {
  get(target, key, receiver) {
    track(target, key);
    // 返回函数属性
    return Reflect.get(target, key, receiver); // ➕
  },

  set(target, key, newVal, receiver) {
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 执行副作用
    trigger(target, key);

    return res;
  },

  has(target, key) {
    track(target, key);
    return Reflect.has(target, key);
  },

  // 拦截for...in..., 获取所有对象所有key
  ownKeys(target) {
    // 将副作用函数与ITERATE_KEY关联起来
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target);
  },
});

effect(() =&amp;gt; {
  // for...in...
  for (const key in obj) {
    console.log(key);
  }
});

setTimeout(() =&amp;gt; {
  obj.b = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;优化（区分新属性和旧属性）&lt;/h5&gt;
&lt;p&gt;假如我们修改一个已存在的属性，按道理是不应该去重新出发for...in...副作用函数的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
  a: 1,
};

const obj = new Proxy(data, {
  get(target, key, receiver) {
    track(target, key);
    // 返回函数属性
    return Reflect.get(target, key, receiver); // ➕
  },

  set(target, key, newVal, receiver) {
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 执行副作用
    trigger(target, key);

    return res;
  },

  has(target, key) {
    track(target, key);
    return Reflect.has(target, key);
  },

  // 拦截for...in..., 获取所有对象所有key
  ownKeys(target) {
    // 将副作用函数与ITERATE_KEY关联起来
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target);
  },
});

effect(() =&amp;gt; {
  // for...in...
  for (const key in obj) {
    console.log(key);
  }
});

setTimeout(() =&amp;gt; {
  obj.a = 2;
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，目前我们会重新去触发副作用函数，这会造成不必要的性能开销。需要做限制&lt;/p&gt;
&lt;p&gt;所以我们真正要做的是，在set的拦截器中去对新增属性和设置 场景 做区分&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先定义一个全局的枚举，表示场景&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 枚举 -- 触发类型，set为设置属性，add为添加属性
const TRIGGER_TYPE = {
  SET:&apos;set&apos;,
  ADD:&apos;add&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;改造setter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  set(target, key, newVal, receiver) {
    // 如果属性不存在，则说明是在添加属性，否则是在设置属性
    const triggerType = Object.prototype.hasOwnProperty.call(target, key) // ➕
      ? TRIGGER_TYPE.SET
      : TRIGGER_TYPE.ADD;
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 执行副作用
    // 将triggerType作为trigger的第三个参数
    trigger(target, key, triggerType);
    return res;
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过hasOwnProperty去判断，当前对象上是否存在这个属性了，从而知道是新的属性还是旧的属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;改造trigger函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key, triggerType) {
  // 根据target引用从桶中取得对应的Map
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 取得target 上key对应的Set数据，遍历，执行里头的副作用
  const effects = depsMap.get(key);

  const effectsToRun = new Set();

  if (effects) {
    effects.forEach((current) =&amp;gt; {
      if (current !== activeEffect) {
        // 如果当前trigger执行的，和activeEffect不一样，加入
        effectsToRun.add(current);
      }
    });
  }
  // 只有当 triggerType为 ADD的时候才会去触发与ITERATE_KEY相关联的副作用函数的重新执行
  if (triggerType === TRIGGER_TYPE.ADD) { // ➕
    // 取得for...in... 也就是ITERATE_KEY相关的副作用函数
    const iterateEffects = depsMap.get(ITERATE_KEY);
    // 将与ITERATE_KEY挂钩的副作用函数也加入到effectsToRun
    if (iterateEffects) {
      iterateEffects.forEach((current) =&amp;gt; {
        if (current !== activeEffect) {
          // 如果当前trigger执行的，和activeEffect不一样，加入
          effectsToRun.add(current);
        }
      });
    }
  }

  effectsToRun.forEach((fn) =&amp;gt; {
    if (fn.options.scheduler) {
      // 调度器
      fn.options.scheduler(fn); // 调度器
    } else {
      fn();
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入triggerType做区分，只有在triggerType为ADD的时候才去触发与ITERATE_KEY相关的副作用函数&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;拦截delete操作符&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;delete obj.a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除了a属性时候，其实我们要做的不是去触发，那些和a关联的副作用函数；&lt;/p&gt;
&lt;p&gt;其实和for...in...一样，删除了某个对象的key，其实会影响for...in...关联的副作用函数，会导致for...in...减少一层循环，应该合理的去触发这个副作用函数&lt;/p&gt;
&lt;p&gt;同样，我们需要拦截delete操作符，也需要提前了解EcMA的规范&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220907140633477.png&quot; alt=&quot;image-20220907140633477.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从 5.d 的步骤可以知道，delete操作符的行为依赖 &lt;strong&gt;[[Delete]]&lt;/strong&gt; 函数槽。对应的是Proxy deleteProperty拦截函数&lt;/p&gt;
&lt;p&gt;从上就知道了其实我们要做的就是在 删除 某个对象的属性 的时候去触发 那个和 ITERATE_KEY 绑定的副作用函数&lt;/p&gt;
&lt;h5&gt;加入“删除”类型的TRIGGER_TYPE&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 枚举 -- 触发类型，set为设置属性，add为添加属性，delete为删除属性
const TRIGGER_TYPE = {
  SET: &quot;set&quot;,
  ADD: &quot;add&quot;,
  DELETE: &quot;delete&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;改造代理，加入deleteProperty拦截&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;判断当前删除的属性是否存在于自身对象上（如果不在自己的对象上，就不应该触发对应的副作用）&lt;/li&gt;
&lt;li&gt;使用Reflect.deleteProperty进行删除&lt;/li&gt;
&lt;li&gt;当1 2两者的条件全部都满足的时候，则去触发trigger函数，并且传入 DELETE的标识告诉trigger函数，目前进行的是删除属性操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  // 拦截delete操作符
  deleteProperty(target, key){
    // 检查被操作的属性是否是对象自己的属性
    const hasKey = Object.prototype.hasOwnProperty.call(target,key);
    // 使用Reflect.deleteProperty 完成属性的删除
    const res = Reflect.deleteProperty(target,key);

    if(hasKey &amp;amp;&amp;amp; res){
      // 只有删除的是自己的属性，并且成功删除，才回去触发trigger函数
      trigger(target, key, TRIGGER_TYPE.DELETE)
    }

    return res;
  }
  ....
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;改造trigger函数，加入delete判断条件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;function trigger(target, key, triggerType) {
  // 根据target引用从桶中取得对应的Map
  const depsMap = buckect.get(target);
  if (!depsMap) return;
  // 取得target 上key对应的Set数据，遍历，执行里头的副作用
  const effects = depsMap.get(key);

  const effectsToRun = new Set();

  if (effects) {
    effects.forEach((current) =&amp;gt; {
      if (current !== activeEffect) {
        // 如果当前trigger执行的，和activeEffect不一样，加入
        effectsToRun.add(current);
      }
    });
  }
  // 只有当 triggerType为 ADD 或者 DELETE 的![]()时候才会去触发与ITERATE_KEY相关联的副作用函数的重新执行 // ➕
  if ([TRIGGER_TYPE.ADD, TRIGGER_TYPE.DELETE].includes(triggerType)) { // ➕
    // 取得for...in... 也就是ITERATE_KEY相关的副作用函数
    const iterateEffects = depsMap.get(ITERATE_KEY);
    // 将与ITERATE_KEY挂钩的副作用函数也加入到effectsToRun
    if (iterateEffects) {
      iterateEffects.forEach((current) =&amp;gt; {
        if (current !== activeEffect) {
          // 如果当前trigger执行的，和activeEffect不一样，加入
          effectsToRun.add(current);
        }
      });
    }
  }

  effectsToRun.forEach((fn) =&amp;gt; {
    if (fn.options.scheduler) {
      // 调度器
      fn.options.scheduler(fn); // 调度器
    } else {
      fn();
    }
   });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入 type === &apos;DELETE&apos;的判断，从而是删除属性的操作也能够触发 与 &lt;code&gt;ITERATE_KEY&lt;/code&gt; 绑定的操作。&lt;/p&gt;
&lt;h2&gt;Step6: 合理的触发响应&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;合理的触发响应，就是在一些情况下，我们其实是不需要去做副作用函数的重新执行的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前后值不改变情况的处理&lt;/h3&gt;
&lt;p&gt;如下例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };
const obj = new Proxy(obj, {
    ....
})

effect(()=&amp;gt;{
    console.log(obj.a)
})

obj.a = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，我们去修改a的值为1，其实实质上是并没有改动到obj的a属性值得，但是在这种情况下还是会疫情副作用函数的重新执行&lt;/p&gt;
&lt;p&gt;那是因为并没有做值比较的拦截&lt;/p&gt;
&lt;h4&gt;修改setter拦截&lt;/h4&gt;
&lt;p&gt;我们在set拦截器中，进行前后值的全等比较&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
    set(target, key, newVal, receiver) {
    // 首先获取旧值
    const oldValue = target[key];

    // 如果属性不存在，则说明是在添加属性，否则是在设置属性
    const triggerType = Object.prototype.hasOwnProperty(target, key)
      ? TRIGGER_TYPE.SET
      : TRIGGER_TYPE.ADD;
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);

    if (oldValue !== newVal) { // ➕
      // 执行副作用
      // 将triggerType作为trigger的第三个参数
      trigger(target, key, triggerType);
    }

    return res;
  },
    .....
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上加入了一个全等的比较，但是其实这样是不妥当的。&lt;/p&gt;
&lt;p&gt;我们没有考虑到NaN的情况&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在js中NaN和NaN无论如何全等比较都是得到false的。&lt;/p&gt;
&lt;p&gt;NaN  !== NaN&lt;/p&gt;
&lt;p&gt;NaN === NaN&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此加入NaN的限制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = new Proxy(data, {
  get(target, key, receiver) {
    track(target, key);
    // 返回函数属性
    return Reflect.get(target, key, receiver);
  },

  set(target, key, newVal, receiver) {
    // 首先获取旧值
    const oldValue = target[key];

    // 如果属性不存在，则说明是在添加属性，否则是在设置属性
    const triggerType = Object.prototype.hasOwnProperty(target, key)
      ? TRIGGER_TYPE.SET
      : TRIGGER_TYPE.ADD;
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的 ➕
    if (oldValue !== newVal &amp;amp;&amp;amp; (oldValue === oldValue || newVal === newVal)) { // ➕
      // 执行副作用
      // 将triggerType作为trigger的第三个参数
      trigger(target, key, triggerType);
    }

    return res;
  },
  ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;封装为reactive&lt;/h3&gt;
&lt;p&gt;我们将上述的所有的拦截器都封装到一个reactive的函数当中，后续直接以reactive为例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reactive(data) {
  return new Proxy(data, {
    get(target, key, receiver) {
      track(target, key);
      // 返回函数属性
      return Reflect.get(target, key, receiver);
    },

    set(target, key, newVal, receiver) {
      // 首先获取旧值
      const oldValue = target[key];

      // 如果属性不存在，则说明是在添加属性，否则是在设置属性
      const triggerType = Object.prototype.hasOwnProperty(target, key)
        ? TRIGGER_TYPE.SET
        : TRIGGER_TYPE.ADD;
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver);
      // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的
      if (oldValue !== newVal &amp;amp;&amp;amp; (oldValue === oldValue || newVal === newVal)) {
        // 执行副作用
        // 将triggerType作为trigger的第三个参数
        trigger(target, key, triggerType);
      }

      return res;
    },

    // 拦截in操作符
    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    // 拦截for...in..., 获取所有对象所有key
    ownKeys(target) {
      // 将副作用函数与ITERATE_KEY关联起来
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

    // 拦截delete操作符
    deleteProperty(target, key) {
      // 检查被操作的属性是否是对象自己的属性
      const hasKey = Object.prototype.hasOwnProperty.call(target, key);
      // 使用Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key);
      if (hasKey &amp;amp;&amp;amp; res) {
        // 只有删除的是自己的属性，并且成功删除，才回去触发trigger函数
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以直接使用了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = {
  a: 1,
  b: 2,
};

const obj = reactive(data);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;考虑原型情况&lt;/h3&gt;
&lt;p&gt;存在一种情况，原型和代理的对象都是响应式数据的情况下&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {};
const prototype = { a: &apos;weng&apos; };

const child = reactive(obj);
const parent = reactive(prototype);

Object.setPrototypeOf(child, parent);

effect(()=&amp;gt;{
    console.log(child.a);
});


child.a = &apos;kaimin&apos;; // 会走两次副作用函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么会存在两次执行的情况？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分析如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在读取child.a的时候，会触发get拦截函数&lt;/p&gt;
&lt;p&gt;最终的结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reflect.get(obj, &apos;a&apos;, child)
// obj是child的原生对象
// child在这里就相当于receiver
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其实最终是通过访问obj.a来访问属性的默认行为的。&lt;/p&gt;
&lt;p&gt;引擎内部是通过调用obj对象所部署的[[Get]]内部方法得到最终结果&lt;/p&gt;
&lt;p&gt;查看ECMA的规范：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-09-11-11-52-09-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在第三步骤，判定，如果我们自身的读取对象上没有这个属性的时候，会去原型上去调用原型对象属性的[[Get]]，因此这里就是parent.a，而parent的本身也是一个响应式数据，所以在副作用函数中相当于也访问了parent.a的值；&lt;strong&gt;这样就导致 child.a 和parent.a都与副作用函数建立的响应的联系&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在我们修改child.a的值的时候，会触发obj原生对象的[[Set]]&lt;/p&gt;
&lt;p&gt;引擎内部的定义如下：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: /Users/leo/Library/Application%20Support/marktext/images/2022-09-11-12-02-12-image.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;由第二步的步骤知道，如果对象上面设置的这个属性不存在的话，那么就会获取它的原型，这里就是parent，由于parent是响应数据，就会调用parent的[[Set]]，所以，我们修改child.a的值的时候，由于在[[Get]]的时候，child.a和parent.a都收集到了副作用函数，所以会执行两次副作用函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结来说&lt;/strong&gt;，两次的触发分别是这样的效果的&lt;/p&gt;
&lt;p&gt;第一次，作用在receiver 为child和target为 obj原生对象上&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Reflect.set(obj, &apos;a&apos;, &apos;kaimin&apos;, child)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于引擎的[[Get]]机制，发现没有a属性，就去parent上找了&lt;/p&gt;
&lt;p&gt;第二次，作用在在receiver 为child和target为 prototype原生对象上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reflect.set(prototype, &apos;a&apos;, &apos;kaimin&apos;, child)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;第一次和第二次的差距就是，所设置的target的原生对象不一样的。但是receiver永远是那个child响应数据，所以可以利用这个特点来作区分。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;target变化，receiver不变，我们只需要做到一次的屏蔽更新就可以了。去掉那一次由于原型而引起的更新&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以接下来的问题就是确定receiver是不是target的代理对象就可以了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;修改getter拦截&lt;/h4&gt;
&lt;p&gt;新增加一个代理对象的raw属性，这个属性是指向这个需要代理的对象的，当我们访问这个代理对象的时候，如果key为raw，那我们就直接返回这个代理的对象，为后续的setter触发时间做判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reactive(data) {
  return new Proxy(data, {
    get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) { // ➕
        return target; // ➕
      }

      track(target, key);
      // 返回函数属性
      return Reflect.get(target, key, receiver);
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样代理对象就可以这么搞了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;child.raw === obj; // true;
parent.raw === prototype; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了它，我们就能够在setter当中拦截了&lt;/p&gt;
&lt;h4&gt;修改setter&lt;/h4&gt;
&lt;p&gt;有了raw的属性，我们可以在setter当中去访问receiver的raw属性，从而将其和当前的target做判断，进行拦截&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reactive(data) {
  return new Proxy(data, {
    set(target, key, newVal, receiver) {
      // 首先获取旧值
      const oldValue = target[key];

      // 如果属性不存在，则说明是在添加属性，否则是在设置属性
      const triggerType = Object.prototype.hasOwnProperty(target, key)
        ? TRIGGER_TYPE.SET
        : TRIGGER_TYPE.ADD;
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver);
      //   如果target === receiver.raw（raw指向原生对象）就说明receiver就是target的代理对象
      if (receiver.raw === target) { // ➕
        // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的
        if (
          oldValue !== newVal &amp;amp;&amp;amp;
          (oldValue === oldValue || newVal === newVal)
        ) {
          // 执行副作用
          // 将triggerType作为trigger的第三个参数
          trigger(target, key, triggerType);
        }
      }

      return res;
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过如上的改造，我们增加了判断条件，这样就可以屏蔽那些 由原型值引起的不必要的更新。&lt;/p&gt;
&lt;p&gt;因此上面的例子就能解决两次执行副作用的问题&lt;/p&gt;
&lt;h2&gt;Step7: 引入“深”和“浅”概念&lt;/h2&gt;
&lt;h3&gt;浅与深响应&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在vue中，有&lt;code&gt;shallowReactive&lt;/code&gt;的浅响应的方法，也有&lt;code&gt;reactive&lt;/code&gt;深响应的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目前我们所实现的&lt;code&gt;reactive&lt;/code&gt;是浅响应&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: { b: &apos;weng &apos;} };
const obj = reactive(data);

effect(()=&amp;gt;{
    console.log(obj.a.b)
});

// 修改b的值并不会触发副作用函数
obj.a.b = &apos;kaimin&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前这种情况造成的原因是因为，在我们getter拦截器中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) {
        return target;
      }

      track(target, key);
      // 返回函数属性
      return Reflect.get(target, key, receiver);
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在读取obj.a.b的时候&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;需要先访问&lt;code&gt;obj.a&lt;/code&gt;，通过getter拿到的是&lt;code&gt;Reflect.get(data, &apos;a&apos;, obj)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结果拿到的data.a其实是非响应式的数据：&lt;code&gt;{ b: &apos;weng&apos; }&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所以说这种情况我们我们无法追踪这个对象的，得不到响应&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此我们称这种为浅响应，如果需要在修改b值的时候，也触发响应，那就是深入到了下面一层级，或者更深一层级，这样的就叫做深响应。&lt;/p&gt;
&lt;p&gt;如果要实现深响应的话，我们可以改造getter，让他深入到下一层的数据&lt;/p&gt;
&lt;h4&gt;改造reactive和getter实现深响应&lt;/h4&gt;
&lt;p&gt;当读取一个对象的某个属性的时候&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先判断这个属性是否是对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是对象的话，则递归地调用reactive函数将其包装成响应式数据并且返回。&lt;/p&gt;
&lt;p&gt;这样的话在下一层级访问属性的时候，就可以简历响应的联系了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;function reactive(data) {
  return new Proxy(data, {
    get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) {
        return target;
      }

      track(target, key);
      // 返回函数属性，得到原始值结果
      const res = Reflect.get(target, key, receiver); // ➕
      if (typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null) { // ➕
        // 调用 reactive 将结果包装成响应式数据并返回
        return reactive(res);  // ➕
      }
      // 返回res
      return res;
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;封装深响应reactive和浅响应shallowReactive&lt;/h4&gt;
&lt;p&gt;实现了上层的深响应的方案之后我们希望能够将两者变成vue中的reactive和shallowReactive函数。&lt;/p&gt;
&lt;p&gt;封装一个createReactive公共函数，接收一个参数isShallow参数，在这里代表是否为浅层，默认为false，即深层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 封装
function createReactive(data, isShallow = false) {
  return new Proxy(data, {
    get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) {
        return target;
      }

      track(target, key);
      // 返回函数属性，得到原始值结果
      const res = Reflect.get(target, key, receiver);
      //   如果是浅响应直接返回
      if (isShallow) { // ➕
        return res; // ➕
      }
      if (typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null) {
        // 调用 reactive 将结果包装成响应式数据并返回
        return reactive(res);
      }
      // 返回res
      return res;
    },

    set(target, key, newVal, receiver) {
      // 首先获取旧值
      const oldValue = target[key];

      // 如果属性不存在，则说明是在添加属性，否则是在设置属性
      const triggerType = Object.prototype.hasOwnProperty(target, key)
        ? TRIGGER_TYPE.SET
        : TRIGGER_TYPE.ADD;
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver);
      //   如果target === receiver.raw（raw指向原生对象）就说明receiver就是target的代理对象
      if (receiver.raw === target) {
        // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的
        if (
          oldValue !== newVal &amp;amp;&amp;amp;
          (oldValue === oldValue || newVal === newVal)
        ) {
          // 执行副作用
          // 将triggerType作为trigger的第三个参数
          trigger(target, key, triggerType);
        }
      }

      return res;
    },

    // 拦截in操作符
    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    // 拦截for...in..., 获取所有对象所有key
    ownKeys(target) {
      // 将副作用函数与ITERATE_KEY关联起来
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

    // 拦截delete操作符
    deleteProperty(target, key) {
      // 检查被操作的属性是否是对象自己的属性
      const hasKey = Object.prototype.hasOwnProperty.call(target, key);
      // 使用Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key);
      if (hasKey &amp;amp;&amp;amp; res) {
        // 只有删除的是自己的属性，并且成功删除，才回去触发trigger函数
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后的reactive和shallowReactive就调用穿不同参数就好了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reactive(data) {
    return createReactive(data)
}

function shallowReactive(data){
    return createReactive(data, true)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: { b: &quot;weng&quot; } };
const data2 = { a: { b: &quot;weng2&quot; } };

const obj = reactive(data);
const obj2 = shallowReactive(data2);

effect(() =&amp;gt; {
  console.log(obj.a.b);
});

effect(() =&amp;gt; {
  console.log(obj2.a.b);
});

obj.a.b = &quot;kaimin&quot;;
obj2.a.b = &quot;kaimin2&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就OK&lt;/p&gt;
&lt;h3&gt;浅与深的只读&lt;/h3&gt;
&lt;p&gt;只读的概念，就是尝试&lt;strong&gt;修改&lt;/strong&gt;只读数据的时候，会阻止其值的修改，或者&lt;strong&gt;删除&lt;/strong&gt;某个只读数据的时候，会阻止，并且抛出警告。&lt;/p&gt;
&lt;p&gt;我们提供一个 readonly的函数，将某个数据变成只读的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = readonly({ a: &apos;weng&apos; });

effect(()=&amp;gt;{
    console.log(obj.a); // 可以读，但是在这里就不需要在追踪数据
})

obj.a = &apos;kaimin&apos;; // 修改失败，并且抛出警告
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只读本质也是对数据对象做了一层代理，我们可以服用上述封装的createReactive的函数来改造，并且加入isReadonly参数&lt;/p&gt;
&lt;h4&gt;改造createReative&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 新增isShallow表示浅还是深响应，isReadonly表示浅还是深只读
function createReactive(data, isShallow = false, isReadonly = false) {
  return new Proxy(data, {
    get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) {
        return target;
      }

      // 非只读的数据才需要建立响应式的联系
      if (!isReadonly) { // ➕
        track(target, key);
      }

      // 返回函数属性，得到原始值结果
      const res = Reflect.get(target, key, receiver);
      //   如果是浅响应直接返回
      if (isShallow) {
        return res;
      }
      if (typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null) {
        // 调用 reactive 将结果包装成响应式数据并返回
        return reactive(res);
      }
      // 返回res
      return res;
    },

    set(target, key, newVal, receiver) {
      // 如果是只读的，则打印警告信息并返回
      if (isReadonly) {  // ➕
        console.warn(`the param ${key} is readonly`);
        return true;
      }
      // 首先获取旧值
      const oldValue = target[key];

      // 如果属性不存在，则说明是在添加属性，否则是在设置属性
      const triggerType = Object.prototype.hasOwnProperty(target, key)
        ? TRIGGER_TYPE.SET
        : TRIGGER_TYPE.ADD;
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver);
      //   如果target === receiver.raw（raw指向原生对象）就说明receiver就是target的代理对象
      if (receiver.raw === target) {
        // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的
        if (
          oldValue !== newVal &amp;amp;&amp;amp;
          (oldValue === oldValue || newVal === newVal)
        ) {
          // 执行副作用
          // 将triggerType作为trigger的第三个参数
          trigger(target, key, triggerType);
        }
      }

      return res;
    },

    // 拦截in操作符
    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    // 拦截for...in..., 获取所有对象所有key
    ownKeys(target) {
      // 将副作用函数与ITERATE_KEY关联起来
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

    // 拦截delete操作符
    deleteProperty(target, key) {
      // 如果是只读的，删除失败，提示
      if (isReadonly) {  // ➕
        console.warn(`the param ${key} is readonly`);
        return true;
      }
      // 检查被操作的属性是否是对象自己的属性
      const hasKey = Object.prototype.hasOwnProperty.call(target, key);
      // 使用Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key);
      if (hasKey &amp;amp;&amp;amp; res) {
        // 只有删除的是自己的属性，并且成功删除，才回去触发trigger函数
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加入第三个参数isReadonly表示是只读还是非只读&lt;/li&gt;
&lt;li&gt;在getter中加入判断，当只有非只读的数据，才需要进行响应式的追踪，也就是调用track函数&lt;/li&gt;
&lt;li&gt;在setter当中加入判断，只读的数据直接返回true，并且抛出警告&lt;/li&gt;
&lt;li&gt;在拦截delete操作符中，加入判断，只读的数据直接返回true，并且抛出警告&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;封装深响应readonly和浅响应shallowReadonly&lt;/h4&gt;
&lt;p&gt;如上的实现，只是对数据进行了一层浅层的readonly，也就是vue中的shallowReadonly，我们可以定义深只读readonly函数，对数据进行深层的只读，并且对getter进行改造。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function readonly(data){
  return createReactive(data, false, true)
}

// 新增isShallow表示浅还是深响应，isReadonly表示浅还是深只读
function createReactive(data, isShallow = false, isReadonly = false) {
  return new Proxy(data, {
    get(target, key, receiver) {
      // 代理对象可以通过raw的属性，访问原始的数据
      if (key === &quot;raw&quot;) {
        return target;
      }

      // 非只读的数据才需要建立响应式的联系
      if (!isReadonly) {
        track(target, key);
      }

      // 返回函数属性，得到原始值结果
      const res = Reflect.get(target, key, receiver);
      //   如果是浅响应直接返回
      if (isShallow) {
        return res;
      }
      if (typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null) {
        // 调用 reactive / readonly 将结果包装成响应式数据并返回
        return isReadonly ? readonly(res) : reactive(res); // ➕
      }
      // 返回res
      return res;
    },

    set(target, key, newVal, receiver) {
      // 如果是只读的，则打印警告信息并返回
      if (isReadonly) {
        console.warn(`the param ${key} is readonly`);
        return true;
      }
      // 首先获取旧值
      const oldValue = target[key];

      // 如果属性不存在，则说明是在添加属性，否则是在设置属性
      const triggerType = Object.prototype.hasOwnProperty(target, key)
        ? TRIGGER_TYPE.SET
        : TRIGGER_TYPE.ADD;
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver);
      //   如果target === receiver.raw（raw指向原生对象）就说明receiver就是target的代理对象
      if (receiver.raw === target) {
        // 比较新值和旧值，只有他们不全等，并且都不是NaN的时候才触发响应的
        if (
          oldValue !== newVal &amp;amp;&amp;amp;
          (oldValue === oldValue || newVal === newVal)
        ) {
          // 执行副作用
          // 将triggerType作为trigger的第三个参数
          trigger(target, key, triggerType);
        }
      }

      return res;
    },

    // 拦截in操作符
    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    // 拦截for...in..., 获取所有对象所有key
    ownKeys(target) {
      // 将副作用函数与ITERATE_KEY关联起来
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

    // 拦截delete操作符
    deleteProperty(target, key) {
      // 如果是只读的，删除失败，提示
      if (isReadonly) {
        console.warn(`the param ${key} is readonly`);
        return true;
      }
      // 检查被操作的属性是否是对象自己的属性
      const hasKey = Object.prototype.hasOwnProperty.call(target, key);
      // 使用Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key);
      if (hasKey &amp;amp;&amp;amp; res) {
        // 只有删除的是自己的属性，并且成功删除，才回去触发trigger函数
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在getter返回属性之前，判断他是否是只读的，再去调用readonly对值包装并且返回&lt;/p&gt;
&lt;p&gt;对于shallowReadonly我们只需要把第二个参数设置为true就好了（isShallow）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function shallowReadonly(data) {
  return createReactive(data, true, true);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: { b: &quot;weng&quot; } };
const data2 = { a: { b: &quot;weng2&quot; } };

const obj = readonly(data);
const obj2 = shallowReadonly(data2);

effect(() =&amp;gt; {
  console.log(obj.a.b);
});

effect(() =&amp;gt; {
  console.log(obj2.a.b);
});

obj.a.b = &quot;kaimin&quot;;
obj2.a.b = &quot;kaimin2&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step8：原始值的响应实现&lt;/h2&gt;
&lt;p&gt;如上的step1～step7都是在讨论 引用类型的数据响应式（目前到这里只讲过obj的引用代理）&lt;/p&gt;
&lt;p&gt;因为我们上述的讨论都是基于js 的proxy的来实现的，但是Proxy不能用于原始值的代理&lt;/p&gt;
&lt;p&gt;现在说一下原始数据类型的响应式方案（Boolean，Number，BigInt，String，Symbol，undifined，null）。&lt;/p&gt;
&lt;h3&gt;实现ref&lt;/h3&gt;
&lt;p&gt;在vue3中，我们可以用ref或者reactive实现数据的响应，比较常用实现原始数据的代理就是用ref包裹实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const num = ref(2);

// 读取
console.log(num.value)
// 修改
num.value = 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参照vue3的实现方案，我们可以很容易实现一个ref的工厂函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ref函数：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ref函数
function ref(val) {
  // 在ref函数内部创建包裹对象
  const wrapper = {
    value: val,
  };
  // 将包裹对象变成响应式数据
  return reactive(wrapper);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vue3中对ref类型的数据做了很多判断，所以前提是我们需要告诉使用到这个ref的地方，或者某些场景下（例如模板中自动脱离ref的操作）。所以&lt;/p&gt;
&lt;h4&gt;加入ref标识&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// ref函数
function ref(val) {
  // 在ref函数内部创建包裹对象
  const wrapper = {
    value: val,
  };

  //   在wrapper对象上定义一个不可枚举的属性__v_isRef，设置值为true
  Object.defineProperties(wrapper, &quot;__v_isRef&quot;, {
    value: true,
    enumerable: false,
  });

  // 将包裹对象变成响应式数据
  return reactive(wrapper);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现toRef以及toRefs&lt;/h3&gt;
&lt;p&gt;在实现toRef和toRefs之前，需要了解一个Vue3常用场景，就是&lt;strong&gt;响应式丢失&lt;/strong&gt;的场景&lt;/p&gt;
&lt;p&gt;在vue3中，我们经常使用setup这么搞&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
    setup(){
        const obj = reactive({ a:1, b:2 });
        return {
            ...obj
        }    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在模板中，我们就能读取a, b的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;temlplate&amp;gt;
    &amp;lt;span&amp;gt;{{ a }} / {{ b }}&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是其实这么做会造成响应式的丢失。&lt;/p&gt;
&lt;p&gt;因为在我们的setup中，最终return的是一个 新的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
    ...obj
}
// 等价于
return {
    a: 1,
    b: 2,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用另外一个种方式描述响应式丢失就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };

const obj = reactive(data);

const newObj = {
  ...obj,
};

effect(() =&amp;gt; {
  console.log(newObj.a);
});

setTimeout(() =&amp;gt; {
  obj.a = 2; // 无法触发副作用函数的重新执行
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们最终的副作用函数（模板里头就是render函数）读取的其实是newObj对象，这个并不是响应的数据&lt;/p&gt;
&lt;p&gt;当然也是有解决的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };

const obj = reactive(data);

const newObj = {
  a: {
    get value() {
      return obj.a;
    },
  },
  b: {
    get value() {
      return obj.b;
    },
  },
};
effect(() =&amp;gt; {
  console.log(newObj.a.value);
});

setTimeout(() =&amp;gt; {
  obj.a = 2; // 无法触发副作用函数的重新执行
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对newObj的a属性或者b属性都植入一个访问器的属性value，当去读取value的值的时候，最终读取的是响应式数据obj下面的同名属性。这样就可以与副作用函数建立起响应联系&lt;/p&gt;
&lt;p&gt;因此我们可以封装一下结构体，提取公共的一个封装函数toRef&lt;/p&gt;
&lt;h4&gt;toRef&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function toRef(obj, key){
    const wrapper = {
        get value(){
            return obj[key]
        },
        set value(val){
            return obj[key] = val
        }
    }

    return wrapper
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;toRef接受两个参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;obj: 响应式数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;key: 响应式数据的一个key&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;return: 返回一个类似与ref的一个wrapper对象&lt;/p&gt;
&lt;p&gt;并且加入setter&lt;/p&gt;
&lt;p&gt;这样的话，我们就可以重新定义newObj对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const newObj = {
    a: toRef(obj, &apos;a&apos;),
    b: toRef(obj, &apos;b&apos;),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;toRefs&lt;/h4&gt;
&lt;p&gt;如上的toRef只能实现单一的key值转化&lt;/p&gt;
&lt;p&gt;如果需要转化的键值很多，就需要toRefs批量转换的能力&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function toRefs(obj){
    const res = {};
    // 使用for...in...循环遍历对象
    for(const key in obj) {
        // 逐个调用toRef完成转换
        res[key] = toRef(obj, key)
    }
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此如上的例子可以简化为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = { a: 1 };

const obj = reactive(data);

const { a } = toRefs(obj);

effect(() =&amp;gt; {
  console.log(a.value);
});

setTimeout(() =&amp;gt; {
  obj.a = 2; // 重新触发读了a的副作用函数
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;加入ref标识&lt;/h4&gt;
&lt;p&gt;因为我们无论使用toRef还是toRefs转化出来的数据，在vue3中都是被标识为ref类型的数据的。所以需要在toRef中增加和如上一致的 &lt;code&gt;__v_isRef&lt;/code&gt;标识&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key];
    },
  };

  //   在wrapper对象上定义一个不可枚举的属性__v_isRef，设置值为true
  Object.defineProperties(wrapper, &quot;__v_isRef&quot;, {
    value: true,
    enumerable: false,
  });

  return wrapper;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;脱离ref&lt;/h3&gt;
&lt;p&gt;上面的toRef包裹对象，最终生成的ref数据是需要通过&apos;.value&apos;属性访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = reactive({
    a: 1,
    b: 2,
})


const newObj = toRefs(obj);

console.log(newObj.a.value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在vue3中的模板内，他是存在自动脱离ref的能力&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;span&amp;gt; {{ newObj.a }} &amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
    const obj = reactive({
        a: 1,
        b: 2,
    })

    const newObj = toRefs(obj);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
    setup(){
        const count = ref(12121)
        
        return { count }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终这个返回的对象都会在vue3中被处理一次，将ref的数据脱离ref&lt;/p&gt;
&lt;p&gt;其实最终在setup里头返回的对象，会被vue3通过 &lt;strong&gt;proxyRefs&lt;/strong&gt;脱离ref&lt;/p&gt;
&lt;h4&gt;实现proxyRefs&lt;/h4&gt;
&lt;p&gt;结合之前加入的 &lt;code&gt;__v_isRef&lt;/code&gt;标识，使用proxy为ref创建一个代理对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function proxyRefs(target){
  return new Proxy(target, {
    get(target, key, receiver){
      const value = Reflect.get(target, key, receiver)
      // 自动脱离ref，通过__v_isRef的标识，判断是否是ref，是的话返回value值
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver){
      // 通过target读取真实的值
      const value = target[key];
      // 如果是Ref， 则设置其对应的value的属性值
      if(value.__v_isRef){
        value.value = newValue;
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以实现自动脱离ref的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const newObj = proxyRefs({ ...toRefs(obj) });

console.log(newObj.a); // 1
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>为何Vue3 Proxy 更快</title><link>https://nollieleo.github.io/posts/%E4%B8%BA%E4%BD%95vue3-proxy-%E6%9B%B4%E5%BF%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E4%B8%BA%E4%BD%95vue3-proxy-%E6%9B%B4%E5%BF%AB/</guid><description>相比于Vue2.x Object.defineProperty的响应式原理，Vue3 Proxy的优势在哪里呢。以下我们从两者源码角度分析下使用Proxy的优势。  Proxy优势：  1. ES6原生Proxy语法，更快的初始化，懒加载，不用递归的定义Object.defineProperty 2...</description><pubDate>Thu, 21 Apr 2022 17:46:14 GMT</pubDate><content:encoded>&lt;p&gt;相比于Vue2.x Object.defineProperty的响应式原理，Vue3 Proxy的优势在哪里呢。以下我们从两者源码角度分析下使用Proxy的优势。&lt;/p&gt;
&lt;p&gt;Proxy优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ES6原生Proxy语法，更快的初始化，懒加载，不用递归的定义Object.defineProperty&lt;/li&gt;
&lt;li&gt;支持动态的添加object新属性&lt;/li&gt;
&lt;li&gt;支持原生array数组操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设有如下的响应式对象时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data() {
    return { a: { b: { c: { d: { e: 11 } } } } }
}
//  以上等价于以下代码
const data = { a: { b: { c: { d: { e: 11 } } } } }
// Vue2.x
Vue.observe(data)
// Vue3：
reactive(data)

 
        Copied!
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://lq782655835.github.io/blogs/vue/vue3-code-4.why-proxy-faster.html#vue2-x-object-defineproperty&quot;&gt;#&lt;/a&gt;Vue2.x Object.defineProperty&lt;/h2&gt;
&lt;p&gt;vue2初始化时，会递归的调用Object.defineProperty。当第一层对象属性定义后，再会递归调用下一层属性的Object.defineProperty（为了是依赖收集）。所以在初始化时Vue2.x需要更多时间，去同步递归定义Object.defineProperty操作。&lt;/p&gt;
&lt;p&gt;另外一个缺点也可以看出，初始化时把data的属性递归遍历收集了，当data在业务运行过程中，动态新增属性该怎么办？在Vue2.x中由于不是懒加载，所以需要用户主动告诉Vue框架，告诉哪些新增属性是要去依赖收集的，这就是&lt;a href=&quot;https://vuejs.org/v2/api/#Vue-set&quot;&gt;Vue.set (opens new window)&lt;/a&gt;API的来由。同理&lt;a href=&quot;https://vuejs.org/v2/api/#Vue-delete&quot;&gt;Vue.delete (opens new window)&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;以下是精简主流程的源码，Vue2.x中定义响应式对象API是：&lt;code&gt;Vue.observe(data)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob = new Observer(value)
  return ob
}

export class Observer {
  constructor (value: any) {
      this.walk(value)
  }

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

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean // default: false
) {
  const dep = new Dep()
  const getter = property &amp;amp;&amp;amp; property.get
  const setter = property &amp;amp;&amp;amp; property.setter

  // 同步实时：递归子层级
  let childOb = !shallow &amp;amp;&amp;amp; observe(val)
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // ... dep依赖收集
      return value
    },
    set: function reactiveSetter (newVal) {
      setter.call(obj, newVal)
      // ... dep触发更新
    }
  })
}

 
        Copied!
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://lq782655835.github.io/blogs/vue/vue3-code-4.why-proxy-faster.html#vue3-proxy%E5%A4%84%E7%90%86&quot;&gt;#&lt;/a&gt;Vue3 Proxy处理&lt;/h2&gt;
&lt;p&gt;Vue3中定义响应式对象API是：&lt;code&gt;reactive(data)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Proxy提供了对JS的元数据编程，即可以使用JS提供的语法特性，创建新的JS语法。&lt;/p&gt;
&lt;p&gt;可以看到Vue3中定义响应式非常简单，即原生的 new Proxy(target, handlers)。此时这里没有递归调用初始化，即可看成是懒加载去依赖收集（用到才去依赖收集）。&lt;/p&gt;
&lt;p&gt;再看看代理操作baseHandlers中get定义是如何的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  return createReactiveObject(target)
}

function createReactiveObject(target: Target) {
  const observed = new Proxy(
    target,
    baseHandlers // {get, set, deleteProperty}
  )
  return observed
}

 
        Copied!
    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;get定义，主要用来依赖收集，同时如果属性的value为Object对象时，自定进行Proxy代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/baseHandlers.ts
const get = createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)

    // ...依赖收集

    // 懒加载，当访问响应式对象时，再去构造下一个Proxy(res, { getter })
    if (isObject(res)) {
      return reactive(res)
    }

    return res
  }
}

 
        Copied!
    
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>tsconfig.json全解析</title><link>https://nollieleo.github.io/posts/tsconfig-json%E5%85%A8%E8%A7%A3%E6%9E%90/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/tsconfig-json%E5%85%A8%E8%A7%A3%E6%9E%90/</guid><description>解析   TypeScript带来的类型系统以及强大的IDE支持，让前端开发也变得严谨而流畅。但TypeScript不是原生的Javascript代码，需要进行编译才能转换为Javascript代码。   tsconfig.json是编译TypeScript的配置文件，对书写TypeScript代码...</description><pubDate>Thu, 21 Apr 2022 17:42:55 GMT</pubDate><content:encoded>&lt;h1&gt;解析&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;TypeScript带来的类型系统以及强大的IDE支持，让前端开发也变得严谨而流畅。但TypeScript不是原生的Javascript代码，需要进行编译才能转换为Javascript代码。&lt;/p&gt;
&lt;p&gt;tsconfig.json是编译TypeScript的配置文件，对书写TypeScript代码十分重要。因为有些选项如果你没配置，则需要严格按照TypeScript的规则来书写，对初期使用TypeScript的同学而言，稍不留神就会书写出不符合规则的代码，从而导致编译报错，打击自信心。其实早期可以通过关闭一些规则设置，从而更愉快的从js转为ts开发。笔者根据项目实战经历来解释一些常用的编译选项，文末也会附上笔者整理的所有tsconfig.json选项的解释。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. experimentalDecorators&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;是否启用实验性的ES装饰器&lt;/code&gt;。boolean类型，默认值：false。&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/decorators.html&quot;&gt;官方解释(opens new window)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;TypeScript和ES6中引入了Class的概念，同时在&lt;a href=&quot;https://github.com/tc39/proposal-decorators&quot;&gt;stage 2 proposal (opens new window)&lt;/a&gt;提出了Java等服务器端语言早就有的装饰器模式。通过引入装饰器模式，能极大简化书写代码，把一些通用逻辑封装到装饰器中。很多库都有用到该特性，比如vue-class-component 及 vuex-class等库。&lt;strong&gt;当你使用这些库时，必须开启experimentalDecorators&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function f() {
    console.log(&quot;f(): evaluated&quot;);
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log(&quot;f(): called&quot;);
    }
}

class C {
    @f()
    method() {}
}   
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;启用 vuex-class同时需要设置&lt;code&gt;strictFunctionTypes&lt;/code&gt;选项为false&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. strictPropertyInitialization&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;是否类的非undefined属性已经在构造函数里初始化&lt;/code&gt;。 boolean类型，默认值：false&lt;/p&gt;
&lt;p&gt;直白点，就是所有的属性值，都需要赋有初始值。&lt;strong&gt;建议把strictPropertyInitialization设置为false&lt;/strong&gt;，这样就不需要定义一个变量就必须赋有初始值。对使用vuex-class库的同学，建议请把这个值设为false，绝对能省很多事。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default class Home extend Vue{
    jobId: string // 如果开启strictPropertyInitialization，则这里会报错，因为没有赋值默认值

    method1() :void {
        console.log(this.jobId)
    }
}
 
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果设置该选项为true，需要同时启用--strictNullChecks或启用--strict&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. noImplicitAny&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;有隐含的 any类型时是否报错&lt;/code&gt;。boolean值，默认值：false&lt;/p&gt;
&lt;p&gt;ts是有默认推导的，同时还有any类型，所以不是每个变量或参数定义需要明确告知类型是什么。如果开启该值，当有隐含any类型时，会报错。&lt;strong&gt;建议初次上手TypeScript，把该选项设置为false。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当开启noImplicitAny时，需要隐含当any需要明确指出
arr.find(item =&amp;gt; item.name === name) // error
arr.find((item: any) =&amp;gt; item.name === name) // ok
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. target&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;指定编译的ECMAScript目标版本&lt;/code&gt;。枚举值：&quot;ES3&quot;， &quot;ES5&quot;， &quot;ES6&quot;/ &quot;ES2015&quot;， &quot;ES2016&quot;， &quot;ES2017&quot;，&quot;ESNext&quot;。默认值： “ES3”&lt;/p&gt;
&lt;p&gt;TypeScript是ES6的超集，所以你可以使用ES6来编写ts代码（通常我们也的确这么做）。然而，当编译ts代码时，可以把ts转为ES5或更早的js代码。所以需要选择一个编译的目标版本。vue-cli3的typescript模板，设置为“ESNext”，因为现代大部分应用项目都会使用Webpack（Parcel也很棒）进行打包，Webpack会把你的代码转换成在所有浏览器中可运行的代码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;target: &quot;ESNext&quot; 是指tc39最新的&lt;a href=&quot;https://github.com/tc39/proposals&quot;&gt;ES proposed features(opens new window)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. module&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;指定生成哪个模块系统代码&lt;/code&gt;。枚举值：&quot;None&quot;， &quot;CommonJS&quot;， &quot;AMD&quot;， &quot;System&quot;， &quot;UMD&quot;， &quot;ES6&quot;， &quot;ES2015&quot;，&quot;ESNext&quot;。默认值根据--target选项不同而不同，当target设置为ES6时，默认module为“ES6”，否则为“commonjs”&lt;/p&gt;
&lt;p&gt;通常使用ES6的模块来写ts代码，然而2016年1月以前，基本上没有浏览器原生支持ES6的模块系统，所以需要转换为不同的模块系统，如：CommonJS、AMD、SystemJS等，而module选项就是指定编译使用对应的模块系统。&lt;/p&gt;
&lt;h2&gt;6. lib&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;编译过程中需要引入的库文件的列表&lt;/code&gt;。string[]类型，可选的值有很多，常用的有ES5，ES6，ESNext，DOM，DOM.Iterable、WebWorker、ScriptHost等。该值默认值是根据--target选项不同而不同。当target为ES5时，默认值为[&apos;DOM &apos;, &apos;ES5&apos;, &apos;ScriptHost&apos;];当target为ES6时，默认值为[&apos;DOM&apos;, &apos;ES6&apos;, &apos;DOM.Iterable&apos;, &apos;ScriptHost&apos;]&lt;/p&gt;
&lt;p&gt;为了在ts代码中使用ES6中的类，比如Array.form、Set、Reflect等，需要设置lib选项，在编译过程中把这些标准库引入。这样在编译过程中，如果遇到属于这些标准库的class或api时，ts编译器不会报错。&lt;/p&gt;
&lt;h2&gt;7. moduleResolution&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;决定如何处理模块&lt;/code&gt;。string类型，“node”或者“classic”，默认值：“classic”。&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/module-resolution.html&quot;&gt;官方解释(opens new window)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;说直白点，也就是遇到import { AAA } from &apos;./aaa&apos;该如何去找对应文件模块解析。对于工程项目，笔者&lt;strong&gt;建议大家使用node&lt;/strong&gt;（vue-cli3 ts模板默认设置为node策略），因为这个更符合平时我们的书写习惯以及认知（平时都是webpack打包，webpack又基于node之上）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在源文件/root/src/A.ts中import { b } from &quot;./moduleB&quot;
// 两种解析方式查找文件方式不同

// classic模块解析方式
1. /root/src/moduleB.ts
2. /root/src/moduleB.d.ts

// node模块解析方式
1. /root/src/moduleB.ts
2. /root/src/moduleB.tsx
3. /root/src/moduleB.d.ts
4. /root/src/moduleB/package.json (if it specifies a &quot;types&quot; property)
5. /root/src/moduleB/index.ts
6. /root/src/moduleB/index.tsx
7. /root/src/moduleB/index.d.ts

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. paths&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;模块名或路径映射的列表&lt;/code&gt;。Object值&lt;/p&gt;
&lt;p&gt;这是一个非常有用的选项，比如我们经常使用&apos;@/util/help&apos;来代替&apos;./src/util/help&apos;，省的每次在不同层级文件import模块时,都纠结于是&apos;./&apos;还是&apos;../&apos;。该选项告诉编译器遇到匹配的值时，去映射的路径下加载模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;baseUrl&quot;: &quot;.&quot;, // 注意：baseUrl不可少
    &quot;paths&quot;: {
      // 映射列表
      &quot;@/*&quot;: [
        &quot;src/*&quot;
      ],
      &quot;moduleA&quot;: [
        &quot;src/libs/moduleA&quot;
      ]
    }
}

// in ts code
import Setting from &apos;@/components/Setting.vue&apos; // 模块实际位置: src/components/Setting.vue
import TestModule from &apos;moduleA/index.js&apos; // 模块实际位置: src/libs/moduleA/index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. strictNullChecks&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;是否启用严格的 null检查模式&lt;/code&gt;。boolean值，默认值：false&lt;/p&gt;
&lt;p&gt;未处理的null和undefined经常会导致BUG的产生，所以TypeScript包含了strictNullChecks选项来帮助我们减少对这种情况的担忧。当启用了strictNullChecks，null和undefined获得了它们自己各自的类型null和undefined。开启该模式有助于发现并处理可能为undefined的赋值。&lt;strong&gt;如果是正式项目，笔者建议开启该选项；如果只是练手TypeScirpt，可以关闭该选项&lt;/strong&gt;，不然所有可能为null/undefined的赋值，都需要写联合类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 未开启strictNullChecks，number类型包含了null和undefined类型
let foo: number = 123;
foo = null; // Okay
foo = undefined; // Okay

// 开启strictNullChecks
let foo: string[] | undefined = arr.find(key =&amp;gt; key === &apos;test&apos;)
// foo.push(&apos;1&apos;) // error - &apos;foo&apos; is possibly &apos;undefined&apos;
foo &amp;amp;&amp;amp; foo.push(&apos;1&apos;) // okay
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictNullChecks, --strictFunctionTypes和--strictPropertyInitialization&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;10. noUnusedLocals&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;有未使用的变量时，是否抛出错误&lt;/code&gt;。boolean值，默认值： false&lt;/p&gt;
&lt;p&gt;顾名思义，当发现变量定义但没有使用时，编译不报错。eslint的rule中也有该条，&lt;strong&gt;建议正式项目将该选项开启，设置为true&lt;/strong&gt;，使得代码干净整洁。&lt;/p&gt;
&lt;h2&gt;11. noUnusedParameters&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;有未使用的参数时，是否抛出错误&lt;/code&gt;。boolean值，默认值： false&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建议正式项目开启该选项，设置为true&lt;/strong&gt;，理由同上。&lt;/p&gt;
&lt;h2&gt;12. allowJs&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;是否允许编译javascript文件&lt;/code&gt;。boolean值，默认值：false&lt;/p&gt;
&lt;p&gt;如果设置为true，js后缀的文件也会被typescript进行编译。&lt;/p&gt;
&lt;h2&gt;13. typeRoots和types&lt;/h2&gt;
&lt;p&gt;默认所有可见的&quot;@types&quot;包会在编译过程中被包含进来。如果指定了typeRoots，只有typeRoots下面的包才会被包含进来。如果指定了types，只有被列出来的npm包才会被包含进来。&lt;a href=&quot;https://www.tslang.cn/docs/handbook/tsconfig-json.html#types-typeroots-and-types&quot;&gt;详细内容可看此处(opens new window)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以指定&quot;types&quot;: []来禁用自动引入@types包&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;14. files、include和exclude&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;编译文件包含哪些文件以及排除哪些文件&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;未设置include时，编译器默认包含当前目录和子目录下所有的TypeScript文件（.ts, .d.ts 和 .tsx）。如果allowJs被设置成true，JS文件（.js和.jsx）也被包含进来。exclude排除那些不需要编译的文件或文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;compilerOptions&quot;: {},
    &quot;include&quot;: [
        &quot;src/**/*&quot;
    ],
    &quot;exclude&quot;: [
        &quot;node_modules&quot;,
        &quot;**/*.spec.ts&quot;
    ]
}    
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;配置项&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    /* 基本选项 */
    &quot;target&quot;: &quot;es5&quot;,                       // 指定 ECMAScript 目标版本: &apos;ES3&apos; (default), &apos;ES5&apos;, &apos;ES2015&apos;, &apos;ES2016&apos;, &apos;ES2017&apos;, or &apos;ESNEXT&apos;（&quot;ESNext&quot;表示最新的ES语法，包括还处在stage X阶段）
    &quot;module&quot;: &quot;commonjs&quot;,                  // 指定使用模块: &apos;commonjs&apos;, &apos;amd&apos;, &apos;system&apos;, &apos;umd&apos; or &apos;es2015&apos;
    &quot;lib&quot;: [],                             // 指定要包含在编译中的库文件
    &quot;allowJs&quot;: true,                       // 允许编译 javascript 文件
    &quot;checkJs&quot;: true,                       // 报告 javascript 文件中的错误
    &quot;jsx&quot;: &quot;preserve&quot;,                     // 指定 jsx 代码的生成: &apos;preserve&apos;, &apos;react-native&apos;, or &apos;react&apos;
    &quot;declaration&quot;: true,                   // 生成相应的 &apos;.d.ts&apos; 文件
    &quot;sourceMap&quot;: true,                     // 生成相应的 &apos;.map&apos; 文件
    &quot;outFile&quot;: &quot;./&quot;,                       // 将输出文件合并为一个文件
    &quot;outDir&quot;: &quot;./&quot;,                        // 指定输出目录
    &quot;rootDir&quot;: &quot;./&quot;,                       // 用来控制输出目录结构 --outDir.
    &quot;removeComments&quot;: true,                // 删除编译后的所有的注释
    &quot;noEmit&quot;: true,                        // 不生成输出文件
    &quot;importHelpers&quot;: true,                 // 从 tslib 导入辅助工具函数
    &quot;isolatedModules&quot;: true,               // 将每个文件做为单独的模块 （与 &apos;ts.transpileModule&apos; 类似）.

    /* 严格的类型检查选项 */
    &quot;strict&quot;: true,                        // 启用所有严格类型检查选项
    &quot;noImplicitAny&quot;: true,                 // 在表达式和声明上有隐含的 any类型时报错
    &quot;strictNullChecks&quot;: true,              // 启用严格的 null 检查
    &quot;noImplicitThis&quot;: true,                // 当 this 表达式值为 any 类型的时候，生成一个错误
    &quot;alwaysStrict&quot;: true,                  // 以严格模式检查每个模块，并在每个文件里加入 &apos;use strict&apos;

    /* 额外的检查 */
    &quot;noUnusedLocals&quot;: true,                // 有未使用的变量时，抛出错误
    &quot;noUnusedParameters&quot;: true,            // 有未使用的参数时，抛出错误
    &quot;noImplicitReturns&quot;: true,             // 并不是所有函数里的代码都有返回值时，抛出错误
    &quot;noFallthroughCasesInSwitch&quot;: true,    // 报告 switch 语句的 fallthrough 错误。（即，不允许 switch 的 case 语句贯穿）

    /* 模块解析选项 */
    &quot;moduleResolution&quot;: &quot;node&quot;,            // 选择模块解析策略： &apos;node&apos; (Node.js) or &apos;classic&apos; (TypeScript pre-1.6)。默认是classic
    &quot;baseUrl&quot;: &quot;./&quot;,                       // 用于解析非相对模块名称的基目录
    &quot;paths&quot;: {},                           // 模块名到基于 baseUrl 的路径映射的列表 例如：&quot;@/*&quot;: [&quot;src/*&quot;]
    &quot;rootDirs&quot;: [],                        // 根文件夹列表，其组合内容表示项目运行时的结构内容
    &quot;typeRoots&quot;: [],                       // 包含类型声明的文件列表
    &quot;types&quot;: [],                           // 需要包含的类型声明文件名列表
    &quot;allowSyntheticDefaultImports&quot;: true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    &quot;sourceRoot&quot;: &quot;./&quot;,                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    &quot;mapRoot&quot;: &quot;./&quot;,                       // 指定调试器应该找到映射文件而不是生成文件的位置
    &quot;inlineSourceMap&quot;: true,               // 生成单个 soucemaps 文件，而不是将 sourcemaps 生成不同的文件
    &quot;inlineSources&quot;: true,                 // 将代码与 sourcemaps 生成到一个文件中，要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    &quot;experimentalDecorators&quot;: true,        // 启用装饰器
    &quot;emitDecoratorMetadata&quot;: true,         // 为装饰器提供元数据的支持
    &quot;strictFunctionTypes&quot;: false           // 禁用函数参数双向协变检查。
  },
  /* 指定编译文件或排除指定编译文件 */
  &quot;include&quot;: [
      &quot;src/**/*&quot;
  ],
  &quot;exclude&quot;: [
      &quot;node_modules&quot;,
      &quot;**/*.spec.ts&quot;
  ],
  &quot;files&quot;: [
    &quot;core.ts&quot;,
    &quot;sys.ts&quot;
  ],
  // 从另一个配置文件里继承配置
  &quot;extends&quot;: &quot;./config/base&quot;,
  // 让IDE在保存文件的时候根据tsconfig.json重新生成文件
  &quot;compileOnSave&quot;: true // 支持这个特性需要Visual Studio 2015， TypeScript1.8.4以上并且安装atom-typescript插件
}
 
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>vue2~vue3迁移记录</title><link>https://nollieleo.github.io/posts/vue2-vue3%E8%BF%81%E7%A7%BB%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/vue2-vue3%E8%BF%81%E7%A7%BB%E8%AE%B0%E5%BD%95/</guid><description>记录vue2到vue3版本迁移事项      api变化   全局api变化   new Vue --- createApp 🚩 ➕   vue2中没有app 的概念，通过Vue的统一构造函数进行全局的配置，单页应用中无法创建多个不同全局配置的根应用（）   vue3中有了app概念，通过创建返回...</description><pubDate>Sat, 16 Apr 2022 10:59:12 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;记录vue2到vue3版本迁移事项&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/migration/introduction.html&quot;&gt;官方迁移文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;api变化&lt;/h1&gt;
&lt;h2&gt;全局api变化&lt;/h2&gt;
&lt;h3&gt;new Vue ---&amp;gt; createApp 🚩 ➕&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;vue2中没有app 的概念，通过&lt;code&gt;Vue&lt;/code&gt;的统一构造函数进行全局的配置，单页应用中无法创建多个不同全局配置的根应用（&lt;a href=&quot;https://v3.cn.vuejs.org/guide/migration/global-api.html#%E4%B8%80%E4%B8%AA%E6%96%B0%E7%9A%84%E5%85%A8%E5%B1%80-api-createapp&quot;&gt;会造成全局配置污染&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;vue3中有了app概念，通过&lt;a href=&quot;https://github.com/vuejs/core/blob/4951d4352605eb9f4bcbea40ecc68fc6cbc3dce2/packages/runtime-dom/src/index.ts#L53&quot;&gt;createApp&lt;/a&gt;创建返回的实例&lt;strong&gt;暴露全局api&lt;/strong&gt;，解决了vue2中的问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Vue2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Vue from &quot;vue&quot;;
import App from &apos;./App.vue&apos;

new Vue({
  render: (h) =&amp;gt; h(App)
}).$mount(&quot;#app&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vue3&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;createApp 生成一个app实例，该实例拥有全局的可配置上下文&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;

const app = createApp(App).mount(&apos;#app&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以现在所有全局会改变Vue行为的api都改到了app应用实例上了&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220416124853316.png&quot;&amp;gt;&lt;/p&gt;
&lt;h3&gt;internal Apis  🚩&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/migration/global-api-treeshaking.html#_2-x-%E8%AF%AD%E6%B3%95&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;vue2中不少global-api是作为静态函数直接挂在构造函数上的，例如&lt;code&gt;Vue.nextTick()&lt;/code&gt;，如果我们从未在代码中用过它们，就会形成所谓的&lt;code&gt;dead code&lt;/code&gt;，这类global-api造成的&lt;code&gt;dead code&lt;/code&gt;无法使用webpack的tree-shaking排除掉。&lt;/p&gt;
&lt;p&gt;vue3中做了相应的变化，将它们抽取成为独立函数，这样打包工具的摇树优化可以将这些dead code排除掉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Vue2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Vue from &apos;vue&apos;;
Vue.nextTick(()=&amp;gt;{})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vue3&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { nextTick } from &apos;vue&apos;
nextTick(() =&amp;gt; {})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方文档列出受影响的api&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20220416125617961]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;app.config&lt;/code&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;globalProperties&lt;/code&gt;  🚩➕&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;添加可在程序内的任何组件实例中访问的全局属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Vue2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Vue from &apos;vue&apos;
Vue.prototype.$http = axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vue3&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Vue3
const app = Vue.createApp({})
app.config.globalProperties.$http = axios
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;devtools&lt;/code&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;配置是否允许 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue-devtools&quot;&gt;vue-devtools&lt;/a&gt; 检查代码。开发版本默认为 &lt;code&gt;true&lt;/code&gt;，生产版本默认为 &lt;code&gt;false&lt;/code&gt;。生产版本设为 &lt;code&gt;true&lt;/code&gt; 可以启用检查。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;- Vue.config.devtools = true
+ app.config.devtools = true    

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;errorHandler&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息，比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}
+ app.config.errorHandler = (err, vm, info) =&amp;gt; {
  // handle error
  // `info` 是 Vue 特定的错误信息，比如错误所在的生命周期钩子
  // 这里能发现错误
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时，可获取错误信息和 Vue 实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;错误追踪服务 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fsentry.io%2F&quot;&gt;Sentry&lt;/a&gt; 和 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdocs.bugsnag.com%2Fplatforms%2Fbrowsers%2Fvue%2F&quot;&gt;Bugsnag&lt;/a&gt; 都通过此选项提供了官方支持。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;code&gt;warnHandler&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- Vue.config.warnHandler = function (msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}
+ app.config.warnHandler = function(msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为 Vue 的运行时警告赋予一个自定义处理函数。注意这只会在开发者环境下生效，在生产环境下它会被忽略。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;code&gt;isCustomElement&lt;/code&gt; ➕&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;替代掉Vue2.x的ignoredElements&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- Vue.config.ignoredElements = [
  // 用一个 `RegExp` 忽略所有“ion-”开头的元素
  // 仅在 2.5+ 支持
  /^ion-/
]

// 一些组件以&apos;ion-&apos;开头将会被解析为自定义组件
+ app.config.isCustomElement = tag =&amp;gt; tag.startsWith(&apos;ion-&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;指定一个方法来识别在Vue之外定义的自定义组件(例如，使用&lt;a href=&quot;https://link.juejin.cn?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2019%2F08%2Fweb_components.html&quot;&gt;Web Component API&lt;/a&gt;)。如果组件符合这个条件，它就不需要本地或全局注册，Vue也不会抛出关于Unknown custom element的警告&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，这个函数中不需要匹配所有原生HTML和SVG标记—Vue解析器会自动执行此检查&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;code&gt;optionMergeStrategies&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const app = Vue.createApp({
  mounted() {
    console.log(this.$options.hello)
  }
})

app.config.optionMergeStrategies.hello = (parent, child, vm) =&amp;gt; {
  return `Hello, ${child}`
}

app.mixin({
  hello: &apos;Vue&apos;
})

// &apos;Hello, Vue

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;定义自定义选项的合并策略。&lt;/p&gt;
&lt;p&gt;合并策略接收在&lt;strong&gt;父实例&lt;/strong&gt;options和∗∗子实例∗∗options和&lt;strong&gt;子实例&lt;/strong&gt;options和∗∗子实例∗∗options，分别作为第一个和第二个参数。上下文Vue实例作为第三个参数传递&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;【自定义选项合并策略】&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fdocs-next%2Fblob%2Fmaster%2Fsrc%2Fguide%2Fmixins.md%23custom-option-merge-strategies&quot;&gt;mixin&lt;/a&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const app = Vue.createApp({
  custom: &apos;hello!&apos;
})

app.config.optionMergeStrategies.custom = (toVal, fromVal) =&amp;gt; {
  console.log(fromVal, toVal)
  // =&amp;gt; &quot;goodbye!&quot;, undefined
  // =&amp;gt; &quot;hello!&quot;, &quot;goodbye!&quot;
  return fromVal || toVal
}

app.mixin({
  custom: &apos;goodbye!&apos;,
  created() {
    console.log(this.$options.custom) // =&amp;gt; &quot;hello!&quot;
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;optionMergeStrategies先获取到子实例的$options的mixin而没有父实例【custom第一次改变从undefined到goodbye---&amp;gt;打印&quot;goodbye!&quot;, undefined】&lt;/li&gt;
&lt;li&gt;父实例的options替换掉子实例的options替换掉子实例的options替换掉子实例的options【custom第二次从goodbye到hello!---&amp;gt;打印了&quot;hello&quot;, &quot;goodbye!&quot;】&lt;/li&gt;
&lt;li&gt;最后在打印app.config.optionMergeStrategies.custom返回的父实例的$options&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;无论如何this.options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options,然后返回计算等操作得到所需要的值】optionMergeStrategies合并$options变化&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;code&gt;performance&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- Vue.config.performance=true;
+ app.config.performance=true;

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;app.directive&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fdocs-next%2Fblob%2Fmaster%2Fsrc%2Fguide%2Fcustom-directive.md&quot;&gt;教程文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;注册或获取全局指令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;
const app = createApp({})

// 注册
app.directive(&apos;my-directive&apos;, {
  // 指令的生命周期
  // 在绑定元素的父组件被挂载之前调用
  beforeMount(el, binding, vnode) {},
  // 在挂载绑定元素的父组件时调用
  mounted(el, binding, vnode) {},
  // 在更新包含组件的VNode之前调用
  beforeUpdate(el, binding, vnode, prevNode) {},
  // 组件的VNode及其子组件的VNode更新之后调用
  updated(el, binding, vnode, prevNode) {},
  // 在卸载绑定元素的父组件之前调用
  beforeUnmount(el, binding, vnode) {},
  // 在卸载绑定元素的父组件时调用
  unmounted(el, binding, vnode) {}
})

// 注册 (指令函数)
app.directive(&apos;my-directive&apos;, (el, binding, vnode, prevNode) =&amp;gt; {
  // 这里将会被 `mounted` 和 `updated` 调用
})

// getter，返回已注册的指令
const myDirective = app.directive(&apos;my-directive&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;el: 指令绑定到的元素。这可以用来直接操作DOM。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;binding【包含下列属性的对象】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;instance：使用指令的组件的实例&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;value：指令的绑定值，例如：&lt;code&gt;v-my-directive=&quot;1 + 1&quot;&lt;/code&gt;中，绑定值为 &lt;code&gt;2&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;oldValue：指令绑定的前一个值，仅在 &lt;code&gt;beforeUpdate&lt;/code&gt; 和 &lt;code&gt;updated&lt;/code&gt; 钩子中可用。无论值是否改变都可用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;arg：传给指令的参数，可选。例如 &lt;code&gt;v-my-directive:foo&lt;/code&gt; 中，参数为 &lt;code&gt;&quot;foo&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;modifiers：一个包含修饰符的对象。例如：&lt;code&gt;v-my-directive.foo.bar&lt;/code&gt; 中，修饰符对象为 &lt;code&gt;{ foo: true, bar: true }&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dir：一个对象，在注册指令时作为参数传递;  举个例子，看下面指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.directive(&apos;focus&apos;, {
  mounted(el) {
    el.focus()
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;dir就是下面的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  mounted(el) {
    el.focus()
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vnode&lt;/p&gt;
&lt;p&gt;编译生成的虚拟节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;prevNode&lt;/p&gt;
&lt;p&gt;前一个虚拟节点，仅在beforeUpdate和updated钩子中可用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tips:除了 &lt;code&gt;el&lt;/code&gt; 之外，其它参数都应该是只读的，切勿进行修改。如果需要在钩子之间共享数据，建议通过元素的 &lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FHTMLElement%2Fdataset&quot;&gt;&lt;code&gt;dataset&lt;/code&gt;&lt;/a&gt; 来进行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;app.unmount&lt;/code&gt; 🚩➕&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在所提供的DOM元素上卸载应用程序实例的根组件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;

const app = createApp({})
// 做一些必要的准备
app.mount(&apos;#my-app&apos;)

// 应用程序将在挂载后5秒被卸载
setTimeout(() =&amp;gt; app.unmount(&apos;#my-app&apos;), 5000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;app.component&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Vue2.x【注册或获取全局组件。注册还会自动使用给定的 &lt;code&gt;id&lt;/code&gt; 设置组件的名称】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注册组件，传入一个选项对象 (自动调用 Vue.extend) 

Vue.component(&apos;my-component&apos;, { /* ... */ }) 

// 获取注册的组件 (始终返回构造器) 
var MyComponent = Vue.component(&apos;my-component&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vue3【注册或获取全局组件. 注册还会自动使用给定的 name组件 设置组件的名称】&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fcodepen.io%2Fteam%2FVue%2Fpen%2FrNVqYvM&quot;&gt;全局组件&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;基本vue2写法一致&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;

const app = createApp({})

// 注册组件，传入一个选项对象
app.component(&apos;my-component&apos;, {
  /* ... */
})

// 获取注册的组件 (始终返回构造器) 
const MyComponent = app.component(&apos;my-component&apos;, {})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;watch&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以&lt;code&gt;.&lt;/code&gt;分割的表达式不再被watch支持，可以使用计算函数作为&lt;em&gt;w&lt;strong&gt;a&lt;/strong&gt;t&lt;strong&gt;c&lt;/strong&gt;h&lt;/em&gt;支持，可以使用计算函数作为watch参数实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vue2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;watch: {
    &quot;data.id&quot;(val) {  
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = reactive({
  id:121
});
watch(data.id,()=&amp;gt;{})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;emits ➕&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/migration/emits-option.html#emits-%E9%80%89%E9%A1%B9&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;emits 可以是数组或对象&lt;/li&gt;
&lt;li&gt;触发自定义事件&lt;/li&gt;
&lt;li&gt;如果emits是对象，则允许我们配置和事件验证。验证函数应返回布尔值，以表示事件参数是否有效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;依赖注入provide/inject&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;与vue2中使用方法没有什么很大的差异，但是亮点是可以提供相应式的数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;基础使用方法&lt;/h3&gt;
&lt;p&gt;看文档&lt;/p&gt;
&lt;h3&gt;响应式方法  🚩➕&lt;/h3&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ref, reactive } from &apos;vue&apos;

// 提供者
setup() {
  const book = reactive({
    title: &apos;Vue 3 Guide&apos;,
    author: &apos;Vue Team&apos;
  })
  const year = ref(&apos;2020&apos;)

 /*提供reactive响应式*/
  provide(&apos;book&apos;, book)
 /*提供ref响应式*/
  provide(&apos;year&apos;, year)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;弊端&lt;/h4&gt;
&lt;p&gt;提供相应式的方法之后，子组建就可以尝试对这个引用值进行修改，从而导致单向数据流通的紊乱&lt;/p&gt;
&lt;p&gt;为了避免这种情况，基于provide进行封装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @Author: 翁恺敏
 * @Date: 2022-04-10 16:11:32
 * @LastEditors: 翁恺敏
 * @LastEditTime: 2022-04-16 15:21:11
 * @FilePath: /vue3-vite-test/src/hooks/useProvide.ts
 * @Description: provide （observerable provide）
 */
import { provide, readonly, reactive, ref, isReactive } from &quot;vue&quot;;
import forEach from &quot;lodash/forEach&quot;;

const useProvide = (shouldReactive: Boolean = true) =&amp;gt; {
  const handleProvide = (providers: Record&amp;lt;string, any&amp;gt;): void =&amp;gt; {
    forEach(providers, (value, key) =&amp;gt; {
      let provideValue;
      const isFunction = typeof value === &quot;function&quot;;
      if (!isFunction) {
        provideValue =
          shouldReactive &amp;amp;&amp;amp; !isReactive(value) ? ref(value) : value;
      }
      provide(key, isFunction ? provideValue : readonly(provideValue));
    });
  };

  return handleProvide;
};

export default useProvide;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;defineAsyncComponent(异步组件)&lt;/h2&gt;
&lt;h1&gt;生命周期函数&lt;/h1&gt;
&lt;h2&gt;&lt;strong&gt;与 2.x 版本生命周期相对应的组合式 API&lt;/strong&gt; 🚩➕&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;&lt;code&gt;beforeCreate&lt;/code&gt;&lt;/s&gt; -&amp;gt; 使用 &lt;code&gt;setup()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;&lt;code&gt;created&lt;/code&gt;&lt;/s&gt; -&amp;gt; 使用 &lt;code&gt;setup()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beforeMount&lt;/code&gt; -&amp;gt; &lt;code&gt;onBeforeMount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mounted&lt;/code&gt; -&amp;gt; &lt;code&gt;onMounted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beforeUpdate&lt;/code&gt; -&amp;gt; &lt;code&gt;onBeforeUpdate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updated&lt;/code&gt; -&amp;gt; &lt;code&gt;onUpdated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beforeDestroy&lt;/code&gt; -&amp;gt; &lt;code&gt;onBeforeUnmount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destroyed&lt;/code&gt; -&amp;gt; &lt;code&gt;onUnmounted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;errorCaptured&lt;/code&gt; -&amp;gt; &lt;code&gt;onErrorCaptured&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;非组合式api&lt;/h2&gt;
&lt;p&gt;只是改了名字&lt;/p&gt;
&lt;h1&gt;内置指令变化&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;v-model&lt;/code&gt; 🚩&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/component-basics.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;组件使用&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;vue2 --- v-model&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ChildComponent v-model=&quot;pageTitle&quot; /&amp;gt;

&amp;lt;!-- 简写: --&amp;gt;
&amp;lt;ChildComponent :value=&quot;pageTitle&quot; @input=&quot;pageTitle = $event&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要将属性或事件名称更改为其他名称，则需要在 &lt;code&gt;ChildComponent&lt;/code&gt; 组件中添加 &lt;code&gt;model&lt;/code&gt; 选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ParentComponent.vue --&amp;gt;
&amp;lt;ChildComponent v-model=&quot;pageTitle&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ChildComponent.vue
export default {
  model: {
    prop: &apos;title&apos;,
    event: &apos;change&apos;
  },
  props: {
    // 这将允许 `value` 属性用于其他用途
    value: String,
    // 使用 `title` 代替 `value` 作为 model 的 prop
    title: {
      type: String,
      default: &apos;Default title&apos;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，在这个例子中 &lt;code&gt;v-model&lt;/code&gt; 的简写如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ChildComponent :title=&quot;pageTitle&quot; @change=&quot;pageTitle = $event&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue2 --- v-bind.sync&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在某些情况下，我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 &lt;code&gt;v-model&lt;/code&gt; 绑定 prop 的情况)。建议使用 &lt;code&gt;update:myPropName&lt;/code&gt; 抛出事件。例如，对于在上一个示例中带有 &lt;code&gt;title&lt;/code&gt; prop 的 &lt;code&gt;ChildComponent&lt;/code&gt;，我们可以通过下面的方式将分配新 value 的意图传达给父级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.$emit(&apos;update:title&apos;, newValue)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要的话，父级可以监听该事件并更新本地 data property。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ChildComponent :title=&quot;pageTitle&quot; @update:title=&quot;pageTitle = $event&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了方便起见，我们可以使用 &lt;code&gt;.sync&lt;/code&gt; 修饰符来缩写，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ChildComponent :title.sync=&quot;pageTitle&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3 --- v-model&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ChildComponent v-model=&quot;pageTitle&quot; /&amp;gt;

&amp;lt;!-- 简写: --&amp;gt;

&amp;lt;ChildComponent
  :modelValue=&quot;pageTitle&quot;
  @update:modelValue=&quot;pageTitle = $event&quot;
/&amp;gt;

&amp;lt;ChildComponent v-model:title=&quot;pageTitle&quot; /&amp;gt;

&amp;lt;!-- 简写: --&amp;gt;

&amp;lt;ChildComponent
  :title=&quot;pageTitle&quot;
  @update:title=&quot;pageTitle = $event&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;v-is&lt;/code&gt; 🚩➕&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;V-is 仅限于indom的模版&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Vue3中只能使用is在内置的component组件上面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;vue2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
  &amp;lt;tr :is=&quot;&apos;my-component&apos;&quot;&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;vue3&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;:is不再适用于indom的模版&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
  &amp;lt;tr v-is=&quot;&apos;my-component&apos;&quot;&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;v-slot&lt;/code&gt; 🚩➕&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;插槽在vue3中统一了vue2的slot和scope-slot&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vue2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--  子组件中：--&amp;gt;
&amp;lt;slot name=&quot;title&quot;&amp;gt;&amp;lt;/slot&amp;gt;

&amp;lt;!--  父组件中：--&amp;gt;
&amp;lt;template slot=&quot;title&quot;&amp;gt;
    &amp;lt;h1&amp;gt;歌曲：成都&amp;lt;/h1&amp;gt;
&amp;lt;template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们要&lt;strong&gt;在 slot 上面绑定数据，可以使用作用域插槽&lt;/strong&gt;，实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 子组件
&amp;lt;slot name=&quot;content&quot; :data=&quot;data&quot;&amp;gt;&amp;lt;/slot&amp;gt;
export default {
    data(){
        return{
            data:[&quot;走过来人来人往&quot;,&quot;不喜欢也得欣赏&quot;,&quot;陪伴是最长情的告白&quot;]
        }
    }
}

&amp;lt;!-- 父组件中使用 --&amp;gt;
&amp;lt;template slot=&quot;content&quot; slot-scope=&quot;scoped&quot;&amp;gt;
    &amp;lt;div v-for=&quot;item in scoped.data&quot;&amp;gt;{{item}}&amp;lt;/div&amp;gt;
&amp;lt;template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Vue2.x 中具名插槽和作用域插槽分别使用&lt;code&gt;slot&lt;/code&gt;和&lt;code&gt;slot-scope&lt;/code&gt;来实现， 在 Vue3.0 中将&lt;code&gt;slot&lt;/code&gt;和&lt;code&gt;slot-scope&lt;/code&gt;进行了合并同意使用。 Vue3.0 中&lt;code&gt;v-slot&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 父组件中使用 --&amp;gt;
 &amp;lt;template v-slot:content=&quot;scoped&quot;&amp;gt;
   &amp;lt;div v-for=&quot;item in scoped.data&quot;&amp;gt;{{item}}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;!-- 也可以简写成： --&amp;gt;
&amp;lt;template #content=&quot;{data}&quot;&amp;gt;
    &amp;lt;div v-for=&quot;item in data&quot;&amp;gt;{{item}}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;自定义指令变化 🚩➕&lt;/h1&gt;
&lt;p&gt;vue3中指令api和组件保持一致，具体表现在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bind → &lt;strong&gt;beforeMount&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;inserted → &lt;strong&gt;mounted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;beforeUpdate&lt;/strong&gt;: new! 元素自身更新前调用, 和组件生命周期钩子很像&lt;/li&gt;
&lt;li&gt;update → removed! 和updated基本相同，因此被移除之，使用updated代替。&lt;/li&gt;
&lt;li&gt;componentUpdated → &lt;strong&gt;updated&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;beforeUnmount&lt;/strong&gt; new! 和组件生命周期钩子相似, 元素将要被移除之前调用。&lt;/li&gt;
&lt;li&gt;unbind  →  &lt;strong&gt;unmounted&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;内置组件&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;teleport&lt;/code&gt; 🚩➕&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/vuejs/core/blob/main/packages/runtime-core/src/components/Teleport.ts&quot;&gt;源码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Props&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;to&lt;/code&gt; - &lt;code&gt;string&lt;/code&gt; 必填属性，必须是一个有效的query选择器，或者是元素(如果在浏览器环境中使用）。中的内容将会被放置到指定的目标元素中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 正确的 --&amp;gt;
&amp;lt;teleport to=&quot;#some-id&quot; /&amp;gt;
&amp;lt;teleport to=&quot;.some-class&quot; /&amp;gt;
 /*元素*/
&amp;lt;teleport to=&quot;[data-teleport]&quot; /&amp;gt;

&amp;lt;!-- 错误的 --&amp;gt;
&amp;lt;teleport to=&quot;h1&quot; /&amp;gt;
&amp;lt;teleport to=&quot;some-string&quot; /&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;disabled&lt;/code&gt;  - &lt;code&gt;boolean&lt;/code&gt; 这是一个可选项 ，做一个是可以用来禁用的功能，这意味着它的插槽内容不会移动到任何地方，而是按没有&lt;code&gt;teleport&lt;/code&gt;组件一般来呈现【默认为false】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;teleport to=&quot;#popup&quot; :disabled=&quot;displayVideoInline&quot;&amp;gt;
  &amp;lt;h1&amp;gt;999999&amp;lt;/h1&amp;gt;
&amp;lt;/teleport&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这将移动实际的DOM节点，而不是销毁和重新创建，并且还将保持任何组件实例是活动的。所有有状态HTML元素(比如一个正在播放的视频)将保持它们的状态。【控制displayVideoInline并不是销毁重建，它保持实例是存在的，不会被注销】&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;Suspense&lt;/code&gt; 🚩➕&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/migration/suspense.html#%E4%BB%8B%E7%BB%8D&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;官方文档目前还是标注为试验性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;该 &lt;code&gt;&amp;lt;suspense&amp;gt;&lt;/code&gt; 组件提供了另一个方案，允许将等待过程提升到组件树中处理，而不是在单个组件中。&lt;/p&gt;
&lt;p&gt;自带两个 &lt;code&gt;slot&lt;/code&gt; 分别为 &lt;code&gt;default、fallback&lt;/code&gt;。顾名思义，当要加载的组件不满足状态时,&lt;code&gt;Suspense&lt;/code&gt; 将回退到 &lt;code&gt;fallback&lt;/code&gt;状态一直到加载的组件满足条件，才会进行渲染。&lt;/p&gt;
&lt;p&gt;Suspense.vue&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;loadAsyncComponent&quot;&amp;gt;点击加载异步组件&amp;lt;/button&amp;gt;
  &amp;lt;Suspense v-if=&quot;loadAsync&quot;&amp;gt;
    &amp;lt;template #default&amp;gt;
      &amp;lt;!-- 加载对应的组件 --&amp;gt;
      &amp;lt;MAsynComp&amp;gt;&amp;lt;/MAsynComp&amp;gt;
    &amp;lt;/template&amp;gt;
    &amp;lt;template #fallback&amp;gt;
      &amp;lt;div class=&quot;loading&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/template&amp;gt;
  &amp;lt;/Suspense&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import { ref, defineAsyncComponent } from &apos;vue&apos;

export default {
  components: {
    MAsynComp: defineAsyncComponent(() =&amp;gt; import(&apos;./AsynComp.vue&apos;)),
  },
  setup() {
    const loadAsync = ref(false)
    const loadAsyncComponent = () =&amp;gt; {
      loadAsync.value = true
    }
    return {
      loadAsync,
      loadAsyncComponent,
    }
  },
}
&amp;lt;/script&amp;gt;

&amp;lt;style lang=&quot;less&quot; scoped&amp;gt;

button {
  padding: 12px 12px;
  background-color: #1890ff;
  outline: none;
  border: none;
  border-radius: 4px;
  color: #fff;
  cursor: pointer;
}
.loading {
  position: absolute;
  width: 36px;
  height: 36px;
  top: 50%;
  left: 50%;
  margin: -18px 0 0 -18px;
  background-image: url(&apos;../assets/loading.png&apos;);
  background-size: 100%;
  animation: rotate 1.4s linear infinite;
}
@keyframes rotate {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
&amp;lt;/style&amp;gt;


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AsynComp.vue&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;this is async component&amp;lt;/h1&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import { setup } from &apos;vue&apos;
export default {
  name: &apos;AsyncComponent&apos;,
  async setup() {
    const sleep = (time) =&amp;gt; {
      return new Promise((reslove, reject) =&amp;gt; {
        setTimeout(() =&amp;gt; {
          reslove()
        }, time)
      })
    }
    await sleep(3000) //模拟数据请求
  },
}
&amp;lt;/script&amp;gt;


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;Fragments&lt;/code&gt; 🚩➕&lt;/h2&gt;
&lt;p&gt;Vue3.0组件中可以允许有多个根组件，避免了多个没必要的div渲染&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;头部&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;内容&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;少了很多没有意义的div&lt;/li&gt;
&lt;li&gt;可以实现平级递归，对实现tree组件有很大帮助&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;相应式系统 🚩🚩🚩&lt;/h1&gt;
&lt;h2&gt;响应式系统 API&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;reactive&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;desc: 接收一个普通对象然后返回该普通对象的响应式代理【等同于 2.x 的 &lt;code&gt;Vue.observable()&lt;/code&gt;】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ssss&lt;/p&gt;
&lt;p&gt;tips:&lt;code&gt;Proxy&lt;/code&gt;对象是目标对象的一个代理器，任何对目标对象的操作（实例化，添加/删除/修改属性等等），都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;响应式转换是“深层的”：会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现，返回的代理对象&lt;strong&gt;不等于&lt;/strong&gt;原始对象。建议仅使用代理对象而避免依赖原始对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;reactive&lt;/code&gt; 类的 api 主要提供了将复杂类型的数据处理成响应式数据的能力，其实这个复杂类型是要在&lt;code&gt;object array map set weakmap weakset&lt;/code&gt; 这五种之中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;因为是组合函数【对象】，所以必须始终保持对这个所返回对象的引用以保持响应性【不能解构该对象或者展开】例如 &lt;code&gt;const { x, y } = useMousePosition()&lt;/code&gt;或者&lt;code&gt;return { ...useMousePosition() }&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
        
    return pos
}




&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://v3.cn.vuejs.org/guide/composition-api-introduction.html#torefs&quot;&gt;&lt;code&gt;toRefs&lt;/code&gt;&lt;/a&gt; API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref【把对象转成了ref】。&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt; function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return toRefs(pos)
}

// x &amp;amp; y 现在是 ref 形式了!
const { x, y } = useMousePosition()

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 &lt;code&gt;.value&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const count = ref(0)
console.log(count.value) // 0

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果传入 ref 的是一个对象，将调用 &lt;code&gt;reactive&lt;/code&gt; 方法进行深层响应转换&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;陷阱&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt; 中&lt;code&gt;return&lt;/code&gt;返回会自动解套【在模板中不需要&lt;code&gt;.value&lt;/code&gt;】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ref 作为 reactive 对象的 property 被访问或修改时，也将自动解套 &lt;code&gt;.value&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const count = ref(0)
/*当做reactive的对象属性----解套*/
const state = reactive({
  count,
})
/* 不需要.value*/
console.log(state.count) // 0

/*修改reactive的值*/
state.count = 1
/*修改了ref的值*/
console.log(count.value) // 1

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意如果将一个新的 ref 分配给现有的 ref， 将替换旧的 ref&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*创建一个新的ref*/
const otherCount = ref(2)

/*赋值给reactive的旧的ref，旧的会被替换掉*/
state.count = otherCount
/*修改reactive会修改otherCount*/
console.log(state.count) // 2
/*修改reactive会count没有被修改 */
console.log(count.value) // 1

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套在 reactive &lt;code&gt;Object&lt;/code&gt; 中时，ref 才会解套。从 &lt;code&gt;Array&lt;/code&gt; 或者 &lt;code&gt;Map&lt;/code&gt; 等原生集合类中访问 ref 时，不会自动解套【自由数据类型是Object才会解套，&lt;code&gt;array &lt;/code&gt; &lt;code&gt;map &lt;/code&gt; &lt;code&gt;set &lt;/code&gt;  &lt;code&gt;weakmap &lt;/code&gt; &lt;code&gt;weakset&lt;/code&gt;集合类 &lt;strong&gt;访问 ref 时，不会自动解套&lt;/strong&gt;】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const arr = reactive([ref(0)])
// 这里需要 .value
console.log(arr[0].value)

const map = reactive(new Map([[&apos;foo&apos;, ref(0)]]))
// 这里需要 .value
console.log(map.get(&apos;foo&apos;).value)

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;心智负担上 &lt;code&gt;ref &lt;/code&gt;  vs  &lt;code&gt;reactive&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在普通 JavaScript 中区别&lt;code&gt;声明基础类型变量&lt;/code&gt;与&lt;code&gt;对象变量&lt;/code&gt;时一样区别使用 &lt;code&gt;ref&lt;/code&gt; 和 &lt;code&gt;reactive&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所有的地方都用 &lt;code&gt;reactive&lt;/code&gt;，然后记得在组合函数返回响应式对象时使用 &lt;code&gt;toRefs&lt;/code&gt;。这降低了一些关于 ref 的心智负担&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;readonly&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;传入一个对象（响应式或普通）或 ref，返回一个原始对象的&lt;strong&gt;只读&lt;/strong&gt;代理。一个只读的代理是“深层的”，对象内部任何嵌套的属性也都是只读的【返回一个永远不会变的只读代理】【场景可以参数比对等】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() =&amp;gt; {
  // 依赖追踪
  console.log(copy.count)
})

// original 上的修改会触发 copy 上的侦听
original.count++

// 无法修改 copy 并会被警告
copy.count++ // warning!

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;reactive&lt;/code&gt;响应式系统工具集&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;isProxy&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;检查一个对象是否是由 &lt;code&gt;reactive&lt;/code&gt; 或者 &lt;code&gt;readonly&lt;/code&gt; 方法创建的代理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;isReactive&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;检查一个对象是否是由 &lt;code&gt;reactive&lt;/code&gt; 创建的响应式代理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { reactive, isReactive } from &apos;vue&apos;
const state = reactive({
      name: &apos;John&apos;
    })
console.log(isReactive(state)) // -&amp;gt; true

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;如果这个代理是由 &lt;code&gt;readonly&lt;/code&gt; 创建的，但是又被 &lt;code&gt;reactive&lt;/code&gt; 创建的另一个代理包裹了一层，那么同样也会返回 &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { reactive, isReactive, readonly } from &apos;vue&apos;
const state = reactive({
      name: &apos;John&apos;
    })
// 用readonly创建一个只读响应式对象plain
const plain = readonly({
    name: &apos;Mary&apos;
})
//readonly创建的，所以isReactive为false
console.log(isReactive(plain)) // -&amp;gt; false  

// reactive创建的响应式代理对象包裹一层readonly,isReactive也是true,isReadonly也是true
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -&amp;gt; true

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;isReadonly&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;检查一个对象是否是由 &lt;code&gt;readonly&lt;/code&gt; 创建的只读代理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;code&gt;reactive&lt;/code&gt;高级响应式系统API&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;toRaw&lt;/code&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;返回由 &lt;code&gt;reactive&lt;/code&gt; 或 &lt;code&gt;readonly&lt;/code&gt; 方法转换成响应式代理的普通对象。这是一个还原方法，可用于临时读取，访问不会被代理/跟踪，写入时也不会触发更改。不建议一直持有原始对象的引用【&lt;code&gt;**不建议赋值给任何变量**&lt;/code&gt;】。请谨慎使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;被**&lt;code&gt;toRaw&lt;/code&gt;**之后的对象是没有被代理/跟踪的的普通对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true
console.log(toRaw(reactiveFoo) !== reactiveFoo) // true

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;markRaw&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;显式标记一个对象为“永远不会转为响应式代理”，函数返回这个对象本身。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;【&lt;code&gt;markRaw&lt;/code&gt;传入对象，返回的值是永远不会被转为响应式代理的】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = markRaw({
    name: &apos;Mary&apos;
})
console.log(isReactive(reactive(foo))) // false

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;被 markRaw 标记了，即使在响应式对象中作属性，也依然不是响应式的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;code&gt;markRaw&lt;/code&gt;注意点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;markRaw和 shallowXXX 一族的 API允许&lt;strong&gt;选择性的&lt;/strong&gt;覆盖reactive或者readonly 默认创建的 &quot;深层的&quot; 特性【响应式】/或者使用无代理的普通对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设计这种「浅层读取」有很多原因&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一些值的实际上的用法非常简单，并没有必要转为响应式【例如三方库的实例/省市区json/Vue组件对象】&lt;/li&gt;
&lt;li&gt;当渲染一个元素数量庞大，但是数据是不可变的，跳过 Proxy 的转换可以带来性能提升&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这些 API 被认为是高级的，是因为这种特性仅停留在根级别，所以如果你将一个嵌套的，没有 &lt;code&gt;markRaw&lt;/code&gt; 的对象设置为 reactive 对象的属性，在重新访问时，你又会得到一个 Proxy 的版本，在使用中最终会导致&lt;strong&gt;标识混淆&lt;/strong&gt;的严重问题：执行某个操作同时依赖于某个对象的原始版本和代理版本（标识混淆在一般使用当中应该是非常罕见的，但是要想完全避免这样的问题，必须要对整个响应式系统的工作原理有一个相当清晰的认知）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = markRaw({
  nested: {},
})

const bar = reactive({
  // 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
  nested: foo.nested,
})

console.log(foo.nested === bar.nested) // false

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;foo.nested没有被标记为(永远不会转为响应式代理)，导致最后的值一个reactive&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;shallowReactive&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;只为某个对象的私有（第一层）属性创建浅层的响应式代理，不会对“属性的属性”做深层次、递归地响应式代理，而只是保留原样【第一层是响应式代理，深层次只保留原样(不具备响应式代理)】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 变更 state 的自有属性是响应式的【第一层次响应式】
state.foo++
// ...但不会深层代理【深层次不是响应式】(渲染性能)
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;shallowReadonly&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;类似于&lt;code&gt;shallowReactive&lt;/code&gt;，区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一层将会是响应式代理【第一层修改属性会失败】，属性为响应式&lt;/li&gt;
&lt;li&gt;深层次的对象属性可以修改，属性不是响应式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 变更 state 的自有属性会失败
state.foo++
// ...但是嵌套的对象是可以变更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套属性依然可修改

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;ref&lt;/code&gt; 响应式系统工具集&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;unref&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unref&lt;/code&gt;是&lt;code&gt;val = isRef(val) ? val.value : val&lt;/code&gt; 的语法糖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unref(ref(0))===unref(0)===0   返回number

function useFoo(x: number | Ref&amp;lt;number&amp;gt;) {
  const unwrapped = unref(x) // unwrapped 一定是 number 类型
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;toRef&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;toRef&lt;/code&gt; 可以用来为一个 reactive 对象的&lt;code&gt;属性&lt;/code&gt;【某个属性区别toRefs每一个属性】创建一个 ref。这个 ref 可以被传递并且能够保持响应性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = reactive({
  foo: 1,
  bar: 2,
})

//reactive获取单个属性转为ref【fooRef只是一个代理】
const fooRef = toRef(state, &apos;foo&apos;)

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;toRefs&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;把一个响应式对象转换成普通对象，该普通对象的每个 property 都是一个 ref ，和响应式对象 property 一一对应&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = reactive({
  foo: 1,
  bar: 2,
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:

{
  foo: Ref&amp;lt;number&amp;gt;,
  bar: Ref&amp;lt;number&amp;gt;
}
*/

// ref 对象 与 原属性的引用是 &quot;链接&quot; 上的
state.foo++
console.log(stateAsRefs.foo) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;可以通过&lt;code&gt;toRefs&lt;/code&gt;返回可解构的reactive，因为&lt;code&gt;toRefs&lt;/code&gt;包裹之后返回一一对应的ref属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  })

  // 对 state 的逻辑操作

  // 返回时将属性都转为 ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以解构，不会丢失响应性
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar,
    }
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;isRef&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;检查一个值是否为一个 ref 对象&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;ref&lt;/code&gt; 高级响应式系统API&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;customRef&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;用于自定义一个 &lt;code&gt;ref&lt;/code&gt;，可以显式地控制依赖追踪和触发响应，接受一个工厂函数，两个参数分别是用于追踪的 &lt;code&gt;track&lt;/code&gt; 与用于触发响应的 &lt;code&gt;trigger&lt;/code&gt;，并返回一个一个带有 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;set&lt;/code&gt; 属性的对象【实际上就是手动 &lt;code&gt;track&lt;/code&gt;追踪 和 &lt;code&gt;trigger&lt;/code&gt;触发响应】&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以下代码可以使得v-model防抖&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) =&amp;gt; {
    return {
      get() {
          /*初始化手动追踪依赖讲究什么时候去触发依赖收集*/
        track()
        return value
      },
      set(newValue) {
          /*修改数据的时候会把上一次的定时器清除【防抖】*/
        clearTimeout(timeout)
        timeout = setTimeout(() =&amp;gt; {
            /*把新设置的数据给到ref数据源*/
          value = newValue
            /*再有依赖追踪的前提下触发响应式*/
          trigger()
        }, delay)
      },
    }
  })
}

setup() {
    return {
        /*暴露返回的数据加防抖*/
      text: useDebouncedRef(&apos;hello&apos;),
    }
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;shallowRef&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;创建一个 ref ，将会追踪它的 &lt;code&gt;.value&lt;/code&gt; 更改操作，但是并不会对变更后的 &lt;code&gt;.value&lt;/code&gt; 做响应式代理转换（即变更不会调用 &lt;code&gt;reactive&lt;/code&gt;）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前面我们说过如果传入 ref 的是一个对象，将调用 &lt;code&gt;reactive&lt;/code&gt; 方法进行深层响应转换,通过&lt;code&gt;shallowRef&lt;/code&gt;创建的ref,将不会调用reactive【对象不会是响应式的】&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const refOne = shallowRef({});
refOne.value = { id: 1 };
refOne.id == 20;
console.log(isReactive(refOne.value),refOne.value);//false  { id: 1 }

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;triggerRef&lt;/code&gt; 【与&lt;code&gt;shallowRef&lt;/code&gt;配合】&lt;/h3&gt;
&lt;p&gt;手动执行与&lt;code&gt;shallowRef&lt;/code&gt;相关的任何效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const shallow = shallowRef({
  greet: &apos;Hello, world&apos;
})

// 第一次运行打印 &quot;Hello, world&quot; 
watchEffect(() =&amp;gt; {
  console.log(shallow.value.greet)
})

// 这不会触发效果，因为ref是shallow
shallow.value.greet = &apos;Hello, universe&apos;

// 打印 &quot;Hello, universe&quot;
triggerRef(shallow)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Composition API&lt;/h1&gt;
&lt;h2&gt;setup&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt; 函数是一个新的组件选项。作为在组件内使用 &lt;strong&gt;Composition API&lt;/strong&gt; 的入口点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意 &lt;code&gt;setup&lt;/code&gt; 返回的 ref 在模板中会自动解开，不需要写 &lt;code&gt;.value&lt;/code&gt;【&lt;code&gt;setup&lt;/code&gt; 内部需要&lt;code&gt;.value&lt;/code&gt;】&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;调用时机&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;创建组件实例，然后初始化 &lt;code&gt;props&lt;/code&gt; ，紧接着就调用&lt;code&gt;setup&lt;/code&gt; 函数。从生命周期钩子的视角来看，它会在 &lt;code&gt;beforeCreate&lt;/code&gt; 钩子之前被调用&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;setup&lt;/code&gt; 返回一个对象，则对象的属性将会被合并到组件模板的渲染上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;props&lt;/code&gt; 作为其第一个参数&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 &lt;code&gt;props&lt;/code&gt; 对象是响应式的，&lt;code&gt;watchEffect&lt;/code&gt; 或 &lt;code&gt;watch&lt;/code&gt; 会观察和响应 &lt;code&gt;props&lt;/code&gt; 的更新&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要&lt;/strong&gt;解构 &lt;code&gt;props&lt;/code&gt; 对象，那样会使其失去响应性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;export default {
  props: {
    name: String,
  },
  setup(props) {
    console.log(props.name)
     watchEffect(() =&amp;gt; {
      console.log(`name is: ` + props.name)
    })
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第二个参数提供了一个上下文对象【从原来 2.x 中 &lt;code&gt;this&lt;/code&gt; 选择性地暴露了一些 property（attrs/emit/slots）】&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;attrs&lt;/code&gt; 和 &lt;code&gt;slots&lt;/code&gt; 都是内部组件实例上对应项的代理，可以确保在更新后仍然是最新值。所以可以解构，无需担心后面访问到过期的值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么props作为第一个参数？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件使用 &lt;code&gt;props&lt;/code&gt; 的场景更多，有时候甚至只使用 &lt;code&gt;props&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;props&lt;/code&gt; 独立出来作为第一个参数，可以让 TypeScript 对 &lt;code&gt;props&lt;/code&gt; 单独做类型推导，不会和上下文中的其他属性相混淆。这也使得 &lt;code&gt;setup&lt;/code&gt; 、 &lt;code&gt;render&lt;/code&gt; 和其他使用了 TSX 的函数式组件的签名保持一致&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;this&lt;/code&gt; 在 &lt;code&gt;setup()&lt;/code&gt; 中不可用&lt;/strong&gt;。由于 &lt;code&gt;setup()&lt;/code&gt; 在解析 2.x 选项前被调用，&lt;code&gt;setup()&lt;/code&gt; 中的 &lt;code&gt;this&lt;/code&gt; 将与 2.x 选项中的 &lt;code&gt;this&lt;/code&gt; 完全不同。同时在 &lt;code&gt;setup()&lt;/code&gt; 和 2.x 选项中使用 &lt;code&gt;this&lt;/code&gt; 时将造成混乱&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;setup(props, { attrs }) {
    // 一个可能之后回调用的签名
    function onClick() {
      console.log(attrs.foo) // 一定是最新的引用，没有丢失响应性
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>进程通信(electron/node)</title><link>https://nollieleo.github.io/posts/%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1-electron-node/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1-electron-node/</guid><description>进程通信  不同进程之间因为可用的内存不同，所以要通过一个中间介质通信。  信号量  如果是简单的标记，通过一个数字来表示，放在 PCB 的一个属性里，这叫做信号量，比如锁的实现就可以通过信号量。  这种信号量的思想我们写前端代码也经常用，比如实现节流的时候，也要加一个标记变量。  管道  但是信号...</description><pubDate>Sat, 26 Mar 2022 18:51:07 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;进程通信&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;不同进程之间因为可用的内存不同，所以要通过一个中间介质通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信号量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果是简单的标记，通过一个数字来表示，放在 PCB 的一个属性里，这叫做&lt;code&gt;信号量&lt;/code&gt;，比如锁的实现就可以通过信号量。&lt;/p&gt;
&lt;p&gt;这种信号量的思想我们写前端代码也经常用，比如实现节流的时候，也要加一个标记变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;管道&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是信号量不能传递具体的数据啊，传递具体数据还得用别的方式。比如我们可以通过读写文件的方式来通信，这就是&lt;code&gt;管道&lt;/code&gt;，如果是在内存中的文件，叫做匿名管道，没有文件名，如果是真实的硬盘的文件，是有文件名的，叫做命名管道。&lt;/p&gt;
&lt;p&gt;文件需要先打开，然后再读和写，之后再关闭，这也是管道的特点。管道是基于文件的思想封装的，之所以叫管道，是因为只能一个进程读、一个进程写，是单向的（半双工）。而且还需要目标进程同步的消费数据，不然就会阻塞住。&lt;/p&gt;
&lt;p&gt;这种管道的方式实现起来很简单，就是一个文件读写，但是只能用在两个进程之间通信，只能同步的通信。其实管道的同步通信也挺常见的，就是 stream 的 pipe 方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;消息队列&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;管道实现简单，但是同步的通信比较受限制，那如果想做成异步通信呢？加个队列做缓冲（buffer）不就行了，这就是&lt;code&gt;消息队列&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;消息队列也是两个进程之间的通信，但是不是基于文件那一套思路，虽然也是单向的，但是有了一定的异步性，可以放很多消息，之后一次性消费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享内存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;管道、消息队列都是两个进程之间的，如果多个进程之间呢？&lt;/p&gt;
&lt;p&gt;我们可以通过申请一段多进程都可以操作的内存，叫做&lt;code&gt;共享内存&lt;/code&gt;，用这种方式来通信。各进程都可以向该内存读写数据，效率比较高。&lt;/p&gt;
&lt;p&gt;共享内存虽然效率高、也能用于多个进程的通信，但也不全是好处，因为多个进程都可以读写，那么就很容易乱，要自己控制顺序，比如通过进程的信号量（标记变量）来控制。&lt;/p&gt;
&lt;p&gt;共享内存适用于多个进程之间的通信，不需要通过中间介质，所以效率更高，但是使用起来也更复杂。&lt;/p&gt;
&lt;p&gt;上面说的这些几乎就是本地进程通信的全部方式了，为什么要加个本地呢？&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;ipc、rpc、lpc&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;进程通信就是 ipc（Inter-Process Communication），两个进程可能是一台计算机的，也可能网络上的不同计算机的进程，所以进程通信方式分为两种：&lt;/p&gt;
&lt;p&gt;本地过程调用 LPC（local procedure call）、远程过程调用 RPC（remote procedure call）。&lt;/p&gt;
&lt;p&gt;本地过程调用就是我们上面说的信号量、管道、消息队列、共享内存的通信方式，但是如果是网络上的，那就要通过网络协议来通信了，这个其实我们用的比较多，比如 http、websocket。&lt;/p&gt;
&lt;p&gt;所以，当有人提到 ipc 时就是在说进程通信，可以分为本地的和远程的两种来讨论。&lt;/p&gt;
&lt;p&gt;远程的都是基于网络协议封装的，而本地的都是基于信号量、管道、消息队列、共享内存封装出来的，比如我们接下来要探讨的 electron 和 nodejs。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;electron 进程通信&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;electron 会先启动主进程，然后通过 BrowserWindow 创建渲染进程，加载 html 页面实现渲染。这两个进程之间的通信是通过 electron 提供的 ipc 的 api。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ipcMain、ipcRenderer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主进程里面通过 ipcMain 的 on 方法监听事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcMain } from ``&apos;electron&apos;``;` `ipcMain.on(``&apos;异步事件&apos;``, (event, arg) =&amp;gt; {`` ``event.sender.send(``&apos;异步事件返回&apos;``, ``&apos;yyy&apos;``);``})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染进程里面通过 ipcRenderer 的 on 方法监听事件，通过 send 发送消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcRenderer } from ``&apos;electron&apos;``;` `ipcRender.on(``&apos;异步事件返回&apos;``, ``function` `(event, arg) {`` ``const message = `异步消息: ${arg}```})` `ipcRenderer.send(``&apos;异步事件&apos;``, ``&apos;xxx&apos;``)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;api 使用比较简单，这是经过 c++ 层的封装，然后暴露给 js 的事件形式的 api。&lt;/p&gt;
&lt;p&gt;我们可以想一下它是基于哪种机制实现的呢？&lt;/p&gt;
&lt;p&gt;很明显有一定的异步性，而且是父子进程之间的通信，所以是消息队列的方式实现的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;remote&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除了事件形式的 api 外，electron 还提供了远程方法调用 rmi （remote method invoke）形式的 api。&lt;/p&gt;
&lt;p&gt;其实就是对消息的进一步封装，也就是根据传递的消息，调用不同的方法，形式上就像调用本进程的方法一样，但其实是发消息到另一个进程来做的，和 ipcMain、ipcRenderer 的形式本质上一样。&lt;/p&gt;
&lt;p&gt;比如在渲染进程里面，通过 remote 来直接调用主进程才有的 BrowserWindow 的 api。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { BrowserWindow } = require(``&apos;electron&apos;``).remote;` `let win = ``new` `BrowserWindow({ width: 800, height: 600 });``win.loadURL(``&apos;&apos;``);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小结一下，electron 的父子进程通信方式是基于消息队列封装的，封装形式有两种，一种是事件的方式，通过 ipcMain、ipcRenderer 的 api 使用，另一种则是进一步封装成了不同方法的调用（rmi），底层也是基于消息，执行远程方法但是看上去像执行本地方法一样。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;nodejs&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;nodejs 提供了创建进程的 api，有两个模块： c 和 cluster。很明显，一个是用于父子进程的创建和通信，一个是用于多个进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;child_process&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;child_process 提供了 spawn、exec、execFile、fork 的 api，分别用于不同的进程的创建：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;spawn、exec&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果想通过 shell 执行命令，那就用 spawn 或者 exec。因为一般执行命令是需要返回值的，这俩 api 在返回值的方式上有所不同。&lt;/p&gt;
&lt;p&gt;spawn 返回的是 stream，通过 data 事件来取，exec 进一步分装成了 buffer，使用起来简单一些，但是可能会超过 maxBuffer。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { spawn } = require(``&apos;child_process&apos;``);` `var` `app = spawn(``&apos;node&apos;``,``&apos;main.js&apos;` `{env:{}});` `app.stderr.on(``&apos;data&apos;``,``function``(data) {`` ``console.log(``&apos;Error:&apos;``,data);``});` `app.stdout.on(``&apos;data&apos;``,``function``(data) {`` ``console.log(data);``});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实 exec 是基于 spwan 封装出来的，简单场景可以用，有的时候要设置下 maxBuffer。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { exec } = require(``&apos;child_process&apos;``);` `exec(``&apos;find . -type f&apos;``, { maxBuffer: 1024*1024 }(err, stdout, stderr) =&amp;gt; {``  ``if` `(err) {``    ``console.error(`exec error: ${err}`); ``return``;``  ``} ``  ``console.log(stdout);``});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;execFile&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除了执行命令外，如果要执行可执行文件就用 execFile 的 api：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { execFile } = require(``&apos;child_process&apos;``);` `const child = execFile(``&apos;node&apos;``, [``&apos;--version&apos;``], (error, stdout, stderr) =&amp;gt; {``  ``if` `(error) { ``throw` `error; }``  ``console.log(stdout);``});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;fork&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还有如果是想执行 js ，那就用 fork：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { fork } = require(``&apos;child_process&apos;``); ` `const xxxProcess = fork(``&apos;./xxx.js&apos;``);  ``xxxProcess.send(``&apos;111111&apos;``); ``xxxProcess.on(``&apos;message&apos;``, sum =&amp;gt; {  ``  ``res.end(``&apos;22222&apos;``); ``});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;小结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单小结一下 child_process 的 4 个 api：&lt;/p&gt;
&lt;p&gt;如果想执行 shell 命令，用 spawn 和 exec，spawn 返回一个 stream，而 exec 进一步封装成了 buffer。除了 exec 有的时候需要设置下 maxBuffer，其他没区别。&lt;/p&gt;
&lt;p&gt;如果想执行可执行文件，用 execFile。&lt;/p&gt;
&lt;p&gt;如果想执行 js 文件，用 fork。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;child_process 的进程通信&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;说完了 api 我们来说下 child_process 创建的子进程怎么和父进程通信，也就是怎么做 ipc。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pipe&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，支持了 pipe，很明显是通过管道的机制封装出来的，能同步的传输流的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { spawn } = require(&apos;child_process&apos;);
const find = spawn(&apos;cat&apos;, [&apos;./aaa.js&apos;]);
const wc = spawn(&apos;wc&apos;, [&apos;-l&apos;]); 
find.stdout.pipe(wc.stdin);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如上面通过管道把一个进程的输出流传输到了另一个进程的输入流，和下面的 shell 命令效果一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat ./aaa.js | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;message&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;spawn 支持 stdio 参数，可以设置和父进程的 stdin、stdout、stderr 的关系，比如指定 pipe 或者 null。还有第四个参数，可以设置 ipc，这时候就是通过事件的方式传递消息了，很明s显，是基于消息队列实现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { spawn } = require(&apos;child_process&apos;);
const child = spawn(&apos;node&apos;, [&apos;./child.js&apos;], { stdio: [&apos;pipe&apos;, &apos;pipe&apos;, &apos;pipe&apos;, &apos;ipc&apos;]});
child.on(&apos;message&apos;, (m) =&amp;gt; {  console.log(m);});
child.send(&apos;this is weng&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 fork 的 api 创建的子进程自带了 ipc 的传递消息机制，可以直接用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { fork } = require(``&apos;child_process&apos;``); ` `const xxxProcess = fork(``&apos;./xxx.js&apos;``);  ``xxxProcess.send(``&apos;111111&apos;``); ``xxxProcess.on(``&apos;message&apos;``, sum =&amp;gt; {  ``  ``res.end(``&apos;22222&apos;``); ``});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;cluster&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;cluster 不再是父子进程了，而是更多进程，也提供了 fork 的 api。&lt;/p&gt;
&lt;p&gt;比如 http server 会根据 cpu 数启动多个进程来处理请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cluster from ``&apos;cluster&apos;``;``import http from ``&apos;http&apos;``;``import { cpus } from ``&apos;os&apos;``;``import process from ``&apos;process&apos;``;` `const numCPUs = cpus().length;` `if` `(cluster.isPrimary) {`` ``for` `(let i = 0; i &amp;lt; numCPUs; i++) {``  ``cluster.fork();`` ``}``} ``else` `{`` ``const server = http.createServer((req, res) =&amp;gt; {``  ``res.writeHead(200);``  ``res.end(``&apos;hello worldn&apos;``);`` ``})`` ` ` ``server.listen(8000);`` ` ` ``process.on(``&apos;message&apos;``, (msg) =&amp;gt; {``  ``if` `(msg === ``&apos;shutdown&apos;``) {``    ``server.close();``  ``}`` ``});``}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它同样支持了事件形式的 api，用于多个进程之间的消息传递，因为多个进程其实也只是多个父子进程的通信，子进程之间不能直接通信，所以还是基于消息队列实现的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享内存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;子进程之间通信还得通过父进程中转一次，要多次读写消息队列，效率太低了，就不能直接共享内存么？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在 nodejs 还是不支持的，可以通过第三方的包 shm-typed-array 来实现，感兴趣可以看一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;进程包括代码、数据和 PCB，是程序的一次执行的过程，PCB 记录着各种执行过程中的信息，比如分配的资源、执行到的地址、用于通信的数据结构等。&lt;/p&gt;
&lt;p&gt;进程之间需要通信，可以通过信号量、管道、消息队列、共享内存的方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号量就是一个简单的数字的标记，不能传递具体数据。&lt;/li&gt;
&lt;li&gt;管道是基于文件的思想，一个进程写另一个进程读，是同步的，适用于两个进程。&lt;/li&gt;
&lt;li&gt;消息队列有一定的 buffer，可以异步处理消息，适用于两个进程。&lt;/li&gt;
&lt;li&gt;共享内存是多个进程直接操作同一段内存，适用于多个进程，但是需要控制访问顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四种是本地进程的通信方式，而网络进程则基于网络协议的方式也可以做进程通信。&lt;/p&gt;
&lt;p&gt;进程通信叫做 ipc，本地的叫做 lpc，远程的叫 rpc。&lt;/p&gt;
&lt;p&gt;其中，如果把消息再封装一层成具体的方法调用，叫做 rmi，效果就像在本进程执行执行另一个进程的方法一样。&lt;/p&gt;
&lt;p&gt;electron 和 nodejs 都是基于上面的操作系统机制的封装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;elctron 支持 ipcMain 和 ipcRenderer 的消息传递的方式，还支持了 remote 的 rmi 的方式。&lt;/li&gt;
&lt;li&gt;nodejs 有 child_process 和 cluster 两个模块和进程有关，child_process 是父子进程之间，cluster 是多个进程：
&lt;ul&gt;
&lt;li&gt;child_process 提供了用于执行 shell 命令的 spawn、exec，用于执行可执行文件的 execFile，用于执行 js 的 fork。提供了 pipe 和 message 两种 ipc 方式。&lt;/li&gt;
&lt;li&gt;cluster 也提供了 fork，提供了 message 的方式的通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，不管封装形式是什么，都离不开操作系统提供的信号量、管道、消息队列、共享内存这四种机制。&lt;/p&gt;
</content:encoded></item><item><title>eslint rules常用配置项</title><link>https://nollieleo.github.io/posts/eslint-rules%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE%E9%A1%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/eslint-rules%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE%E9%A1%B9/</guid><description>json { &quot;no-alert&quot;: 0,//禁止使用alert confirm prompt &quot;no-array-constructor&quot;: 2,//禁止使用数组构造器 &quot;no-bitwise&quot;: 0,//禁止使用按位运算符 &quot;no-caller&quot;: 1,//禁止使用arguments.calle...</description><pubDate>Sun, 27 Feb 2022 16:19:53 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;{
&quot;no-alert&quot;: 0,//禁止使用alert confirm prompt
&quot;no-array-constructor&quot;: 2,//禁止使用数组构造器
&quot;no-bitwise&quot;: 0,//禁止使用按位运算符
&quot;no-caller&quot;: 1,//禁止使用arguments.caller或arguments.callee
&quot;no-catch-shadow&quot;: 2,//禁止catch子句参数与外部作用域变量同名
&quot;no-class-assign&quot;: 2,//禁止给类赋值
&quot;no-cond-assign&quot;: 2,//禁止在条件表达式中使用赋值语句
&quot;no-console&quot;: 2,//禁止使用console
&quot;no-const-assign&quot;: 2,//禁止修改const声明的变量
&quot;no-constant-condition&quot;: 2,//禁止在条件中使用常量表达式 if(true) if(1)
&quot;no-continue&quot;: 0,//禁止使用continue
&quot;no-control-regex&quot;: 2,//禁止在正则表达式中使用控制字符
&quot;no-debugger&quot;: 2,//禁止使用debugger
&quot;no-delete-var&quot;: 2,//不能对var声明的变量使用delete操作符
&quot;no-div-regex&quot;: 1,//不能使用看起来像除法的正则表达式/=foo/
&quot;no-dupe-keys&quot;: 2,//在创建对象字面量时不允许键重复 {a:1,a:1}
&quot;no-dupe-args&quot;: 2,//函数参数不能重复
&quot;no-duplicate-case&quot;: 2,//switch中的case标签不能重复
&quot;no-else-return&quot;: 2,//如果if语句里面有return,后面不能跟else语句
&quot;no-empty&quot;: 2,//块语句中的内容不能为空
&quot;no-empty-character-class&quot;: 2,//正则表达式中的[]内容不能为空
&quot;no-empty-label&quot;: 2,//禁止使用空label
&quot;no-eq-null&quot;: 2,//禁止对null使用==或!=运算符
&quot;no-eval&quot;: 1,//禁止使用eval
&quot;no-ex-assign&quot;: 2,//禁止给catch语句中的异常参数赋值
&quot;no-extend-native&quot;: 2,//禁止扩展native对象
&quot;no-extra-bind&quot;: 2,//禁止不必要的函数绑定
&quot;no-extra-boolean-cast&quot;: 2,//禁止不必要的bool转换
&quot;no-extra-parens&quot;: 2,//禁止非必要的括号
&quot;no-extra-semi&quot;: 2,//禁止多余的冒号
&quot;no-fallthrough&quot;: 1,//禁止switch穿透
&quot;no-floating-decimal&quot;: 2,//禁止省略浮点数中的0 .5 3.
&quot;no-func-assign&quot;: 2,//禁止重复的函数声明
&quot;no-implicit-coercion&quot;: 1,//禁止隐式转换
&quot;no-implied-eval&quot;: 2,//禁止使用隐式eval
&quot;no-inline-comments&quot;: 0,//禁止行内备注
&quot;no-inner-declarations&quot;: [2, &quot;functions&quot;],//禁止在块语句中使用声明（变量或函数）
&quot;no-invalid-regexp&quot;: 2,//禁止无效的正则表达式
&quot;no-invalid-this&quot;: 2,//禁止无效的this，只能用在构造器，类，对象字面量
&quot;no-irregular-whitespace&quot;: 2,//不能有不规则的空格
&quot;no-iterator&quot;: 2,//禁止使用__iterator__ 属性
&quot;no-label-var&quot;: 2,//label名不能与var声明的变量名相同
&quot;no-labels&quot;: 2,//禁止标签声明
&quot;no-lone-blocks&quot;: 2,//禁止不必要的嵌套块
&quot;no-lonely-if&quot;: 2,//禁止else语句内只有if语句
&quot;no-loop-func&quot;: 1,//禁止在循环中使用函数（如果没有引用外部变量不形成闭包就可以）
&quot;no-mixed-requires&quot;: [0, false],//声明时不能混用声明类型
&quot;no-mixed-spaces-and-tabs&quot;: [2, false],//禁止混用tab和空格
&quot;linebreak-style&quot;: [0, &quot;windows&quot;],//换行风格
&quot;no-multi-spaces&quot;: 1,//不能用多余的空格
&quot;no-multi-str&quot;: 2,//字符串不能用\换行
&quot;no-multiple-empty-lines&quot;: [1, {&quot;max&quot;: 2}],//空行最多不能超过2行
&quot;no-native-reassign&quot;: 2,//不能重写native对象
&quot;no-negated-in-lhs&quot;: 2,//in 操作符的左边不能有!
&quot;no-nested-ternary&quot;: 0,//禁止使用嵌套的三目运算
&quot;no-new&quot;: 1,//禁止在使用new构造一个实例后不赋值
&quot;no-new-func&quot;: 1,//禁止使用new Function
&quot;no-new-object&quot;: 2,//禁止使用new Object()
&quot;no-new-require&quot;: 2,//禁止使用new require
&quot;no-new-wrappers&quot;: 2,//禁止使用new创建包装实例，new String new Boolean new Number
&quot;no-obj-calls&quot;: 2,//不能调用内置的全局对象，比如Math() JSON()
&quot;no-octal&quot;: 2,//禁止使用八进制数字
&quot;no-octal-escape&quot;: 2,//禁止使用八进制转义序列
&quot;no-param-reassign&quot;: 2,//禁止给参数重新赋值
&quot;no-path-concat&quot;: 0,//node中不能使用__dirname或__filename做路径拼接
&quot;no-plusplus&quot;: 0,//禁止使用++，--
&quot;no-process-env&quot;: 0,//禁止使用process.env
&quot;no-process-exit&quot;: 0,//禁止使用process.exit()
&quot;no-proto&quot;: 2,//禁止使用__proto__属性
&quot;no-redeclare&quot;: 2,//禁止重复声明变量
&quot;no-regex-spaces&quot;: 2,//禁止在正则表达式字面量中使用多个空格 /foo bar/
&quot;no-restricted-modules&quot;: 0,//如果禁用了指定模块，使用就会报错
&quot;no-return-assign&quot;: 1,//return 语句中不能有赋值表达式
&quot;no-script-url&quot;: 0,//禁止使用javascript:void(0)
&quot;no-self-compare&quot;: 2,//不能比较自身
&quot;no-sequences&quot;: 0,//禁止使用逗号运算符
&quot;no-shadow&quot;: 2,//外部作用域中的变量不能与它所包含的作用域中的变量或参数同名
&quot;no-shadow-restricted-names&quot;: 2,//严格模式中规定的限制标识符不能作为声明时的变量名使用
&quot;no-spaced-func&quot;: 2,//函数调用时 函数名与()之间不能有空格
&quot;no-sparse-arrays&quot;: 2,//禁止稀疏数组， [1,,2]
&quot;no-sync&quot;: 0,//nodejs 禁止同步方法
&quot;no-ternary&quot;: 0,//禁止使用三目运算符
&quot;no-trailing-spaces&quot;: 1,//一行结束后面不要有空格
&quot;no-this-before-super&quot;: 0,//在调用super()之前不能使用this或super
&quot;no-throw-literal&quot;: 2,//禁止抛出字面量错误 throw &quot;error&quot;;
&quot;no-undef&quot;: 1,//不能有未定义的变量
&quot;no-undef-init&quot;: 2,//变量初始化时不能直接给它赋值为undefined
&quot;no-undefined&quot;: 2,//不能使用undefined
&quot;no-unexpected-multiline&quot;: 2,//避免多行表达式
&quot;no-underscore-dangle&quot;: 1,//标识符不能以_开头或结尾
&quot;no-unneeded-ternary&quot;: 2,//禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
&quot;no-unreachable&quot;: 2,//不能有无法执行的代码
&quot;no-unused-expressions&quot;: 2,//禁止无用的表达式
&quot;no-unused-vars&quot;: [2, {&quot;vars&quot;: &quot;all&quot;, &quot;args&quot;: &quot;after-used&quot;}],//不能有声明后未被使用的变量或参数
&quot;no-use-before-define&quot;: 2,//未定义前不能使用
&quot;no-useless-call&quot;: 2,//禁止不必要的call和apply
&quot;no-void&quot;: 2,//禁用void操作符
&quot;no-var&quot;: 0,//禁用var，用let和const代替
&quot;no-warning-comments&quot;: [1, { &quot;terms&quot;: [&quot;todo&quot;, &quot;fixme&quot;, &quot;xxx&quot;], &quot;location&quot;: &quot;start&quot; }],//不能有警告备注
&quot;no-with&quot;: 2,//禁用with

&quot;array-bracket-spacing&quot;: [2, &quot;never&quot;],//是否允许非空数组里面有多余的空格
&quot;arrow-parens&quot;: 0,//箭头函数用小括号括起来
&quot;arrow-spacing&quot;: 0,//=&amp;gt;的前/后括号
&quot;accessor-pairs&quot;: 0,//在对象中使用getter/setter
&quot;block-scoped-var&quot;: 0,//块语句中使用var
&quot;brace-style&quot;: [1, &quot;1tbs&quot;],//大括号风格
&quot;callback-return&quot;: 1,//避免多次调用回调什么的
&quot;camelcase&quot;: 2,//强制驼峰法命名
&quot;comma-dangle&quot;: [2, &quot;never&quot;],//对象字面量项尾不能有逗号
&quot;comma-spacing&quot;: 0,//逗号前后的空格
&quot;comma-style&quot;: [2, &quot;last&quot;],//逗号风格，换行时在行首还是行尾
&quot;complexity&quot;: [0, 11],//循环复杂度
&quot;computed-property-spacing&quot;: [0, &quot;never&quot;],//是否允许计算后的键名什么的
&quot;consistent-return&quot;: 0,//return 后面是否允许省略
&quot;consistent-this&quot;: [2, &quot;that&quot;],//this别名
&quot;constructor-super&quot;: 0,//非派生类不能调用super，派生类必须调用super
&quot;curly&quot;: [2, &quot;all&quot;],//必须使用 if(){} 中的{}
&quot;default-case&quot;: 2,//switch语句最后必须有default
&quot;dot-location&quot;: 0,//对象访问符的位置，换行的时候在行首还是行尾
&quot;dot-notation&quot;: [0, { &quot;allowKeywords&quot;: true }],//避免不必要的方括号
&quot;eol-last&quot;: 0,//文件以单一的换行符结束
&quot;eqeqeq&quot;: 2,//必须使用全等
&quot;func-names&quot;: 0,//函数表达式必须有名字
&quot;func-style&quot;: [0, &quot;declaration&quot;],//函数风格，规定只能使用函数声明/函数表达式
&quot;generator-star-spacing&quot;: 0,//生成器函数*的前后空格
&quot;guard-for-in&quot;: 0,//for in循环要用if语句过滤
&quot;handle-callback-err&quot;: 0,//nodejs 处理错误
&quot;id-length&quot;: 0,//变量名长度
&quot;indent&quot;: [2, 4],//缩进风格
&quot;init-declarations&quot;: 0,//声明时必须赋初值
&quot;key-spacing&quot;: [0, { &quot;beforeColon&quot;: false, &quot;afterColon&quot;: true }],//对象字面量中冒号的前后空格
&quot;lines-around-comment&quot;: 0,//行前/行后备注
&quot;max-depth&quot;: [0, 4],//嵌套块深度
&quot;max-len&quot;: [0, 80, 4],//字符串最大长度
&quot;max-nested-callbacks&quot;: [0, 2],//回调嵌套深度
&quot;max-params&quot;: [0, 3],//函数最多只能有3个参数
&quot;max-statements&quot;: [0, 10],//函数内最多有几个声明
&quot;new-cap&quot;: 2,//函数名首行大写必须使用new方式调用，首行小写必须用不带new方式调用
&quot;new-parens&quot;: 2,//new时必须加小括号
&quot;newline-after-var&quot;: 2,//变量声明后是否需要空一行
&quot;object-curly-spacing&quot;: [0, &quot;never&quot;],//大括号内是否允许不必要的空格
&quot;object-shorthand&quot;: 0,//强制对象字面量缩写语法
&quot;one-var&quot;: 1,//连续声明
&quot;operator-assignment&quot;: [0, &quot;always&quot;],//赋值运算符 += -=什么的
&quot;operator-linebreak&quot;: [2, &quot;after&quot;],//换行时运算符在行尾还是行首
&quot;padded-blocks&quot;: 0,//块语句内行首行尾是否要空行
&quot;prefer-const&quot;: 0,//首选const
&quot;prefer-spread&quot;: 0,//首选展开运算
&quot;prefer-reflect&quot;: 0,//首选Reflect的方法
&quot;quotes&quot;: [1, &quot;single&quot;],//引号类型 `` &quot;&quot; &apos;&apos;
&quot;quote-props&quot;:[2, &quot;always&quot;],//对象字面量中的属性名是否强制双引号
&quot;radix&quot;: 2,//parseInt必须指定第二个参数
&quot;id-match&quot;: 0,//命名检测
&quot;require-yield&quot;: 0,//生成器函数必须有yield
&quot;semi&quot;: [2, &quot;always&quot;],//语句强制分号结尾
&quot;semi-spacing&quot;: [0, {&quot;before&quot;: false, &quot;after&quot;: true}],//分号前后空格
&quot;sort-vars&quot;: 0,//变量声明时排序
&quot;space-after-keywords&quot;: [0, &quot;always&quot;],//关键字后面是否要空一格
&quot;space-before-blocks&quot;: [0, &quot;always&quot;],//不以新行开始的块{前面要不要有空格
&quot;space-before-function-paren&quot;: [0, &quot;always&quot;],//函数定义时括号前面要不要有空格
&quot;space-in-parens&quot;: [0, &quot;never&quot;],//小括号里面要不要有空格
&quot;space-infix-ops&quot;: 0,//中缀操作符周围要不要有空格
&quot;space-return-throw-case&quot;: 2,//return throw case后面要不要加空格
&quot;space-unary-ops&quot;: [0, { &quot;words&quot;: true, &quot;nonwords&quot;: false }],//一元运算符的前/后要不要加空格
&quot;spaced-comment&quot;: 0,//注释风格要不要有空格什么的
&quot;strict&quot;: 2,//使用严格模式
&quot;use-isnan&quot;: 2,//禁止比较时使用NaN，只能用isNaN()
&quot;valid-jsdoc&quot;: 0,//jsdoc规则
&quot;valid-typeof&quot;: 2,//必须使用合法的typeof的值
&quot;vars-on-top&quot;: 2,//var必须放在作用域顶部
&quot;wrap-iife&quot;: [2, &quot;inside&quot;],//立即执行函数表达式的小括号风格
&quot;wrap-regex&quot;: 0,//正则表达式字面量用小括号包起来
&quot;yoda&quot;: [2, &quot;never&quot;]//禁止尤达条件
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>electron学习记录</title><link>https://nollieleo.github.io/posts/electron%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/electron%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</guid><description>electron   核心模块       快速搭建demo   官方demo快速搭建（electron-quick-template）    shell  Clone this repository git clone https://github.com/electron/electron-qu...</description><pubDate>Wed, 12 Jan 2022 14:58:10 GMT</pubDate><content:encoded>&lt;h2&gt;electron&lt;/h2&gt;
&lt;h3&gt;核心模块&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;[Image missing: WeChat2314efdd8331a8e89163bc315a1a25a4]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;快速搭建demo&lt;/h2&gt;
&lt;h3&gt;官方demo快速搭建（electron-quick-template）&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/electron/electron-quick-start&quot;&gt;git地址&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Clone this repository
git clone https://github.com/electron/electron-quick-start
# Go into the repository
cd electron-quick-start
# Install dependencies
npm install
# Run the app
npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭建vue + electron&lt;/h3&gt;
&lt;h4&gt;使用electron-vue&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://simulatedgreg.gitbooks.io/electron-vue/content/cn/&quot;&gt;中文文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 安装 vue-cli 和 脚手架样板代码
npm install -g vue-cli
vue init simulatedgreg/electron-vue my-project

# 安装依赖并运行你的程序
cd my-project
yarn # 或者 npm install
yarn run dev # 或者 npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用vue cli自带的可视化创建项目&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;vue cli有很多插件化的东西可以用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 打开可视化界面
vue ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: C:\Users\Weng\AppData\Roaming\Typora\typora-user-images\image-20220112153725964.png]&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后点新增一个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112153922959.png&quot; alt=&quot;image-20220112153922959&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;嗯对，继续，想选啥选啥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112154019140.png&quot; alt=&quot;image-20220112154019140&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后就在下载了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112154214278.png&quot; alt=&quot;image-20220112154214278&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后插件中搜索 &lt;code&gt;vue-cli-plugin-electron-builder&lt;/code&gt;, 安装他&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者使用 &lt;code&gt;vue add electron-builder&lt;/code&gt;安装&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112154326504.png&quot; alt=&quot;image-20220112154326504&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来直接就：就ok了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run electron:serve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112154639191.png&quot; alt=&quot;image-20220112154639191&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;如何调试主进程&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;基于vscode通过ws协议进行调试，之后通过端口就可进入调试的页面，就形容调试node  &lt;a href=&quot;https://code.visualstudio.com/docs/editor/debugging#_launch-configurations&quot;&gt;vscode调试官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;使用electron自带的&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;插件 electron-debug&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在dev文件中引入&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115143454095.png&quot; alt=&quot;image-20220115143454095&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在主进程中引入&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115143536900.png&quot; alt=&quot;image-20220115143536900&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;启动之后呢就能看到终端打印了这句话&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115143611685.png&quot; alt=&quot;image-20220115143611685&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开chrome://devices就ok了&lt;/p&gt;
&lt;h3&gt;使用electron-vue + vscode&lt;/h3&gt;
&lt;h4&gt;配置vscode&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找到左侧一个虫子一样的图标&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115112806086.png&quot; alt=&quot;image-20220115112806086&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择上面的下拉面板，添加配置（这里是node，就选择nodejs）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115112904875.png&quot; alt=&quot;image-20220115112904875&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vscode会自动创建一个文件 .vscode/路径下的launch.json文件。&lt;/p&gt;
&lt;p&gt;launch.json&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;基于electron-vue的配置（以下配置再用electron-vue的时候复制进去吧）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      &quot;name&quot;: &quot;Electron Main&quot;,
      &quot;program&quot;: &quot;${workspaceFolder}/.electron-vue/dev-runner.js&quot;,
      &quot;request&quot;: &quot;launch&quot;,
      &quot;skipFiles&quot;: [
        &quot;&amp;lt;node_internals&amp;gt;/**&quot;
      ],
      &quot;type&quot;: &quot;node&quot;,
      &quot;autoAttachChildProcesses&quot;: true,
      &quot;protocol&quot;: &quot;inspector&quot;
    }
	]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的program指的是调试的入口文件，如果是用 官方的快速 模板搭建的话，就是main.js，而electron-vue则得是&lt;code&gt;.electron-vue/dev-runner.js&lt;/code&gt;文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;修改electron-vue相关配置文件&lt;/h4&gt;
&lt;h5&gt;webpack.main.config.js&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;再electron-vue搭建的目录.electron-vue下的webpack.main.config.js&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;添加配置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115121624744.png&quot; alt=&quot;image-20220115121624744&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;dev-runner.js&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;再electron-vue搭建的目录.electron-vue下的dev-runner.js&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把 .electron-vue/dev-runner.js 里以下报错代码注释掉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// // detect yarn or npm and process commandline args accordingly
// if (process.env.npm_execpath.endsWith(&apos;yarn.js&apos;)) {
//   args = args.concat(process.argv.slice(3))
// } else if (process.env.npm_execpath.endsWith(&apos;npm-cli.js&apos;)) {
//   args = args.concat(process.argv.slice(2))
// }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样是为了避免控制台报错&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115121919211.png&quot; alt=&quot;image-20220115121919211&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;开始调试&lt;/h4&gt;
&lt;p&gt;打开vscode&lt;/p&gt;
&lt;p&gt;通过快捷方式&lt;code&gt;F5&lt;/code&gt;或者手动点击头部菜单 的 运行中的调试就可以开始了&lt;/p&gt;
&lt;p&gt;这时候就能在调试控制台看到这句话&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115122235932.png&quot; alt=&quot;image-20220115122235932&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们打开浏览器输入&lt;code&gt;chrome://inspect/#devices&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115122309093.png&quot; alt=&quot;image-20220115122309093&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击inspect就能看到文件了，这时候直接打断点就OK&lt;/p&gt;
&lt;p&gt;（正在调试的文件是有绿色的小点点）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115122412302.png&quot; alt=&quot;image-20220115122412302&quot; /&gt;&lt;/p&gt;
&lt;p&gt;倘若第一次调试，则需要点击上方的添加文件按钮将目录添加到调试工具当中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220115122506756.png&quot; alt=&quot;image-20220115122506756&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;vue-cli + vue-cli-plugin-electron-builder 调试&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#multiple-pages&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;调试demo&lt;a href=&quot;https://github.com/nklayman/electron-vscode-debug-example/blob/master/.vscode/tasks.json&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;其他调试的launch.json配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      //方式一
      &quot;name&quot;: &quot;Debug Main Process&quot;,
      &quot;type&quot;: &quot;node&quot;,
      &quot;request&quot;: &quot;launch&quot;,
      &quot;cwd&quot;: &quot;${workspaceFolder}&quot;,
      &quot;timeout&quot;: 60000, //避免出现can not connect to the target错误
      &quot;runtimeExecutable&quot;: &quot;${workspaceFolder}/node_modules/.bin/electron&quot;,
      &quot;windows&quot;: {
        &quot;runtimeExecutable&quot;: &quot;${workspaceFolder}/node_modules/.bin/electron.cmd&quot;
      },
      &quot;args&quot; : [&quot;.&quot;],
      &quot;env&quot;: {
        &quot;NODE_ENV&quot;: &quot;development&quot;
      },
      &quot;protocol&quot;: &quot;inspector&quot;
    },
    {
      //方式二
      &quot;name&quot;: &quot;Attach&quot;,
      &quot;type&quot;: &quot;node&quot;,
      &quot;request&quot;: &quot;attach&quot;,
      &quot;port&quot;: 5858,
      &quot;sourceMaps&quot;: true,
      &quot;address&quot;: &quot;localhost&quot;,
      &quot;timeout&quot;: 60000 //避免出现can not connect to the target错误
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;进程之间的通信&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;官方文档：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fwww.electronjs.org%2Fdocs%2Fapi%2Fipc-main&quot;&gt;ipcMain&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fwww.electronjs.org%2Fdocs%2Fapi%2Fipc-renderer&quot;&gt;ipcRenderer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fwww.electronjs.org%2Fdocs%2Fapi%2Fweb-contents%23contentssendchannel-args&quot;&gt;webContents&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;主线程&lt;/strong&gt; 到 &lt;strong&gt;渲染线程&lt;/strong&gt; 通过 &lt;code&gt;mainWin.webContents.send&lt;/code&gt; 来发送 ---&amp;gt;&lt;code&gt;ipcRenderer.on&lt;/code&gt; 来监听&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;渲染线程&lt;/strong&gt; 到 &lt;strong&gt;主线程&lt;/strong&gt; 需要通过 &lt;code&gt;ipcRenderer.send&lt;/code&gt;发送 ---&amp;gt; &lt;code&gt;ipcMain.on&lt;/code&gt;来监听&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;electron使用&lt;code&gt;mojo&lt;/code&gt;的框架完成进程间通信的工作&lt;/p&gt;
&lt;p&gt;mojo框架提供了一套地层的ipc实现，包括 消息管道，数据管道，共享缓存缓冲区等等。&lt;/p&gt;
&lt;p&gt;electron在api.mojom文件中定义了相关的通信接口描述文件 &lt;a href=&quot;https://github.com/electron/electron/blob/main/shell/common/api/api.mojom&quot;&gt;源码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220327110355234.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;其中比较重要的就是 &lt;code&gt;ElectronRenderer&lt;/code&gt; 和 &lt;code&gt;ElectronApiIPC&lt;/code&gt; 这两个定义，与主进程和渲染进程通信有关&lt;/p&gt;
&lt;p&gt;在编译Electron源码的过程，mojo框架会把这些通信描述文件转义为具体的实现代码&lt;/p&gt;
&lt;p&gt;之后shell\renderer\api\electron_api_ipc_renderer.cc和shell\browser\api\electron_api_web_contents.cc这两个c++文件都会去引用这个编译之后的文件（shell\common\api\api.mojom.h）头文件。&lt;/p&gt;
&lt;p&gt;在我们js代码中使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await ipcRenderer.invoke(&apos;event&apos;,&apos;message&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实地层走的就是&lt;a href=&quot;https://github.com/electron/electron/blob/main/shell/renderer/api/electron_api_ipc_renderer.cc&quot;&gt;shell\renderer\api\electron_api_ipc_renderer.cc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220327111337555.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;这段代码除了创建一个 Promise 对象之外，还执行了 electron_brower_remote对象（Mojo的通信对象）的invoke的方法&lt;/p&gt;
&lt;p&gt;之后mojo会组织消息，把这个消息发给主进程，并执行了 &lt;a href=&quot;https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_web_contents.cc&quot;&gt;electron_api_web_contents.cc&lt;/a&gt; 中WebContents::Invoke的方法&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220327111912387.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;这个方法会发射一个名字为-ipc-invoke的事件，把渲染进程传递过来的数据都发射出去，之后和这个事件会触发位于&lt;/p&gt;
&lt;p&gt;lib/browser/api/web-contents/ts的ts代码中&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20220327113128169]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这个逻辑当中，electron会去查找一个map对象 是否注册了当前的处理逻辑，有的话执行用户代码，否则异常&lt;/p&gt;
&lt;p&gt;invoke之后会把处理的结果返回渲染进程（基于mojo的进程通信，实现代码&lt;a href=&quot;https://github.com/electron/electron/blob/main/shell/browser/api/event.cc&quot;&gt;shell/brower/api/event.cc&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;我们在调用&lt;code&gt;ipcMain.handle&lt;/code&gt; 方法为主进程注册某事件的处理逻辑时候，实际上执行了 &lt;a href=&quot;https://github.com/electron/electron/blob/main/lib/browser/ipc-main-impl.ts&quot;&gt;/lib/brower/ipc-main-tmpl.ts&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220327114327050.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;就是将用户的处理逻辑包装起来存放到map对象当中，这就对应了上述的invoke触发的时候，做的事情了。&lt;/p&gt;
&lt;p&gt;大致是通信的基本逻辑&lt;/p&gt;
&lt;h3&gt;渲染进程和主进程异步通信&lt;/h3&gt;
&lt;h4&gt;ipcRenderer.send + ipcMain.on/once&lt;/h4&gt;
&lt;p&gt;renderer.js (渲染进程文件)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcRenderer } from &apos;electron&apos;;

function handleMessage(){
  ipcRenderer.send(&apos;renderer-to-main&apos;, &apos;this is weng&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.js (主进程)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcMain } from &apos;electron&apos;;

ipcMain.on(&apos;renderer-to-main&apos;, (event, message)=&amp;gt;{
  console.log(&apos;this is a message from weng&apos;, message)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如主进程需要回复渲染进程的消息&lt;/p&gt;
&lt;p&gt;主进程可以通过 &lt;code&gt;event.reply&lt;/code&gt; 回复&lt;strong&gt;异步消息&lt;/strong&gt;，然后前提是渲染进程也需要监听这个事件&lt;/p&gt;
&lt;p&gt;renderer.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcRenderer } from &apos;electron&apos;;

function handleMessage(){
  ipcRenderer.send(&apos;renderer-to-main&apos;, &apos;this is weng&apos;)
}
ipcRenderer.on(&apos;main-to-renderer&apos;,(event, message)=&amp;gt;{
  console.log(&apos;reply to weng&apos;, message); // 给翁恺敏发消息
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Main.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcMain } from &apos;electron&apos;;

ipcMain.on(&apos;renderer-to-main&apos;, (event, message)=&amp;gt;{
  console.log(&apos;this is a message from weng&apos;, message)
  event.reply(&apos;main-to-renderer&apos;, &quot;给翁恺敏发消息&quot;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、主进程通过 &lt;code&gt;ipcMain.on&lt;/code&gt; 来监听渲染进程的消息；&lt;/p&gt;
&lt;p&gt;2、主进程接收到消息后，可以回复消息，也可以不回复。如果回复的话，通过 &lt;code&gt;event.reply&lt;/code&gt; 发送另一个事件，渲染进程监听这个事件得到回复结果。如果不回复消息的话，渲染进程将接着执行 &lt;code&gt;ipcRenderer.send&lt;/code&gt; 之后的代码。&lt;/p&gt;
&lt;p&gt;上面提到过了, &lt;code&gt;send&lt;/code&gt; 这样的方式，主进程可以给回复，也可以不给回复，但是得通过 &lt;code&gt;event.replay&lt;/code&gt;。如果此时你试图用 &lt;code&gt;return&lt;/code&gt; 的方式传递返回值的话，结果并不能达到你的预期。&lt;/p&gt;
&lt;h4&gt;ipcMain.handle/handleOnce + ipcRenderer.invoke&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;这是另外一种通信手段&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;main.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.js
const { ipcMain } = require(&apos;electron&apos;);

// 返回的数据将会被promise包裹
ipcMain.handle(&apos;render-invoke-to-main&apos;, async (event, message) =&amp;gt; {
  console.log(`receive message from render: ${message}`)
  const result = await asyncWork();
  return result; // 假如需要回复渲染进程直接return就行
})

const asyncWork = async () =&amp;gt; {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve(&apos;延迟 2 秒获取到主进程的返回结果&apos;)
    }, 2000)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;renderer.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// render.js
const { ipcRenderer } = require(&apos;electron&apos;);

async function invokeMessageToMain() {
  const replyMessage = await ipcRenderer.invoke(&apos;render-invoke-to-main&apos;, &apos;我是渲染进程通过 invoke 发送的消息&apos;);
  console.log(&apos;replyMessage&apos;, replyMessage);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、主进程通过 &lt;code&gt;ipcMain.handle&lt;/code&gt; 来处理渲染进程发送的消息；&lt;/p&gt;
&lt;p&gt;2、主进程接收到消息后，可以回复消息，也可以不回复。如果回复消息的话，可以通过 &lt;code&gt;return&lt;/code&gt; 给渲染进程回复消息；如果不回复消息的话，渲染进程将接着执行 &lt;code&gt;ipcRenderer.invoke&lt;/code&gt; 之后的代码。&lt;/p&gt;
&lt;p&gt;3、渲染进程异步等待主进程的回应， &lt;code&gt;invoke&lt;/code&gt; 的返回值是一个 &lt;code&gt;Promise&amp;lt;pending&amp;gt;&lt;/code&gt; 。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;window.webContents.send&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;这种方式依赖于 &lt;code&gt;webContents&lt;/code&gt; 对象，它是我们在项目中新建窗口时，产生的窗口对象上的一个属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 窗口在完成加载时，通过 webContents.send 给渲染进程发送消息
window.webContents.on(&apos;did-finish-load&apos;, () =&amp;gt; {
  window.webContents.send(&apos;main-send-to-render&apos;, &apos;启动完成了&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之前我们通过渲染进程监听主进程的事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// render.js
const { ipcRenderer } = require(&apos;electron&apos;);

ipcRenderer.on(&apos;main-send-to-render&apos;, (event, message) =&amp;gt; {
  console.log(`receive message from main: ${message}`)
})

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;渲染进程和主进程的同步通信&lt;/h3&gt;
&lt;h4&gt;ipcRenderer.sendSync + ipcMain.on/once&lt;/h4&gt;
&lt;p&gt;renderer.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// render.js
const { ipcRenderer } = require(&apos;electron&apos;);

function sendSyncMessageToMain() {
  const replyMessage = ipcRenderer.sendSync(&apos;render-send-sync-to-main&apos;, &apos;我是渲染进程通过 syncSend 发送给主进程的消息&apos;);
  console.log(&apos;replyMessage&apos;, replyMessage); // &apos;主进程回复的消息&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Main.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.js
const { ipcMain } = require(&apos;electron&apos;);

ipcMain.on(&apos;render-send-sync-to-main&apos;, (event, message) =&amp;gt; {
  console.log(`receive message from render: ${message}`)
  event.returnValue = &apos;主进程回复的消息&apos;; 
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、主进程通过 &lt;code&gt;ipcMain.on&lt;/code&gt; 来处理渲染进程发送的消息；&lt;/p&gt;
&lt;p&gt;2、主进程通过 &lt;code&gt;event.returnValue&lt;/code&gt; 回复渲染进程消息；&lt;/p&gt;
&lt;p&gt;3、如果 &lt;code&gt;event.returnValue&lt;/code&gt; 不为 &lt;code&gt;undefined&lt;/code&gt; 的话，渲染进程会等待 &lt;code&gt;sendSync&lt;/code&gt; 的返回值才执行后面的代码；&lt;/p&gt;
&lt;p&gt;4、请保证 &lt;code&gt;event.returnValue&lt;/code&gt;是有值的，否则会造成非预期的影响。&lt;/p&gt;
&lt;p&gt;上面的案例，主进程绑定的处理函数是一个同步的，我们将它换为异步的来看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.js
const { ipcMain } = require(&apos;electron&apos;);

ipcMain.on(&apos;render-send-sync-to-main&apos;, async (event, message) =&amp;gt; {
  console.log(`receive message from render: ${message}`)
  const result = await asyncWork();
  event.returnValue = result;
})

const asyncWork = async () =&amp;gt; {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve(&apos;延迟 2 秒获取到主进程的返回结果&apos;)
    }, 2000)
  })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次我们在执行完一个异步函数 &lt;code&gt;asyncWork&lt;/code&gt; 之后再给 &lt;code&gt;event.returnValue&lt;/code&gt; 赋值。&lt;/p&gt;
&lt;p&gt;结果发现渲染进程那边会在 2 秒之后才打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;replyMessage 延迟 2 秒获取到主进程的返回结果&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且对于渲染进程，以下两种写法，结果都是一样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// render.js
const { ipcRenderer } = require(&apos;electron&apos;);

function sendSyncMessageToMain() {
  const replyMessage = ipcRenderer.sendSync(&apos;render-send-sync-to-main&apos;, &apos;我是渲染进程通过 syncSend 发送给主进程的消息&apos;);
  console.log(&apos;replyMessage&apos;, replyMessage); // &apos;replyMessage 延迟 2 秒获取到主进程的返回结果&apos;&apos;
}

// 或者改用 async 函数
async function sendSyncMessageToMain() {
  const replyMessage = await ipcRenderer.sendSync(&apos;render-send-sync-to-main&apos;, &apos;我是渲染进程发送给主进程的同步消息&apos;);
  console.log(&apos;replyMessage&apos;, replyMessage); // &apos;replyMessage 延迟 2 秒获取到主进程的返回结果&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，不论渲染进程在接收 &lt;code&gt;sendSync&lt;/code&gt; 结果的时候，是不是用 &lt;code&gt;await&lt;/code&gt; 等待，都会等待结果返回后才向下执行。但如果你已经确定你的请求是一个异步的话，建议还是使用 &lt;code&gt;invoke&lt;/code&gt; 去发送消息，这里出于两点原因考虑：&lt;/p&gt;
&lt;p&gt;1、方法名 &lt;code&gt;sendSync&lt;/code&gt; 就很符合语义，发送同步消息；&lt;/p&gt;
&lt;p&gt;2、请求执行的明明是异步代码，但是如果你用 &lt;code&gt;const replyMessage = ipcRenderer.sendSync(&apos;xxx&apos;)&lt;/code&gt; 方式来获取响应信息，会很奇怪。&lt;/p&gt;
&lt;p&gt;OKK，上面的第四点谈到了，请保证 &lt;code&gt;event.returnValue&lt;/code&gt; 是有值的，否则会照成非预期的影响。让我们也来写个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// render.js
const { ipcRenderer } = require(&apos;electron&apos;);

function sendSyncMessageToMain() {
  const replyMessage = ipcRenderer.sendSync(&apos;render-send-sync-to-main&apos;, &apos;我是渲染进程通过 syncSend 发送给主进程的消息&apos;);
  console.log(&apos;replyMessage&apos;, replyMessage); // replyMessage {error: &quot;reply was never sent&quot;}
  console.log(&apos;next&apos;); // 这里也会执行
}

// main.js
ipcMain.on(&apos;render-send-sync-to-main&apos;, async (event, message) =&amp;gt; {
  console.log(`receive message from render: ${message}`)
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，主进程那边不对 &lt;code&gt;event.returnValue&lt;/code&gt; 做处理，在渲染进程这边将会得到一个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{error: &quot;reply was never sent&quot;}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然 &lt;code&gt;next&lt;/code&gt; 也会打印，但是如果你再想去发送一次 &lt;code&gt;render-send-sync-to-main&lt;/code&gt; 你会发现页面已经卡了...&lt;/p&gt;
&lt;h3&gt;渲染进程与渲染进程(窗口与窗口)的通讯&lt;/h3&gt;
&lt;h4&gt;ipcRenderer.sendTo + ipcRenderer.on&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;举个例子：B窗口给你A窗口发送消息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;窗口A&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipcRenderer.on(&apos;B-to-A&apos;,(event,message)=&amp;gt;{
  console.log(message)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;窗口B&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const A_ID = ipcRenderer.sendSync(&apos;getWinId&apos;, &apos;A&apos;)
ipcRenderer.sendTo(A_ID,&apos;B-to-A&apos;,&apos;我是来自B窗口的消息&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;渲染进程和webview的通讯&lt;/h3&gt;
&lt;h4&gt;ipcRenderer.sendToHost + webview.addEventListener(&quot;ipc-message&quot;)&lt;/h4&gt;
&lt;p&gt;渲染进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const webview = document.querySelector(&apos;webview&apos;);
webview.addEventListener(&apos;ipc-message&apos;,(event, message)=&amp;gt;{
  console.log(message)
})

webview.send(&apos;rende-to-webview&apos;,&apos;发给webview里头的界面&apos;); // 给webview发消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;webview内嵌的页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcRenderer } from &apos;electron&apos;;

ipcRenderer.on(&apos;send-to&apos;,(event,args)=&amp;gt;{
	console.log(&apos;收到渲染进程发来的消息&apos;,args);
  ipcRenderer.sendToHost(&apos;message&apos;, &apos;我是webview发过来的消息&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;webview + preload协议 提供桥接方法&lt;/h4&gt;
&lt;p&gt;通过利用webview的preload属性 &lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/webview-tag#preload&quot;&gt;preload&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;preload其实是相当于一个页面运行其他脚本之前，先加载的指定脚本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有一点需要注意的是，webview的preload属性接受的是asar和file协议，注入js脚本&lt;/p&gt;
&lt;p&gt;preload.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ipcRenderer } from &apos;electron&apos;;
const Bridge = {
  ...各种方法或者什么参数
  sayHi(data){
    ipcRenderer.send(&apos;some-event&apos;, dataÏ)
  }
}
  
global.Bridge = Bridge; // 将Bridge的对象注入到全局对象上，webview打开的时候指的就是window
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;webview内嵌的页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myBridge = window.Bridge;

myBridge.sayHi(&apos;我是webview&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const localPreloadFile = `file://${require(&apos;path&apos;).resolve(&apos;./preload.js&apos;)}`
webview.setAtrribute(&apos;preload&apos;,preloadFileÏ)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20220326183153864.png&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;通过桥接的方法，实现electron的通信机制的转发，进而实现webview和渲染进程之间的沟通&lt;/p&gt;
&lt;h3&gt;进程统一实现方案（暂未完善）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import events from &quot;events&quot;;
import { ipcRenderer, ipcMain, webContents } from &quot;electron&quot;;

const PIPE_EVENT = &quot;__eventPipe&quot;;

class Eventer {
  constructor() {
    this.instance = new events.EventEmitter();
    this.instance.setMaxListeners(20);
    this.initEventPipe();
  }

  /**
   * @description: 初始化管道配置
   */
  initEventPipe() {
    if (ipcRenderer) {
      ipcRenderer.on(PIPE_EVENT, (e, { eventName, eventArgs }) =&amp;gt; {
        this.instance.emit(eventName, eventArgs);
      });
    }
    if (ipcMain) {
      ipcMain.handle(PIPE_EVENT, (e, { eventName, eventArgs }) =&amp;gt; {
        this.instance.emit(eventName, eventArgs);
        webContents.getAllWebContents().forEach((wc) =&amp;gt; {
          if (wc.id !== e.sender.id) {
            wc.send(PIPE_EVENT, { eventName, eventArgs });
          }
        });
      });
    }
  }

  /**
   * @description: 监听事件
   * @param {*} eventName 事件名称
   * @param {*} callBack 回调
   * @return {*}
   */
  on(eventName, callBack) {
    this.instance.on(eventName, callBack);
  }

  /**
   * @description: 事件触发
   * @param {*} eventName
   * @param {*} eventArgs
   */
  emit(eventName, eventArgs) {
    this.instance.emit(eventName, eventArgs);
    if (ipcMain) {
      webContents.getAllWebContents().forEach((wc) =&amp;gt; {
        wc.send(PIPE_EVENT, { eventName, eventArgs });
      });
    }
    if (ipcRenderer) {
      ipcRenderer.invoke(PIPE_EVENT, { eventName, eventArgs });
    }
  }
}

const event = new Eventer();

export default event;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import eventer from &apos;@/common/eventer&apos;;

eventer.on(&apos;my-event&apos;, (args)=&amp;gt;{
  console.log(args)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import eventer from &apos;@/common/eventer&apos;;
eventer.emit(&apos;my-event&apos;,(args)=&amp;gt;{
  console.log(args);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一些使用记录&lt;/h2&gt;
&lt;h3&gt;按键配置&lt;/h3&gt;
&lt;p&gt;https://www.electronjs.org/zh/docs/latest/api/accelerator&lt;/p&gt;
&lt;h3&gt;渲染进程和webview的通信&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const webview = document.querySelector(&apos;webview&apos;);
webview.addEventListener(&apos;dom-ready&apos;,()=&amp;gt;{});
webview.addEventListener(&apos;ipc-message&apos;, ()=&amp;gt;{
  webview.send(&apos;sth&apos;)
})


ipcRenderer.sendToHost(&apos;sth&apos;)


// preload
const preloadFile = &apos;file://&apos; + require(&apos;path&apos;).resolve(&apos;./preload.js&apos;);
webview.setAttribute(&apos;preload&apos;, preloadFile);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 客户端
const clientSocket = io(&apos;http://127.0.0.1:3000/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;menu（应用菜单）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const menuTemplate = [
  IS_MAC &amp;amp;&amp;amp; { label: &quot;ismac&quot; }, //  macOS下第一个标签是应用程序名字,所以这里是为了兼容mac的
  {
    label: &quot;选项&quot;,
    submenu: [
      {
        label: &quot;退出&quot;,
        role: &quot;quit&quot;,
      },
      {
        label: &quot;我的博客&quot;,
        click: createBlogWin,
      },
      isDevelopment &amp;amp;&amp;amp; {
        label: &quot;打开devTool&quot;,
        click: createDevTool,
        accelerator: &quot;CommandOrControl + shift + i&quot;,
      },
    ].filter(Boolean),
  },
  { type: &quot;separator&quot; }, // mac下无效
  {
    label: &quot;文件&quot;,
    submenu: [
      {
        label: &quot;子菜单&quot;,
        click: () =&amp;gt; {
          // 调用了dialog（弹窗模块），演示效果
          dialog.showMessageBoxSync({
            type: &quot;info&quot;,
            title: &quot;提示&quot;,
            message: &quot;点击了子菜单&quot;,
          });
        },
      },
    ],
  },
].filter(Boolean);

const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较要注意的就是mac的第一个菜单被系统设置为了应用的名称等等&lt;/p&gt;
&lt;p&gt;所以最上面的数组加了一行&lt;strong&gt;mac&lt;/strong&gt;相关的是配&lt;/p&gt;
&lt;h3&gt;cookie&lt;/h3&gt;
&lt;h4&gt;基本使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const { remote } from &apos;electron&apos;;

const getCookie = async (name)=&amp;gt; {
  let cookie = await remote.session.defautlSession.cookies.get({name});
  if(cookie.length) return cookies[0].value;
  else return &apos;&apos;
}
const setCookie = async (cookie) =&amp;gt; {
  await remote.session.defautlSession.cookies.set(cookie)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;defaultSession是当前浏览器会话的对象实例，也可以通过&lt;code&gt;window.webContents.session&lt;/code&gt;获取session对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;删除浏览器缓存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;await remote.session.defaultSession.cookie.clearStorageData({
  storages: &apos;localstorage, cookie&apos; // 删除localStroage中和cookie的缓存数据
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;storages的参数可以查看 &lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/session#sesclearstoragedataoptions&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;判断操作系统类型&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;process.platform &lt;a href=&quot;http://nodejs.cn/api/process/process_platform.html&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { platform } from &apos;process&apos;;

console.log(`This platform is ${platform}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&apos;aix&apos;&lt;/li&gt;
&lt;li&gt;&apos;darwin&apos;&lt;/li&gt;
&lt;li&gt;&apos;linux&apos;&lt;/li&gt;
&lt;li&gt;&apos;freebsd&apos;&lt;/li&gt;
&lt;li&gt;&apos;openbsd&apos;&lt;/li&gt;
&lt;li&gt;&apos;sunos&apos;&lt;/li&gt;
&lt;li&gt;&apos;win32&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;BrowserWindow&lt;/h3&gt;
&lt;h4&gt;webSecurity&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/tutorial/security#6-%E4%B8%8D%E8%A6%81%E7%A6%81%E7%94%A8websecurity&quot;&gt;官方&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;禁用同源策略 (通常用来测试网站)，设置false禁用网站的同源政策&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;禁用 &lt;code&gt;webSecurity&lt;/code&gt; 将会禁止同源策略并且将 &lt;code&gt;allowRunningInsecureContent&lt;/code&gt; 属性置 &lt;code&gt;true&lt;/code&gt;。 换句话说，这将使得来自其他站点的非安全代码被执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;net请求库（原生）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;主进程&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/net&quot;&gt;官方使用文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;dialog（系统对话框）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;主进程使用&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/dialog&quot;&gt;官方使用文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;*使用webview（不稳定）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;假如要在页面中使用webview的话，需要设置BrowserWindow 初始化的时候的配置 &lt;code&gt;webviewTag: true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/tutorial/web-embeds#webview&quot;&gt;官方建议文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.electronjs.org/zh/docs/latest/api/webview-tag#src&quot;&gt;官方使用文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;main.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
        nodeIntegration: true,
        contextIsolation: false,
    	webviewTag: true
    }
})


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;electron项目中常用的库&lt;/h2&gt;
&lt;h3&gt;mousetrap&lt;/h3&gt;
&lt;p&gt;按键事件的监听库，监听网页的按键事件&lt;/p&gt;
&lt;h3&gt;SQLite持久化数据&lt;/h3&gt;
&lt;p&gt;推荐二次封装库 &lt;code&gt;node-sqlite3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;三次封装库 &lt;code&gt;knextjs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20220301000309311.png]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;lowdb和electron-store&lt;/h3&gt;
&lt;p&gt;小型的数据存储工具&lt;/p&gt;
&lt;h2&gt;离谱的坑&lt;/h2&gt;
&lt;h3&gt;控制台打印乱码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这种情况是由于控制台输出的不是UTF-8的编码格式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./image-20220112210828647.png&quot; alt=&quot;image-20220112210828647&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;执行start的脚本中加入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;start&quot;: &quot;chcp 65001 &amp;amp;&amp;amp; ....&quot;	
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;electron的包装不上&lt;/h3&gt;
&lt;p&gt;**解决：**将electron包的源改为国内的源：&lt;/p&gt;
&lt;p&gt;.npmrc文件中强制指定源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;electron_mirror=&quot;https://npm.taobao.org/mirrors/electron/&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写promise allSettled</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise-allsettled/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise-allsettled/</guid><description>Promise allSettled    js // Promise allSettled Promise._allSettled = function (taskArr) {   return new Promise((resolve, reject) = {     const result ...</description><pubDate>Mon, 13 Dec 2021 16:06:06 GMT</pubDate><content:encoded>&lt;h2&gt;Promise allSettled&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Promise allSettled
Promise._allSettled = function (taskArr) {
  return new Promise((resolve, reject) =&amp;gt; {
    const result = []; // 结果数组
    let count = 0;  // 计数，当count等于taskArr的长度的时候，说明都处理完这些promise了
    taskArr.forEach((task, index) =&amp;gt; {
      if (!(task instanceof Promise)) task = Promise.resolve(task);
      task.then((value) =&amp;gt; {
        result[index] = {
          status: &apos;fullfilled&apos;,
          value
        }
        count++;
        if (count === taskArr.length) {
          resolve(result)
        }
      }, (reason) =&amp;gt; {
        result[index] = {
          status: &apos;rejected&apos;,
          reason
        }
        count++;
        if (count === taskArr.length) {
          resolve(result)
        }
      })
    })
  })
}

Promise._allSettled([
  () =&amp;gt; &apos;我不是一个promise&apos;,
  Promise.resolve(12121),
  new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve(&apos;motherfucker&apos;)
    }, 4000);
  }),
  new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      reject(&apos;hello&apos;)
    }, 3000);
  })
]).then((data) =&amp;gt; {
  console.log(data);
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写promise race</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise-race/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise-race/</guid><description>Promise Race  js // Promise race实现 Promise._race = function (taskArr) {   return new Promise((resolve, reject) = {     for (const iterator of taskArr)...</description><pubDate>Mon, 13 Dec 2021 15:59:30 GMT</pubDate><content:encoded>&lt;h2&gt;Promise Race&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Promise race实现
Promise._race = function (taskArr) {
  return new Promise((resolve, reject) =&amp;gt; {
    for (const iterator of taskArr) {
      Promise.resolve(iterator).then((data) =&amp;gt; {
        resolve(data);
      }, (err) =&amp;gt; {
        reject(err);
        return
      })
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写primise all</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99primise-all/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99primise-all/</guid><description>手写Promise all  js // Promise all的实现 Promise._all = function (tasks) {   return new Promise((resolve, reject) = {     if (!tasks || !tasks.length) {   ...</description><pubDate>Mon, 13 Dec 2021 14:25:01 GMT</pubDate><content:encoded>&lt;h2&gt;手写Promise all&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Promise all的实现
Promise._all = function (tasks) {
  return new Promise((resolve, reject) =&amp;gt; {
    if (!tasks || !tasks.length) {
      return []
    }

    // 结果数组
    let result = [];
    // 做数量记录
    let count = 0;

    for (let i = 0; i &amp;lt; tasks.length; i++) {
      const element = tasks[i]; // 当前的任务
      Promise.resolve(element).then( // 需要用Promise包裹以防止当前task并不是用Promise封装的
        (data) =&amp;gt; {
          result[i] = data;
          if (++count === tasks.length) {
            resolve(result)
          }
        },
        (err) =&amp;gt; {
          reject(err);
          return;
        })
    }
  })
}

Promise._all([
  Promise.resolve(11211),
  Promise.resolve(21213212312),
  new Promise((resolve) =&amp;gt; setTimeout(() =&amp;gt; {
    resolve(&apos;延时&apos;)
  }, 1000))
]).then((data) =&amp;gt; {
  console.log(data);
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写JS并发控制（asyncPool）</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99js%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6asyncpool/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99js%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6asyncpool/</guid><description>并发控制  js // limiteNum: 并发数目 // tasks: 需要处理的任务列表 // iteraterFn: 完成的处理函数  // 并发控制任务 const ayncPool = async (limiteNum, tasks, iteraterFn) = {   // 所有的任务...</description><pubDate>Mon, 13 Dec 2021 13:57:51 GMT</pubDate><content:encoded>&lt;h2&gt;并发控制&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// limiteNum: 并发数目
// tasks: 需要处理的任务列表
// iteraterFn: 完成的处理函数

// 并发控制任务
const ayncPool = async (limiteNum, tasks, iteraterFn) =&amp;gt; {
  // 所有的任务队列
  const rect = [];
  // 正在进行的任务队列
  const pendingTasks = [];

  // 这里使用for of而不是用foreach的原因是
  // forEach await 时候并不会停止遍历
  for (const task of tasks) {
    // 这里对外层的结束处理函数包裹一层promise
    let promiseTask = Promise.resolve().then(() =&amp;gt; iteraterFn(task, tasks));
    // rect去存储了promiseTask
    rect.push(promiseTask);

    if (limiteNum &amp;lt;= tasks.length) {
      const excutingTask = promiseTask.then(() =&amp;gt; pendingTasks.splice(pendingTasks.indexOf(excutingTask), 1))
      pendingTasks.push(excutingTask)
      if (pendingTasks.length &amp;gt;= limiteNum) {
        // 这里去获取跑的最快的一个task
        await Promise.race(pendingTasks);
      }
    }
  }

  return Promise.all(rect)
}

const iterater = (i) =&amp;gt; {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve(i);
      console.log(i);
    }, i);
  })
}

ayncPool(2, [1000, 5000, 3000, 2000], iterater).then((data) =&amp;gt; {
  console.log(data)
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写发布订阅</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/</guid><description>js  // 发布订阅 class EventEmitter {   constructor() {     this.cache = {}   }    // 订阅事件   on(name, callback, once = false) {     if (!this.cache[name]) ...</description><pubDate>Mon, 13 Dec 2021 11:14:56 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt; // 发布订阅
class EventEmitter {
  constructor() {
    this.cache = {}
  }

  // 订阅事件
  on(name, callback, once = false) {
    if (!this.cache[name]) {
      this.cache[name] = []
    }
    this.cache[name].push({
      callback,
      once
    })
  }

  // 只订阅一次
  once(name, callback) {
    this.on(name, callback, true)
  }

  // 发布
  emit(name, ...rest) {
    if (!this.cache[name]) {
      return
    }
    this.cache[name].forEach((param, index) =&amp;gt; {
      const { once, callback } = param;
      if (once) {
        this.cache[name].splice(index, 1);
        if (!this.cache[name]?.length) delete this.cache[name]
      }
      callback.call(this, ...rest)
    })
  }

  // 取消订阅
  off(name, callback) {
    if (!name) { this.cache = {}; return; }
    if (!callback) { delete this.cache[name]; return }
    if (!this.cache[name]) { return }

    const i = this.cache[name].findIndex((f) =&amp;gt; f.callback === callback)
    this.cache[name].splice(i, 1);
    if (!this.cache[name].length) delete this.cache[name]
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写curry</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99curry/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99curry/</guid><description>要求实现如下  js const add = (a, b, c) = a + b + c; const a1 = currying(add, 1); const a2 = a1(2); console.log(a2(3)) // 6     js // curry pro function curr...</description><pubDate>Mon, 13 Dec 2021 11:02:58 GMT</pubDate><content:encoded>&lt;p&gt;要求实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const add = (a, b, c) =&amp;gt; a + b + c;
const a1 = currying(add, 1);
const a2 = a1(2);
console.log(a2(3)) // 6
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// curry pro
function curryPro(fn, ...outerProps) {
  function curried(...middleProps) {
    const concatArr = [...middleProps];
    if (concatArr.length &amp;gt;= fn.length) {
      return fn.call(this, ...concatArr)
    }
    return function (...innerProps) {
      return curried.call(this, ...concatArr.concat(innerProps))
    }
  }
  if(outerProps.length){
    return curried(...outerProps);
  }
  return curried
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写compose</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99compose/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99compose/</guid><description>compose简介  compose就是执行一系列的任务（函数），比如有以下任务队列  js let tasks = [step1, step2, step3, step4]    每一个step都是一个步骤，按照步骤一步一步的执行到结尾，这就是一个compose  compose在函数式编程中是一...</description><pubDate>Mon, 13 Dec 2021 10:40:26 GMT</pubDate><content:encoded>&lt;h2&gt;compose简介&lt;/h2&gt;
&lt;p&gt;compose就是执行一系列的任务（函数），比如有以下任务队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let tasks = [step1, step2, step3, step4]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一个step都是一个步骤，按照步骤一步一步的执行到结尾，这就是一个&lt;strong&gt;compose&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;compose在函数式编程中是一个很重要的工具函数，在这里实现的compose有三点说明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个函数是多元的（接受多个参数），后面的函数都是单元的（接受一个参数）&lt;/li&gt;
&lt;li&gt;执行顺序的自右向左的&lt;/li&gt;
&lt;li&gt;所有函数的执行都是同步的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还是用一个例子来说，比如有以下几个函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let init = (...args) =&amp;gt; args.reduce((ele1, ele2) =&amp;gt; ele1 + ele2, 0)
let step2 = (val) =&amp;gt; val + 2
let step3 = (val) =&amp;gt; val + 3
let step4 = (val) =&amp;gt; val + 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这几个函数组成一个任务队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;steps = [step4, step3, step2, init]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用compose组合这个队列并执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let composeFunc = compose(...steps)

console.log(composeFunc(1, 2, 3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6 -&amp;gt; 6 + 2 = 8 -&amp;gt; 8 + 3 = 11 -&amp;gt; 11 + 4 = 15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以流程就是从init自右到左依次执行，下一个任务的参数是上一个任务的返回结果，并且任务都是同步的，这样就能保证任务可以按照有序的方向和有序的时间执行。&lt;/p&gt;
&lt;h2&gt;手写&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// compose
function compose(...args) {
  const fnArr = Array.from(args);
  let result;
  return function composer(...rest) {
    const current = fnArr.shift();
    result = current.call(this, ...rest);
    if (!fnArr.length) {
      return result
    }
    return composer(result)
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>深拷贝最新优化代码</title><link>https://nollieleo.github.io/posts/%E6%B7%B1%E6%8B%B7%E8%B4%9D%E6%9C%80%E6%96%B0%E4%BC%98%E5%8C%96%E4%BB%A3%E7%A0%81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B7%B1%E6%8B%B7%E8%B4%9D%E6%9C%80%E6%96%B0%E4%BC%98%E5%8C%96%E4%BB%A3%E7%A0%81/</guid><description>js const map = new WeakMap()      const handleArrAndObject = (varias) = {       const constructor = Object.getPrototypeOf(varias).constructor;       c...</description><pubDate>Sun, 12 Dec 2021 15:46:41 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const map = new WeakMap()

    const handleArrAndObject = (varias) =&amp;gt; {
      const constructor = Object.getPrototypeOf(varias).constructor;
      const object = new constructor();
      for (const key in varias) {
        if (Object.hasOwnProperty.call(varias, key)) {
          const element = varias[key];
          object[key] = deepClone(element)
        }
      }
      return object;
    }

    const handleMap = (varias) =&amp;gt; {
      const constructor = Object.getPrototypeOf(varias).constructor;
      const currentMap = new constructor();
      varias.forEach((value, key) =&amp;gt; {
        currentMap.set(deepClone(key), deepClone(value))
      })
      return currentMap
    }

    const handleSet = (varias) =&amp;gt; {
      const constructor = Object.getPrototypeOf(varias).constructor;
      const currentSet = new constructor();
      varias.forEach((value) =&amp;gt; {
        currentSet.set(deepClone(value))
      })
      return currentSet
    }

    const handleRegx = (varias) =&amp;gt; {
      const constructor = Object.getPrototypeOf(varias).constructor;
      const { flags, source } = varias;
      const currentRegx = new constructor(source, flags);
      return currentRegx;
    }

    const handleOthers = (varias) =&amp;gt; {
      const constructor = Object.getPrototypeOf(varias).constructor;
      return new constructor(Object.prototype.valueOf.call(varias));
    }

    const handleFn = (vaira) =&amp;gt; {
      return vaira;
    }


    const cloneMap = {
      &apos;[object Array]&apos;: handleArrAndObject,
      &apos;[object Object]&apos;: handleArrAndObject,
      &apos;[object Map]&apos;: handleMap,
      &apos;[object WeakMap]&apos;: handleMap,
      &apos;[object Set]&apos;: handleSet,
      &apos;[object WeakSet]&apos;: handleSet,
      &apos;[object RegExp]&apos;: handleRegx,
      &apos;[object Function]&apos;: handleFn,
    }

    const getStringType = (varias) =&amp;gt; {
      return Object.prototype.toString.call(varias);
    }

    const getCloneVarias = (varias) =&amp;gt; {
      let fn = handleOthers;
      const stringType = getStringType(varias);
      if (cloneMap.hasOwnProperty(stringType)) {
        fn = cloneMap[stringType]
      }
      return fn?.(varias);
    }

    const isObject = (target) =&amp;gt; {
      return typeof target === &quot;object&quot; &amp;amp;&amp;amp; target !== null;
    }

    function deepClone(obj) {
      if (!isObject(obj)) {
        return obj;
      }
      if (map.get(obj)) {
        return obj
      }
      map.set(obj, true)
      return getCloneVarias(obj)
    }

    const obj = {
      arr: [1212, 121],
      name: &apos;weng&apos;,
      name2: new String(&apos;weng&apos;),
      map: new Map(),
      set: new Set(),
      date: Date.now(),
      regexp: new RegExp(),
      string: new String(),
      sayHi: () =&amp;gt; { },
      sayMyName: function () { }
    }

    obj.target = obj;

    const obj3 = deepClone(obj);

    obj3.arr[1] = &apos;wahahahah&apos;;


    console.log(obj, obj3)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>自定义loader记录</title><link>https://nollieleo.github.io/posts/%E8%87%AA%E5%AE%9A%E4%B9%89loader%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%87%AA%E5%AE%9A%E4%B9%89loader%E8%AE%B0%E5%BD%95/</guid><description>调试loader   1. 准备好自己的webpack和loader  这里我用的项目是我自己的     2. vscode配置debug配置文件  按照这个步骤配置完会生成一个.vscode文件夹下面有个launch.json文件      然后替换它  js {   &quot;version&quot;: &quot;0....</description><pubDate>Sun, 10 Oct 2021 16:01:49 GMT</pubDate><content:encoded>&lt;h2&gt;调试loader&lt;/h2&gt;
&lt;h3&gt;1. 准备好自己的webpack和loader&lt;/h3&gt;
&lt;p&gt;这里我用的项目是我自己的&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/NollieLeo/own-ReactTsAppCreateTemplate&quot;&gt;webpack template&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2. vscode配置debug配置文件&lt;/h3&gt;
&lt;p&gt;按照这个步骤配置完会生成一个.vscode文件夹下面有个launch.json文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae53a2653c7e43799c9802a100886a37~tplv-k3u1fbpfcp-watermark.awebp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后替换它&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      &quot;type&quot;: &quot;node&quot;,
      &quot;request&quot;: &quot;launch&quot;,
      &quot;name&quot;: &quot;Webpack Debug&quot;,
      &quot;cwd&quot;: &quot;${workspaceFolder}&quot;,
      &quot;runtimeExecutable&quot;: &quot;npm&quot;,
      &quot;runtimeArgs&quot;: [
        &quot;run&quot;,
        &quot;debug&quot;
      ],
      &quot;port&quot;: 5858
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用以上配置信息，我们创建了一个 &lt;strong&gt;Webpack Debug&lt;/strong&gt; 的调试任务。当运行该任务的时候，会在当前工作目录下执行 &lt;code&gt;npm run debug&lt;/code&gt; 命令。因此，接下来我们需要在 &lt;strong&gt;package.json&lt;/strong&gt; 文件中增加 &lt;strong&gt;debug&lt;/strong&gt; 命令，具体内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;debug&quot;: &quot;webpack --config build/webpack.dev.js&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 把自定义loader写道自己的rule里头&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./image-20211010161220568.png&quot; alt=&quot;image-20211010161220568&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后打断点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20211010161333822.png&quot; alt=&quot;image-20211010161333822&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.启动调试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./image-20211010161437329.png&quot; alt=&quot;image-20211010161437329&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>自定义脚手架搭建记录</title><link>https://nollieleo.github.io/posts/%E8%87%AA%E5%AE%9A%E4%B9%89%E8%84%9A%E6%89%8B%E6%9E%B6%E6%90%AD%E5%BB%BA%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%87%AA%E5%AE%9A%E4%B9%89%E8%84%9A%E6%89%8B%E6%9E%B6%E6%90%AD%E5%BB%BA%E8%AE%B0%E5%BD%95/</guid><description>首先先知道有哪些工具可以用  | 名称                                                         | 简介                                                         | | ---------...</description><pubDate>Sun, 05 Sep 2021 10:41:37 GMT</pubDate><content:encoded>&lt;p&gt;首先先知道有哪些工具可以用&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ftj%2Fcommander.js%2Fblob%2Fmaster%2FReadme_zh-CN.md&quot;&gt; commander&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;命令行自定义指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FSBoudrias%2FInquirer.js%2F&quot;&gt; inquirer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;命令行询问用户问题，记录回答结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fchalk&quot;&gt; chalk&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;控制台输出内容样式美化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fora&quot;&gt; ora&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;控制台 loading 样式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Ffiglet&quot;&gt; figlet&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;控制台打印 logo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Feasy-table&quot;&gt; easy-table&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;控制台输出表格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fdownload-git-repo&quot;&gt; download-git-repo&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;下载远程模版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Ffs-extra&quot;&gt; fs-extra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;系统fs模块的扩展，提供了更多便利的 API，并继承了fs模块的 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fcross-spawn&quot;&gt; cross-spawn&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;支持跨平台调用系统上的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://plopjs.com/documentation/#add&quot;&gt;plop&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;轻量级快速构建模板&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/ftHiZLKvipbt9KJBjMT-RA&quot;&gt;前端黑科技篇章之plop，让你也拥有自己的脚手架&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6966119324478079007#heading-19&quot;&gt;从 0 构建自己的脚手架/CLI知识体系&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>前端面试知识整理</title><link>https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86/</guid><description>整合版       React           html         JS篇            CSS           模块化       TypeScript             webpack       package.json       网络     HTTP/HTTP...</description><pubDate>Mon, 30 Aug 2021 23:30:21 GMT</pubDate><content:encoded>&lt;h2&gt;整合版&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6987070062490288165#heading-83&quot;&gt;手撕钉钉前端考试卷，offer，拿来吧你~&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;React&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6941546135827775525#heading-6&quot;&gt;「2021」高频前端面试题汇总之React篇（上）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6940942549305524238#heading-14&quot;&gt;「2021」高频前端面试题汇总之React篇（下）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;html&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6905294475539513352#heading-22&quot;&gt;「2021」高频前端面试题汇总之HTML篇&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904180943945742#heading-62&quot;&gt;html篇--这可能是目前较为全面的html面试知识点了吧&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;JS篇&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904194474770445#heading-0&quot;&gt;javascript篇--1.6万字带你回忆那些遗忘的JS知识点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6940945178899251230#heading-92&quot;&gt;「2021」高频前端面试题汇总之JavaScript篇（上）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6941194115392634888#heading-8&quot;&gt;「2021」高频前端面试题汇总之JavaScript篇（下）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6968713283884974088#heading-15&quot;&gt;JS手写篇&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6905539198107942919#heading-51&quot;&gt;「2021」高频前端面试题汇总之CSS篇&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904185847087111#heading-43&quot;&gt;css篇--100道近两万字帮你巩固css知识点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903824428105735#heading-0&quot;&gt;前端响应式布局原理与方案（详细版）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;模块化&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903744518389768#heading-38&quot;&gt;详解模块化&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6872111128135073806#heading-49&quot;&gt;一份不可多得的 TS 学习指南（1.8W字）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6926794697553739784#heading-23&quot;&gt;TypeScript 高级用法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904147167215624#heading-3&quot;&gt;手撕ts面试题——不能不掌握的ts高级特性（三） &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844904145732763655#heading-12&quot;&gt;typescript不能不掌握的高级特性（二）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;webpack&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903824428105735#heading-0&quot;&gt;「吐血整理」再来一打Webpack面试题&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;package.json&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6987179395714646024#heading-2&quot;&gt;你真的了解package.json吗？来看看吧，这可能是最全的package解析&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;网络&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6939691851746279437#heading-18&quot;&gt;字节跳动最爱考的前端面试题：计算机网络基础&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;HTTP/HTTPS&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6857287743966281736#heading-9&quot;&gt;「查缺补漏」巩固你的HTTP知识体系&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;TCP&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6939691851746279437#heading-18&quot;&gt;(建议收藏)TCP协议灵魂之问，巩固你的网路底层基础&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前端跨域&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903767226351623#heading-11&quot;&gt;九种跨域方式实现原理（完整版）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前端安全&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903685122703367#heading-1&quot;&gt;前端安全系列（一）：如何防止XSS攻击？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903689702866952#heading-4&quot;&gt;前端安全系列之二：如何防止CSRF攻击？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903781704925191#heading-12&quot;&gt;前端面试查漏补缺--(七) XSS攻击与CSRF攻击&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;浏览器&lt;/h2&gt;
&lt;h3&gt;垃圾回收机制&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6981588276356317214#heading-15&quot;&gt;「硬核JS」你真的了解垃圾回收机制吗&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前端性能优化&lt;/h2&gt;
&lt;h3&gt;SEO&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903824428105735#heading-0&quot;&gt;SEO&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>React源码学习-render阶段-update递归-part7</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5-update%E9%80%92%E5%BD%92-part7/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5-update%E9%80%92%E5%BD%92-part7/</guid><description>...</description><pubDate>Sun, 29 Aug 2021 17:08:37 GMT</pubDate><content:encoded/></item><item><title>React源码学习-render阶段-mount递归-part6</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5-mount%E9%80%92%E5%BD%92-part6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5-mount%E9%80%92%E5%BD%92-part6/</guid><description>这里说一下 mount时候的beginWork和completeWork的流程吧     mount递归   断点调试  render阶段的递阶段起点是beginWork，归阶段的起点是completeWork，那我们就在源码上打断点。      应用demo如下    刷新页面正式进入调试。   ...</description><pubDate>Mon, 23 Aug 2021 21:53:25 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这里说一下 mount时候的beginWork和completeWork的流程吧&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;mount递归&lt;/h1&gt;
&lt;h2&gt;断点调试&lt;/h2&gt;
&lt;p&gt;render阶段的递阶段起点是beginWork，归阶段的起点是completeWork，那我们就在源码上打断点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823220759254.png&quot; alt=&quot;image-20210823220759254&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823220834921.png&quot; alt=&quot;image-20210823220834921&quot; /&gt;&lt;/p&gt;
&lt;p&gt;应用demo如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823223552306.png&quot; alt=&quot;image-20210823223552306&quot; /&gt;&lt;/p&gt;
&lt;p&gt;刷新页面正式进入调试。&lt;/p&gt;
&lt;p&gt;1.发现第一次进入页面&lt;code&gt;beginWork&lt;/code&gt;中，current有值，并且此时的tag是等于3的，之前说的我们在createFiberRoot的那个阶段这个tag也等于三，我们不妨猜测这个tag为3指的是 fiberRootNode&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823221225561.png&quot; alt=&quot;image-20210823221225561&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找到react-reconciler包下的ReactWorkTag，可以看到tag = 3对应的是叫 &lt;code&gt;HostRoot&lt;/code&gt;的tag&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823222916683.png&quot; alt=&quot;image-20210823222916683&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.点击下一个断点&lt;/p&gt;
&lt;p&gt;我们发现&lt;strong&gt;current&lt;/strong&gt;的值为null（之前说过，只有根节点才存在current值，而其他节点只存在&lt;strong&gt;workInProgress&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823223122090.png&quot; alt=&quot;image-20210823223122090&quot; /&gt;&lt;/p&gt;
&lt;p&gt;且当前节点的elementType为function App()，也就是写的一个app函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823223335090.png&quot; alt=&quot;image-20210823223335090&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之后就是div了，然后是div的子节点header&lt;/p&gt;
&lt;p&gt;之后又想走header的子节点title（其实这里算不上是子节点，只是个文本节点，react对文本节点进行了优化，无自己的beginWork，&lt;strong&gt;react会对只有唯一一个文本子节点的节点，做了优化，这样这个文本节点无自己的fiber节点，也就是不走beginWork&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;因为header的文本节点不算，所以直接进入了header节点的completeWork的阶段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823224738688.png&quot; alt=&quot;image-20210823224738688&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之后header 归阶段结束，会去找header的sibling，也就是兄弟节点，兄弟节点进入beginWork（递阶段）&lt;/p&gt;
&lt;p&gt;就这样以此往复&lt;strong&gt;深度优先遍历&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之后走完main，发现main无兄弟了，就开始走main父节点的&lt;strong&gt;completeWork&lt;/strong&gt;阶段也就是div，然后就这样一直走，走到了App的completeWork然后走到了 &lt;code&gt;rootFiber&lt;/code&gt;结束&lt;/p&gt;
&lt;p&gt;这时候一个render阶段就结束了&lt;/p&gt;
&lt;h2&gt;beginWork做了啥？&lt;/h2&gt;
&lt;p&gt;这里我们以 走到第二步的div节点举例子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823233545095.png&quot; alt=&quot;image-20210823233545095&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 依照tag判断component类型&lt;/h3&gt;
&lt;p&gt;首先根据当前的workInProgress的tag进入不同的Component处理逻辑&lt;/p&gt;
&lt;p&gt;这里的div节点，tag为5，进入的是 HostComponent的处理逻辑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823234419849.png&quot; alt=&quot;image-20210823234419849&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. updateHostComponents$1&lt;/h3&gt;
&lt;p&gt;首先会对一些参数做赋值操作，之后用isDirectTextChild字段判断当前节点是否只有一个文本节点（上面提到了），如果是，则不会去创建这个文本节点的fiber，算是react的一个优化手段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210823235701602.png&quot; alt=&quot;image-20210823235701602&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. reconcileChildren&lt;/h3&gt;
&lt;p&gt;从名字可以看出这个玩意应该是reconciler调试器的比较重要的函数。这个函数就是用来创建当前节点的子节点的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;code&gt;mount&lt;/code&gt;的组件，他会创建新的&lt;code&gt;子Fiber节点&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对于&lt;code&gt;update&lt;/code&gt;的组件，他会将当前组件与该组件在上次更新时对应的&lt;code&gt;Fiber节点&lt;/code&gt;比较（也就是俗称的&lt;code&gt;Diff&lt;/code&gt;算法），将比较的结果生成新&lt;code&gt;Fiber节点&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210824000029762.png&quot; alt=&quot;image-20210824000029762&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果当前不存在current就走&lt;code&gt;mountChildFibers&lt;/code&gt;，否者就走&lt;code&gt;reconcileChildFibers&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也就是说存在current的时候是整个应用的根节点了&lt;/p&gt;
&lt;p&gt;我们找到ReactChildFiber这个文件&lt;/p&gt;
&lt;p&gt;找到&lt;code&gt;reconcileChildFibers&lt;/code&gt;和 &lt;code&gt;mountChildFibers&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210826215700516.png&quot; alt=&quot;image-20210826215700516&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从代码可以看出，和&lt;code&gt;beginWork&lt;/code&gt;一样，他也是通过&lt;code&gt;current === null ?&lt;/code&gt;区分&lt;code&gt;mount&lt;/code&gt;与&lt;code&gt;update&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发现这两个走的都是同一个函数，只是参数不一样&lt;/p&gt;
&lt;h3&gt;4. 打上不一样的effectTag或者直接return&lt;/h3&gt;
&lt;p&gt;上面的 childReconciler的参数 代表的是否追踪副作用&lt;/p&gt;
&lt;p&gt;例如下面的这个&lt;code&gt;deleteChild&lt;/code&gt;函数是表示删除当前节点的子节点的操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    if (!shouldTrackSideEffects) {
      // Noop.
      return;
    }
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete];
      returnFiber.flags |= ChildDeletion;
    } else {
      deletions.push(childToDelete);
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果说需要追踪副作用的话，会给删除节点的fiber打上一个&lt;code&gt;flags&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;搜索reactFiberFlags文件看这些&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactSideEffectTags.js&quot;&gt;Flags&lt;/a&gt;都有些啥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;众所周知，render阶段不会操作dom的，commit阶段才会，所以render阶段这个flags的操作只是为了为fiber打上tag告诉后续这个节点要进行什么操作&lt;/p&gt;
&lt;h4&gt;为什么是二进制掩码格式的flags呢？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;这里有一种情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;p&gt;当一个fiber节点需要插入并且需要更新属性的话&lt;/p&gt;
&lt;p&gt;那就得绑定2个flags，一个是 &lt;code&gt;Update &lt;/code&gt;一个是 &lt;code&gt;Placement&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里用二进制的话就可以直接进行按位 &lt;code&gt;或 &lt;/code&gt;的操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const effectTag = NoEffect;
effectTag |= Update;
effectTag |= Placement;

(effectTag &amp;amp; PlacementAndUpdate) !== NoEffect; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论如何最终，都会走到&lt;code&gt;reconcileChildFibers&lt;/code&gt;这个方法里头，而这个方法是写在 &lt;code&gt;ChildReconciler&lt;/code&gt;函数的，总而言之就是加了一层去判断是否要追踪副作用。&lt;/p&gt;
&lt;h3&gt;5. reconcileChildFibers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactChildFiber.old.js#L1272&quot;&gt;地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个方法中会判断当前child的类型，进而对不同的类型做不一样的操作&lt;/p&gt;
&lt;p&gt;主要分为两个大类&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;type === &apos;object&apos;&lt;/li&gt;
&lt;li&gt;type === &apos;string&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然最需要细分的就是为object的情况，里头还包括了child是children等情形&lt;/p&gt;
&lt;p&gt;我们走到object里头细看一下，假设我们现在走到了一个节点是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;header&amp;gt;
        &amp;lt;span&amp;gt;title&amp;lt;/span&amp;gt;
&amp;lt;/header&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那他就会去调用 &lt;code&gt;reconcileSingleElement&lt;/code&gt;的方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829105504513.png&quot; alt=&quot;image-20210829105504513&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终这个方法会走到createFiberFromElement&lt;/p&gt;
&lt;p&gt;也就是通过reactElement的数据创建一个fiber节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829110028352.png&quot; alt=&quot;image-20210829110028352&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并且这个方法内部会调用一个方法叫做``createFiberFromTypeAndProps`&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829110240512.png&quot; alt=&quot;image-20210829110240512&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这个方法的内部会根据当前的component type去走不同的逻辑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829110319749.png&quot; alt=&quot;image-20210829110319749&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里呢直接将fibertag 赋值为了HostComponent&lt;/p&gt;
&lt;p&gt;接下来去创建对应的fiber节点，调用createFiber，也就是&lt;/p&gt;
&lt;p&gt;new了一个 FiberNode，这里头的属性在Fiber那篇讲了一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829110436790.png&quot; alt=&quot;image-20210829110436790&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;最终他会生成新的子&lt;code&gt;Fiber节点&lt;/code&gt;并赋值给&lt;code&gt;workInProgress.child&lt;/code&gt;，作为本次&lt;code&gt;beginWork&lt;/code&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L1158&quot;&gt;返回值 &lt;/a&gt;，并作为下次&lt;code&gt;performUnitOfWork&lt;/code&gt;执行时&lt;code&gt;workInProgress&lt;/code&gt;的&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1702&quot;&gt;传参&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;要注意的点是，当你调用&lt;code&gt;reconcileChildFibers&lt;/code&gt;时候子节点可能是一个数组，&lt;strong&gt;这种情况react仍然是只针对数组的第一个元素创建fiber&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;completeWork做了啥？&lt;/h2&gt;
&lt;h3&gt;1. 根据fiber节点的tag进入不同的case&lt;/h3&gt;
&lt;p&gt;这里首先进入completeWork阶段的时候span标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function App() {
  return (
    &amp;lt;div className=&quot;node1&quot;&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;span&amp;gt;title&amp;lt;/span&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;
        main
        &amp;lt;p&amp;gt;test&amp;lt;/p&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829152223028.png&quot; alt=&quot;image-20210829152223028&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里是span 标签所以先进入 HostComponent的case逻辑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829152900269.png&quot; alt=&quot;image-20210829152900269&quot; /&gt;&lt;/p&gt;
&lt;p&gt;事先判断current是否是空，首次渲染非fiberRoot不存current&lt;/p&gt;
&lt;p&gt;接下来比较重要的是创建一个dom叫做createInstance&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829153657555.png&quot; alt=&quot;image-20210829153657555&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. createInstance&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829160429387.png&quot; alt=&quot;image-20210829160429387&quot; /&gt;&lt;/p&gt;
&lt;p&gt;createInstance会去 通过 &lt;code&gt;createElement&lt;/code&gt;创建一个dom元素&lt;/p&gt;
&lt;p&gt;这里domElement就是一个span&lt;/p&gt;
&lt;p&gt;之后返回一个instance去执行appendAllChidren函数，由于span是我们第一个创建的元素，所以append会被跳过&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829161632571.png&quot; alt=&quot;image-20210829161632571&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 保存创建出来的dom到fiber节点的stateNode中&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;workInProgress.stateNode = instance;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 将属性绑定到创建出来的dom元素上&lt;/h3&gt;
&lt;p&gt;执行finalizerInitialChildren, 将所有的属性绑到我们新创建的dom元素上面，到这里一个节点span的completework就大致完成了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829161958750.png&quot; alt=&quot;image-20210829161958750&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210829162021263.png&quot; alt=&quot;image-20210829162021263&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;⭐appendAllChildren逻辑&lt;/h3&gt;
&lt;p&gt;appendAllChildren的原理是，主要是将已经创建好的fiber节点挂载到当前的节点下。&lt;/p&gt;
&lt;p&gt;这也就是为什么之前上面的span的标签第一次创建的时候不走这个的逻辑的原因&lt;/p&gt;
&lt;p&gt;走到了APP时候，就构建了一颗完整的fiber树了&lt;/p&gt;
</content:encoded></item><item><title>React源码学习-render阶段-part5</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5%E7%AE%80%E4%BB%8B-part5/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-render%E9%98%B6%E6%AE%B5%E7%AE%80%E4%BB%8B-part5/</guid><description>之前fiber那边说过，当我们第一次创建完了整个应用的根节点（fiberRootNode的时候，我们就进入首屏渲染的阶段）  通过sheduleUpdateOnFiber去调度更新，调度成功之后会走performSyncWorkOnRoot，也就是说从根节点开始执行这次的更新。  这时候也肯定是没有...</description><pubDate>Sun, 22 Aug 2021 17:44:31 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;之前fiber那边说过，当我们第一次创建完了整个应用的根节点（fiberRootNode的时候，我们就进入首屏渲染的阶段）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过sheduleUpdateOnFiber去调度更新，调度成功之后会走&lt;code&gt;performSyncWorkOnRoot&lt;/code&gt;，也就是说从根节点开始执行这次的更新。&lt;/p&gt;
&lt;p&gt;这时候也肯定是没有一个workInProgress的fiber tree的，所以从根节点出发，去触发更新（第一次构建tree）操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822213436338.png&quot; alt=&quot;image-20210822213436338&quot; /&gt;&lt;/p&gt;
&lt;p&gt;协调器工作主要是入口是 workSyncLoop，递阶段开始于某个fiber其beginWork，归阶段则是调用completeWork&lt;/p&gt;
&lt;p&gt;这里我们说一下协调器的工作，渲染器后面讲（渲染器这里我们只要知道它是将变化的节点渲染到视图上，所以分为渲染到&lt;strong&gt;视图-之前-中-之后&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822215752746.png&quot; alt=&quot;image-20210822215752746&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因此协调器工作开始到结束，我们称呼为 &lt;strong&gt;&lt;code&gt;render&lt;/code&gt;阶段，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;渲染器开始到结束，称呼为&lt;code&gt;commit&lt;/code&gt;阶段&lt;/p&gt;
&lt;p&gt;整个流程就是首屏渲染了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;render阶段&lt;/h1&gt;
&lt;h2&gt;流程概览&lt;/h2&gt;
&lt;p&gt;首先我们先看调用栈里头render阶段是从哪里开始的呢？&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20210822181130206]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;render阶段&lt;/code&gt;开始于&lt;code&gt;performSyncWorkOnRoot&lt;/code&gt;或&lt;code&gt;performConcurrentWorkOnRoot&lt;/code&gt;方法的调用。这取决于本次更新是同步更新还是异步更新。&lt;/p&gt;
&lt;p&gt;之后调用&lt;code&gt;workSyncLoop&lt;/code&gt;或者&lt;code&gt;workLoopConcurrent&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null &amp;amp;&amp;amp; !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面所说的workInProgress tree构建的过程从这里正式开始&lt;/p&gt;
&lt;p&gt;代码中的workInProgress 代表的是当前已经创建的&lt;code&gt;workInProgress Fiber&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;performUnitOfWork&lt;/code&gt;方法会创建下一个&lt;code&gt;Fiber节点&lt;/code&gt;并赋值给&lt;code&gt;workInProgress&lt;/code&gt;，并将&lt;code&gt;workInProgress&lt;/code&gt;与已创建的&lt;code&gt;Fiber节点&lt;/code&gt;连接起来构成&lt;code&gt;Fiber树&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们知道&lt;code&gt;Fiber Reconciler&lt;/code&gt;是从&lt;code&gt;Stack Reconciler&lt;/code&gt;重构而来，通过遍历的方式实现可中断的递归&lt;/p&gt;
&lt;p&gt;所以具体我们看看&lt;code&gt;performUnitOfWork&lt;/code&gt;的工作可以分为两部分：&lt;strong&gt;“递”和“归”&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;”递“阶段&lt;/h3&gt;
&lt;p&gt;首先从&lt;code&gt;rootFiber&lt;/code&gt;开始向下深度优先遍历。为遍历到的每个&lt;code&gt;Fiber节点&lt;/code&gt;调用&lt;a href=&quot;https://github.com/facebook/react/blob/970fa122d8188bafa600e9b5214833487fbf1092/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3058&quot;&gt;beginWork方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该方法会根据传入的&lt;code&gt;Fiber节点&lt;/code&gt;创建&lt;code&gt;子Fiber节点&lt;/code&gt;，并将这两个&lt;code&gt;Fiber节点&lt;/code&gt;连接起来。&lt;/p&gt;
&lt;p&gt;当遍历到叶子节点（即没有子组件的组件）时就会进入“归”阶段。&lt;/p&gt;
&lt;h3&gt;“归”阶段&lt;/h3&gt;
&lt;p&gt;在“归”阶段会调用&lt;a href=&quot;https://github.com/facebook/react/blob/970fa122d8188bafa600e9b5214833487fbf1092/packages/react-reconciler/src/ReactFiberCompleteWork.new.js#L652&quot;&gt;completeWork&lt;/a&gt;处理&lt;code&gt;Fiber节点&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当某个&lt;code&gt;Fiber节点&lt;/code&gt;执行完&lt;code&gt;completeWork&lt;/code&gt;，如果其存在&lt;code&gt;兄弟Fiber节点&lt;/code&gt;（即&lt;code&gt;fiber.sibling !== null&lt;/code&gt;），会进入其&lt;code&gt;兄弟Fiber&lt;/code&gt;的“递”阶段。&lt;/p&gt;
&lt;p&gt;如果不存在&lt;code&gt;兄弟Fiber&lt;/code&gt;，会进入&lt;code&gt;父级Fiber&lt;/code&gt;的“归”阶段。&lt;/p&gt;
&lt;p&gt;“递”和“归”阶段会交错执行直到“归”到&lt;code&gt;rootFiber&lt;/code&gt;。至此，&lt;code&gt;render阶段&lt;/code&gt;的工作就结束了。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;总体来说递归阶段是下面这段伪代码描述的一样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function performUnitOfWork(fiber) {
  // 执行beginWork

  if (fiber.child) {
    performUnitOfWork(fiber.child);
  }

  // 执行completeWork

  if (fiber.sibling) {
    performUnitOfWork(fiber.sibling);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;根据Fiber的那一讲解咱们可以知道，我们的react中可以最多存在两棵fiber树，一个是workInProgress一个是current，咱们在首次mount的时候是，是没有current的，只是在内存中通过整个应用的根fiberRootNode去构建workinprogess tree&lt;/p&gt;
&lt;p&gt;之后update的时候是存在current的tree的&lt;/p&gt;
&lt;p&gt;所以这两个阶段的“递”和“归”流程是各不一样的&lt;/p&gt;
&lt;p&gt;因此我们接下来按照 mount 和update触发去解释“递归”&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>React源码学习-jsx的理解-part4</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-jsx%E7%9A%84%E7%90%86%E8%A7%A3-part4/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-jsx%E7%9A%84%E7%90%86%E8%A7%A3-part4/</guid><description>jsx理解   JSX和Fiber节点不是同一个东西 。   React Component 和 React Element 也不是一个东西。   从编译来看  JSX在babel中会被编译成React.createElement（这也就是为什么需要手动import React from &apos;reac...</description><pubDate>Sat, 21 Aug 2021 22:26:48 GMT</pubDate><content:encoded>&lt;h1&gt;jsx理解&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;JSX&lt;/code&gt;和&lt;code&gt;Fiber节点&lt;/code&gt;不是同一个东西 。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;React Component 和 React Element 也不是一个东西。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;从编译来看&lt;/h2&gt;
&lt;p&gt;JSX在babel中会被编译成React.createElement（这也就是为什么需要手动&lt;code&gt;import React from &apos;react&apos;&lt;/code&gt;的原因了）&lt;/p&gt;
&lt;p&gt;但是在17之后不需要手动引入了 &lt;a href=&quot;https://zh-hans.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html&quot;&gt;看这篇&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822155213907.png&quot; alt=&quot;image-20210822155213907&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;JSX&lt;/code&gt;并不是只能被编译为&lt;code&gt;React.createElement&lt;/code&gt;方法，你可以通过&lt;a href=&quot;https://babeljs.io/docs/en/babel-plugin-transform-react-jsx&quot;&gt;@babel/plugin-transform-react-jsx (opens new window)&lt;/a&gt;插件显式告诉&lt;code&gt;Babel&lt;/code&gt;编译时需要将&lt;code&gt;JSX&lt;/code&gt;编译为什么函数的调用（默认为&lt;code&gt;React.createElement&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如在&lt;a href=&quot;https://github.com/preactjs/preact&quot;&gt;preact (opens new window)&lt;/a&gt;这个类&lt;code&gt;React&lt;/code&gt;库中，&lt;code&gt;JSX&lt;/code&gt;会被编译为一个名为&lt;code&gt;h&lt;/code&gt;的函数调用。&lt;/p&gt;
&lt;h2&gt;React.createElement&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L348&quot;&gt;React.createElement地址&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;createElement(type, config, children)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;type: react component类型&lt;/li&gt;
&lt;li&gt;config：react component 的一些属性&lt;/li&gt;
&lt;li&gt;children：它的子孙react component&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;执行步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进来createElement我们会发现它定义了一些字段，这些字段都是我们比较常用的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822155830376.png&quot; alt=&quot;image-20210822155830376&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后我们对传进来的config进行校验，我们会发现他做了几个合法性的校验，并且对相对应的变量进行赋值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L31&quot;&gt;hasValidRef&lt;/a&gt;：对config中的ref做合法性校验&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L43&quot;&gt;hasValidKey&lt;/a&gt;：对config中的key做合法性校验&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822160045714.png&quot; alt=&quot;image-20210822160045714&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历config中的属性，将除了保留属性之外的其他属性赋值给&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L241&quot;&gt;Props&lt;/a&gt;（就是内部的一个中间对象）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L16&quot;&gt;保留属性有哪些呢？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以看到react把，ref，key都提出来了，单独的作为 &lt;code&gt;ReactElement&lt;/code&gt;函数的参数传递（这个下面说）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来处理type中的&lt;code&gt;defaultProps&lt;/code&gt;，这里也能明白，因为我们经常需要给class的组件的一些参数设置默认的属性值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822160957936.png&quot; alt=&quot;image-20210822160957936&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来我们走入&lt;code&gt;ReactElement&lt;/code&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L146&quot;&gt;函数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以发现，它最终返回了一个Element对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ReactElement = function(type, key, ref, self, source, owner, props) {
    ......
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

   // 这个是react component的类型   
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   
这里要注意，其中` $$typeof`这个参数很重要，主要是用来[isValidElement](https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L547)函数来判断这个element是不是合法的react element
   
   ```js
   export function isValidElement(object) {
     return (
       typeof object === &apos;object&apos; &amp;amp;&amp;amp;
       object !== null &amp;amp;&amp;amp;
       object.$$typeof === REACT_ELEMENT_TYPE
     );
   }
   
   
   
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;可以看到，&lt;code&gt;$$typeof === REACT_ELEMENT_TYPE&lt;/code&gt;的非&lt;code&gt;null&lt;/code&gt;对象就是一个合法的&lt;code&gt;React Element&lt;/code&gt;。换言之，在&lt;code&gt;React&lt;/code&gt;中，所有&lt;code&gt;JSX&lt;/code&gt;在运行时的返回结果（即&lt;code&gt;React.createElement()&lt;/code&gt;的返回值）都是&lt;code&gt;React Element&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;React Component&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;React&lt;/code&gt;中，我们常使用&lt;code&gt;ClassComponent&lt;/code&gt;与&lt;code&gt;FunctionComponent&lt;/code&gt;构建组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AppClass extends React.Component {
  render() {
    return &amp;lt;p&amp;gt;111&amp;lt;/p&amp;gt;
  }
}
console.log(&apos;这是ClassComponent：&apos;, AppClass);
console.log(&apos;这是Element：&apos;, &amp;lt;AppClass/&amp;gt;);


function AppFunc() {
  return &amp;lt;p&amp;gt;222&amp;lt;/p&amp;gt;;
}
console.log(&apos;这是FunctionComponent：&apos;, AppFunc);
console.log(&apos;这是Element：&apos;, &amp;lt;AppFunc/&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以从Demo控制台打印的对象看出，&lt;code&gt;ClassComponent&lt;/code&gt;对应的&lt;code&gt;Element&lt;/code&gt;的&lt;code&gt;type&lt;/code&gt;字段为&lt;code&gt;AppClass&lt;/code&gt;自身。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FunctionComponent&lt;/code&gt;对应的&lt;code&gt;Element&lt;/code&gt;的&lt;code&gt;type&lt;/code&gt;字段为&lt;code&gt;AppFunc&lt;/code&gt;自身，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
 $$typeof: Symbol(react.element),
 key: null,
 props: {},
 ref: null,
 type: ƒ AppFunc(),
 _owner: null,
 _store: {validated: false},
 _self: null,
 _source: null 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSX与Fiber节点&lt;/h2&gt;
&lt;p&gt;从上面的内容我们可以发现，&lt;code&gt;JSX&lt;/code&gt;是一种描述当前组件内容的数据结构，他不包含组件&lt;strong&gt;schedule&lt;/strong&gt;、&lt;strong&gt;reconcile&lt;/strong&gt;、&lt;strong&gt;render&lt;/strong&gt;所需的相关信息。&lt;/p&gt;
&lt;p&gt;比如如下信息就不包括在&lt;code&gt;JSX&lt;/code&gt;中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件在更新中的&lt;code&gt;优先级&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;组件的&lt;code&gt;state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;组件被打上的用于&lt;strong&gt;Renderer&lt;/strong&gt;的&lt;code&gt;标记&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些内容都包含在&lt;code&gt;Fiber节点&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;所以，在组件&lt;code&gt;mount&lt;/code&gt;时，&lt;code&gt;Reconciler&lt;/code&gt;根据&lt;code&gt;JSX&lt;/code&gt;描述的组件内容生成组件对应的&lt;code&gt;Fiber节点&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;update&lt;/code&gt;时，&lt;code&gt;Reconciler&lt;/code&gt;将&lt;code&gt;JSX&lt;/code&gt;与&lt;code&gt;Fiber节点&lt;/code&gt;保存的数据对比，生成组件对应的&lt;code&gt;Fiber节点&lt;/code&gt;，并根据对比结果为&lt;code&gt;Fiber节点&lt;/code&gt;打上&lt;code&gt;标记&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>React源码学习-fiber原理-part3</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-fiber%E5%8E%9F%E7%90%86-part3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-fiber%E5%8E%9F%E7%90%86-part3/</guid><description>Fiber架构    Fiber并不是计算机术语中的新名词，他的中文翻译叫做纤程，与进程（Process）、线程（Thread）、协程（Coroutine）同为程序执行过程。  在很多文章中将纤程理解为协程的一种实现。在JS中，协程的实现便是Generator。  所以，我们可以将纤程(Fiber)...</description><pubDate>Sat, 21 Aug 2021 17:17:17 GMT</pubDate><content:encoded>&lt;h1&gt;Fiber架构&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/acdlite/react-fiber-architecture&quot;&gt;官方回答什么是fiber&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fiber&lt;/code&gt;并不是计算机术语中的新名词，他的中文翻译叫做&lt;code&gt;纤程&lt;/code&gt;，与进程（Process）、线程（Thread）、协程（Coroutine）同为程序执行过程。&lt;/p&gt;
&lt;p&gt;在很多文章中将&lt;code&gt;纤程&lt;/code&gt;理解为&lt;code&gt;协程&lt;/code&gt;的一种实现。在&lt;code&gt;JS&lt;/code&gt;中，&lt;code&gt;协程&lt;/code&gt;的实现便是&lt;code&gt;Generator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以，我们可以将&lt;code&gt;纤程&lt;/code&gt;(Fiber)、&lt;code&gt;协程&lt;/code&gt;(Generator)理解为&lt;code&gt;代数效应&lt;/code&gt;思想在&lt;code&gt;JS&lt;/code&gt;中的体现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;React Fiber&lt;/code&gt;可以理解为&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;React&lt;/code&gt;内部实现的一套状态更新机制。支持任务不同&lt;code&gt;优先级&lt;/code&gt;，可中断与恢复，并且恢复后可以复用之前的&lt;code&gt;中间状态&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;其中每个任务&lt;strong&gt;更新单元&lt;/strong&gt;为&lt;code&gt;React Element&lt;/code&gt;对应的&lt;code&gt;Fiber节点&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Fiber架构心智模型&lt;/h2&gt;
&lt;p&gt;React核心团队成员&lt;a href=&quot;https://github.com/sebmarkbage/&quot;&gt;Sebastian Markbåge (opens new window)&lt;/a&gt;（&lt;code&gt;React Hooks&lt;/code&gt;的发明者）曾说：我们在&lt;code&gt;React&lt;/code&gt;中做的就是践行&lt;code&gt;代数效应&lt;/code&gt;（Algebraic Effects）。&lt;/p&gt;
&lt;h3&gt;代数效应&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_36968599/article/details/114851241&quot;&gt;这篇文章可以好好看看&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;代数效应&lt;/code&gt;能够将&lt;code&gt;副作用&lt;/code&gt;从函数逻辑中分离，使函数关注点保持纯粹。&lt;/p&gt;
&lt;p&gt;就比如我们平时用await来等待一个值的返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getData(){
    const res = await loadData();
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代数效应相当于，我关注的是 await loadData()能给我什么东西，而不是关注他里头是异步还是同步，怎么处理这个数据的；&lt;/p&gt;
&lt;h3&gt;代数效应 in React&lt;/h3&gt;
&lt;p&gt;在react中有个结合Suspense的例子&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codesandbox.io/s/frosty-hermann-bztrp?file=/src/index.js:152-160&quot;&gt;Suspense Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;而我们日常用hooks，例如useState什么的，我们不需要关注函数组件的这个state状态是怎么处理的，我们只要知道这个东西能干嘛就行。&lt;/p&gt;
&lt;h3&gt;为什么不用Generator&lt;/h3&gt;
&lt;p&gt;新老架构那片说过，React16之后引入了一个 &lt;strong&gt;scheduler（调度器）&lt;/strong&gt;，并且重构了&lt;strong&gt;Reconciler&lt;/strong&gt;（协调器），就是为了将react的老一套同步更新 的架构变成 **异步可中断 **的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;异步可中断更新&lt;/code&gt;可以理解为：&lt;code&gt;更新&lt;/code&gt;在执行过程中可能会被打断（浏览器时间分片用尽或有&lt;strong&gt;更高优任务插队&lt;/strong&gt;），当可以继续执行时恢复之前执行的中间状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generator&lt;/code&gt;它具有传染性和async await一样，就像你用async的函数那东西，你需要写await一样&lt;/li&gt;
&lt;li&gt;中间状态上下文相关，可以看看这个 &lt;a href=&quot;https://github.com/facebook/react/issues/7942#issuecomment-254987818&quot;&gt;解释&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fiber实现原理&lt;/h2&gt;
&lt;h3&gt;Fiber含义&lt;/h3&gt;
&lt;p&gt;首先我们要明白：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;react15的时候采用递归方式去执行，数据保存在递归调用栈中，故被称为&lt;code&gt;stack reconciler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;react16 的reconciler 基于 &lt;code&gt;fiber节点&lt;/code&gt;实现的，故称为 &lt;code&gt;fiber reconciler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个 &lt;code&gt;React Element&lt;/code&gt; 对应一个&lt;code&gt;Fiber&lt;/code&gt;节点
&lt;ul&gt;
&lt;li&gt;作为静态结构理解 --- 保存了该组件类型（原生/类组件/函数组件/...），以及对应dom节点信息&lt;/li&gt;
&lt;li&gt;作为动态结构理解 --- 每个&lt;code&gt;Fiber&lt;/code&gt;保存了本次更新中该组件改变的状态，要执行的工作（插入/删除/更新...）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Fiber数据结构&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiber.new.js#L117&quot;&gt;Fiber数据结构&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // 作为动态的工作单元的属性
  this.effectTag = NoEffect;
  this.subtreeTag = NoSubtreeEffect;
  this.deletions = null;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber， current tree中的fiber节点和workinprogress tree的fiber节点连接
  this.alternate = null;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Fiber节点之间关系&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Fiber&lt;/code&gt;节点之间是通过以下三个属性建立起连接的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如下面的代码和对应的Fiber树如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function App(){
    return (
    	&amp;lt;div&amp;gt;
        	&amp;lt;span&amp;gt;hello world&amp;lt;/span&amp;gt;
            &amp;lt;a&amp;gt;
                weng
                &amp;lt;span/&amp;gt;
            &amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210821213628488.png&quot; alt=&quot;image-20210821213628488&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要提一下，为什么父级指针叫做&lt;code&gt;return&lt;/code&gt;而不是&lt;code&gt;parent&lt;/code&gt;或者&lt;code&gt;father&lt;/code&gt;呢？因为作为一个工作单元，&lt;code&gt;return&lt;/code&gt;指节点执行完&lt;code&gt;completeWork&lt;/code&gt;（后面会说）后会返回的下一个节点。子&lt;code&gt;Fiber节点&lt;/code&gt;及其兄弟节点完成工作后会返回其父级节点，所以用&lt;code&gt;return&lt;/code&gt;指代父级节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;作为静态的数据结构&lt;/h4&gt;
&lt;p&gt;作为一种静态的数据结构，保存了组件相关的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type，某些情况不同，比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent，指函数本身，对于ClassComponent，指class，对于HostComponent，指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;作为动态的工作单元&lt;/h4&gt;
&lt;p&gt;作为动态的工作单元，&lt;code&gt;Fiber&lt;/code&gt;中如下参数保存了本次更新相关的信息，我们会在后续的更新流程中使用到具体属性时再详细介绍&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下两个字段保存调度优先级相关的信息，会在学习&lt;code&gt;Scheduler&lt;/code&gt;时说&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fiber工作原理&lt;/h2&gt;
&lt;p&gt;首先，上面说了，Fiber节点直接会构成一棵Fiber树，并且存有节点以及各种信息每个Fiber。&lt;/p&gt;
&lt;p&gt;所以一个Fiber是根据一个dom或者说React Element的出来的，那么树的结构也和dom或者组件树相同。&lt;/p&gt;
&lt;p&gt;主要 &lt;strong&gt;更新&lt;/strong&gt; 工作原理，这里用到了一个叫做 &lt;strong&gt;双缓存&lt;/strong&gt;的技术&lt;/p&gt;
&lt;h3&gt;what is 双缓存？&lt;/h3&gt;
&lt;p&gt;当我们用&lt;code&gt;canvas&lt;/code&gt;绘制动画，每一帧绘制前都会调用&lt;code&gt;ctx.clearRect&lt;/code&gt;清除上一帧的画面。&lt;/p&gt;
&lt;p&gt;如果当前帧画面计算量比较大，导致清除上一帧画面到绘制当前帧画面之间有较长间隙，就会出现白屏。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们可以在内存中绘制当前帧动画，绘制完毕后&lt;strong&gt;直接用当前帧替换&lt;/strong&gt;上一帧画面，由于省去了两帧替换间的计算时间，不会出现从白屏到出现画面的闪烁情况。&lt;/p&gt;
&lt;p&gt;这种&lt;strong&gt;在内存中构建并直接替换&lt;/strong&gt;的技术叫做&lt;a href=&quot;https://baike.baidu.com/item/%E5%8F%8C%E7%BC%93%E5%86%B2&quot;&gt;双缓存 &lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;React&lt;/code&gt;使用“双缓存”来完成&lt;code&gt;Fiber树&lt;/code&gt;的构建与替换——对应着&lt;code&gt;DOM树&lt;/code&gt;的创建与更新。&lt;/p&gt;
&lt;h3&gt;Fiber树和双缓存&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;React中最多会同时存在两棵Fiber树&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Current Fiber&lt;/code&gt; 树：当前显示在屏幕面前的树&lt;/li&gt;
&lt;li&gt;&lt;code&gt;workInProgress Fiber&lt;/code&gt; 树：正在内存中构建的树&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;current Fiber树&lt;/code&gt;中的&lt;code&gt;Fiber节点&lt;/code&gt;被称为&lt;code&gt;current fiber&lt;/code&gt;，&lt;code&gt;workInProgress Fiber树&lt;/code&gt;中的&lt;code&gt;Fiber节点&lt;/code&gt;被称为&lt;code&gt;workInProgress fiber&lt;/code&gt;，他们通过&lt;code&gt;alternate&lt;/code&gt;属性连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;React&lt;/code&gt;应用的根节点通过使&lt;code&gt;current&lt;/code&gt;指针在不同&lt;code&gt;Fiber树&lt;/code&gt;的&lt;code&gt;rootFiber&lt;/code&gt;间切换来完成&lt;code&gt;current Fiber&lt;/code&gt;树指向的切换。&lt;/p&gt;
&lt;p&gt;即当&lt;code&gt;workInProgress Fiber树&lt;/code&gt;构建完成交给&lt;code&gt;Renderer&lt;/code&gt;渲染在页面上后，应用根节点的&lt;code&gt;current&lt;/code&gt;指针指向&lt;code&gt;workInProgress Fiber树&lt;/code&gt;，此时&lt;code&gt;workInProgress Fiber树&lt;/code&gt;就变为&lt;code&gt;current Fiber树&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每次状态更新都会产生新的&lt;code&gt;workInProgress Fiber树&lt;/code&gt;，通过&lt;code&gt;current&lt;/code&gt;与&lt;code&gt;workInProgress&lt;/code&gt;的替换，完成&lt;code&gt;DOM&lt;/code&gt;更新。&lt;/p&gt;
&lt;h4&gt;mount时候&lt;/h4&gt;
&lt;p&gt;考虑如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function App() {
  const [num, add] = useState(0);
  return (
    &amp;lt;p onClick={() =&amp;gt; add(num + 1)}&amp;gt;{num}&amp;lt;/p&amp;gt;
  )
}

ReactDOM.render(&amp;lt;App/&amp;gt;, document.getElementById(&apos;root&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;首次执行&lt;code&gt;ReactDOM.render&lt;/code&gt;会创建&lt;code&gt;fiberRootNode&lt;/code&gt;（源码中叫&lt;code&gt;fiberRoot&lt;/code&gt;）和&lt;code&gt;rootFiber&lt;/code&gt;。其中&lt;code&gt;fiberRootNode&lt;/code&gt;是整个应用的根节点，&lt;code&gt;rootFiber&lt;/code&gt;是``所在组件树的根节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之所以要区分&lt;code&gt;fiberRootNode&lt;/code&gt;与&lt;code&gt;rootFiber&lt;/code&gt;，是因为在应用中我们可以多次调用&lt;code&gt;ReactDOM.render&lt;/code&gt;渲染不同的组件树，他们会拥有不同的&lt;code&gt;rootFiber&lt;/code&gt;。但是整个应用的根节点只有一个，那就是&lt;code&gt;fiberRootNode&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fiberRootNode&lt;/code&gt;的&lt;code&gt;current&lt;/code&gt;会指向当前页面上已渲染内容对应&lt;code&gt;Fiber树&lt;/code&gt;，即&lt;code&gt;current Fiber树&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/rootfiber.png&quot; alt=&quot;rootFiber&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于是&lt;strong&gt;首屏渲染&lt;/strong&gt;，页面中还没有挂载任何&lt;code&gt;DOM&lt;/code&gt;，所以&lt;code&gt;fiberRootNode.current&lt;/code&gt;指向的&lt;code&gt;rootFiber&lt;/code&gt;没有任何&lt;code&gt;子Fiber节点&lt;/code&gt;（即&lt;code&gt;current Fiber树&lt;/code&gt;为空）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;接下来进入&lt;code&gt;render阶段&lt;/code&gt;，根据组件返回的&lt;code&gt;JSX&lt;/code&gt;在内存中依次创建&lt;code&gt;Fiber节点&lt;/code&gt;并连接在一起构建&lt;code&gt;Fiber树&lt;/code&gt;，被称为&lt;code&gt;workInProgress Fiber树&lt;/code&gt;。（下图中右侧为内存中构建的树，左侧为页面显示的树）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在构建&lt;code&gt;workInProgress Fiber树&lt;/code&gt;时会尝试复用&lt;code&gt;current Fiber树&lt;/code&gt;中已有的&lt;code&gt;Fiber节点&lt;/code&gt;内的属性，在&lt;code&gt;首屏渲染&lt;/code&gt;时只有&lt;code&gt;rootFiber&lt;/code&gt;存在对应的&lt;code&gt;current fiber&lt;/code&gt;（即&lt;code&gt;rootFiber.alternate&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/workInProgressFiber.png&quot; alt=&quot;workInProgressFiber&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/wipTreeFinish.png&quot; alt=&quot;workInProgressFiberFinish&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;update时&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;接下来我们点击&lt;code&gt;p节点&lt;/code&gt;触发状态改变，这会开启一次新的&lt;code&gt;render阶段&lt;/code&gt;并构建一棵新的&lt;code&gt;workInProgress Fiber 树&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/wipTreeUpdate.png&quot; alt=&quot;wipTreeUpdate&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个决定是否复用的过程就是Diff算法，后面会说&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;workInProgress Fiber 树&lt;/code&gt;在&lt;code&gt;render阶段&lt;/code&gt;完成构建后进入&lt;code&gt;commit阶段&lt;/code&gt;渲染到页面上。&lt;strong&gt;渲染完毕后&lt;/strong&gt;，&lt;code&gt;workInProgress Fiber 树&lt;/code&gt;变为&lt;code&gt;current Fiber 树&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/currentTreeUpdate.png&quot; alt=&quot;currentTreeUpdate&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;查看源码中的FiberRootNode&lt;/h1&gt;
&lt;p&gt;上面曾说到，我们首次创建react的应用的时候会创建整个应用的一个根节点 叫做 &lt;code&gt;FiberRootNode&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后每次调用render方法都会创建当前组件的一个根节点叫做 &lt;code&gt;rootFiber&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里我们验证以下这个 &lt;code&gt;FiberRootNode&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210821223750870.png&quot; alt=&quot;image-20210821223750870&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中圈出的部分就是react应用首次运行时候，创建一个**应用根节点&lt;code&gt;FiberRootNode&lt;/code&gt;**的过程&lt;/p&gt;
&lt;p&gt;我们顺着调用栈找到这个创建的调用方法 creatFiberRoot --- createFiber（创建应用根的方法）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210821224000472.png&quot; alt=&quot;image-20210821224000472&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找到源码，打个断点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822153431608.png&quot; alt=&quot;image-20210822153431608&quot; /&gt;&lt;/p&gt;
&lt;p&gt;刷新页面之后&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822153527527.png&quot; alt=&quot;image-20210822153527527&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现首次渲染，创建fiberRoot节点，这个tag为3，那么代表什么意思呢？&lt;/p&gt;
&lt;p&gt;我们可以在它右边的调用栈中找到上层函数&lt;code&gt;createHostRootFiber&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210822153638347.png&quot; alt=&quot;image-20210822153638347&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实发现这个tag，也就是这里的HostRoot值为3&lt;/p&gt;
</content:encoded></item><item><title>React源码学习</title><link>https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-%E5%85%A5%E5%8F%A3-part1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-%E5%85%A5%E5%8F%A3-part1/</guid><description>官方： React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式       首先学习一个框架的源码或者库的源码，我们都要从他们的入口函数出手。而react这个js库就是从ReactDOM.render入手的   前言：  React的快速响应是什么意思呢？  - 遇到...</description><pubDate>Mon, 16 Aug 2021 13:52:58 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;官方： React 是用 JavaScript 构建&lt;strong&gt;快速响应&lt;/strong&gt;的大型 Web 应用程序的首选方式&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://react.iamkasong.com/preparation/jsx.html&quot;&gt;卡颂react深度解析&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;首先学习一个框架的源码或者库的源码，我们都要从他们的入口函数出手。而react这个js库就是从ReactDOM.render入手的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言：&lt;/h2&gt;
&lt;p&gt;React的快速响应是什么意思呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;遇到大量计算不会让页面掉帧或者导致卡顿（这里要想到每秒60帧（HZ），每帧16.6ms）&lt;/p&gt;
&lt;p&gt;也就是说在一帧里头做了太多的计算工作了，这里具体要去看看浏览器渲染进程里头的js线程和GUI线程怎么做操作的，目前有一个兼容性不是很好的调度api叫做&lt;code&gt;requestIdelCallback&lt;/code&gt;是用来申请浏览器调度的，但是react除了这个因为其他种种原因肯定不能用这个，所以在16之后自己实现了调度方案，也就是下面要说的&lt;code&gt;scheduler&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络请求数据返回后才能进行操作，因此不能快速响应（这里涉及到用户体验上面，意思就是想要让用户把这种异步的请求，自我感知成同步的，也就是说我根本没反应过来，你这个居然就可以用了的意思）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两类对应着&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU的瓶颈&lt;/li&gt;
&lt;li&gt;IO的瓶颈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们引出： react是如何从这两个方面进行优化的呢？&lt;/p&gt;
&lt;h3&gt;CPU瓶颈&lt;/h3&gt;
&lt;p&gt;加入我们一个页面中需要一次性渲染30k个dom元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function App() {
  const len = 3000;
  return (
    &amp;lt;ul&amp;gt;
      {Array(len).fill(0).map((_, i) =&amp;gt; &amp;lt;li&amp;gt;{i}&amp;lt;/li&amp;gt;)}
    &amp;lt;/ul&amp;gt;
  );
}

const rootEl = document.querySelector(&quot;#root&quot;);
ReactDOM.render(&amp;lt;App/&amp;gt;, rootEl); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而大部分目前设备浏览器刷新比率都是60HZ（每秒60帧），16.6ms刷新一帧&lt;/p&gt;
&lt;p&gt;而， JS可以操作DOM，&lt;code&gt;GUI渲染线程&lt;/code&gt;与&lt;code&gt;JS线程&lt;/code&gt;是互斥的， &lt;strong&gt;JS脚本执行&lt;/strong&gt;和&lt;strong&gt;浏览器布局、绘制&lt;/strong&gt;不能同时执行。&lt;/p&gt;
&lt;p&gt;在每16.6ms时间内，需要完成如下工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JS脚本执行 -----  样式布局 ----- 样式绘制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当JS执行时间过长，超出了16.6ms，这次刷新就没有时间执行&lt;strong&gt;样式布局&lt;/strong&gt;和&lt;strong&gt;样式绘制&lt;/strong&gt;了。&lt;/p&gt;
&lt;p&gt;在Demo中，由于组件数量繁多（3000个），JS脚本执行时间过长，页面掉帧，造成卡顿。&lt;/p&gt;
&lt;p&gt;可以从打印的执行堆栈图看到，JS执行时间为73.65ms，远远多于一帧的时间&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/long-task.png&quot; alt=&quot;长任务&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如何解决这个问题呢？&lt;/p&gt;
&lt;p&gt;答案是：在浏览器每一帧的时间中，预留一些时间给JS线程，&lt;code&gt;React&lt;/code&gt;利用这部分时间更新组件（可以看到，在&lt;a href=&quot;https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L119&quot;&gt;源码 (opens new window)&lt;/a&gt;中，预留的初始时间是5ms）。&lt;/p&gt;
&lt;p&gt;当预留的时间不够用时，&lt;code&gt;React&lt;/code&gt;将线程控制权交还给浏览器使其有时间渲染UI，&lt;code&gt;React&lt;/code&gt;则等待下一帧时间到来继续被中断的工作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这种将长任务分拆到每一帧中，像蚂蚁搬家一样一次执行一小段任务的操作，被称为&lt;code&gt;时间切片&lt;/code&gt;（time slice）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来我们开启&lt;code&gt;Concurrent Mode&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(&amp;lt;App/&amp;gt;, rootEl);  
ReactDOM.unstable_createRoot(rootEl).render(&amp;lt;App/&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们的长任务被拆分到每一帧不同的&lt;code&gt;task&lt;/code&gt;中，&lt;code&gt;JS脚本&lt;/code&gt;执行时间大体在&lt;code&gt;5ms&lt;/code&gt;左右，这样浏览器就有剩余时间执行&lt;strong&gt;样式布局&lt;/strong&gt;和&lt;strong&gt;样式绘制&lt;/strong&gt;，减少掉帧的可能性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/time-slice.png&quot; alt=&quot;长任务&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以，解决&lt;code&gt;CPU瓶颈&lt;/code&gt;的关键是实现&lt;code&gt;时间切片&lt;/code&gt;，而&lt;code&gt;时间切片&lt;/code&gt;的关键是：将&lt;strong&gt;同步的更新&lt;/strong&gt;变为&lt;strong&gt;可中断的异步更新&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;IO瓶颈&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;网络延迟&lt;/code&gt;是前端开发者无法解决的。如何在&lt;code&gt;网络延迟&lt;/code&gt;客观存在的情况下，减少用户对&lt;code&gt;网络延迟&lt;/code&gt;的感知？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;React&lt;/code&gt;给出的答案是&lt;a href=&quot;https://zh-hans.reactjs.org/docs/concurrent-mode-intro.html#putting-research-into-production&quot;&gt;将人机交互研究的结果整合到真实的 UI 中 (opens new window)&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这里我们以业界人机交互最顶尖的苹果举例，在IOS系统中：&lt;/p&gt;
&lt;p&gt;点击“设置”面板中的“通用”，进入“通用”界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/legacy-move.gif&quot; alt=&quot;同步&quot; /&gt;&lt;/p&gt;
&lt;p&gt;作为对比，再点击“设置”面板中的“Siri与搜索”，进入“Siri与搜索”界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://react.iamkasong.com/img/concurrent-mov.gif&quot; alt=&quot;异步&quot; /&gt;&lt;/p&gt;
&lt;p&gt;事实上，点击“通用”后的交互是同步的，直接显示后续界面。而点击“Siri与搜索”后的交互是异步的，需要等待请求返回后再显示后续界面。但从用户感知来看，这两者的区别微乎其微。&lt;/p&gt;
&lt;p&gt;这里的窍门在于：点击“Siri与搜索”后，先在当前页面停留了一小段时间，这一小段时间被用来请求数据。&lt;/p&gt;
&lt;p&gt;当“这一小段时间”足够短时，用户是无感知的。如果请求时间超过一个范围，再显示&lt;code&gt;loading&lt;/code&gt;的效果。&lt;/p&gt;
&lt;p&gt;试想如果我们一点击“Siri与搜索”就显示&lt;code&gt;loading&lt;/code&gt;效果，即使数据请求时间很短，&lt;code&gt;loading&lt;/code&gt;效果一闪而过。用户也是可以感知到的。&lt;/p&gt;
&lt;p&gt;为此，&lt;code&gt;React&lt;/code&gt;实现了&lt;a href=&quot;https://zh-hans.reactjs.org/docs/concurrent-mode-suspense.html&quot;&gt;Suspense (opens new window)&lt;/a&gt;功能及配套的&lt;code&gt;hook&lt;/code&gt;——&lt;a href=&quot;https://zh-hans.reactjs.org/docs/concurrent-mode-reference.html#usedeferredvalue&quot;&gt;useDeferredValue (opens new window)&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;而在源码内部，为了支持这些特性，同样需要将&lt;strong&gt;同步的更新&lt;/strong&gt;变为&lt;strong&gt;可中断的异步更新&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;通过以上内容，我们可以看到，&lt;code&gt;React&lt;/code&gt;为了践行“构建&lt;strong&gt;快速响应&lt;/strong&gt;的大型 Web 应用程序”理念做出的努力。&lt;/p&gt;
&lt;p&gt;其中的关键是解决CPU的瓶颈与IO的瓶颈。而落实到实现上，则需要将&lt;strong&gt;同步的更新&lt;/strong&gt;变为&lt;strong&gt;可中断的异步更新&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;新老React架构对比（15 vs 16++）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么react16要对15进行内部重构呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Fiber架构&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/acdlite/react-fiber-architecture&quot;&gt;官方回答什么是fiber&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>团队规范以及解决方法</title><link>https://nollieleo.github.io/posts/%E5%9B%A2%E9%98%9F%E8%A7%84%E8%8C%83%E5%B8%B8%E7%94%A8%E5%8C%85/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9B%A2%E9%98%9F%E8%A7%84%E8%8C%83%E5%B8%B8%E7%94%A8%E5%8C%85/</guid><description>我们在平时日常开发的时候, 肯定是要制定一套项目规范，去进行团队的开发代码管理的，因此需要涉及到很多方面；比如eslint代码规则检查，husky这种为npm script提供git的钩子的，lint-staged结合husky，然后使用commitlint去规范提交代码消息等等   代码检查   ...</description><pubDate>Thu, 12 Aug 2021 22:35:15 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;我们在平时日常开发的时候, 肯定是要制定一套项目规范，去进行团队的开发代码管理的，因此需要涉及到很多方面；比如eslint代码规则检查，husky这种为npm script提供git的钩子的，lint-staged结合husky，然后使用commitlint去规范提交代码消息等等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;代码检查&lt;/h1&gt;
&lt;h2&gt;husky&lt;/h2&gt;
&lt;h2&gt;ts-lint&lt;/h2&gt;
&lt;h2&gt;eslint&lt;/h2&gt;
&lt;h2&gt;lint-staged&lt;/h2&gt;
&lt;h2&gt;commitlint&lt;/h2&gt;
&lt;p&gt;https://github.com/conventional-changelog/commitlint&lt;/p&gt;
&lt;p&gt;https://commitlint.js.org/#/reference-rules?id=references-empty&lt;/p&gt;
</content:encoded></item><item><title>bug记录以及解决方案</title><link>https://nollieleo.github.io/posts/bug%E8%AE%B0%E5%BD%95%E4%BB%A5%E5%8F%8A%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/bug%E8%AE%B0%E5%BD%95%E4%BB%A5%E5%8F%8A%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</guid><description>Electron  -      chrome devtools  -      PostCss  - Error: PostCSS plugin autoprefixer requires PostCSS 8. Update PostCSS or downgrade this plugin。   ...</description><pubDate>Thu, 12 Aug 2021 09:45:16 GMT</pubDate><content:encoded>&lt;h2&gt;Electron&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;chrome devtools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/61339968/error-message-devtools-failed-to-load-sourcemap-could-not-load-content-for-chr&quot;&gt;Error message &quot;DevTools failed to load SourceMap: Could not load content for chrome-extension://...&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;PostCss&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Error: PostCSS plugin autoprefixer requires PostCSS 8. Update PostCSS or downgrade this plugin。&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_38385108/article/details/108693404&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;VScode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解决VSCODE&quot;因为在此系统上禁止运行脚本&quot;报错&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/larpland/article/details/101349586&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;UMI&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/tigerK/p/14902074.html&quot;&gt;解决umi项目引入React无智能提示，报错“React”指 UMD 全局，但当前文件是模块。请考虑改为添加导入。ts(2686)的问题。&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;eslint&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;Oops! Something went wrong!
No files matching the pattern &quot;./src/assets/scripts/**/*.js&quot; were found.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/54543063/how-can-i-suppress-the-no-files-matching-the-pattern-message-in-eslint&quot;&gt;解决方法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&apos;lodash&apos; should be listed in the project&apos;s dependencies, not devDependencies.eslint&lt;a href=&quot;https://github.com/import-js/eslint-plugin-import/blob/v2.24.2/docs/rules/no-extraneous-dependencies.md&quot;&gt;import/no-extraneous-dependencies&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/50421664/eslint-html-webpack-plugin-should-be-listed-in-the-projects-dependencies-not&quot;&gt;解决方法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;webpack5&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/65018431/webpack-5-uncaught-referenceerror-process-is-not-defined&quot;&gt;Webpack 5 - Uncaught ReferenceError: process is not defined&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;husky&lt;/h2&gt;
&lt;p&gt;husky &amp;gt; commit-msg hook failed (add --no-verify to bypass)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://img2020.cnblogs.com/blog/130424/202011/130424-20201103070144537-2082531156.png&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方法&lt;/p&gt;
&lt;p&gt;commitlint.config.js的编码修改为UTF-8&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img2020.cnblogs.com/blog/130424/202011/130424-20201103070158487-1521358444.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>eslint在vscode中不生效的原因</title><link>https://nollieleo.github.io/posts/eslint%E5%9C%A8vscode%E4%B8%AD%E4%B8%8D%E7%94%9F%E6%95%88%E7%9A%84%E5%8E%9F%E5%9B%A0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/eslint%E5%9C%A8vscode%E4%B8%AD%E4%B8%8D%E7%94%9F%E6%95%88%E7%9A%84%E5%8E%9F%E5%9B%A0/</guid><description>检查是否配置以下内容  - package.json中是否配置了eslint依赖      - 工程目录下是否有.eslintrc.js和.eslintignore文件        2.查看vscode是否安装了eslint插件并启用      - setting.json里是否有eslint的配...</description><pubDate>Thu, 12 Aug 2021 09:44:34 GMT</pubDate><content:encoded>&lt;h4&gt;检查是否配置以下内容&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;package.json中是否配置了eslint依赖&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-39efc1542b6d04c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/579/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;工程目录下是否有.eslintrc.js和.eslintignore文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-7f70a6381f9c2629.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/344/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.查看vscode是否安装了eslint插件并&lt;strong&gt;启用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-617563f3d59c558f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/839/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;setting.json里是否有eslint的配置项&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-8269c7c303d8cbda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/743/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vscode状态栏 eslitn是否开启，显示打勾状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-6fb08c702d2a7a73.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/483/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;5.备注：vscode状态栏显示禁用或者报错都会导致eslint不生效&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-f28eed3552a52128.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/702/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解决方法：以上两种情况点击状态栏上eslint分别弹出以下弹窗，点击allow按钮即可&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https:////upload-images.jianshu.io/upload_images/7254079-31a01ce5957c6e76.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/674/format/webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>babel</title><link>https://nollieleo.github.io/posts/babel/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/babel/</guid><description>@babel/plugin-transform-runtime  babel官方提供的一个插件，作用是减少冗余的代码。  例如：  class extend 的语法在转换后会在ES5的代码里头注入_extend辅助函数用于实现继承  这导致每个使用class extend语法的文件都会被注入重复的_...</description><pubDate>Sat, 07 Aug 2021 10:37:22 GMT</pubDate><content:encoded>&lt;h2&gt;@babel/plugin-transform-runtime&lt;/h2&gt;
&lt;p&gt;babel官方提供的一个插件，作用是减少冗余的代码。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;class extend 的语法在转换后会在ES5的代码里头注入_extend辅助函数用于实现继承&lt;/p&gt;
&lt;p&gt;这导致每个使用class extend语法的文件都会被注入重复的_extends辅助函数代码&lt;/p&gt;
&lt;p&gt;因此这个插件可以将这个注入函数改成require的形式导入语句来减少代码文件大小&lt;/p&gt;
&lt;p&gt;需要和babel-runtime结合使用&lt;/p&gt;
&lt;h2&gt;babel-loader&lt;/h2&gt;
&lt;p&gt;https://www.npmjs.com/package/babel-loader&lt;/p&gt;
&lt;p&gt;这个包允许使用 Babel 和 webpack 转译 JavaScript 文件。&lt;/p&gt;
&lt;h2&gt;@babel/core&lt;/h2&gt;
&lt;p&gt;Babel 编译器核心。&lt;/p&gt;
&lt;h2&gt;@babel/preset-env&lt;/h2&gt;
&lt;p&gt;https://babel.dev/docs/en/babel-preset-env&lt;/p&gt;
&lt;p&gt;@babel/preset-env 是一个智能预设，它允许您使用最新的 JavaScript，而无需对目标环境需要哪些语法转换（以及可选的浏览器 polyfill）进行微观管理。&lt;/p&gt;
&lt;h2&gt;@babel/runtime&lt;/h2&gt;
&lt;p&gt;@babel/runtime 是一个包含 Babel 模块化运行时助手和 regenerator-runtime 版本的库。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;@babel/eslint-parser&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;ESLint 的默认解析器和核心规则仅支持最新的最终 ECMAScript 标准，不支持 Babel 提供的实验性（如新特性）和非标准（如 Flow 或 TypeScript 类型）语法&lt;/p&gt;
&lt;p&gt;@babel/eslint-parser 是一个解析器，它允许 ESLint 在 Babel 转换的源代码上运行。&lt;/p&gt;
&lt;p&gt;在**.eslintrc.js**配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  parser: &quot;@babel/eslint-parser&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有些新东西可能还得用上&lt;code&gt;@babel/eslint-plugin&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>常用plugins</title><link>https://nollieleo.github.io/posts/%E5%B8%B8%E7%94%A8plugins/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%B8%B8%E7%94%A8plugins/</guid><description>CSS   css-minimizer-webpack-plugin  压缩css文件，webpack5以上   mini-css-extract-plugin    这个插件将 CSS 提取到单独的文件中。它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。它支持按需加载 CSS 和 So...</description><pubDate>Thu, 05 Aug 2021 21:40:40 GMT</pubDate><content:encoded>&lt;h2&gt;CSS&lt;/h2&gt;
&lt;h3&gt;css-minimizer-webpack-plugin&lt;/h3&gt;
&lt;p&gt;压缩css文件，webpack5以上&lt;/p&gt;
&lt;h3&gt;mini-css-extract-plugin&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/webpack-contrib/mini-css-extract-plugin&quot;&gt;mini-csss-extract-plugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个插件将 CSS 提取到单独的文件中。它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。它支持按需加载 CSS 和 SourceMaps。&lt;/p&gt;
&lt;p&gt;它建立在新的 webpack v5 功能之上，并且需要 webpack 5 才能工作。&lt;/p&gt;
&lt;p&gt;提供了一个loader，通过&lt;code&gt;MiniCsseExtractPlugin.loader&lt;/code&gt;配置loader使用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有时候我们会摒弃style-loader的代码去发布线上代码，直接用这个插件的原因是因为 &lt;a href=&quot;https://survivejs.com/webpack/styling/separating-css/&quot;&gt;css-separating&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;css抽离成文件可以，让js和css并行加载，提前解析css&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;postcss-preset-env&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/postcss-preset-env&quot;&gt;postcss-preset-env&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PostCSS Preset Env 允许您将现代 CSS 转换为大多数浏览器可以理解的内容，根据您的目标浏览器或运行时环境确定您需要的 polyfill。&lt;/p&gt;
&lt;h3&gt;autoprefixer&lt;/h3&gt;
&lt;p&gt;PostCSS 插件，用于解析 CSS 并使用 Can I Use 中的值向 CSS 规则添加供应商前缀。它由 Google 推荐并用于 Twitter 和阿里巴巴。&lt;/p&gt;
&lt;p&gt;Autoprefixer 将使用基于当前浏览器流行度和属性支持的数据为您应用前缀&lt;/p&gt;
&lt;p&gt;列如：在&lt;code&gt;postcss.config.js&lt;/code&gt;中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  plugins: [
    [
      require(&quot;autoprefixer&quot;)({
        overrideBrowserslist: [
          &quot;last 2 versions&quot;,
          &quot;Firefox ESR&quot;,
          &quot;&amp;gt; 1%&quot;,
          &quot;ie &amp;gt;= 8&quot;,
          &quot;iOS &amp;gt;= 8&quot;,
          &quot;Android &amp;gt;= 4&quot;,
        ],
      })
    ],
  ],
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然在webpack中也需要配置postcss-loader这个loader是执行所有postcss的插件的关键loader&lt;/p&gt;
&lt;h2&gt;html&lt;/h2&gt;
&lt;h3&gt;HtmlWebpackPlugin&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/html-webpack-plugin&lt;/p&gt;
&lt;p&gt;这是一个 webpack 插件，它简化了 HTML 文件的创建以服务于你的 webpack 包。这对于在文件名中包含哈希值的 webpack 包特别有用，该哈希值会更改每次编译。&lt;/p&gt;
&lt;p&gt;您可以让插件为您生成 HTML 文件，使用 lodash 模板提供您自己的模板或使用您自己的加载器。&lt;/p&gt;
&lt;h2&gt;js&lt;/h2&gt;
&lt;h3&gt;terser-webpack-plugin&lt;/h3&gt;
&lt;p&gt;使用该插件来压缩js的代码&lt;/p&gt;
&lt;p&gt;如果你使用的是 webpack v5 或以上版本，你不需要安装这个插件。webpack v5 自带最新的 &lt;code&gt;terser-webpack-plugin&lt;/code&gt;。如果使用 webpack v4，则必须安装 &lt;code&gt;terser-webpack-plugin&lt;/code&gt; v4 的版本。&lt;/p&gt;
&lt;h2&gt;静态资源&lt;/h2&gt;
&lt;h3&gt;复制静态资源&lt;/h3&gt;
&lt;p&gt;有些时候有些第三方的 js 插件没有提供 npm 包，只提供了一个 cdn 地址或者一份文件需要自己下载下来。通常我们下载下来之后放在我们的 &lt;code&gt;public/js&lt;/code&gt; 目录下面，然后 &lt;code&gt;public/index.html&lt;/code&gt; 文件里直接用 &lt;code&gt;script&lt;/code&gt; 标签引入。这个时候不管是 &lt;code&gt;npm run dev&lt;/code&gt; 开发时，还是 &lt;code&gt;npm run build:pro&lt;/code&gt; 构建后，这个 js 文件都是找不到的&lt;/p&gt;
&lt;p&gt;copy-webpack-plugin&lt;/p&gt;
&lt;h2&gt;打包分析相关&lt;/h2&gt;
&lt;h3&gt;webpack-bundle-analyzer&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/webpack-bundle-analyzer&lt;/p&gt;
&lt;p&gt;webpack-bundle-analyzer 是打包分析神器，可以看到每个包的大小，以及是否有包被重复打包。&lt;/p&gt;
&lt;h3&gt;speed-measure-webpack-plugin&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/speed-measure-webpack-plugin&lt;/p&gt;
&lt;p&gt;这个插件帮助我们分析整个打包的总耗时，以及每一个loader 和每一个 plugins 构建所耗费的时间，从而帮助我们快速定位到可以优化 Webpack 的配置。&lt;/p&gt;
&lt;h2&gt;代码检查&lt;/h2&gt;
&lt;p&gt;代码检测肯定不当当针对于代码的，我们可以在装了如下的几个plugin之后可以基于git为项目接入git hook，这里推荐使用husky来进行提交前的代码检测 &lt;a href=&quot;https://typicode.github.io/husky/#/&quot;&gt;husky&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;eslint-webpack-plugin&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/eslint-webpack-plugin&lt;/p&gt;
&lt;p&gt;针对于js的代码进行代码检测，使用 eslint 来查找和修复 JavaScript 代码中的问题&lt;/p&gt;
&lt;p&gt;当然用这个之前肯定是要装eslint的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通常react的项目需要结合&lt;code&gt;eslint-config-airbnb&lt;/code&gt;， &lt;code&gt;eslint-plugin-import&lt;/code&gt;,  &lt;code&gt;eslint-plugin-react&lt;/code&gt;, &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;, &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt;,&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;当然你可以结合husky为commit之前提供lint指令hooks，然后使用lint-staged提供运行时候的lint&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;stylelint&lt;/h3&gt;
&lt;p&gt;https://github.com/stylelint/stylelint/blob/HEAD/docs/user-guide/get-started.md&lt;/p&gt;
&lt;p&gt;这玩意基于postCss, 能检查任何PostCss能解析的代码&lt;/p&gt;
</content:encoded></item><item><title>常用loader</title><link>https://nollieleo.github.io/posts/%E5%B8%B8%E7%94%A8loader/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%B8%B8%E7%94%A8loader/</guid><description>以下讲的都是针对webpack5版本的     CSS   css-loaders    webpack识别不了css的代码，所以需要一个css-loader去加载css的文件     style-loader    将css（以字符串形式）注入到js的代码当中（相当于做了存储），不会额外生成一个C...</description><pubDate>Thu, 05 Aug 2021 20:50:16 GMT</pubDate><content:encoded>&lt;p&gt;以下讲的都是针对webpack5版本的&lt;/p&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;h3&gt;css-loaders&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/webpack-contrib/css-loader&quot;&gt;css-loader&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;webpack识别不了css的代码，所以需要一个css-loader去加载css的文件&lt;/p&gt;
&lt;h3&gt;style-loader&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/style-loader&quot;&gt;style-loader&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;将css（以字符串形式）注入到js的代码当中（相当于做了存储），不会额外生成一个CSS文件，之后通过网页的js的操作的时候生成dom（style标签）插入到html中，是一个动态的过程&lt;/p&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;p&gt;将css的代码直接放到js当中，不会生成独立css文件，有缓存作用&lt;/p&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;p&gt;js文件变大，网页加载时间变长（可以考虑单独抽离css文件，异步加载）&lt;/p&gt;
&lt;h3&gt;postcss-loader&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/postcss-loader&quot;&gt;postcss-loader&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用 PostCSS 处理 CSS 的加载器。一般需要配合&lt;code&gt;postCSS&lt;/code&gt;的一堆plugin去使用，可以自动为css加上前缀去适配不同浏览器规则，使用css-next的语法等等&lt;/p&gt;
&lt;h2&gt;ts&lt;/h2&gt;
&lt;h3&gt;awesome-typescript-loader&lt;/h3&gt;
&lt;h3&gt;ts-loader&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/376867546&quot;&gt;为什么有时候不用ts的loader去编译ts而是直接用babel呢&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;静态文件&lt;/h2&gt;
&lt;h3&gt;file-loader&lt;/h3&gt;
&lt;p&gt;可以将js和css中导入图片的语句替换成正确的地址，同时将文件输出到对应的位置&lt;/p&gt;
&lt;h3&gt;url-loader&lt;/h3&gt;
&lt;p&gt;可以将文件内容经过编码之后注入js或者css当中&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原因也是显而易见的，再http1.x中，浏览器会对每个域名下的TCP链接做限制，如果图片很多再服务端的话，可能会造成一种情况，请求很多，TCP连接很多，图片请求就很慢，所以直接转成base64就能大大减少请求次数&lt;/p&gt;
&lt;p&gt;当然这块大文件还是不建议转base64的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;webpack 5内置了这些玩意，所以咱们可以直接用webpack5里头的东西了 &lt;a href=&quot;https://webpack.docschina.org/guides/asset-modules/&quot;&gt;assests&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;mini-svg-data-uri&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/mini-svg-data-uri&lt;/p&gt;
&lt;p&gt;该工具将 SVG 转换为最紧凑、可压缩的数据：支持 SVG 的浏览器可以容忍的 URI。结果如下所示（169 字节）：&lt;/p&gt;
&lt;p&gt;比起转成base64的会小一些，还有那些直接转成url的&lt;/p&gt;
&lt;h3&gt;svg-sprite-loader&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/svg-sprite-loader#why-its-cool&lt;/p&gt;
&lt;p&gt;svg-sprite-loader 将加载的 svg 图片拼接成 雪碧图，放到页面中，其它地方通过 &amp;lt;use&amp;gt; 复用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般用在菜单的Icon显示以及搭建一些UI的字体库的时候需要用的到&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;raw-loader&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/raw-loader&lt;/p&gt;
&lt;p&gt;可以将文本文件的内容读取出来注入js或者css中&lt;/p&gt;
&lt;h2&gt;Source Map&lt;/h2&gt;
&lt;h3&gt;source-map-loader&lt;/h3&gt;
&lt;p&gt;https://www.npmjs.com/package/source-map-loader&lt;/p&gt;
&lt;p&gt;使用这个loader去加载别的包的sourcemap方便自己的调试&lt;/p&gt;
&lt;h2&gt;代码检查&lt;/h2&gt;
</content:encoded></item><item><title>webpack记录</title><link>https://nollieleo.github.io/posts/webpack%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/webpack%E8%AE%B0%E5%BD%95/</guid><description>前置包   webpack-cli  https://github.com/webpack/webpack-cli/issues   webpack-dev-server  将 webpack 与提供实时重新加载的开发服务器一起使用。这应该仅用于开发。它在底层使用 webpack-dev-middl...</description><pubDate>Thu, 05 Aug 2021 20:24:49 GMT</pubDate><content:encoded>&lt;h2&gt;前置包&lt;/h2&gt;
&lt;h3&gt;webpack-cli&lt;/h3&gt;
&lt;p&gt;https://github.com/webpack/webpack-cli/issues&lt;/p&gt;
&lt;h3&gt;webpack-dev-server&lt;/h3&gt;
&lt;p&gt;将 webpack 与提供实时重新加载的开发服务器一起使用。这应该仅用于开发。它在底层使用 webpack-dev-middleware，它提供对 webpack 资产的快速内存访问。&lt;/p&gt;
&lt;h2&gt;source map 配置详情&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b6d1ed68645848688ab9cb5a5790724d~tplv-k3u1fbpfcp-watermark.awebp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;优化手段&lt;/h2&gt;
&lt;h3&gt;output&lt;/h3&gt;
&lt;p&gt;浏览器缓存，就是进入某个网站后，加载的静态资源被浏览器缓存，再次进入该网站后，将直接拉取缓存资源，加快加载速度。&lt;/p&gt;
&lt;p&gt;webpack 支持根据资源内容，创建 hash id，当资源内容发生变化时，将会创建新的 hash id。&lt;/p&gt;
&lt;p&gt;配置 JS bundle hash，&lt;code&gt;webpack.js&lt;/code&gt; 配置方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  // 输出
  output: {
    // 仅在生产环境添加 hash
    filename: ctx.isEnvProduction ? &apos;[name].[contenthash].bundle.js&apos; : &apos;[name].bundle.js&apos;,
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;tree shaking&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;js的shaking看这个 &lt;a href=&quot;https://juejin.cn/post/6993275177647751182&quot;&gt;原理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;loader优化&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;babel-loader&lt;/code&gt;可以开启缓存，在第2次编译时，直接使用&lt;strong&gt;缓存&lt;/strong&gt;，不用重新编译，缓存一般只适用于开发环境&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;include&lt;/code&gt;或&lt;code&gt;exclude&lt;/code&gt;适当缩小&lt;code&gt;loader&lt;/code&gt;的适用范围，让其更快找到要解析的文件，开发和生产环境皆适用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; module: {
      rules: [
        {
          test: /\.(js|jsx|ts|tsx)$/i,
          use: [&apos;babel-loader?cacheDirectory&apos;],
          exclude: /node_modules/,
          include: /src/,
        },
      ]
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;压缩 css 文件&lt;/h3&gt;
&lt;p&gt;使用css-minimizer-webpack-plugin&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const CssMinimizerPlugin = require(&quot;css-minimizer-webpack-plugin&quot;);

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
          parallel: 4,
        }),
    ],
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;输出结果不携带路径信息&lt;/h3&gt;
&lt;p&gt;默认 webpack 会在输出的 bundle 中生成路径信息，将路径信息删除可小幅提升构建速度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    output: {
        pathinfo: false,
      },
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;React的优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;可以使用  &lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fpmmmwh%2Freact-refresh-webpack-plugin&quot;&gt;react-refresh-webpack-plugin&lt;/a&gt; 这个plugin来实现react组件的热更新&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优化 resolve 配置&lt;/h3&gt;
&lt;h4&gt;1. alias&lt;/h4&gt;
&lt;p&gt;alias 可以创建 &lt;code&gt;import&lt;/code&gt; 或 &lt;code&gt;require&lt;/code&gt; 的别名，用来简化模块引入。&lt;/p&gt;
&lt;h4&gt;2. extensions&lt;/h4&gt;
&lt;p&gt;根据项目中的文件类型，定义 extensions，以覆盖 webpack 默认的 extensions，加快解析速度。&lt;/p&gt;
&lt;p&gt;由于 webpack 的解析顺序是从左到右，因此要将使用频率高的文件类型放在左侧，如下我将 &lt;code&gt;tsx&lt;/code&gt; 放在最左侧。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;webpack.common.js&lt;/code&gt; 配置方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    resolve: {
        extensions: [&apos;.tsx&apos;, &apos;.js&apos;], // 因为我的项目只有这两种类型的文件，如果有其他类型，需要添加进去。
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. modules&lt;/h4&gt;
&lt;p&gt;modules 表示 webpack 解析模块时需要解析的目录。&lt;/p&gt;
&lt;p&gt;指定目录可缩小 webpack 解析范围，加快构建速度。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;webpack.common.js&lt;/code&gt; 配置方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    modules: [
      &apos;node_modules&apos;,
       paths.appSrc,
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. symlinks&lt;/h4&gt;
&lt;p&gt;如果项目不使用 symlinks（例如 &lt;code&gt;npm link&lt;/code&gt; 或者 &lt;code&gt;yarn link&lt;/code&gt;），可以设置 &lt;code&gt;resolve.symlinks: false&lt;/code&gt;，减少解析工作量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;webpack.common.js&lt;/code&gt; 配置方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    resolve: {
        symlinks: false,
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;缓存&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;webpack5之前&lt;/p&gt;
&lt;p&gt;利用 &lt;code&gt;cache-loader&lt;/code&gt; 将结果缓存中磁盘中；利用 &lt;code&gt;hard-source-webpack-plugin&lt;/code&gt; 将结果缓存在 &lt;code&gt;node_modules/.cache&lt;/code&gt; 下提升二次打包速度；利用 &lt;code&gt;DllReferencePlugin&lt;/code&gt; 将变化不频繁的第三方库&lt;code&gt;提前单独&lt;/code&gt;打包成动态链接库，提升真正业务代码的打包速度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而。。webpack5自带了配置项&lt;/p&gt;
&lt;p&gt;开发环境 &lt;code&gt;webpack.dev.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache: {
    type: &apos;memory&apos;
},
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产环境 &lt;code&gt;webpack.pro.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache: {
    type: &apos;filesystem&apos;,
    buildDependencies: {
      config: [__filename]
    }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打包时清除上次构建产物&lt;/h3&gt;
&lt;p&gt;打包目录下面可能会存在大量上次打包留下来的产物，时间长了就会有很多无用的代码&lt;/p&gt;
&lt;p&gt;webpack5.20之前这里推荐使用&lt;a href=&quot;https://www.npmjs.com/package/clean-webpack-plugin&quot;&gt;CleanWebpackPlugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;之后就可以用&lt;code&gt;output.clean&lt;/code&gt;为true清除&lt;/p&gt;
&lt;h3&gt;js代码压缩&lt;/h3&gt;
&lt;p&gt;webpack5之前我们是需要&lt;code&gt;terser-webpack-plugin&lt;/code&gt;这个包的并且需要以下配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TerserPlugin = require(&apos;terser-webpack-plugin&apos;)

module.exports = { 
// ...other config
optimization: {
  minimize: !isDev,
  minimizer: [
    new TerserPlugin({
      extractComments: false, 
      terserOptions: { 
        compress: { 
          pure_funcs: [&apos;console.log&apos;] 
        }
      }
    }) ]
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是webpack5自带了代码压缩&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // webpack.config.js中
  module.exports = {
     optimization: {
       usedExports: true, //只导出被使用的模块
       minimize : true // 启动压缩
     }
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然你如果需要自定义的话也可以安装这个插件，然后自定义配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TerserPlugin = require(&apos;terser-webpack-plugin&apos;);
module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
              parallel: 4,
              terserOptions: {
                parse: {
                  ecma: 8,
                },
                compress: {
                  ecma: 5,
                  warnings: false,
                  comparisons: false,
                  inline: 2,
                },
                mangle: {
                  safari10: true,
                },
                output: {
                  ecma: 5,
                  comments: false,
                  ascii_only: true,
                },
              },
            }),
        ]
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;合并模块&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;普通打包只是将一个模块最终放到一个单独的立即执行函数中，如果你有很多模块，那么就有很多立即执行函数。concatenateModules 可以要所有的模块都合并到一个函数里面去。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;热更新&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自动刷新
是指在修改模块内存时，浏览器会自动刷新页面来更新视图内容，是整个页面刷新，速度较慢；刷新页面还会导致临时状态丢失（比如表单内容）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;热更新
是在不刷新页面的情况下，使新代码生效，整个网面不会刷新，状态也不会丢失&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;target: &apos;web&apos;,
plugins: [
	new webpack.HotModuleReplacementPlugin({})
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;noParse&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;noParse&lt;/code&gt;是用来过滤不需要解析的模块，比如&lt;code&gt;jquery&lt;/code&gt;,&lt;code&gt;lodash&lt;/code&gt;之类的，这些库一般不会再引入其它库，所以不需要&lt;code&gt;webpack&lt;/code&gt;去解析其依赖，也不用打包，只是直接引用即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
  noParse: /jquery|lodash/,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多线程打包&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/thread-loader&quot;&gt;thread-loader&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;将耗时的 loader 放在一个独立的 worker 池中运行，加快 loader 构建速度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Fguides%2Fbuild-performance%2F%23sass&quot;&gt;webpack 官网&lt;/a&gt; 提到 &lt;code&gt;node-sass&lt;/code&gt; 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 &lt;code&gt;thread-loader&lt;/code&gt; 时，需要设置 &lt;code&gt;workerParallelJobs: 2&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于 thread-loader 引入后，需要 0.6s 左右的时间开启新的 node 进程，如果项目代码量小，引入 thread-loader就没什么必要了&lt;/p&gt;
&lt;p&gt;我们应该仅在非常耗时的 loader 前引入 thread-loader。&lt;/p&gt;
&lt;h3&gt;优化图片&lt;/h3&gt;
&lt;h2&gt;使用打包大小分析工具&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/50260262/how-to-run-webpack-bundle-analyzer/50260397&quot;&gt;how to use webpack-bundle-analyzer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/webpack-bundle-analyzer&quot;&gt;webpack-bundle-analyzer&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;升级webpack5遇到的问题&lt;/h2&gt;
&lt;h3&gt;node的一些模块webpack不自带了&lt;/h3&gt;
</content:encoded></item><item><title>useRef能做啥？</title><link>https://nollieleo.github.io/posts/useref%E8%83%BD%E5%81%9A%E5%95%A5/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/useref%E8%83%BD%E5%81%9A%E5%95%A5/</guid><description>useRef  &lt;uuseRef主要的功能就是。&lt;/u  1. 帮助我们获取到DOM元素或者组件实例 2. 保存在组件生命周期内不会变化的值  如下：  jsx function TextInputWithFocusButton() {   const inputEl = useRef(null);...</description><pubDate>Tue, 03 Aug 2021 15:29:45 GMT</pubDate><content:encoded>&lt;h2&gt;useRef&lt;/h2&gt;
&lt;p&gt;&amp;lt;u&amp;gt;&lt;strong&gt;&lt;code&gt;useRef&lt;/code&gt;主要的功能就是。&lt;/strong&gt;&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;帮助我们获取到&lt;strong&gt;DOM元素或者组件实例&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;保存在组件&lt;strong&gt;生命周期内不会变化&lt;/strong&gt;的值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () =&amp;gt; {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input ref={inputEl} type=&quot;text&quot; /&amp;gt;
      &amp;lt;button onClick={onButtonClick}&amp;gt;Focus the input&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个创建ref的过程是这样的&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;组件初始化，执行到useRef，初始化了一个参数null&lt;/li&gt;
&lt;li&gt;这时候触发render了，这个过程实现了一个ref的挂载，从&lt;code&gt;null&lt;/code&gt;到相对应的组件实例或者DOM挂载，相当于对&lt;code&gt;ref.current&lt;/code&gt;的赋值了，这个过程会有一个ref的数据变化&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;两个特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每次组件重新渲染useRef的返回值都是同一个（引用不变）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref.current&lt;/code&gt;发生变化的时候，不会触发组件的重新渲染，区别于其他的hooks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此引出两个场景&lt;/p&gt;
&lt;h4&gt;1.不要单独拿ref作为依赖项&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;useEffect(()=&amp;gt;{
    ....
},[ref]);

useEffect(()=&amp;gt;{
  ...  
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两者相当于是一样的了，因为ref始终是同一个引用。&lt;/p&gt;
&lt;h4&gt;2.手动调用自己的ref挂载函数&lt;/h4&gt;
&lt;p&gt;这里我们需要通过&lt;code&gt;callback ref&lt;/code&gt;的形式去挂载&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node =&amp;gt; {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1 ref={measuredRef}&amp;gt;Hello, world&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;The above header is {Math.round(height)}px tall&amp;lt;/h2&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：这里的ref挂载针对的是DOM元素，如果是要拿组件的ref，则往下看&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;forwardRef&lt;/h2&gt;
&lt;p&gt;我们用&lt;code&gt;forwardRef&lt;/code&gt;包裹函数式组件，见下例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const childRef = useRef(null);

  useEffect(() =&amp;gt; {
    ref.current.focus();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Child ref={ref} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};
const Child = forwardRef((props, ref) =&amp;gt; {
  return &amp;lt;input type=&quot;text&quot; name=&quot;child&quot; ref={ref} /&amp;gt;;
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;forwardRef&lt;/code&gt;包裹之后，函数式组件会获得被分配给自己的ref（作为第二个参数）。如果你没有使用&lt;code&gt;forwardRef&lt;/code&gt;而直接去&lt;code&gt;ref&lt;/code&gt;的话，&lt;code&gt;React&lt;/code&gt;会报错。&lt;/p&gt;
&lt;h2&gt;useImperativeHandle&lt;/h2&gt;
&lt;p&gt;上面&lt;code&gt;forwardRef&lt;/code&gt;的例子中，&lt;code&gt;Parent&lt;/code&gt;中的&lt;code&gt;ref&lt;/code&gt;拿到了&lt;code&gt;Child&lt;/code&gt;组件的完整实例，它不但可以使用&lt;code&gt;focus&lt;/code&gt;方法，还可以使用其它所有的DOM方法，比如&lt;code&gt;blur&lt;/code&gt;,&lt;code&gt;style&lt;/code&gt;。这种方式是不推荐的，我们需要严格的控制&lt;code&gt;ref&lt;/code&gt;的权力，控制它所能调用到的方法。&lt;/p&gt;
&lt;p&gt;所以我们要使用&lt;code&gt;useImperativeHandle&lt;/code&gt;来限制暴露给父组件的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const childRef = useRef(null);

  useEffect(() =&amp;gt; {
    // 这里只能调用到focus方法
    ref.current.focus();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Child ref={ref} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};
const Child = forwardRef((props, ref) =&amp;gt; {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () =&amp;gt; ({
    focus: () =&amp;gt; {
      inputRef.current.focus();
    }
  }));
  return &amp;lt;input type=&quot;text&quot; name=&quot;child&quot; ref={inputRef} /&amp;gt;;
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样子，我们就可以手动控制需要暴露给父组件的方法。&lt;/p&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h3&gt;获取上一次的值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function usePrevious(value) {
  const ref = useRef();
  useEffect(() =&amp;gt; {
    ref.current = value;
  });
  return ref.current;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个hooks返回出来的值，在渲染的过程中，总是会显示上一次的值。我们来解析一下这个函数的运行步骤。 假设上例中&lt;code&gt;ref&lt;/code&gt;的初始值传入的&lt;code&gt;value&lt;/code&gt;是0,每次数据更新传入的都是递增的数据，比如1，2，3。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化，ref.current = 0，渲染出来&lt;/li&gt;
&lt;li&gt;数据变化，&lt;code&gt;value&lt;/code&gt;传入1, 因为&lt;code&gt;useEffect&lt;/code&gt;会在渲染完毕之后才执行，所以这次的渲染过程中，这个为1的&lt;code&gt;value&lt;/code&gt;值不会赋值给&lt;code&gt;ref.current&lt;/code&gt;。渲染出来的还是上一个值0，渲染完毕了，&lt;code&gt;ref.current&lt;/code&gt;变为1。但是&lt;code&gt;ref.current&lt;/code&gt;变化不会触发组件的重新渲染，所以需要等到下次的渲染才能显示到页面上。&lt;/li&gt;
&lt;li&gt;如此往复，渲染的就总是上一次的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用useRef来保存不需要变化的值&lt;/h3&gt;
&lt;p&gt;因为&lt;code&gt;useRef&lt;/code&gt;的返回值在组件的每次&lt;code&gt;redner&lt;/code&gt;之后都是同一个，所以它可以用来保存一些在组件整个生命周期都不需要变化的值。最常见的就是定时器的清除场景。&lt;/p&gt;
&lt;p&gt;刚开始在&lt;code&gt;React&lt;/code&gt;里写定时器，你可能会这样写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const App = () =&amp;gt; {
  let timer;

  useEffect(() =&amp;gt; {
    timer = setInterval(() =&amp;gt; {
      console.log(&apos;触发了&apos;);
    }, 1000);
  },[]);

  const clearTimer = () =&amp;gt; {
    clearInterval(timer);
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Button onClick={clearTimer}&amp;gt;停止&amp;lt;/Button&amp;gt;
    &amp;lt;/&amp;gt;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是上面这个写法有个巨大的问题，如果这个&lt;code&gt;App&lt;/code&gt;组件里有&lt;code&gt;state&lt;/code&gt;变化或者他的父组件重新&lt;code&gt;render&lt;/code&gt;等原因导致这个&lt;code&gt;App&lt;/code&gt;组件重新&lt;code&gt;render&lt;/code&gt;的时候，我们会发现，点击按钮停止，定时器依然会不断的在控制台打印，定时器清除事件无效了。&lt;/p&gt;
&lt;p&gt;为什么呢？因为组件重新渲染之后，这里的&lt;code&gt;timer&lt;/code&gt;以及&lt;code&gt;clearTimer &lt;/code&gt;方法都会&lt;strong&gt;重新创建&lt;/strong&gt;，&lt;code&gt;timer&lt;/code&gt;已经不是定时器的变量了。&lt;/p&gt;
&lt;p&gt;所以对于定时器，我们都会使用&lt;code&gt;useRef&lt;/code&gt;来定义变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const App = () =&amp;gt; {
  const timer = useRef();

  useEffect(() =&amp;gt; {
    timer.current = setInterval(() =&amp;gt; {
      console.log(&apos;触发了&apos;);
    }, 1000);
  },[]);

  const clearTimer = () =&amp;gt; {
    clearInterval(timer.current);
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Button onClick={clearTimer}&amp;gt;停止&amp;lt;/Button&amp;gt;
    &amp;lt;/&amp;gt;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现深度比较useEffect&lt;/h3&gt;
&lt;p&gt;普通的&lt;code&gt;useEffect&lt;/code&gt;只是一个浅比较的方法，如果我们依赖的&lt;code&gt;state&lt;/code&gt;是一个对象，组件重新渲染，这个&lt;code&gt;state&lt;/code&gt;对象的值没变，但是内存引用地址变化了，一样会触发&lt;code&gt;useEffect&lt;/code&gt;的重新渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const createObj = () =&amp;gt; ({
    name: &apos;zouwowo&apos;
});
useEffect(() =&amp;gt; {
  // 这个方法会无限循环
}, [createObj()]);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来使用&lt;code&gt;useRef&lt;/code&gt;实现一个深度依赖对比的&lt;code&gt;useDeepEffect&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import equal from &apos;fast-deep-equal&apos;;
export useDeepEffect = (callback, deps) =&amp;gt; {
  const emitEffect = useRef(0);
  const prevDeps = useRef(deps);
  if (!equal(prevDeps.current, deps)) {
    // 当深比较不相等的时候，修改emitEffect.current的值，触发下面的useEffect更新
    emitEffect.current++;
  }
  prevDeps.current = deps;
  return useEffect(callback, [emitEffect.current]);
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>在线编辑网站</title><link>https://nollieleo.github.io/posts/%E5%9C%A8%E7%BA%BF%E7%BC%96%E8%BE%91%E7%BD%91%E7%AB%99/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9C%A8%E7%BA%BF%E7%BC%96%E8%BE%91%E7%BD%91%E7%AB%99/</guid><description>...</description><pubDate>Sun, 01 Aug 2021 17:23:42 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://babeljs.io/repl/#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&amp;amp;build=&amp;amp;builtIns=false&amp;amp;corejs=3.6&amp;amp;spec=false&amp;amp;loose=false&amp;amp;code_lz=GYVwdgxgLglg9mABACwKYBt1wBQEpEDeAUIogE6pQhlIA8AJjAG4B8AEhlogO5xnr0AhLQD0jVgG4iAXyJA&amp;amp;debug=false&amp;amp;forceAllTransforms=false&amp;amp;shippedProposals=false&amp;amp;circleciRepo=&amp;amp;evaluate=false&amp;amp;fileSize=false&amp;amp;timeTravel=false&amp;amp;sourceType=module&amp;amp;lineWrap=true&amp;amp;presets=react&amp;amp;prettier=false&amp;amp;targets=&amp;amp;version=7.14.9&amp;amp;externalPlugins=&quot;&gt;babel转换js，react&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAZwIYE8ASMAUZUC2ApgFzJQBOMYA5gJQDeAUIq4hAsnADZEB03ODRwADABZFugxABIG+YgF8RdJoqZM0WXAHIA7kVo66Abg0cw5FCQDK6AgCMeiALyI7jnjh0BGAEw+xmYWXLwCQjjIpkA&quot;&gt;ts转js&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>21年7月17面试</title><link>https://nollieleo.github.io/posts/21%E5%B9%B47%E6%9C%8817%E9%9D%A2%E8%AF%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/21%E5%B9%B47%E6%9C%8817%E9%9D%A2%E8%AF%95/</guid><description>微医集团面试（1面）（将近一小时）   介绍下自己   介绍一下你的项目  这里我们目前的项目是猪齿鱼devops集成平台，所以他问的很深，包括流水线，部署等等的东西，这块集成的流程是什么   js基础   1. var,let,const 什么区别  老生常谈，就没啥好说的了   2. js中存在...</description><pubDate>Sat, 17 Jul 2021 19:55:52 GMT</pubDate><content:encoded>&lt;h1&gt;微医集团面试（1面）（将近一小时）&lt;/h1&gt;
&lt;h2&gt;介绍下自己&lt;/h2&gt;
&lt;h2&gt;介绍一下你的项目&lt;/h2&gt;
&lt;p&gt;这里我们目前的项目是猪齿鱼devops集成平台，所以他问的很深，包括流水线，部署等等的东西，这块集成的流程是什么&lt;/p&gt;
&lt;h2&gt;js基础&lt;/h2&gt;
&lt;h3&gt;1. var,let,const 什么区别&lt;/h3&gt;
&lt;p&gt;老生常谈，就没啥好说的了&lt;/p&gt;
&lt;h3&gt;2. js中存在基本数据类型和引用数据类型，你能说说他们的区别吗？&lt;/h3&gt;
&lt;p&gt;这里我提到了存储方式的不同，一个是在栈中存储，一个是在堆中存储&lt;/p&gt;
&lt;h2&gt;算法优化&lt;/h2&gt;
&lt;h3&gt;1. 你在项目中有用到平时刷的算法去优化代码吗？&lt;/h3&gt;
&lt;p&gt;给他列了个例子，从深度优先和广度优先来说（空间复杂度和时间复杂度）&lt;/p&gt;
&lt;h2&gt;数据结构&lt;/h2&gt;
&lt;p&gt;上面第二点被他套话了，“您刚刚提到了堆和栈，那你提堆和栈还能想起什么吗？”&lt;/p&gt;
&lt;h3&gt;计算机数据结构有哪些&lt;/h3&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;除了计算机数据结构提起堆和栈你还能想到什么吗？&lt;/p&gt;
&lt;p&gt;不晓得应该说什么了，我给他提了一下执行上下文栈&lt;/p&gt;
&lt;h2&gt;浏览器网络相关&lt;/h2&gt;
&lt;h3&gt;http的缓存策略&lt;/h3&gt;
&lt;p&gt;强缓存 + 协商缓存 ，什么时候进行强缓存什么时候进行协商&lt;/p&gt;
&lt;h3&gt;强缓存是通过什么标识来进行的？&lt;/h3&gt;
&lt;h3&gt;什么情况下不开启缓存（什么字段标识）&lt;/h3&gt;
&lt;h3&gt;协商缓存通过什么标识来进行（304）&lt;/h3&gt;
&lt;h3&gt;浏览器存储（localstorage, session, cookie）区别&lt;/h3&gt;
&lt;h2&gt;js引擎&lt;/h2&gt;
&lt;h3&gt;说一说垃圾回收机制，IE和谷歌的垃圾回收机制有什么区别吗？&lt;/h3&gt;
&lt;h3&gt;说一说事件循环机制&lt;/h3&gt;
&lt;h3&gt;你能解释一下为什么js的单线程的吗&lt;/h3&gt;
&lt;h1&gt;涂鸦智能面试（1面）（面了30分钟）&lt;/h1&gt;
&lt;h2&gt;简单介绍一些你和你的项目&lt;/h2&gt;
&lt;h2&gt;这里你提到了react hooks，你平时自己有封装过吗&lt;/h2&gt;
&lt;h2&gt;你这里项目所说到了一个叫axios的节流缓存方案，你能详细描述一下吗&lt;/h2&gt;
&lt;h2&gt;你项目上常用的react hooks有哪些呢，简单介绍一下&lt;/h2&gt;
&lt;h2&gt;你能说说受控组件和非受控组件的区别吗&lt;/h2&gt;
&lt;h2&gt;ES6中有那些特性你经常用到呢&lt;/h2&gt;
&lt;h2&gt;你能讲一下Promise吗？&lt;/h2&gt;
&lt;h2&gt;Promise中的构造函数是同步还是异步的？&lt;/h2&gt;
&lt;h2&gt;什么是回调地狱你能说说吗？&lt;/h2&gt;
</content:encoded></item><item><title>js常见面试题</title><link>https://nollieleo.github.io/posts/js%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98/</guid><description>手写篇              7.     数据类型  1. js有哪些数据类型 2. js有哪些内置对象 3. 数据类型的检测方式（typeof, intanceof, Object.prototype.toString三者区分） 4. null， undefined区别 5. 0.1+0.2...</description><pubDate>Thu, 01 Jul 2021 21:22:53 GMT</pubDate><content:encoded>&lt;h2&gt;手写篇&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/01/%E6%89%8B%E5%86%99new/&quot;&gt;1. 手写new&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/02/%E6%89%8B%E5%86%99call-apply/&quot;&gt;2. 手写call, apply&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/02/%E6%89%8B%E5%86%99instanceof/&quot;&gt;3. 手写instanceof&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/03/%E6%89%8B%E5%86%99bind/&quot;&gt;4. 手写bind&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6844903625769091079&quot;&gt;5. 手写promise&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/17/%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D/&quot;&gt;6. 深拷贝浅拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;7.&lt;a href=&quot;https://nollieleo.github.io/2020/05/03/%E9%98%B2%E6%8A%96%E5%8A%A8%E5%92%8C%E8%8A%82%E6%B5%81/&quot;&gt;防抖节流&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;数据类型&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;js有哪些数据类型&lt;/li&gt;
&lt;li&gt;js有哪些内置对象&lt;/li&gt;
&lt;li&gt;数据类型的检测方式（typeof, intanceof, Object.prototype.toString三者区分）&lt;/li&gt;
&lt;li&gt;null， undefined区别&lt;/li&gt;
&lt;li&gt;0.1+0.2 !== 0.3 为什么，如果使其相等&lt;/li&gt;
&lt;li&gt;== 操作符强制类型转换规则&lt;/li&gt;
&lt;li&gt;什么是js的包装类型&lt;/li&gt;
&lt;li&gt;biginit&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;类型转换&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/05/28/js%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/&quot;&gt;类型转换&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;ES6&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;箭头函数和普通函数区别&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Proxy使用场景&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;异步&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/10/%E5%AE%8F%E4%BB%BB%E5%8A%A1%E5%92%8C%E5%BE%AE%E4%BB%BB%E5%8A%A1/&quot;&gt;宏任务微任务&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/10/Event-Loop%E8%BD%AE%E8%AF%A2%E5%A4%84%E7%90%86%E7%BA%BF%E7%A8%8B/&quot;&gt;EvenLoop&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;原型，原型链，继承&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对原型，原型链的理解&lt;/li&gt;
&lt;li&gt;ES5实现继承的几种方法（原型链，盗用构造函数，组合继承，原型式继承，寄生式继承，组合寄生式继承）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/tags/%E7%BB%A7%E6%89%BF/&quot;&gt;继承 模块&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;作用域，作用域链，this指向，执行上下文，闭包&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/05/js%E7%9A%84%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BB%A5%E5%8F%8A%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E6%A0%88/&quot;&gt;执行上下文和执行上下文栈&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/05/js%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E9%93%BE/&quot;&gt;作用域链&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/05/js%E7%9A%84%E9%9D%99%E6%80%81%E4%BD%9C%E7%94%A8%E5%9F%9F/&quot;&gt;静态作用域&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nollieleo.github.io/2021/06/06/js%E7%9A%84%E9%97%AD%E5%8C%85/&quot;&gt;闭包&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>基于猪齿鱼平台的axios缓存封装</title><link>https://nollieleo.github.io/posts/%E5%9F%BA%E4%BA%8E%E7%8C%AA%E9%BD%BF%E9%B1%BC%E5%B9%B3%E5%8F%B0%E7%9A%84axios%E7%BC%93%E5%AD%98%E5%B0%81%E8%A3%85/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9F%BA%E4%BA%8E%E7%8C%AA%E9%BD%BF%E9%B1%BC%E5%B9%B3%E5%8F%B0%E7%9A%84axios%E7%BC%93%E5%AD%98%E5%B0%81%E8%A3%85/</guid><description>需求说明   情况1：  猪齿鱼平台是个相对复杂的多表格多表单数据处理平台，有些模块的数据量庞大，并且有些喜欢嵌入在tab页面当中，如果频繁的去访问这些页面就会频繁的请求，如果数据量庞大，每次访问都会去请求数据，等待时间就变长了，例如下面这个界面：    这个页面相当于是历史的执行记录，因为数据量庞...</description><pubDate>Thu, 24 Jun 2021 14:02:44 GMT</pubDate><content:encoded>&lt;h2&gt;需求说明&lt;/h2&gt;
&lt;h3&gt;情况1：&lt;/h3&gt;
&lt;p&gt;猪齿鱼平台是个相对复杂的多表格多表单数据处理平台，有些模块的数据量庞大，并且有些喜欢嵌入在tab页面当中，如果频繁的去访问这些页面就会频繁的请求，如果数据量庞大，每次访问都会去请求数据，等待时间就变长了，例如下面这个界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.gif&quot; alt=&quot;7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个页面相当于是历史的执行记录，因为数据量庞大，我们可以看到这个页面请求过后响应速度较慢，假设我在短时间内切到别的记录又切回来要看，那还得重新再去请求这1000多条的数据，这个是没有必要的，用户等待时间就变长了&lt;/p&gt;
&lt;h3&gt;情况2：&lt;/h3&gt;
&lt;p&gt;在一个页面渲染的过程中，或者在某些不正当的交互操作中，用户可能频繁的向后台去发送同一个请求（参数url等待都一样），或又是再渲染页面的一瞬间触发了多次同一请求，这样就很浪费带宽，多次请求前端还需要进行多次的响应处理，例如以下情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210624155243688.png&quot; alt=&quot;image-20210624155243688&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在渲染页面时，同时发送了多个相同请求，大多时候是页面逻辑处理的有问题，多次向后台获取数据了，或者是我频繁的去点击一个没有做防抖的按钮或者没有节流的一些频繁请求&lt;/p&gt;
&lt;h3&gt;情况3：&lt;/h3&gt;
&lt;p&gt;假设我错误的点击了一个菜单的同时又去切到了另外一个菜单，这时候虽然说已经切换到了想要的菜单路由下对应的界面，但是由于你错误的操作，虽然上个页面在切路由瞬间被销毁，但是初始化页面的一些获取后台数据的请求已经发出去了，你访问的当前页面是不需要这些请求的。例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./test.gif&quot; alt=&quot;test&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个例子就是，我先点击了代码管理的菜单，代码管理模块组件瞬间被加载出来，并且发送页面请求，这时候一瞬间切换到应用流水线的菜单，这时候我们会看见，代码管理模块的请求很多没必要的都发送出去了，这些请求并不是我们应用流水线模块需要用的到的。&lt;/p&gt;
&lt;p&gt;如果上个页面的请求很慢或者很多情况下，都会占用不必要的资源以及前端处理时间，占用资源。&lt;/p&gt;
&lt;h2&gt;需求分析&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;针对情况1, 我们可以做缓存, 类似于DataSet的lookup，但又不完全类似。也可以做类似tab的数据缓存处理， 但是如果页面多起来，我们就需要多次分别对数据进行缓存处理。&lt;/li&gt;
&lt;li&gt;针对第二种的情况，可以将重复的请求给取消掉，或者说在这一瞬间发送的多次相同请求都共用第一次发送的那个请求的响应状态。一些按钮或者频繁请求的操作做节流和防抖处理，这个就不再这次的讨论范围内了&lt;/li&gt;
&lt;li&gt;第三种情况在切换路由的时候可以想办法把上个页面还在pending中的请求给取消掉&lt;/li&gt;
&lt;li&gt;猪齿鱼平台用的是axios + DataSet来进行前后端数据交互以及请求处理的，所以基于他们要如何完美实现呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;**综上所述：**我们要实现一个，能够实现取消，又能实现缓存的，重点是实现相同请求的状态数据的公用以及不影响dataset的使用的东西&lt;/p&gt;
&lt;p&gt;可以想到的是，我们肯定是要对axios的拦截器或者一些特殊属性例如axios的适配器，做处理。&lt;/p&gt;
&lt;h3&gt;缓存分析&lt;/h3&gt;
&lt;p&gt;参考猪齿鱼lookup的缓存原理，以下放出lookup缓存的部分源码；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625112724872.png&quot; alt=&quot;image-20210625112724872&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625113745262.png&quot; alt=&quot;image-20210625113745262&quot; /&gt;&lt;/p&gt;
&lt;p&gt;lookup通过标识确定是否缓存，之后将适配器返回的期约直接给一个新得期约，之后存储这个新得期约，在下次发送相同请求时候直接将这个期约抛给它，从而实现请求的复用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先我们要明白axios 的 adpater的原理：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;源码是这样的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625160815760.png&quot; alt=&quot;image-20210625160815760&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以知道的是，axios 的 transformRequest以及他的各个请求拦截器是在return这个期约之前发生的，之后请求完成，adapter将请求后的结果传递给期约，之后期约落定，后面才触发响应数据转换（transformReponse）以及响应拦截器（这个顺序非常的重要）。&lt;/p&gt;
&lt;p&gt;既然要缓存，一定要把第一次落定的请求期约，想办法（包含了响应码数据等等等）存储起来，需要用到的时候才去使用，所以可以确定的是我们缓存需要对adapter进行操作，并且在响应和请求的拦截器中做标识等等操作&lt;/p&gt;
&lt;h3&gt;多次相同请求处理&lt;/h3&gt;
&lt;p&gt;一开始我想用XMLHttpRequest的abort也就是axios的CancelToken（axios对abort进行了封装）去将页面正在渲染的时候一个个重复的请求cancel掉，但是这样有多种弊端，留到最后一点讲。&lt;/p&gt;
&lt;p&gt;多次相同请求是一瞬间的事情，要在这一瞬间复用请求必须用到上面缓存所说的存储期约。&lt;/p&gt;
&lt;p&gt;一瞬间咱们就可以定一个默认时间戳，例如1000ms，这个时间戳表示的是，在这1000ms中进来的请求都共用一个adapter的期约，这样定义一个默认时间戳，就可以做到类似取消重复请求的作用（实际上是用了缓存）。&lt;/p&gt;
&lt;p&gt;当然既然加了缓存，又加了时间戳，灵活应用一下就可以实现一个，在对应时间内不单单是1000ms，我发的相同请求都共用，我们可以将时间戳直接定义在axios默认属性上或者axios创建出来的实例上面，定义默认值1000ms，之后如果想要缓存时间长一点的话直接在dataset的transport对应的方法配置项下面或者是axios实例上添加时间戳，用来覆盖默认的时间戳，因此可以实现长效缓存。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625173040963.png&quot; alt=&quot;image-20210625173040963&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625173136449.png&quot; alt=&quot;image-20210625173136449&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;切换路由取消pending请求&lt;/h3&gt;
&lt;p&gt;这里必须用到axios的CancelToken。上面说到CancelToken的其实就是对&lt;code&gt;XMLhttpRequest&lt;/code&gt;的abort方法进行封装， &lt;code&gt;XMLHttpRequest&lt;/code&gt; 对象是我们发起一个网络请求的根本，在它底下有怎么一个方法 &lt;code&gt;.abort()&lt;/code&gt;，就是中断一个已被发出的请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625211727335.png&quot; alt=&quot;image-20210625211727335&quot; /&gt;&lt;/p&gt;
&lt;p&gt;简单理解就是通过 &lt;code&gt;new axios.CancelToken()&lt;/code&gt;给每个请求带上一个专属的CancelToken，之后会接收到一个&lt;code&gt;cancel()&lt;/code&gt; 取消方法，用于后续的取消动作。&lt;/p&gt;
&lt;p&gt;所以，每次请求进来的时候去获取本次请求的CancelToken，之后将其存起来（这里假设用CancelQueue的一个Map数据类型（&lt;code&gt;Object.prototype.toString.call(CancelQueue) = &apos;[object Map]&apos;&lt;/code&gt;）），之后假如这个请求落定了，就将这个存储CancelToken的请求标识删除。&lt;/p&gt;
&lt;p&gt;如果这时候切换路由了，我们就去CancelQueue中，遍历一遍所有的请求标识（因为这时候再队列里头的肯定是还在pending的请求），调用其存储的CancelToken调用cancel方法进行请求取消，并删除标识。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 获取 axios CancelToken
const { CancelToken } = axios;

// 2. 获取当前请求的source，并且将其cancel方法存储起来
const source = CancelToken.source();

// 3. 之后调用这个cancel方法
source.cancel();

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;axios的实例化&lt;/h3&gt;
&lt;p&gt;首先实例化axios(这是我们全局封装的axios)，在其中加入缓存标识，标识开启缓存（重复请求共用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const instance:AxiosStatic = axios.create({
  timeout: 30000,
  baseURL: API_HOST,
});

// 这里配置一个缓存请求得标识
instance.defaults.enabledCancelCache = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;标识的实现&lt;/h3&gt;
&lt;p&gt;对于每一个请求创建一个标识，标识唯一，标识是通过每一个请求的&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;params&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, 以及 &lt;code&gt;data&lt;/code&gt;建立标识，只要通过这4者建立的标识相同，那我们就能确定某些请求是相同的，&lt;/p&gt;
&lt;p&gt;如下&lt;code&gt;getMark&lt;/code&gt;是建立主标识，&lt;code&gt;getDataMark&lt;/code&gt;是对响应体内的data的标识&lt;/p&gt;
&lt;p&gt;params要进行参数拼接，现在主流基本在用&lt;code&gt;application/json&lt;/code&gt;形式，Axios默认以这种形式工作，我们给后端接口传递参数也简单 ，但是有时候需要 Content-Type必须以application/x-www-form-urlencoded形式 ，以JSON格式后台是收不到的，而这时候就要进行序列化处理（先前是有前辈在这里做了处理的），但是这里不多说参数序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { get } from &apos;lodash&apos;;
import { AxiosRequestConfig } from &apos;axios&apos;;
import JSONbig from &apos;json-bigint&apos;;
import paramsSerializer from &apos;./paramsSerializer&apos;;

// 单独处理data，response里面返回的config.data是个字符串对象
function getDataMark(data:any) {
  let stringifyData = data;
  if (typeof stringifyData === &apos;string&apos;) {
    stringifyData = JSONbig.parse(stringifyData);
  }
  return stringifyData;
}

// 区别请求的唯一标识，这里用方法名+请求路径
// 如果一个项目里有多个不同baseURL的请求 + 参数
export default function getMark(config:AxiosRequestConfig) {
  const getKey = (key:string) =&amp;gt; get(config, key);

  // params标识处理，将其处理成?key=value&amp;amp;key2=value的形式
  const tempQueryString = (getKey(&apos;paramsSerializer&apos;) ?? paramsSerializer)(getKey(&apos;params&apos;));

  // data标识处理
  const dataMark = JSONbig.stringify(getDataMark(getKey(&apos;data&apos;)));

  // base标识
  const requestMark = [
    config?.method?.toLowerCase() || &apos;unknownMethod&apos;,
    config?.url,
  ];

  getKey(&apos;params&apos;) &amp;amp;&amp;amp; requestMark.push(tempQueryString);
  getKey(&apos;data&apos;) &amp;amp;&amp;amp; requestMark.push(dataMark);
  return requestMark.join(&apos;&amp;amp;&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的拼接形式是 &lt;code&gt;method &amp;amp; url &amp;amp; params &amp;amp; data&lt;/code&gt;的形式，&lt;/p&gt;
&lt;p&gt;例如下面这个请求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625210606969.png&quot; alt=&quot;image-20210625210606969&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210625210632981.png&quot; alt=&quot;image-20210625210632981&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终拼接出来的是&lt;/p&gt;
&lt;p&gt;&lt;code&gt;post&amp;amp;http://172.23.16.92:30094/iam/choerodon/v1/permissions/menus/check-permissions&amp;amp;tenantId=2&amp;amp;[&quot;choerodon.code.organization.project.ps.create&quot;]&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;缓存实现&lt;/h3&gt;
&lt;p&gt;这里我们采用拦截器的形式去添加缓存逻辑，以下是添加了请求拦截器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210626094315248.png&quot; alt=&quot;image-20210626094315248&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面标红的是添加响应拦截器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210626094721131.png&quot; alt=&quot;image-20210626094721131&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至于这里拦截器的顺序为什么是这样的，后续再说&lt;/p&gt;
&lt;p&gt;其次，我们要明白请求的几种情况&lt;/p&gt;
&lt;p&gt;假设现在有一个A请求进来了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;还在pending的时候，在默认时间戳内（1000ms）又进来了一个一模一样的A请求（重复了,共用状态）&lt;/li&gt;
&lt;li&gt;A请求已经落定了&lt;strong&gt;成功了&lt;/strong&gt;，在定义的时间戳范围内又进来了多个一模一样的请求（缓存）&lt;/li&gt;
&lt;li&gt;A请求落定&lt;strong&gt;失败了&lt;/strong&gt;，在定义的时间戳范围内又进来了多个一模一样的请求（不缓存）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后明确需要存储在缓存实例中的数据格式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210626101834505.png&quot; alt=&quot;image-20210626101834505&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;存储数据&lt;/h4&gt;
&lt;p&gt;我们需要以一种键值对的形式去存储，可选项就属Object或者Map。&lt;/p&gt;
&lt;p&gt;但是这里我们需要频繁的对缓存的数据进行查找，插入，赋值，删除操作等等。&lt;/p&gt;
&lt;p&gt;在小数量数据情况下Object的查找数据相对Map来的快，但是涉及更多插入的和删除的情况下都是Map性能来的优势，并且相同内存下Map能够存储更多的键值对，这里显然存在庞大数据的情况&lt;/p&gt;
&lt;p&gt;于是这里封装一个&lt;code&gt;AxiosCache&lt;/code&gt;缓存类，然后写一些私有方法在里头&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AxiosCache {
  constructor() {
    super();
    this.cacheData = new Map();
  }
    
  get size() {
    return this.cacheData.size;
  }

  get(key) {
    return this.cacheData.get(key);
  }

  delete(key) {
    this.cacheData.delete(key);
  }

  has(key) {
    return this.cacheData.has(key);
  }

  set(key, value) {
    this.cacheData.set(key, value);
  }

  isEmpty() {
    return !!this.cacheData.size;
  }

  clear() {
    this.cacheData = new Map();
  }
}
const axiosCache = new AxiosCache();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;请求拦截器实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当A请求进来时候发现先前并没有在缓存队列中做过标识，也就是说标识里头不存在以上的那些字段，这时候我们就将A请求做个标识，并且设置它的isPending（正在请求）为&lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;axiosCache.set(cancelCacheKey, {
    isPending: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当A请求已经做过标识的时候，这时候又进来一个A1请求&lt;/p&gt;
&lt;p&gt;倘若第一次做过标识的A请求已经不是在pending状态并且在这次请求的时间戳的范围内（在后面讲响应拦截器的时候会把A的isPending字段在某种条件下设置为&lt;code&gt;false&lt;/code&gt;, 并且存储A请求响应后的数据），A1请求就直接去取A缓存的数据&lt;/p&gt;
&lt;p&gt;这里就涉及到上面所说的适配器的用处，直接在A1请求config的适配器返回一个成功落定的期约，通过标识拿到cache实例中缓存的数据数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// isPending为false的情况下，并且在时间戳的范围内
if (expire &amp;amp;&amp;amp; expire &amp;gt; Date.now()) {
  tempConfig.adapter = () =&amp;gt; {
    const resolveData: AxiosResponse = {
      data,
      headers: tempConfig.headers,
      config: {
        ...tempConfig,
        useCache: true,
      },
      // @ts-expect-error
      request: tempConfig,
    };
    return Promise.resolve(resolveData);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里定义一个&lt;code&gt;useCache&lt;/code&gt;的字段表明这个是再用缓存数据，后续的响应拦截器中会讲到&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当A请求已经做过标识的时候，这时候又进来一个A1请求&lt;/p&gt;
&lt;p&gt;倘若第一次做过标识的A请求还在pending的状态，我又想共用缓存数据，但是并不清楚什么时候A请求才会结束。&lt;/p&gt;
&lt;p&gt;这时候我们就想，用一个发布订阅的模式，将A1请求的&lt;strong&gt;期约落定时机&lt;/strong&gt;订阅起来，在A请求落定的时候告诉A1你是时候也该落定了就去&lt;strong&gt;发布&lt;/strong&gt;信息，这时候缓存的数据就是在A请求落定时候，去发布顺便给A1传递的.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实这个过程可以有多个A1进来&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不好理解的这里有流程图&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;./image-20210626111840112.png&quot; alt=&quot;image-20210626111840112&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210626111852379.png&quot; alt=&quot;image-20210626111852379&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 说明找到了请求但是找到的这个缓存的请求还在pending，这时候订阅一个期约待会要用
tempConfig.adapter = () =&amp;gt; new Promise((resolve) =&amp;gt; {
    axiosEvent.once(cancelCacheKey, (res:unknown) =&amp;gt; {
      const resolveData: AxiosResponse = {
        data: res,
        headers: tempConfig.headers,
        config: {
          ...tempConfig,
          useCache: true,
        },
        // @ts-expect-error
        request: tempConfig,
      };
      resolve(resolveData);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里A1请求订阅了以一个以cancelCacheKey为标识的信号。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至于A请求落定的时机，到响应拦截器中做处理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果A请求请求失败怎么办？那这时候我们不做处理，也不做标识，请求失败走的是请求失败的拦截器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下是整个请求拦截器代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { AxiosRequestConfig, AxiosResponse } from &apos;axios&apos;;
import { get } from &apos;lodash&apos;;
import getMark, { transformDataToString } from &apos;../utils/getMark&apos;;
import { axiosCache, axiosEvent } from &apos;../instances&apos;;

export function handleCancelCacheRequest(config:AxiosRequestConfig) {
  const tempConfig = config;
  // 是否开启了缓存（复用重复请求状态）
  const enabledCancelCache = get(tempConfig, &apos;enabledCancelCache&apos;);
  if (enabledCancelCache) {
    // 获取标识
    const cancelCacheKey = getMark(tempConfig);

    if (!axiosCache.has(cancelCacheKey)) {
      axiosCache.set(cancelCacheKey, {});
    }

    const {
      data, // 缓存的数据
      isPending, // 请求是否是pending状态
      expire, // 时间戳
    } = axiosCache.get(cancelCacheKey);

    if (isPending) {
      // 说明找到了请求但是找到的这个缓存的请求还在pending，这时候订阅一个期约待会要用
      tempConfig.adapter = () =&amp;gt; new Promise((resolve) =&amp;gt; {
        axiosEvent.once(cancelCacheKey, (res:unknown) =&amp;gt; {
          const resolveData: AxiosResponse = {
            data: res,
            headers: tempConfig.headers,
            config: {
              ...tempConfig,
              useCache: true,
            },
            // @ts-expect-error
            request: tempConfig,
          };
          resolve(resolveData);
        });
      });
    } else if (expire &amp;amp;&amp;amp; expire &amp;gt; Date.now()) {
      tempConfig.adapter = () =&amp;gt; {
        const resolveData: AxiosResponse = {
          data: transformDataToString(data),
          headers: tempConfig.headers,
          config: {
            ...tempConfig,
            useCache: true,
          },
          // @ts-expect-error
          request: tempConfig,
        };
        return Promise.resolve(resolveData);
      };
    } else {
      axiosCache.set(cancelCacheKey, {
        isPending: true,
      });
    }
  }

  return tempConfig;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;响应拦截器的实现&lt;/h4&gt;
&lt;p&gt;实现了请求拦截器之后，我们来处理在先前拦截器中的一些字段，以及请求失败的数据处理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;我们先对响应成功的的拦截器做处理&lt;/p&gt;
&lt;p&gt;首先是响应成功的情况下而且也成功拿到想要的数据，假设这个响应成功的请求是上述的A请求，这时候我们就应该记录下A请求最终的所有数据以及它需要的缓存时间，并且设置它isPending字段为false，最重要的是给订阅了cancelCacheKey的信息的请求们发布一个信号，告诉你们可以来拿我的（A请求）数据了！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (enabledCancelCache &amp;amp;&amp;amp; !useCache) {
    const finalData = resData;
    axiosCache.set(config?.cancelCacheKey || cancelCacheKey, {
      data: finalData,
      isPending: false,
      expire: Date.now() + Number(enabledCancelCache) * 500,
    });
    // 发布这个信号让所有订阅了A的都能接受到信息
    axiosEvent.emit(cancelCacheKey, finalData);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里为什么要多一次useCache的判断呢?&lt;/p&gt;
&lt;p&gt;因为我先前在请求拦截器中处理了adapter用来处理缓存, 走完了adapter才会走响应拦截器，也就是说，我在响应拦截器里头还得判断，这次到底是走的缓存，还是第一次需要做缓存呢？所以才知道为什么我在上面的adapter里头加入了useCache的字段。&lt;/p&gt;
&lt;p&gt;简单来说，就是A请求第一次进来，useCache就是false，就得去存值，接下来进来A1，那我就得走缓存，我不再去重新设置新值了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有成功拿到想要的数据，也就是后端返回错误信息的情况&lt;/p&gt;
&lt;p&gt;这里我们的后台返回的错误标识是failed字段，就直接将isPending设置为false不做后续处理，并且将在订阅了的请求信号都删除(删除是因为A请求失败就已经报错，其他请求就不必再重复报错)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;axiosCache.set(cancelCacheKey, {
  isPending: false,
});
axiosEvent.delete(cancelCacheKey);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当然响应失败拦截器中也需要做上述这样的处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是最终的响应成功的拦截器代码👇&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { AxiosResponse } from &apos;axios&apos;;
import get from &apos;lodash/get&apos;;
import {
  prompt,
} from &apos;@/utils&apos;;
import { axiosCache, axiosEvent } from &apos;../instances&apos;;
import getMark from &apos;../utils/getMark&apos;;

export default function handleResponseInterceptor(response:AxiosResponse) {
  const resData = get(response, &apos;data&apos;);
  const config = get(response, &apos;config&apos;) || {};

  const { enabledCancelCache, useCache } = config;
  const cancelCacheKey = getMark(config);
  if (get(response, &apos;status&apos;) === 204) {
    return response;
  }
  if (resData?.failed === true) {
    axiosCache.set(cancelCacheKey, {
      ...(axiosCache.get(cancelCacheKey) || {}),
      isPending: false,
    });

    if (!response?.config?.noPrompt) {
      prompt(resData.message, &apos;error&apos;);
    }
    throw resData;
  }

  if (enabledCancelCache &amp;amp;&amp;amp; !useCache) {
    const finalData = resData;
    axiosCache.set(config?.cancelCacheKey || cancelCacheKey, {
      data: finalData,
      isPending: false,
      expire: Date.now() + Number(enabledCancelCache) * 500,
    });
    axiosEvent.emit(cancelCacheKey, finalData);
  }

  return resData;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;切换路由取消pending请求&lt;/h3&gt;
&lt;p&gt;这里的思路就相对比较简单，按照上面需求分析所说的存储cancelToken给每一个请求，之后切换路由将pending的请求取消掉&lt;/p&gt;
&lt;p&gt;因此我们不单单需要实现存储cancelToken的一个队列，还有处理他的拦截器，我们还需要监听路由的变化，找到这个时机将所有pending请求cancel掉&lt;/p&gt;
&lt;h4&gt;拦截器的实现&lt;/h4&gt;
&lt;p&gt;这里我们封装一个拦截器&lt;code&gt;routeCancelInterceptor&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加切换路由取消pending请求拦截器
instance.interceptors.request.use(routeCancelInterceptor); // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要一个取消请求的队，列于是封装一个实例用来存储对应的请求，包含相对应的get，set，等方法，最重要的是&lt;/p&gt;
&lt;p&gt;cancelAllRequest方法，用于遍历所有存储的pending请求，之后在切路由的时候，遍历数据取消它。这里为什么还需要引入前面做了缓存的axiosCache，后头会细讲&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { axiosCache } from &apos;./index&apos;;

class RouteAxios {
  constructor() {
    this.pendingRequest = new Map();
  }

  get size() {
    return this.pendingRequest.size;
  }

  get(key) {
    return this.pendingRequest.get(key);
  }

  delete(key) {
    this.pendingRequest.delete(key);
  }

  has(key) {
    return this.pendingRequest.has(key);
  }

  set(key, value) {
    this.pendingRequest.set(key, value);
  }

  isEmpty() {
    return !!this.pendingRequest.size;
  }

  clear() {
    this.pendingRequest = new Map();
  }

 cancelAllRequest() {
    for (const [key, value] of this.pendingRequest) {
      if (value?.cancel &amp;amp;&amp;amp; typeof value.cancel === &apos;function&apos;) {
        value.cancel();
        axiosCache.delete(key); // 这里为什么这么做？后续在说
      }
    }
    this.clear();
  }
}
export const axiosRoutesCancel = new RouteAxios();


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后要我们就在请求拦截器里头处理cancelToken&lt;/p&gt;
&lt;p&gt;当然首先也是先检测是否有这个&lt;code&gt;enabledCancelRoute&lt;/code&gt;字段表明是否要将这个请求加入切换路由取消请求的方案，之后调用source()，获取存储其&lt;code&gt;source.cancel&lt;/code&gt;方法，重点是要在config上加入cancelToken，表明这个请求待会可能会被取消（简单来讲这个标识对应这个source.cancel的执行目标）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios, { AxiosRequestConfig } from &apos;axios&apos;;
import get from &apos;lodash/get&apos;;
import { axiosRoutesCancel } from &apos;../instances&apos;;
import getMark from &apos;../utils/getMark&apos;;

export function routeCancelInterceptor(config:AxiosRequestConfig) {
  const tempConfig = config;
  const enabledCancelRoute = get(tempConfig, &apos;enabledCancelRoute&apos;);
  if (enabledCancelRoute) {
    const cancelRouteKey = tempConfig?.cancelCacheKey || getMark(config);
    const { CancelToken } = axios;
    const source = CancelToken.source();
    tempConfig.cancelToken = source.token;
    axiosRoutesCancel.set(cancelRouteKey, {
      cancel: source.cancel,
      name: cancelRouteKey,
    });
  }

  return tempConfig;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果请求在还没切换路由的时候就已经响应成功（这里不管是否拿到正确数据），就应该直接删除这个存储的key值&lt;/p&gt;
&lt;p&gt;这里就在响应成功的拦截器中处理，在失败拦截器中也是一样的处理方案&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (enabledCancelRoute) {
    axiosRoutesCancel.delete(cancelCacheKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;路由守卫的实现&lt;/h4&gt;
&lt;p&gt;实现了拦截器我们还需要简单的实现一个路由守卫，用来监听路由变化&lt;/p&gt;
&lt;p&gt;在全局，我们封装了一个叫做PermissionRoute的守卫，这守卫是对&lt;code&gt;react-router-dom&lt;/code&gt;的route做了变向封装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const PermissionRoute: React.FC&amp;lt;PermissionRouteProps&amp;gt; =()=&amp;gt; React.ReactDOM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给这个组件配置参数，叫做&lt;code&gt;enabledRouteChangedAjaxBlock&lt;/code&gt;，默认为true, 标识这个路由下的请求，在切换的时候都会被取消，当然每个请求自己也会标识自己是否在切换路由的时候要被取消，后者的优先级大于前者&lt;/p&gt;
&lt;p&gt;之后我们写一个组件销毁的生命周期函数（路由切换的时候，这个路由对应的路由守卫组件就会被销毁，随之销毁（取消）的也就是当前页面正在pending的请求）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; function () {
    if (enabledRouteChangedAjaxBlock &amp;amp;&amp;amp; axiosRoutesCancel.size) {
      axiosRoutesCancel.cancelAllRequest();
    }
},
[location]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;useEffect返回一个销毁的函数，并且cancel掉所有pending请求。&lt;/p&gt;
&lt;p&gt;这里具体可以看看PermissionRoute的封装代码，方便理解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useEffect, useMemo } from &apos;react&apos;;
import { Route, RouteProps, useLocation } from &apos;react-router-dom&apos;;
import { noaccess as NoAccess, Permission } from &apos;@/index&apos;;
import useQueryString from &apos;@/hooks/useQueryString&apos;;
import Skeleton from &apos;@/containers/components/c7n/master/skeleton&apos;;
import { axiosRoutesCancel } from &apos;@/containers/components/c7n/tools/axios/instances&apos;;

interface PermissionRouteProps extends RouteProps {
  service: string[] | ((type: &apos;project&apos; | &apos;organization&apos; | &apos;site&apos;) =&amp;gt; string[]),
  enabledRouteChangedAjaxBlock: boolean,
}
const isFunction = (something: unknown): something is Function =&amp;gt; typeof something === &apos;function&apos;;

const PermissionRoute: React.FC&amp;lt;PermissionRouteProps&amp;gt; = ({ enabledRouteChangedAjaxBlock = true, service, ...rest }) =&amp;gt; {
  const { type } = useQueryString();
  const location = useLocation();
  const codes = useMemo(() =&amp;gt; (isFunction(service) ? service(type) : (service || [])), [service, type]);
  const route = (
    &amp;lt;Route
      {...rest}
    /&amp;gt;
  );

  useEffect(() =&amp;gt; function () {
    if (enabledRouteChangedAjaxBlock &amp;amp;&amp;amp; axiosRoutesCancel.size) {
      axiosRoutesCancel.cancelAllRequest();
    }
  },
  [location]);

  return (codes.length &amp;gt; 0)
    ? (
      &amp;lt;Permission
        service={codes}
        noAccessChildren={&amp;lt;NoAccess /&amp;gt;}
        defaultChildren={&amp;lt;Skeleton /&amp;gt;}
      &amp;gt;
        {route}
      &amp;lt;/Permission&amp;gt;
    )
    : (
      &amp;lt;Route
        {...rest}
      /&amp;gt;
    );
};
export default PermissionRoute;


// 用法
&amp;lt;PermissionRoute
    service={[&apos;choerodon.code.project.project.overview.ps.default&apos;]}
    exact
    path={`${match.url}agile/project-overview`}
    component={ProjectOverview}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;遗留问题&lt;/h2&gt;
&lt;h3&gt;为什么不用CancelToken去取消重复请求而用缓存原理？&lt;/h3&gt;
&lt;p&gt;我们知道取消重复请求原理是abort，定义是：请求如果发出，则立刻终止请求；&lt;/p&gt;
&lt;h4&gt;原因一：&lt;/h4&gt;
&lt;p&gt;这个终止就很微妙，终止之后的请求我们可以知道它走了响应失败的拦截器，也就是说我们前端还是把数据发给了后端，后端还会做数据处理比如（post，put等），只是终止了，但是后端并不知道我们终止了请求，只是前端终止没有在像响应成功拦截器一样对数据做处理&lt;/p&gt;
&lt;p&gt;但是实际上我们要的效果不是这样，我们要的是在那一瞬间的所有相同请求状态复用，重复的请求们是不可以在那一瞬间再发送给后端的，而是只有那个第一次的请求才发出去，如果使用CancelToken这样就没有意义了&lt;/p&gt;
&lt;h4&gt;原因二&lt;/h4&gt;
&lt;p&gt;abort会导致浏览器中的network中一堆报红，看上去比较不美观。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210627124622241.png&quot; alt=&quot;image-20210627124622241&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;原因三&lt;/h4&gt;
&lt;p&gt;如果用了cancelToken我又想做缓存，还不如直接用缓存来，也可以达到效果&lt;/p&gt;
&lt;h3&gt;为什么拦截器的添加顺序会影响到缓存？&lt;/h3&gt;
&lt;p&gt;这也是在上面讲axios的adapter的时候提及过&lt;/p&gt;
&lt;p&gt;请求拦截器是的执行顺序是后添加的先执行&lt;/p&gt;
&lt;p&gt;响应拦截器是先添加的先执行&lt;/p&gt;
&lt;p&gt;例如：我们项目的请求拦截器是这样添加的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加切换路由取消pending请求拦截器
instance.interceptors.request.use(routeCancelInterceptor); // 4

// 添加缓存(复用重复请求)请求拦截器
instance.interceptors.request.use(handleCancelCacheRequest, handleRequestError); // 3

// 分页数据转换拦截器
instance.interceptors.request.use(transformRequestPage); // 2

// 添加头部拦截器， 以及请求失败拦截器
instance.interceptors.request.use(addCustomHeader); // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按顺序是从下到上执行，按照标注的1，2，3，4执行&lt;/p&gt;
&lt;p&gt;响应拦截器是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加响应拦截器
instance.interceptors.response.use(transformResponsePage); // 1
instance.interceptors.response.use(handleResponseInterceptor, handelResponseError); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由取消cancelAllRequest方法中为什么要删除cache中的数据？&lt;/h3&gt;
&lt;h2&gt;难点解决&lt;/h2&gt;
</content:encoded></item><item><title>算法之二叉树的所有路径</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84/</guid><description>给定一个二叉树，返回所有从根节点到叶子节点的路径。  说明: 叶子节点是指没有子节点的节点。      思路：  前序遍历，每一次访问下一个节点都把本次的路径字符串传递下去，每次走到节点没有左右子树了就把最终的这个字符串给存到最终数组里头  js /   Definition for a binar...</description><pubDate>Sun, 20 Jun 2021 16:45:45 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-paths/&quot;&gt;257. 二叉树的所有路径&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个二叉树，返回所有从根节点到叶子节点的路径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明:&lt;/strong&gt; 叶子节点是指没有子节点的节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620164623685.png&quot; alt=&quot;image-20210620164623685&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;前序遍历，每一次访问下一个节点都把本次的路径字符串传递下去，每次走到节点没有左右子树了就把最终的这个字符串给存到最终数组里头&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {string[]}
 */
var binaryTreePaths = function (root) {
    if (!root) {
        return root;
    }
    let res = [];

    const getPath = (root, str) =&amp;gt; {
        if (root) {
            if (str) {
                str += `-&amp;gt;${root.val}`;
            } else {
                str = String(root.val);
            }
            if (root.left) {
                getPath(root.left, str)
            }
            if (root.right) {
                getPath(root.right, str)
            }
            if (!root.right &amp;amp;&amp;amp; !root.left) {
                res.push(str);
            }
        }
    }

    getPath(root, &apos;&apos;);

    return res;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之无重复字符最长子串</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/</guid><description>给定一个字符串，请你找出其中不含有重复字符的 最长子串 的长度。      解法：  1.滑动窗口          js var lengthOfLongestSubstring = function(s) {     // 哈希集合，记录每个字符是否出现过     const occ = new...</description><pubDate>Sun, 20 Jun 2021 16:30:50 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/&quot;&gt;3. 无重复字符的最长子串&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个字符串，请你找出其中不含有重复字符的 &lt;strong&gt;最长子串&lt;/strong&gt; 的长度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620163148398.png&quot; alt=&quot;image-20210620163148398&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.滑动窗口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620163418701.png&quot; alt=&quot;image-20210620163418701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620163439373.png&quot; alt=&quot;image-20210620163439373&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620163452684.png&quot; alt=&quot;image-20210620163452684&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var lengthOfLongestSubstring = function(s) {
    // 哈希集合，记录每个字符是否出现过
    const occ = new Set();
    const n = s.length;
    // 右指针，初始值为 -1，相当于我们在字符串的左边界的左侧，还没有开始移动
    let rk = -1, ans = 0;
    for (let i = 0; i &amp;lt; n; ++i) {
        if (i != 0) {
            // 左指针向右移动一格，移除一个字符
            occ.delete(s.charAt(i - 1));
        }
        while (rk + 1 &amp;lt; n &amp;amp;&amp;amp; !occ.has(s.charAt(rk + 1))) {
            // 不断地移动右指针
            occ.add(s.charAt(rk + 1));
            ++rk;
        }
        // 第 i 到 rk 个字符是一个极长的无重复字符子串
        ans = Math.max(ans, rk - i + 1);
    }
    return ans;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉搜索树的众数</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E4%BC%97%E6%95%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E4%BC%97%E6%95%B0/</guid><description>给定一个有相同值的二叉搜索树（BST），找出 BST 中的所有众数（出现频率最高的元素）。  假定 BST 有如下定义：  - 结点左子树中所含结点的值小于等于当前结点的值、 - 结点右子树中所含结点的值大于等于当前结点的值 - 左子树和右子树都是二叉搜索树      解法：  1.递归中序遍历（开...</description><pubDate>Sun, 20 Jun 2021 15:59:30 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/&quot;&gt;501. 二叉搜索树中的众数&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个有相同值的二叉搜索树（BST），找出 BST 中的所有众数（出现频率最高的元素）。&lt;/p&gt;
&lt;p&gt;假定 BST 有如下定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结点左子树中所含结点的值小于等于当前结点的值、&lt;/li&gt;
&lt;li&gt;结点右子树中所含结点的值大于等于当前结点的值&lt;/li&gt;
&lt;li&gt;左子树和右子树都是二叉搜索树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620160046650.png&quot; alt=&quot;image-20210620160046650&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;1.递归中序遍历（开辟新空间）&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620160831230.png&quot; alt=&quot;image-20210620160831230&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620160856742.png&quot; alt=&quot;image-20210620160856742&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620160915942.png&quot; alt=&quot;image-20210620160915942&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var findMode = function(root) {
    let base = 0, count = 0, maxCount = 0;
    let answer = [];

    const update = (x) =&amp;gt; {
        if (x === base) {
            ++count;
        } else {
            count = 1;
            base = x;
        }
        if (count === maxCount) {
            answer.push(base);
        }
        if (count &amp;gt; maxCount) {
            maxCount = count;
            answer = [base];
        }
    }

    const dfs = (o) =&amp;gt; {
        if (!o) {
            return;
        }
        dfs(o.left);
        update(o.val);
        dfs(o.right);
    }

    dfs(root);
    return answer;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之有效括号</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7/</guid><description>给定一个只包括 &apos;(&apos;，&apos;)&apos;，&apos;{&apos;，&apos;}&apos;，&apos;[&apos;，&apos;]&apos; 的字符串 s ，判断字符串是否有效。  有效字符串需满足：  - 左括号必须用相同类型的右括号闭合。、 - 左括号必须以正确的顺序闭合。      解法：  1.利用栈  思路：  根据题意，我们可以推断出以下要点：  - 有效括号字符...</description><pubDate>Sun, 20 Jun 2021 15:34:33 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/valid-parentheses/&quot;&gt;20. 有效的括号&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个只包括 &apos;(&apos;，&apos;)&apos;，&apos;{&apos;，&apos;}&apos;，&apos;[&apos;，&apos;]&apos; 的字符串 s ，判断字符串是否有效。&lt;/p&gt;
&lt;p&gt;有效字符串需满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左括号必须用相同类型的右括号闭合。、&lt;/li&gt;
&lt;li&gt;左括号必须以正确的顺序闭合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153513595.png&quot; alt=&quot;image-20210620153513595&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.利用栈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;根据题意，我们可以推断出以下要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有效括号字符串&lt;/li&gt;
&lt;li&gt;长度，一定是偶数！&lt;/li&gt;
&lt;li&gt;右括号前面，必须是相对应的左括号，才能抵消！&lt;/li&gt;
&lt;li&gt;右括号前面，不是对应的左括号，那么该字符串，一定不是有效的括号！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153831576.png&quot; alt=&quot;image-20210620153831576&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153851332.png&quot; alt=&quot;image-20210620153851332&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153901841.png&quot; alt=&quot;image-20210620153901841&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153913790.png&quot; alt=&quot;image-20210620153913790&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153926387.png&quot; alt=&quot;image-20210620153926387&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620153936050.png&quot; alt=&quot;image-20210620153936050&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {string} s
 * @return {boolean}
 */
let isValid = function (s) {
    if (s.length % 2) {
        return false;
    }
    let stack = [];
    for (let item of s) {
        switch (item) {
            case &quot;{&quot;:
                stack.push(item);
                break;
            case &quot;[&quot;:
                stack.push(item);
                break;
            case &quot;(&quot;:
                stack.push(item);
                break;
            case &quot;}&quot;:
                if (stack.pop() !== &quot;{&quot;) return false;
                break;
            case &quot;]&quot;:
                if (stack.pop() !== &quot;[&quot;) return false;
                break;
            case &quot;)&quot;:
                if (stack.pop() !== &quot;(&quot;) return false;
                break;
        }
    }
    return !stack.length;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉树的剪枝</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%AA%E6%9E%9D/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%AA%E6%9E%9D/</guid><description>给定二叉树根结点 root ，此外树的每个结点的值要么是 0，要么是 1。  返回移除了所有不包含 1 的子树的原二叉树。  ( 节点 X 的子树为 X 本身，以及所有 X 的后代。)      解法：  1.递归  思路：  首先看以下什么情况会移除这个节点，两个条件都必须满足  1. 这个节点为...</description><pubDate>Sun, 20 Jun 2021 15:09:05 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-pruning/&quot;&gt;814. 二叉树剪枝&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定二叉树根结点 root ，此外树的每个结点的值要么是 0，要么是 1。&lt;/p&gt;
&lt;p&gt;返回移除了所有不包含 1 的子树的原二叉树。&lt;/p&gt;
&lt;p&gt;( 节点 X 的子树为 X 本身，以及所有 X 的后代。)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620151025318.png&quot; alt=&quot;image-20210620151025318&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;首先看以下什么情况会移除这个节点，两个条件都必须满足&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个节点为0&lt;/li&gt;
&lt;li&gt;并且这个节点的左右子树节点，要么子树不存在，要么子树所有节点都为0、&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;按照上面的点就可以使用递归，这个节点等于0，并且没有左右子树的情况，删除这个节点，&lt;/p&gt;
&lt;p&gt;就返回null让他等于父节点左或者右子树&lt;/p&gt;
&lt;p&gt;所以我们要去走二叉树的后序遍历，左右中，&lt;/p&gt;
&lt;p&gt;这题的重点就是一定要走后序遍历，先去判断左右节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var pruneTree = function (root) {
    if (!root) {
        return root
    }
    return getNode(root)
};

function getNode(node) {
    if (node.left) {
        node.left = getNode(node.left)
    }
    if (node.right) {
        node.right = getNode(node.right)
    }
    if (!node.left &amp;amp;&amp;amp; !node.right &amp;amp;&amp;amp; node.val === 0) {
        return null
    }

    return node
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之重建二叉树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%87%8D%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%87%8D%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91/</guid><description>输入某二叉树的前序遍历和中序遍历的结果，请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。    解法：  1.递归  思路：  递归，六句代码包含着大智慧。 首先我们要明白前序遍历和中序遍历的节点遍历顺序。 前序遍历：根-左-右 中序遍历：左-根-右 结合题目的数组我们可以得到...</description><pubDate>Sun, 20 Jun 2021 14:32:20 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/&quot;&gt;剑指 Offer 07. 重建二叉树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;输入某二叉树的前序遍历和中序遍历的结果，请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620143940113.png&quot; alt=&quot;image-20210620143940113&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;递归，六句代码包含着大智慧。
首先我们要明白前序遍历和中序遍历的节点遍历顺序。
前序遍历：根-&amp;gt;左-&amp;gt;右
中序遍历：左-&amp;gt;根-&amp;gt;右
结合题目的数组我们可以得到一个信息：
preorder得到的根节点，在inorder中的它的位置，左侧是左子树，右侧是右子树
而在inorder获取的索引可以帮助我们划分preorder数组
于是我们可以通过划分数组的方式递归得到叶子节点，
然后通过一步步的回溯把它们组装起来，就是一棵完整的树&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620143830588.png&quot; alt=&quot;image-20210620143830588&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function (preorder, inorder) {
    if (preorder.length === 0) return null
    const cur = new TreeNode(preorder[0])
    const index = inorder.indexOf(preorder[0])
    cur.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index))
    cur.right = buildTree(preorder.slice(index + 1), inorder.slice(index + 1))
    return cur
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之验证二叉搜索树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/</guid><description>给定一个二叉树，判断其是否是一个有效的二叉搜索树。  假设一个二叉搜索树具有如下特征：  - 节点的左子树只包含小于当前节点的数。 - 节点的右子树只包含大于当前节点的数。 - 所有左子树和右子树自身必须也是二叉搜索树。      解法：  1.递归  思路：  根据二叉搜索树的特性：  中序遍历的...</description><pubDate>Sun, 20 Jun 2021 14:13:30 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/validate-binary-search-tree/&quot;&gt;98. 验证二叉搜索树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个二叉树，判断其是否是一个有效的二叉搜索树。&lt;/p&gt;
&lt;p&gt;假设一个二叉搜索树具有如下特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点的左子树只包含小于当前节点的数。&lt;/li&gt;
&lt;li&gt;节点的右子树只包含大于当前节点的数。&lt;/li&gt;
&lt;li&gt;所有左子树和右子树自身必须也是二叉搜索树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620141619034.png&quot; alt=&quot;image-20210620141619034&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;根据二叉搜索树的特性：&lt;/p&gt;
&lt;p&gt;中序遍历的时候遍历的顺序是&lt;strong&gt;由小到大&lt;/strong&gt;逐渐遍历的，也就是升序排列，&lt;/p&gt;
&lt;p&gt;所以我们保存上一个值prev，与当前值current作比较。&lt;/p&gt;
&lt;p&gt;如果当前值小于等于上一个值，那么这个树不是二叉搜索树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isValidBST = function (node) {
    var prev = -Infinity

    function inorder(node) {
        if (!node) {
            return true
        }

        var preResult = inorder(node.left)
        var inResult = node.val &amp;gt; prev
        prev = node.val
        var postResult = inorder(node.right)
        return preResult &amp;amp;&amp;amp; inResult &amp;amp;&amp;amp; postResult
    }

    return  inorder(node)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.非递归（迭代）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var isValidBST = function (node) {
    var stack = []
    var prev = -Infinity

    while(stack.length || node) {
        while(node) {
            stack.push(node)
            node = node.left
        }

        node = stack.pop()

        if (node.val &amp;lt;= prev) {
            return false
        }

        prev = node.val
        node = node.right
    }

    return true
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉树的直径</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E7%9B%B4%E5%BE%84/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E7%9B%B4%E5%BE%84/</guid><description>给定一棵二叉树，你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。      解法：  1.DFS递归  思路：  既然是求二叉树中直径长度是任意两个节点中的最大路径值  那我们直接求每个节点的左子树和右子树深度，然后一个个节点进行左右...</description><pubDate>Sun, 20 Jun 2021 14:03:36 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/diameter-of-binary-tree/&quot;&gt;543. 二叉树的直径&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一棵二叉树，你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210620140459295.png&quot; alt=&quot;image-20210620140459295&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.DFS递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;既然是求二叉树中直径长度是任意两个节点中的最大路径值&lt;/p&gt;
&lt;p&gt;那我们直接求每个节点的左子树和右子树深度，然后一个个节点进行左右深度和比较就能找出、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var diameterOfBinaryTree = function (root) {
    if (!root) {
        return 0;
    }
    function maxDepth(node) {
        if (!node) {
            return 0
        }
        let dep = 1;
        dep += Math.max(maxDepth(node.left), maxDepth(node.right));
        return dep;
    }

    return Math.max(
        maxDepth(root.left) + maxDepth(root.right),
        diameterOfBinaryTree(root.left),
        diameterOfBinaryTree(root.right)
    );
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之删除二叉树中的节点</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/</guid><description>给定一个二叉搜索树的根节点 root 和一个值 key，删除二叉搜索树中的 key 对应的节点，并保证二叉搜索树的性质不变。返回二叉搜索树（有可能被更新）的根节点的引用。  一般来说，删除节点可分为两个步骤：  1. 首先找到需要删除的节点； 2. 如果找到了，删除它.  说明： 要求算法时间复杂度...</description><pubDate>Sat, 19 Jun 2021 16:48:11 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/delete-node-in-a-bst/&quot;&gt;450. 删除二叉搜索树中的节点&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个二叉搜索树的根节点 root 和一个值 key，删除二叉搜索树中的 key 对应的节点，并保证二叉搜索树的性质不变。返回二叉搜索树（有可能被更新）的根节点的引用。&lt;/p&gt;
&lt;p&gt;一般来说，删除节点可分为两个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先找到需要删除的节点；&lt;/li&gt;
&lt;li&gt;如果找到了，删除它.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;说明： 要求算法时间复杂度为 O(h)，h 为树的高度&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619165224426.png&quot; alt=&quot;image-20210619165224426&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为BST的左子树总是比根节点小，右子树总是比根节点大，所以我们将根节点的值与要删除的 key 值对比，就知道要删除的值大概在哪个位置：
• 相等：要删除的节点就是当前根节点，即递归退出条件
• key更大：则要递归朝右子树去删除
• key更小：则要递归朝左子树去删除
找到要删除后的节点会出现四种情况：
• 待删除的节点左右子树均为空。证明是叶子节点，直接删除即可，即将该节点置为null
• 待删除的节点左子树为空，让待删除节点的右子树替代自己。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619165359299.png&quot; alt=&quot;image-20210619165359299&quot; /&gt;&lt;/p&gt;
&lt;p&gt;• 待删除的节点右子树为空，让待删除节点的左子树替代自己。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619165415885.png&quot; alt=&quot;image-20210619165415885&quot; /&gt;&lt;/p&gt;
&lt;p&gt;• 如果待删除的节点的左右子树都不为空。我们需要找到比当前节点小的最大节点（前驱）[或比当前节点大的最小节点（后继）]，来替换自己.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619165605714.png&quot; alt=&quot;image-20210619165605714&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const deleteNode = function (root, key) {
  if (root == null) return root

  if (root.val &amp;gt; key) {
    // 往左子树找
    root.left = deleteNode(root.left, key)
  } else if (root.val &amp;lt; key) {
    // 往右子树找
    root.right = deleteNode(root.right, key)
  } else {
    // 找到了
    if (!root.left &amp;amp;&amp;amp; !root.right) {
      // 待删除的节点左右子树均为空。证明是叶子节点，直接删除即可
      root = null
    } else if (root.left &amp;amp;&amp;amp; !root.right) {
      // 待删除的节点右子树为空，让待删除节点的左子树替代自己。
      root = root.left
    } else if (!root.left &amp;amp;&amp;amp; root.right) {
      // 待删除的节点左子树为空，让待删除节点的右子树替代自己。
      root = root.right
    } else if (root.left &amp;amp;&amp;amp; root.right) {
      // 如果待删除的节点的左右子树都不为空。我们需要找到比当前节点小的最大节点（前驱）来替换自己
      let last = root.left
      while (last.right) {
        last = last.left
      }
      // 最终的last就是比当前节点小的最大节点，将值进行替换
      root.val = last.val
      // 删除该最大节点
      root.left = deleteNode(root.left, last.val)
    }
  }
  return root
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之修剪二叉搜索树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/</guid><description>给你二叉搜索树的根节点 root ，同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树，使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构（即，如果没有被移除，原有的父代子代关系都应当保留）。 可以证明，存在唯一的答案。  所以结果应当返回修剪好的二...</description><pubDate>Sat, 19 Jun 2021 16:41:05 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/trim-a-binary-search-tree/&quot;&gt;669. 修剪二叉搜索树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给你二叉搜索树的根节点 root ，同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树，使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构（即，如果没有被移除，原有的父代子代关系都应当保留）。 可以证明，存在唯一的答案。&lt;/p&gt;
&lt;p&gt;所以结果应当返回修剪好的二叉搜索树的新的根节点。注意，根节点可能会根据给定的边界发生改变。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619164548851.png&quot; alt=&quot;image-20210619164548851&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619164609679.png&quot; alt=&quot;image-20210619164609679&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1.递归&lt;/p&gt;
&lt;p&gt;思路&lt;/p&gt;
&lt;p&gt;结合二叉搜索树的特点，节点值大于它的左节点并且小于它的右节点
那么：
当节点值 &amp;lt; L ，把它的左子树抛弃掉，继续修剪它的右子树
当节点值 &amp;gt; R ，把它的右子树抛弃掉，继续修剪它的左子树&lt;/p&gt;
&lt;p&gt;否则当前节点值满足 L &amp;lt; node.val &amp;lt; R ，那么它的左右子树都有可能仍然有符合条件
的节点值，所以要继续修剪左、右子树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} L
 * @param {number} R
 * @return {TreeNode}
 */

var trimBST = function(root, L, R) {
  if (root === null) return root;
  
  if (root.val &amp;lt; L) return trimBST(root.right, L, R);
  if (root.val &amp;gt; R) return trimBST(root.left, L, R);
  
  root.left = trimBST(root.left, L, R);
  root.right = trimBST(root.right, L, R);
  
  return root;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉搜索树的最近公共祖先</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88i/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88i/</guid><description>给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。  百度百科中最近公共祖先的定义为：“对于有根树 T 的两个结点 p、q，最近公共祖先表示为一个结点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”  例如，给定如下二叉搜索树:  root = ...</description><pubDate>Sat, 19 Jun 2021 16:25:13 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/&quot;&gt;剑指 Offer 68 - I. 二叉搜索树的最近公共祖先&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。&lt;/p&gt;
&lt;p&gt;百度百科中最近公共祖先的定义为：“对于有根树 T 的两个结点 p、q，最近公共祖先表示为一个结点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”&lt;/p&gt;
&lt;p&gt;例如，给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619163242688.png&quot; alt=&quot;image-20210619163242688&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;核心思路：
1. 当 传入的祖先节点 同时大于/小于 p、q节点 更新祖先节点位置
a. root 同时小于 p、q 基于BST的特性可知 期望的祖先节点 应该在右子树 root = root.right
b. root 同时大于 p、q 基于BST的特性可知 期望的祖先节点 应该在左子树 root = root.left
2. 当遇到/第一次 祖先节点root 不同时大于或小于 p、q的节点 即为我我们期望的最近公共祖先节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if (!root) return root

    if (root.val &amp;lt; p.val &amp;amp;&amp;amp; root.val &amp;lt; q.val) {
        return lowestCommonAncestor(root.right, p, q)
    } else if (root.val &amp;gt; p.val &amp;amp;&amp;amp; root.val &amp;gt; q.val) {
        return lowestCommonAncestor(root.left, p, q)
    } else {
        return root
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.&lt;strong&gt;迭代&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;利用二叉搜索树的特点
首先判断 p 和 q 是否相等，若相等，则直接返回 p 或 q 中的任意一个，程序结束&lt;/p&gt;
&lt;p&gt;若不相等，则判断 p 和 q 在向左还是向右的问题上，是否达成了一致
如果 p 和 q 都小于root, 哥俩一致认为向左👈，则 root = root.left
如果 p 和 q 都大于root, 哥俩一致认为向右👉，则 root = root.right
如果 p 和 q 哥俩对下一步的路线出现了分歧，说明 p 和 q 在当前的节点上就要分道扬镳了，当前的 root 是哥俩临别前一起走的最后一站
返回当前 root
程序结束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function (root, p, q) {
    if (!root) {
        return null
    }
    while (root) {
        if (root.val &amp;lt; q.val &amp;amp;&amp;amp; root.val &amp;lt; p.val) {
            root = root.right
        } else if (root.val &amp;gt; q.val &amp;amp;&amp;amp; root.val &amp;gt; p.val) {
            root = root.left
        } else {
            return root
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉搜素树范围和</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A0%E6%A0%91%E8%8C%83%E5%9B%B4%E5%92%8C/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A0%E6%A0%91%E8%8C%83%E5%9B%B4%E5%92%8C/</guid><description>给定二叉搜索树的根结点 root，返回值位于范围 [low, high] 之间的所有结点的值的和。       解法  1.递归深度优先搜索   按深度优先搜索的顺序计算范围和。记当前子树根节点为 root， 分4种情况讨论  1. root节点为空，返回0 2. root.val值大于high的值...</description><pubDate>Sat, 19 Jun 2021 15:38:52 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/range-sum-of-bst/&quot;&gt;938. 二叉搜索树的范围和&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定二叉搜索树的根结点 &lt;code&gt;root&lt;/code&gt;，返回值位于范围 &lt;em&gt;&lt;code&gt;[low, high]&lt;/code&gt;&lt;/em&gt; 之间的所有结点的值的和。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619160458047.png&quot; alt=&quot;image-20210619160458047&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.递归深度优先搜索&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按深度优先搜索的顺序计算范围和。记当前子树根节点为 root， 分4种情况讨论&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;root节点为空，返回0&lt;/li&gt;
&lt;li&gt;root.val值大于high的值，根据二叉搜索树的性质知道，应该此时去找root的左子树，无需考虑右子树&lt;/li&gt;
&lt;li&gt;root.val值小于low的值，根据二叉搜索树的性质知道，应该此时去找root的右子树，无需考虑左子树&lt;/li&gt;
&lt;li&gt;root.val值必须在[low, high]范围内&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终应该返回这个节点值和左右子树值的和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var rangeSumBST = function(root, low, high) {
    if (!root) {
        return 0;
    }
    if (root.val &amp;gt; high) {
        return rangeSumBST(root.left, low, high);
    }
    if (root.val &amp;lt; low) {
        return rangeSumBST(root.right, low, high);
    }
    return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.迭代（广度优先）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用广度优先搜索的方法，用一个队列 q 存储需要计算的节点。每次取出队首节点时，若节点为空则跳过该节点，否则按方法一中给出的大小关系来决定加入队列的子节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var rangeSumBST = function(root, low, high) {
    let sum = 0;
    const q = [root];
    while (q.length) {
        const node = q.shift();
        if (!node) {
            continue;
        }
        if (node.val &amp;gt; high) {
            q.push(node.left);
        } else if (node.val &amp;lt; low) {
            q.push(node.right);
        } else {
            sum += node.val;
            q.push(node.left);
            q.push(node.right);
        }
    }
    return sum;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之最后一块石头重量</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E9%87%8D%E9%87%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E9%87%8D%E9%87%8F/</guid><description>有一堆石头，每块石头的重量都是正整数。  每一回合，从中选出两块 最重的 石头，然后将它们一起粉碎。假设石头的重量分别为 x 和 y，且 x &lt;= y。那么粉碎的可能结果如下：  - 如果 x == y，那么两块石头都会被完全粉碎；  - 如果 x != y，那么重量为 x 的石头将会完全粉碎，而重...</description><pubDate>Sat, 19 Jun 2021 15:29:42 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/last-stone-weight/&quot;&gt;1046. 最后一块石头的重量&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;有一堆石头，每块石头的重量都是正整数。&lt;/p&gt;
&lt;p&gt;每一回合，从中选出两块 最重的 石头，然后将它们一起粉碎。假设石头的重量分别为 x 和 y，且 x &amp;lt;= y。那么粉碎的可能结果如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 x == y，那么两块石头都会被完全粉碎；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 x != y，那么重量为 x 的石头将会完全粉碎，而重量为 y 的石头新重量为 y-x。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下，就返回 0。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619153349043.png&quot; alt=&quot;image-20210619153349043&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.非递归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;排序后比较最后面两个&lt;/p&gt;
&lt;p&gt;不同&lt;code&gt;差&lt;/code&gt;放数组，递归直至&lt;code&gt;边界&lt;/code&gt;数组长度 &amp;lt;= 1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number[]} stones
 * @return {number}
 */
var lastStoneWeight = function (stones) {
  if (stones.length &amp;lt;= 1) {
    return stones;
  }
  stones.sort((a, b) =&amp;gt; a - b);
  while (stones.length &amp;gt; 1) {
    const pre = stones.pop();
    const next = stones.pop();
    if (pre !== next) {
      const temp = pre - next;
      let i = 0;
      while (stones[i] &amp;lt; temp) {
        i++;
      }
      stones.splice(i, 0, temp);
    }
  }
  return stones.length ? stones[0] : 0;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之合并二叉树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91/</guid><description>给定两个二叉树，想象当你将它们中的一个覆盖到另一个上时，两个二叉树的一些节点便会重叠。  你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠，那么将他们的值相加作为节点合并后的新值，否则不为 NULL 的节点将直接作为新二叉树的节点。    思路  - 同步地遍历两棵树上的节点，直接在 ...</description><pubDate>Sat, 19 Jun 2021 15:15:41 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/merge-two-binary-trees/&quot;&gt;617. 合并二叉树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定两个二叉树，想象当你将它们中的一个覆盖到另一个上时，两个二叉树的一些节点便会重叠。&lt;/p&gt;
&lt;p&gt;你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠，那么将他们的值相加作为节点合并后的新值，否则不为 NULL 的节点将直接作为新二叉树的节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619151739614.png&quot; alt=&quot;image-20210619151739614&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;同步地遍历两棵树上的节点，直接在 t1 上修改。&lt;/p&gt;
&lt;p&gt;如果把 mergeTrees 函数作为递归函数，参数 t1 和 t2 是指：当前遍历的节点（子树）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;递归。总是关注当前节点
t1 为 null 、t2 不是 null，t1t1 换成 t2 。（return t2）
t2 为 null、t1t1 不是 null，t1t1 依然 t1 。（return t1）&lt;/p&gt;
&lt;p&gt;t1 和 t2 都为 null，t1t1 依然 t1。（return t1）&lt;/p&gt;
&lt;p&gt;t1、t2 都存在，将 t2 的值加给 t1 。（t1.val += t2.val）&lt;/p&gt;
&lt;p&gt;『子树的合并』交给递归去做，它会对每一个节点做同样的事情。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;t1.left = mergeTrees(t1.left, t2.left);
t1.right = mergeTrees(t1.right, t2.right);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619152328260.png&quot; alt=&quot;image-20210619152328260&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mergeTrees = (t1, t2) =&amp;gt; {
  if (t1 == null &amp;amp;&amp;amp; t2) {
    return t2;
  }
  if ((t1 &amp;amp;&amp;amp; t2 == null) || (t1 == null &amp;amp;&amp;amp; t2 == null)) {
    return t1;
  }
  t1.val += t2.val;

  t1.left = mergeTrees(t1.left, t2.left);
  t1.right = mergeTrees(t1.right, t2.right);

  return t1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;如果不在原树修改，新建一个树呢？&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const mergeTrees = (t1, t2) =&amp;gt; {
  if (t1 == null &amp;amp;&amp;amp; t2) {
    return t2;
  }
  if ((t1 &amp;amp;&amp;amp; t2 == null) || (t1 == null &amp;amp;&amp;amp; t2 == null)) {
    return t1;
  }
  const root=new TreeNode(t1.val + t2.val)

  root.left = mergeTrees(t1.left, t2.left);
  root.right = mergeTrees(t1.right, t2.right);

  return root;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之平衡二叉树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/</guid><description>给定一个二叉树，判断它是否是高度平衡的二叉树。  本题中，一棵高度平衡二叉树定义为：   一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。    分解子问题： 当前节点是否是平衡节点，判断依据为：  - 左子树高度与右子树高度之差不超过1 - 左节点是平衡节点 - 右节点是平衡节点 ...</description><pubDate>Sat, 19 Jun 2021 15:08:13 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/balanced-binary-tree/&quot;&gt;110. 平衡二叉树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;给定一个二叉树，判断它是否是高度平衡的二叉树。&lt;/p&gt;
&lt;p&gt;本题中，一棵高度平衡二叉树定义为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个二叉树&lt;em&gt;每个节点&lt;/em&gt; 的左右两个子树的高度差的绝对值不超过 1 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619150948102.png&quot; alt=&quot;image-20210619150948102&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分解子问题：
当前节点是否是平衡节点，判断依据为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左子树高度与右子树高度之差不超过1&lt;/li&gt;
&lt;li&gt;左节点是平衡节点&lt;/li&gt;
&lt;li&gt;右节点是平衡节点
有了思路，代码就很简单了，四行搞定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里其中也运用了 判断二叉树的深度 的递归思想&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function (root) {
    if (!root) {
        return true;
    }
    return isBalanced(root.left) &amp;amp;&amp;amp; isBalanced(root.right) &amp;amp;&amp;amp; Math.abs(getSum(root.left) - getSum(root.right)) &amp;lt;= 1

};

function getSum(node) {
    if (!node) {
        return 0;
    }
    let dep = 1;
    dep += Math.max(getSum(node.left), getSum(node.right));
    return dep;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉树的深度</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%B7%B1%E5%BA%A6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%B7%B1%E5%BA%A6/</guid><description>（leecode 104）  输入一棵二叉树的根节点，求该树的深度。从根节点到叶节点依次经过的节点（含根、叶节点）形成树的一条路径，最长路径的长度为树的深度。  例如：  给定二叉树 [3,9,20,null,null,15,7]，      1.递归实现  思路：在存在节点的情况下，每次设置一个起...</description><pubDate>Sat, 19 Jun 2021 14:48:30 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/&quot;&gt;剑指 Offer 55 - I. 二叉树的深度&lt;/a&gt;（leecode 104）&lt;/h4&gt;
&lt;p&gt;输入一棵二叉树的根节点，求该树的深度。从根节点到叶节点依次经过的节点（含根、叶节点）形成树的一条路径，最长路径的长度为树的深度。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;给定二叉树 [3,9,20,null,null,15,7]，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619144925385.png&quot; alt=&quot;image-20210619144925385&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.递归实现&lt;/p&gt;
&lt;p&gt;思路：在存在节点的情况下，每次设置一个起始深度1，之后去遍历他的左子树和右子树找出最大层级，就像这样一个个节点去找&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
    if(!root){
        return 0
    };
    let dep = 1;
    dep += Math.max(maxDepth(root.left), maxDepth(root.right));
    return dep
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.栈迭代实现&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;通过前序遍历或者其他什么遍历，每次向栈中存储一个对象保存节点以及上面遍历过的节点深度，之后拿max值和节点深度值做Math.max比较，找出最大的深度就行，重点是每一次都需要存深度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (!root) return 0

    let max = 0
    const stack = [[root, 0]]

    while (stack.length) {
        const [node, p] = stack.pop()

        max = Math.max(max, p + 1)

        node.left &amp;amp;&amp;amp; stack.push([node.left, p + 1])
        node.right &amp;amp;&amp;amp; stack.push([node.right, p + 1])
    }

    return max
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之对称二叉树</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91/</guid><description>请实现一个函数，用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样，那么它是对称的。  例如，二叉树 [1,2,2,3,4,4,3] 是对称的。    思路：  要想实现对称的二叉树需要满足：    实现：  js /   Definition for a binary tree node...</description><pubDate>Sat, 19 Jun 2021 14:39:04 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof/&quot;&gt;剑指 Offer 28. 对称的二叉树&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;请实现一个函数，用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样，那么它是对称的。&lt;/p&gt;
&lt;p&gt;例如，二叉树 [1,2,2,3,4,4,3] 是对称的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619144112285.png&quot; alt=&quot;image-20210619144112285&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;要想实现对称的二叉树需要满足：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619144459361.png&quot; alt=&quot;image-20210619144459361&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function (root) {
    if (!root) {
        return true
    }
    return isEqual(root.left, root.right)
};

const isEqual = (left, right) =&amp;gt; {
    if (!left &amp;amp;&amp;amp; !right) {
        return true
    }
    if (!left || !right) {
        return false
    }
    if (left.val !== right.val) {
        return false;
    }
    return isEqual(left.left, right.right) &amp;amp;&amp;amp; isEqual(left.right, right.left);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之二叉树层序遍历</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/</guid><description>从上到下按层打印二叉树，同一层的节点按从左到右的顺序打印，每一层打印到一行。      思路：  稍微改变一下对队列的使用，就可以在遍历过程中体现出层次，大致过程如下：  - 初始化 queue，用于存储当前层的节点 - 检查 queue 是否为空   - 如果不为空：依次遍历当前 queue 内的...</description><pubDate>Sat, 19 Jun 2021 14:16:29 GMT</pubDate><content:encoded>&lt;h4&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/&quot;&gt;剑指 Offer 32 - II. 从上到下打印二叉树 II&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;从上到下按层打印二叉树，同一层的节点按从左到右的顺序打印，每一层打印到一行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619141848957.png&quot; alt=&quot;image-20210619141848957&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;稍微改变一下对队列的使用，就可以在遍历过程中体现出层次，大致过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化 queue，用于存储当前层的节点&lt;/li&gt;
&lt;li&gt;检查 queue 是否为空
&lt;ul&gt;
&lt;li&gt;如果不为空：依次遍历当前 queue 内的所有节点，检查每个节点的左右子节点，将不为空的子节点放入 queue，继续循环&lt;/li&gt;
&lt;li&gt;如果为空：跳出循环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if (!root) return [];
    const queue = [root];
    const res = []; // 存放遍历结果
    let level = 0; // 代表当前层数
    while (queue.length) {
        res[level] = []; // 第level层的遍历结果
        // 这里一开始我以为每层节点数量都是满的。。。然后就每层乘以2了。。。
        let levelNum = queue.length; // 第level层的节点数量
        while (levelNum--) {
            const front = queue.shift();
            res[level].push(front.val);
            if (front.left) queue.push(front.left);
            if (front.right) queue.push(front.right);
        }

        level++;
    }
    return res;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路2：递归&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var levelOrder = function (root) {
    let res = [];
    const traversal = (root, level = 0) =&amp;gt; {
        if (root) {
            if (!res[level]) res[level] = [];
            res[level].push(root.val);
            traversal(root.left, level + 1)
            traversal(root.right, level + 1)
        }
    }
    traversal(root);
    return res;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>浅拷贝和深拷贝</title><link>https://nollieleo.github.io/posts/%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D/</guid><description>什么是拷贝？  js let arr = [1, 2, 3]; let newArr = arr; newArr[0] = 100;  console.log(arr);//[100, 2, 3]     这是直接赋值的情况，不涉及任何拷贝。当改变newArr的时候，由于是同一个引用，arr指向的值...</description><pubDate>Thu, 17 Jun 2021 17:29:37 GMT</pubDate><content:encoded>&lt;h2&gt;什么是拷贝？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是直接赋值的情况，不涉及任何拷贝。当改变newArr的时候，由于是同一个引用，arr指向的值也跟着改变。&lt;/p&gt;
&lt;h2&gt;浅拷贝&lt;/h2&gt;
&lt;p&gt;上面的那个例子进行浅拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当修改newArr的时候，arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果，newArr和arr现在引用的已经不是同一块空间！！！&lt;/p&gt;
&lt;p&gt;这就是浅拷贝！&lt;/p&gt;
&lt;p&gt;但是这又会带来一个潜在的问题:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;咦!不是已经不是同一块空间的引用了吗？为什么改变了newArr改变了第二个元素的val值，arr也跟着变了。&lt;/p&gt;
&lt;p&gt;这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套，那么浅拷贝将无能为力。但幸运的是，深拷贝就是为了解决这个问题而生的，它能 解决无限极的对象嵌套问题，实现彻底的拷贝。当然，这是我们下一篇的重点。 现在先让大家有一个基本的概念。&lt;/p&gt;
&lt;p&gt;接下来，我们来研究一下JS中实现浅拷贝到底有多少种方式？&lt;/p&gt;
&lt;h3&gt;浅拷贝API&lt;/h3&gt;
&lt;h4&gt;1. Object.assign&lt;/h4&gt;
&lt;p&gt;但是需要注意的是，&lt;code&gt;Object.assgin()&lt;/code&gt; 拷贝的是对象的属性的&lt;strong&gt;引用&lt;/strong&gt;，而不是对象本身。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let obj = { name: &apos;sy&apos;, age: 18 };
const obj2 = Object.assign({}, obj, {name: &apos;sss&apos;});
console.log(obj2);//{ name: &apos;sss&apos;, age: 18 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. concat浅拷贝数组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. slice&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. ...展开运算符&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手动实现浅拷贝&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const shallowClone = (target) =&amp;gt; {
  if (typeof target === &apos;object&apos; &amp;amp;&amp;amp; target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;深拷贝&lt;/h2&gt;
&lt;h3&gt;1. JSON一套组合拳&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;JSON.parse(JSON.stringify());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;估计这个api能覆盖大多数的应用场景，没错，谈到深拷贝，我第一个想到的也是它。但是实际上，对于某些严格的场景来说，这个方法是有巨大的坑的。问题如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无法解决&lt;code&gt;循环引用&lt;/code&gt;的问题。举个例子：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const a = {val:2};
a.target = a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拷贝a会出现系统栈溢出，因为出现了&lt;code&gt;无限递归&lt;/code&gt;的情况。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无法拷贝一写&lt;code&gt;特殊的对象&lt;/code&gt;，诸如 RegExp, Date, Set, Map等&lt;/li&gt;
&lt;li&gt;无法拷贝&lt;strong&gt;函数&lt;/strong&gt;（划重点）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 简单实现&lt;/h3&gt;
&lt;p&gt;只需将上面手写的浅拷贝骚做修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function deepClone(target) {
    if (typeof target === &quot;object&quot; &amp;amp;&amp;amp; target !== null) {
      let temp = Array.isArray(target) ? [] : {};
      for (const key in target) {
        if (Object.hasOwnProperty.call(target, key)) {
          const element = target[key];
          temp[key] = deepClone(element);
        }
      }
      return temp;
    }
    return target;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 细节以及问题处理最终实现&lt;/h3&gt;
&lt;p&gt;如上所述，JSON.parse(JSON.stringfy(obj))有三个问题，我们根据这三个问题一个个解决&lt;/p&gt;
&lt;h4&gt;1.循环引用问题解决&lt;/h4&gt;
&lt;p&gt;假如是这样的情况下，a对象中的一个属性有引用了自己&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  const a = {
    name: &quot;weng&quot;,
    age: 12,
  };

  a.target = a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以使用一个Map，记录下已经拷贝过了的对象，如果已经拷贝过了，就直接返回这个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function deepClone(target, map = new Map()) {
    if (map.get(target)) {
      return target;
    }
    if (
      (typeof target === &quot;object&quot; || typeof target === &quot;function&quot;) &amp;amp;&amp;amp;
      target !== null
    ) {
      map.set(target, true);
      let temp = Array.isArray(target) ? [] : {};
      for (const key in target) {
        if (Object.hasOwnProperty.call(target, key)) {
          const element = target[key];
          temp[key] = deepClone(element, map);
        }
      }
      return temp;
    }
    return target;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了咱们试一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a = {
    name: &quot;weng&quot;,
    age: 12,
};

a.target = a;
console.log(deepClone(a));

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210619073628733.png&quot; alt=&quot;image-20210619073628733&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了&lt;code&gt;强引用关系&lt;/code&gt;，这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在计算机程序设计中，弱引用与强引用相对，&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用，则被认为是不可访问（或弱可访问）的，并因此可能在任何时刻被回收。 --百度百科&lt;/p&gt;
&lt;p&gt;说的有一点绕，我用大白话解释一下，被弱引用的对象可以在&lt;code&gt;任何时候被回收&lt;/code&gt;，而对于强引用来说，只要这个强引用还在，那么对象&lt;code&gt;无法被回收&lt;/code&gt;。拿上面的例子说，map 和 a一直是强引用的关系， 在程序结束之前，a 所占的内存空间一直&lt;code&gt;不会被释放&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;怎么解决这个问题？&lt;/p&gt;
&lt;p&gt;很简单，让 map 的 key 和 map 构成&lt;code&gt;弱引用&lt;/code&gt;即可。ES6给我们提供了这样的数据结构，它的名字叫&lt;code&gt;WeakMap&lt;/code&gt;，它是一种特殊的Map, 其中的键是&lt;code&gt;弱引用&lt;/code&gt;的。其键必须是对象，而值可以是任意的。&lt;/p&gt;
&lt;p&gt;稍微改造一下即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function deepClone(target, map = new Map()) {
    .....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 特殊对象的拷贝&lt;/h4&gt;
&lt;p&gt;对于特殊的对象，我们使用以下方式来鉴别:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object.prototype.toString.call(obj);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;梳理一下对于可遍历对象会有什么结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;object Map&quot;
&quot;object WeakMap&quot;
&quot;object Set&quot;
&quot;object WeakSet&quot;
&quot;object Array&quot;
&quot;object Object&quot;
&quot;object Arguments&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及不可便利的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;object Boolean&quot;
&quot;object Symbol&quot;
&quot;object Number&quot;
&quot;object String&quot;
&quot;object Date&quot;
&quot;object Error&quot;
&quot;object RegExp&quot;
&quot;object Function&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于不同的对象有不同的处理方案，但是很多是相互类似的&lt;/p&gt;
&lt;p&gt;之后改进之后的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  function getType(target) {
    return Object.prototype.toString.call(target);
  }

  function isObject(target) {
    return typeof target === &quot;object&quot; &amp;amp;&amp;amp; target !== null;
  }

  function deepClone(target, map = new WeakMap()) {
    if (!isObject(target)) {
      return target;
    }

    if (map.get(target)) {
      return target;
    }

    // 获取target是Object的哪种衍生类型
    const targetType = getType(target);

    let cloneTarget;

    map.set(target, true);

    const structFn = Object.getPrototypeOf(target).constructor;

    switch (targetType) {
      case &quot;[object RegExp]&quot;:
        const { source, flags } = target;
        cloneTarget = new structFn(source, flags);
        break;
      case &quot;[object Function]&quot;:
        // 函数这块独立出来处理
        break;
      case &quot;[object Map]&quot;:
      case &quot;[object WeakMap]&quot;:
        cloneTarget = new structFn();
        target.forEach((value, key) =&amp;gt; {
          cloneTarget.set(deepClone(key), deepClone(value));
        });
        break;
      case &quot;[object Set]&quot;:
      case &quot;[object WeakSet]&quot;:
        cloneTarget = new structFn();
        target.forEach((value) =&amp;gt; {
          cloneTarget.add(deepClone(value));
        });
        break;
      case &quot;[object Array]&quot;:
      case &quot;[object Object]&quot;:
        cloneTarget = new structFn();
        for (const key in target) {
          if (Object.hasOwnProperty.call(target, key)) {
            const element = target[key];
            cloneTarget[key] = deepClone(element, map);
          }
        }
        break;
      default:
        // case &quot;[object String]&quot;:
        // case &quot;[object Number]&quot;:
        // case &quot;[object Boolean]&quot;:
        // case &quot;[object Error]&quot;:
        // case &quot;[object Date]&quot;:
        cloneTarget = new structFn(Object.prototype.valueOf.call(target));
        break;
    }

    return cloneTarget;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中这行代码&lt;code&gt;    const structFn = Object.getPrototypeOf(target).constructor;&lt;/code&gt;是为了获取衍生对象或者对象的构造函数，是为了防止丢失原型的情况，之后利用构造函数去整活。&lt;/p&gt;
&lt;h4&gt;3. 处理函数类型&lt;/h4&gt;
&lt;p&gt;虽然函数也是对象，但是它过于特殊，我们单独把它拿出来拆解。&lt;/p&gt;
&lt;p&gt;提到函数，在JS种有两种函数，一种是普通函数，另一种是箭头函数。每个普通函数都是 Function的实例，而箭头函数不是任何类的实例，每次调用都是不一样的引用。那我们只需要 处理普通函数的情况，箭头函数直接返回它本身就好了。&lt;/p&gt;
&lt;p&gt;那么如何来区分两者呢？&lt;/p&gt;
&lt;p&gt;答案是: 利用原型。箭头函数是不存在原型的。&lt;/p&gt;
&lt;p&gt;处理代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const handleFunc = (func) =&amp;gt; {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?&amp;lt;={)(.|\n)+(?=})/m;
  const paramReg = /(?&amp;lt;=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(&apos;,&apos;);
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 优化代码&lt;/h4&gt;
&lt;p&gt;在上面的代码中，我们在处理一些”基本类型“对象的时候，我们去拿他们的构造函数，然后给他new出来，但是ES6中不推荐这样直接new String, 或者 new Number的写法，&lt;strong&gt;并且Symbol类型是无法被new操作符调用的&lt;/strong&gt;，所以我们改成 new Object的形式并且传他们的value值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cloneTarget = new Object(Object.prototype.valueOf.call(target));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;最终代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  function getType(target) {
    return Object.prototype.toString.call(target);
  }

  function isObject(target) {
    return typeof target === &quot;object&quot; &amp;amp;&amp;amp; target !== null;
  }

  function deepClone(target, map = new WeakMap()) {
    if (!isObject(target)) {
      return target;
    }

    if (map.get(target)) {
      return target;
    }

    // 获取target是Object的哪种衍生类型
    const targetType = getType(target);

    let cloneTarget;

    map.set(target, true);

    const structFn = Object.getPrototypeOf(target).constructor;

    switch (targetType) {
      case &quot;[object RegExp]&quot;:
        const { source, flags } = target;
        cloneTarget = new structFn(source, flags);
        break;
      case &quot;[object Function]&quot;:
        // 函数这块独立出来处理
        cloneTarget = handleFunc(target);
        break;
      case &quot;[object Map]&quot;:
      case &quot;[object WeakMap]&quot;:
        cloneTarget = new structFn();
        target.forEach((value, key) =&amp;gt; {
          cloneTarget.set(deepClone(key), deepClone(value));
        });
        break;
      case &quot;[object Set]&quot;:
      case &quot;[object WeakSet]&quot;:
        cloneTarget = new structFn();
        target.forEach((value) =&amp;gt; {
          cloneTarget.add(deepClone(value));
        });
        break;
      case &quot;[object Array]&quot;:
      case &quot;[object Object]&quot;:
        cloneTarget = new structFn();
        for (const key in target) {
          if (Object.hasOwnProperty.call(target, key)) {
            const element = target[key];
            cloneTarget[key] = deepClone(element, map);
          }
        }
        break;
      default:
        cloneTarget = new Object(Object.prototype.valueOf.call(target));
        break;
    }

    return cloneTarget;
  }

const handleFunc = (func) =&amp;gt; {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?&amp;lt;={)(.|\n)+(?=})/m;
  const paramReg = /(?&amp;lt;=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(&apos;,&apos;);
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>数组扁平化</title><link>https://nollieleo.github.io/posts/%E6%95%B0%E7%BB%84%E6%89%81%E5%B9%B3%E5%8C%96/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%95%B0%E7%BB%84%E6%89%81%E5%B9%B3%E5%8C%96/</guid><description>对于前端项目开发过程中，偶尔会出现层叠数据结构的数组，我们需要将多层级数组转化为一级数组（即提取嵌套数组元素最终合并为一个数组），使其内容合并且展开。那么该如何去实现呢？  需求:多维数组=一维数组  js let ary = [1, [2, [3, [4, 5]]], 6];// - [1, 2,...</description><pubDate>Wed, 16 Jun 2021 17:04:21 GMT</pubDate><content:encoded>&lt;p&gt;对于前端项目开发过程中，偶尔会出现层叠数据结构的数组，我们需要将多层级数组转化为一级数组（即提取嵌套数组元素最终合并为一个数组），使其内容合并且展开。那么该如何去实现呢？&lt;/p&gt;
&lt;p&gt;需求:多维数组=&amp;gt;一维数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let ary = [1, [2, [3, [4, 5]]], 6];// -&amp;gt; [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 调用ES6中的flat方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ary = ary.flat(Infinity);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. replace + split&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ary = str.replace(/(\[|\])/g, &apos;&apos;).split(&apos;,&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. replace + JSON.parse&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;str = str.replace(/(\[|\])/g, &apos;&apos;);
str = &apos;[&apos; + str + &apos;]&apos;;
ary = JSON.parse(str);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 普通递归&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let result = [];
let fn = function(ary) {
  for(let i = 0; i &amp;lt; ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 利用reduce函数迭代&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function flatten(ary) {
    return ary.reduce((pre, cur) =&amp;gt; {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6：扩展运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//只要有一个元素有数组，那么循环继续
while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>forEach中怎么跳出循环</title><link>https://nollieleo.github.io/posts/foreach%E4%B8%AD%E6%80%8E%E4%B9%88%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/foreach%E4%B8%AD%E6%80%8E%E4%B9%88%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF/</guid><description>总所周知：forEach是不可以跳出循环的，所以应该想办法给他跳出去     1. forEach中写try catch   使用try监视代码块，在需要中断的地方抛出异常。   js let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];   arr.forEach((val...</description><pubDate>Wed, 16 Jun 2021 16:41:03 GMT</pubDate><content:encoded>&lt;p&gt;总所周知：forEach是不可以跳出循环的，所以应该想办法给他跳出去&lt;/p&gt;
&lt;h2&gt;1. forEach中写try catch&lt;/h2&gt;
&lt;p&gt;使用try监视代码块，在需要中断的地方抛出异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  arr.forEach((value) =&amp;gt; {
    try {
      if (!(value % 2)) {
        throw &apos;&apos;;
      } else {
        console.log(value);
      }
    } catch (error) {
      throw error
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 替换方法（使用every或者some替换）&lt;/h2&gt;
&lt;p&gt;官方推荐方法（替换方法）：用every和some替代forEach函数。every在碰到return false的时候，中止循环。some在碰到return true的时候，中止循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    arr.some((item) =&amp;gt; {
        if (item % 2) {
            console.log(item);
            return false;
        }
    return true;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    arr.every((item) =&amp;gt; {
        if (item % 2) {
          console.log(item);
          return true;
        }
    return false;
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>垃圾回收与内存泄漏</title><link>https://nollieleo.github.io/posts/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E4%B8%8E%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E4%B8%8E%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/</guid><description>1. 浏览器的垃圾回收机制   （1）垃圾回收的概念  垃圾回收：JavaScript代码运行时，需要分配内存空间来储存变量和值。当变量不在参与运行时，就需要系统收回被占用的内存空间，这就是垃圾回收。  回收机制：  - Javascript 具有自动垃圾回收机制，会定期对那些不再使用的变量、对象所...</description><pubDate>Tue, 15 Jun 2021 16:59:12 GMT</pubDate><content:encoded>&lt;h3&gt;1. 浏览器的垃圾回收机制&lt;/h3&gt;
&lt;h4&gt;（1）垃圾回收的概念&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;垃圾回收&lt;/strong&gt;：JavaScript代码运行时，需要分配内存空间来储存变量和值。当变量不在参与运行时，就需要系统收回被占用的内存空间，这就是垃圾回收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回收机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Javascript 具有自动垃圾回收机制，会定期对那些不再使用的变量、对象所占用的内存进行释放，原理就是找到不再使用的变量，然后释放掉其占用的内存。&lt;/li&gt;
&lt;li&gt;JavaScript中存在两种变量：局部变量和全局变量。全局变量的生命周期会持续要页面卸载；而局部变量声明在函数中，它的生命周期从函数执行开始，直到函数执行结束，在这个过程中，局部变量会在堆或栈中存储它们的值，当函数执行结束后，这些局部变量不再被使用，它们所占有的空间就会被释放。&lt;/li&gt;
&lt;li&gt;不过，当局部变量被外部函数使用时，其中一种情况就是闭包，在函数执行结束后，函数外部的变量依然指向函数内部的局部变量，此时局部变量依然在被使用，所以不会回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（2）垃圾回收的方式&lt;/h4&gt;
&lt;p&gt;浏览器通常使用的垃圾回收方法有两种：标记清除，引用计数。 &lt;strong&gt;1）标记清除&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标记清除是浏览器常见的垃圾回收方式，当变量进入执行环境时，就标记这个变量“进入环境”，被标记为“进入环境”的变量是不能被回收的，因为他们正在被使用。当变量离开环境时，就会被标记为“离开环境”，被标记为“离开环境”的变量会被内存释放。&lt;/li&gt;
&lt;li&gt;垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后，它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量，原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作，销毁那些带标记的值，并回收他们所占用的内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2）引用计数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;另外一种垃圾回收机制就是引用计数，这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时，则这个值的引用次数就是1。相反，如果包含对这个值引用的变量又取得了另外一个值，则这个值的引用次数就减1。当这个引用次数变为0时，说明这个变量已经没有价值，因此，在在机回收期下次再运行时，这个变量所占有的内存空间就会被释放出来。&lt;/li&gt;
&lt;li&gt;这种方法会引起&lt;strong&gt;循环引用&lt;/strong&gt;的问题：例如：&lt;code&gt; obj1&lt;/code&gt;和&lt;code&gt;obj2&lt;/code&gt;通过属性进行相互引用，两个对象的引用次数都是2。当使用循环计数时，由于函数执行完后，两个对象都离开作用域，函数执行结束，&lt;code&gt;obj1&lt;/code&gt;和&lt;code&gt;obj2&lt;/code&gt;还将会继续存在，因此它们的引用次数永远不会是0，就会引起循环引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况下，就要手动释放变量占用的内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj1.a =  null
 obj2.a =  null
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）减少垃圾回收&lt;/h4&gt;
&lt;p&gt;虽然浏览器可以进行垃圾自动回收，但是当代码比较复杂时，垃圾回收所带来的代价比较大，所以应该尽量减少垃圾回收。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对数组进行优化：&lt;/strong&gt; 在清空一个数组时，最简单的方法就是给其赋值为[ ]，但是与此同时会创建一个新的空对象，可以将数组的长度设置为0，以此来达到清空数组的目的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对&lt;/strong&gt;&lt;code&gt;object&lt;/code&gt;&lt;strong&gt;进行优化：&lt;/strong&gt; 对象尽量复用，对于不再使用的对象，就将其设置为null，尽快被回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对函数进行优化：&lt;/strong&gt; 在循环中的函数表达式，如果可以复用，尽量放在函数的外面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 哪些情况会导致内存泄漏&lt;/h3&gt;
&lt;p&gt;以下四种情况会造成内存的泄漏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;意外的全局变量：&lt;/strong&gt; 由于使用未声明的变量，而意外的创建了一个全局变量，而使这个变量一直留在内存中无法被回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;被遗忘的计时器或回调函数：&lt;/strong&gt; 设置了 setInterval 定时器，而忘记取消它，如果循环函数有对外部变量的引用的话，那么这个变量会被一直留在内存中，而无法被回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脱离 DOM 的引用：&lt;/strong&gt; 获取一个 DOM 元素的引用，而后面这个元素被删除，由于一直保留了对这个元素的引用，所以它也无法被回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;闭包：&lt;/strong&gt; 不合理的使用闭包，从而导致某些变量一直被留在内存当中
作者：CUGGZ链接：https://juejin.cn/post/6941194115392634888来源：掘金著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Proxy代理应用场景</title><link>https://nollieleo.github.io/posts/proxy%E4%BB%A3%E7%90%86%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/proxy%E4%BB%A3%E7%90%86%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF/</guid><description>Proxy 使用场景   1 增强型数组   定义 enhancedArray 函数  javascript function enhancedArray(arr) {   return new Proxy(arr, {     get(target, property, receiver) {  ...</description><pubDate>Tue, 15 Jun 2021 16:55:27 GMT</pubDate><content:encoded>&lt;h3&gt;Proxy 使用场景&lt;/h3&gt;
&lt;h4&gt;1 增强型数组&lt;/h4&gt;
&lt;h5&gt;定义 enhancedArray 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;function enhancedArray(arr) {
  return new Proxy(arr, {
    get(target, property, receiver) {
      const range = getRange(property);
      const indices = range ? range : getIndices(property);
      const values = indices.map(function (index) {
        const key = index &amp;lt; 0 ? String(target.length + index) : index;
        return Reflect.get(target, key, receiver);
      });
      return values.length === 1 ? values[0] : values;
    },
  });

  function getRange(str) {
    var [start, end] = str.split(&quot;:&quot;).map(Number);
    if (typeof end === &quot;undefined&quot;) return false;

    let range = [];
    for (let i = start; i &amp;lt; end; i++) {
      range = range.concat(i);
    }
    return range;
  }

  function getIndices(str) {
    return str.split(&quot;,&quot;).map(Number);
  }
}
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 enhancedArray 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const arr = enhancedArray([1, 2, 3, 4, 5]);

console.log(arr[-1]); //=&amp;gt; 5
console.log(arr[[2, 4]]); //=&amp;gt; [ 3, 5 ]
console.log(arr[[2, -2, 1]]); //=&amp;gt; [ 3, 4, 2 ]
console.log(arr[&quot;2:4&quot;]); //=&amp;gt; [ 3, 4]
console.log(arr[&quot;-2:3&quot;]); //=&amp;gt; [ 4, 5, 1, 2, 3 ]
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由以上的输出结果可知，增强后的数组对象，就可以支持负数索引、分片索引等功能。除了可以增强数组之外，我们也可以使用 Proxy API 来增强普通对象。&lt;/p&gt;
&lt;h4&gt;2 增强型对象&lt;/h4&gt;
&lt;h5&gt;创建 enhancedObject 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const enhancedObject = (target) =&amp;gt;
  new Proxy(target, {
    get(target, property) {
      if (property in target) {
        return target[property];
      } else {
        return searchFor(property, target);
      }
    },
  });

let value = null;
function searchFor(property, target) {
  for (const key of Object.keys(target)) {
    if (typeof target[key] === &quot;object&quot;) {
      searchFor(property, target[key]);
    } else if (typeof target[property] !== &quot;undefined&quot;) {
      value = target[property];
      break;
    }
  }
  return value;
}
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 enhancedObject 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const data = enhancedObject({
  user: {
    name: &quot;阿宝哥&quot;,
    settings: {
      theme: &quot;dark&quot;,
    },
  },
});

console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码运行后，控制台会输出以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dark
dark
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过观察以上的输出结果可知，使用 &lt;code&gt;enhancedObject&lt;/code&gt; 函数处理过的对象，我们就可以方便地访问普通对象内部的深层属性。&lt;/p&gt;
&lt;h4&gt;3 创建只读的对象&lt;/h4&gt;
&lt;h5&gt;创建 Proxy 对象&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const man = {
  name: &quot;semlinker&quot;,
};

const handler = {
  set: &quot;Read-Only&quot;,
  defineProperty: &quot;Read-Only&quot;,
  deleteProperty: &quot;Read-Only&quot;,
  preventExtensions: &quot;Read-Only&quot;,
  setPrototypeOf: &quot;Read-Only&quot;,
};

const proxy = new Proxy(man, handler);
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 proxy 对象&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;console.log(proxy.name);
proxy.name = &quot;kakuqo&quot;;
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码运行后，控制台会输出以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;semlinker
proxy.name = &quot;kakuqo&quot;;
           ^
TypeError: &apos;Read-Only&apos; returned for property &apos;set&apos; of object &apos;#&amp;lt;Object&amp;gt;&apos; is not a function
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察以上的异常信息可知，导致异常的原因是因为 &lt;code&gt;handler&lt;/code&gt; 对象的 &lt;code&gt;set&lt;/code&gt; 属性值不是一个函数。如果不希望抛出运行时异常，我们可以定义一个 &lt;code&gt;freeze&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function freeze (obj) {
  return new Proxy(obj, {
    set () { return true; },
    deleteProperty () { return false; },
    defineProperty () { return true; },
    setPrototypeOf () { return true; }
  });
}
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义好 &lt;code&gt;freeze&lt;/code&gt; 函数，我们使用数组对象来测试一下它的功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let frozen = freeze([1, 2, 3]);
frozen[0] = 6;
delete frozen[0];
frozen = Object.defineProperty(frozen, 0, { value: 66 });
console.log(frozen); // [ 1, 2, 3 ]
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码成功执行后，控制台会输出 &lt;code&gt;[ 1, 2, 3 ]&lt;/code&gt;，很明显经过 &lt;code&gt;freeze&lt;/code&gt; 函数处理过的数组对象，已经被 “冻结” 了。&lt;/p&gt;
&lt;h4&gt;4 拦截方法调用&lt;/h4&gt;
&lt;h5&gt;定义 traceMethodCalls 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;function traceMethodCalls(obj) {
  const handler = {
    get(target, propKey, receiver) {
      const origMethod = target[propKey]; // 获取原始方法
      return function (...args) {
        const result = origMethod.apply(this, args);
        console.log(
          propKey + JSON.stringify(args) + &quot; -&amp;gt; &quot; + JSON.stringify(result)
        );
        return result;
      };
    },
  };
  return new Proxy(obj, handler);
}
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 traceMethodCalls 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
  multiply(x, y) {
    return x * y;
  },
};

const tracedObj = traceMethodCalls(obj);
tracedObj.multiply(2, 5); // multiply[2,5] -&amp;gt; 10
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码成功执行后，控制台会输出 &lt;code&gt;multiply[2,5] -&amp;gt; 10&lt;/code&gt;，即我们能够成功跟踪 &lt;code&gt;obj&lt;/code&gt; 对象中方法的调用过程。其实，除了能够跟踪方法的调用，我们也可以跟踪对象中属性的访问，具体示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function tracePropAccess(obj, propKeys) {
  const propKeySet = new Set(propKeys);
  return new Proxy(obj, {
    get(target, propKey, receiver) {
      if (propKeySet.has(propKey)) {
        console.log(&quot;GET &quot; + propKey);
      }
      return Reflect.get(target, propKey, receiver);
    },
    set(target, propKey, value, receiver) {
      if (propKeySet.has(propKey)) {
        console.log(&quot;SET &quot; + propKey + &quot;=&quot; + value);
      }
      return Reflect.set(target, propKey, value, receiver);
    },
  });
}

const man = {
  name: &quot;semlinker&quot;,
};
const tracedMan = tracePropAccess(man, [&quot;name&quot;]);

console.log(tracedMan.name); // GET name; semlinker
console.log(tracedMan.age); // undefined
tracedMan.name = &quot;kakuqo&quot;; // SET name=kakuqo
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在以上示例中，我们定义了一个 &lt;code&gt;tracePropAccess&lt;/code&gt; 函数，该函数接收两个参数：obj 和 propKeys，它们分别表示需跟踪的目标和需跟踪的属性列表。调用 &lt;code&gt;tracePropAccess&lt;/code&gt; 函数后，会返回一个代理对象，当我们访问被跟踪的属性时，控制台就会输出相应的访问日志。&lt;/p&gt;
&lt;h4&gt;5 隐藏属性&lt;/h4&gt;
&lt;h5&gt;创建 hideProperty 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const hideProperty = (target, prefix = &quot;_&quot;) =&amp;gt;
  new Proxy(target, {
    has: (obj, prop) =&amp;gt; !prop.startsWith(prefix) &amp;amp;&amp;amp; prop in obj,
    ownKeys: (obj) =&amp;gt;
      Reflect.ownKeys(obj).filter(
        (prop) =&amp;gt; typeof prop !== &quot;string&quot; || !prop.startsWith(prefix)
      ),
    get: (obj, prop, rec) =&amp;gt; (prop in rec ? obj[prop] : undefined),
  });
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 hideProperty 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const man = hideProperty({
  name: &quot;阿宝哥&quot;,
  _pwd: &quot;www.semlinker.com&quot;,
});

console.log(man._pwd); // undefined
console.log(&apos;_pwd&apos; in man); // false
console.log(Object.keys(man)); // [ &apos;name&apos; ]
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过观察以上的输出结果，我们可以知道，利用 Proxy API，我们实现了指定前缀属性的隐藏。除了能实现隐藏属性之外，利用 Proxy API，我们还可以实现验证属性值的功能。&lt;/p&gt;
&lt;h4&gt;6 验证属性值&lt;/h4&gt;
&lt;h5&gt;创建 validatedUser 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const validatedUser = (target) =&amp;gt;
  new Proxy(target, {
    set(target, property, value) {
      switch (property) {
        case &quot;email&quot;:
          const regex = /^(([^&amp;lt;&amp;gt;()\[\]\\.,;:\s@&quot;]+(\.[^&amp;lt;&amp;gt;()\[\]\\.,;:\s@&quot;]+)*)|(&quot;.+&quot;))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
          if (!regex.test(value)) {
            console.error(&quot;The user must have a valid email&quot;);
            return false;
          }
          break;
        case &quot;age&quot;:
          if (value &amp;lt; 20 || value &amp;gt; 80) {
            console.error(&quot;A user&apos;s age must be between 20 and 80&quot;);
            return false;
          }
          break;
      }

      return Reflect.set(...arguments);
    },
  });
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用 validatedUser 函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;let user = {
  email: &quot;&quot;,
  age: 0,
};

user = validatedUser(user);
user.email = &quot;semlinker.com&quot;; // The user must have a valid email
user.age = 100; // A user&apos;s age must be between 20 and 80
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码成功执行后，控制台会输出以下结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The user must have a valid email
A user&apos;s age must be between 20 and 80
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>setInterval和setTimeout优化策略</title><link>https://nollieleo.github.io/posts/setinterval%E5%92%8Csettimeout%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/setinterval%E5%92%8Csettimeout%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5/</guid><description>异步编程当然少不了定时器了，常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。最常用的是setTimeout，很多人认为 setTimeout 是延时多久，那就应该是多久后执行。  其实这个观点是错误的，因为 JS 是单线程执行的，如果前面...</description><pubDate>Tue, 15 Jun 2021 16:55:00 GMT</pubDate><content:encoded>&lt;p&gt;异步编程当然少不了定时器了，常见的定时器函数有 &lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt;、&lt;code&gt;requestAnimationFrame&lt;/code&gt;。最常用的是&lt;code&gt;setTimeout&lt;/code&gt;，很多人认为 &lt;code&gt;setTimeout&lt;/code&gt; 是延时多久，那就应该是多久后执行。&lt;/p&gt;
&lt;p&gt;其实这个观点是错误的，因为 JS 是单线程执行的，如果前面的代码影响了性能，就会导致 &lt;code&gt;setTimeout&lt;/code&gt; 不会按期执行。当然了，可以通过代码去修正 &lt;code&gt;setTimeout&lt;/code&gt;，从而使定时器相对准确：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
  count++
  // 代码执行所消耗的时间
  let offset = new Date().getTime() - (startTime + count * interval);
  let diff = end - new Date().getTime()
  let h = Math.floor(diff / (60 * 1000 * 60))
  let hdiff = diff % (60 * 1000 * 60)
  let m = Math.floor(hdiff / (60 * 1000))
  let mdiff = hdiff % (60 * 1000)
  let s = mdiff / (1000)
  let sCeil = Math.ceil(s)
  let sFloor = Math.floor(s)
  // 得到下一次循环所消耗的时间
  currentInterval = interval - offset 
  console.log(&apos;时：&apos;+h, &apos;分：&apos;+m, &apos;毫秒：&apos;+s, &apos;秒向上取整：&apos;+sCeil, &apos;代码执行时间：&apos;+offset, &apos;下次循环间隔&apos;+currentInterval) 
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来看 &lt;code&gt;setInterval&lt;/code&gt;，其实这个函数作用和 &lt;code&gt;setTimeout&lt;/code&gt; 基本一致，只是该函数是每隔一段时间执行一次回调函数。&lt;/p&gt;
&lt;p&gt;通常来说不建议使用 &lt;code&gt;setInterval&lt;/code&gt;。第一，它和 &lt;code&gt;setTimeout&lt;/code&gt; 一样，不能保证在预期的时间执行任务。第二，它存在执行累积的问题，请看以下伪代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  setInterval(function(){
    console.log(2)
  },1000)
  sleep(2000)
}
demo()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码在浏览器环境中，如果定时器执行过程中出现了耗时操作，多个回调函数会在耗时操作结束以后同时执行，这样可能就会带来性能上的问题。&lt;/p&gt;
&lt;p&gt;如果有循环定时器的需求，其实完全可以通过 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function setInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime
  const loop = () =&amp;gt; {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime &amp;gt;= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }
  timer = window.requestAnimationFrame(loop)
  return timer
}
let a = 0
setInterval(timer =&amp;gt; {
  console.log(1)
  a++
  if (a === 3) cancelAnimationFrame(timer)
}, 1000)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先 &lt;code&gt;requestAnimationFrame&lt;/code&gt; 自带函数节流功能，基本可以保证在 16.6 毫秒内只执行一次（不掉帧的情况下），并且该函数的延时效果是精确的，没有其他定时器时间不准的问题，当然你也可以通过该函数来实现 &lt;code&gt;setTimeout&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>算法之二叉树的3种遍历</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%843%E7%A7%8D%E9%81%8D%E5%8E%86/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%843%E7%A7%8D%E9%81%8D%E5%8E%86/</guid><description>一. 前序遍历（中左右）  144.二叉树前序遍历     1. 递归实现  按照中间节点先遍历，在遍历左右节点  js /   Definition for a binary tree node.   function TreeNode(val, left, right) {       this...</description><pubDate>Mon, 14 Jun 2021 16:15:16 GMT</pubDate><content:encoded>&lt;h2&gt;一. 前序遍历（中左右）&lt;/h2&gt;
&lt;p&gt;144.二叉树前序遍历&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614161741801.png&quot; alt=&quot;image-20210614161741801&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 递归实现&lt;/h3&gt;
&lt;p&gt;按照中间节点先遍历，在遍历左右节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */


var preorderTraversal = function (root) {
    let number = [];
    getNode(root, number);
    return number;
};

const getNode = (node, number) =&amp;gt; {
    if (node) {
        number.push(node.val);
        if (node.left) {
            getNode(node.left, number)
        }
        if (node.right) {
            getNode(node.right, number);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 迭代实现&lt;/h3&gt;
&lt;p&gt;以显示栈的方式模仿递归。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化栈，并将根节点入栈；&lt;/li&gt;
&lt;li&gt;当栈不为空时：
&lt;ul&gt;
&lt;li&gt;弹出栈顶元素 n，并将值添加到结果中&lt;/li&gt;
&lt;li&gt;如果 n 的右节点存在，入栈&lt;/li&gt;
&lt;li&gt;如果 n 的左节点存在，入栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */


var preorderTraversal = function (root) {
    let number = [];
    let stack = [root];
    while (stack.length) {
        const current = stack.shift();
        if(current){
            number.push(current.val);
            if(current.right){
                stack.unshift(current.right)
            }
            if(current.left){
                stack.unshift(current.left);
            }
        }
    }

    return number;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 中序遍历（左中右）&lt;/h2&gt;
&lt;h3&gt;1. 递归实现&lt;/h3&gt;
&lt;p&gt;先左节点，然后中间节点，之后右节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
    const number = [];
    getNode(root, number);
    return number
};

const getNode = (node, number) =&amp;gt; {
    if (node &amp;amp;&amp;amp; node.left) {
        getNode(node.left, number)
    }
    node &amp;amp;&amp;amp; number.push(node.val);
    if (node &amp;amp;&amp;amp; node.right) {
        getNode(node.right, number)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 迭代实现&lt;/h3&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614165726412.png&quot; alt=&quot;image-20210614165726412&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614165746953.png&quot; alt=&quot;image-20210614165746953&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614165812531.png&quot; alt=&quot;image-20210614165812531&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const inorderTraversal = (root) =&amp;gt; {
  const res = [];
  const stack = [];

  while (root) {        // 能压入栈的左子节点都压进来
    stack.push(root);
    root = root.left;
  }
  while (stack.length) {
    let node = stack.pop(); // 栈顶的节点出栈
    res.push(node.val);     // 在压入右子树之前，处理它的数值部分（因为中序遍历）
    node = node.right;      // 获取它的右子树
    while (node) {          // 右子树存在，执行while循环    
      stack.push(node);     // 压入当前root
      node = node.left;     // 不断压入左子节点
    }
  }
  return res;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码二：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
    const number = [];
    let stack = [];
    let tempNode = root;
    while(tempNode || stack.length){
        if(tempNode){
            stack.unshift(tempNode);
            tempNode = tempNode.left;
        }else{
            tempNode = stack.shift();
            number.push(tempNode.val);
            tempNode = tempNode.right
        }
    }
    return number;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三. 后序遍历&lt;/h2&gt;
&lt;h3&gt;1. 递归方式&lt;/h3&gt;
&lt;p&gt;左右中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    let number = [];
    getNode(root,number);
    return number;
};

const getNode = (node, number)=&amp;gt;{
    if(node &amp;amp;&amp;amp; node.left){
        getNode(node.left, number);
    }
    if(node &amp;amp;&amp;amp; node.right){
        getNode(node.right, number)
    }
    node &amp;amp;&amp;amp; number.push(node.val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 迭代方式&lt;/h3&gt;
&lt;p&gt;先序遍历是中左右，后续遍历是左右中，那么我们只需要调整一下先序遍历的代码顺序，就变成中右左的遍历顺序，然后在反转result数组，输出的结果顺序就是左右中了，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614173144699.png&quot; alt=&quot;image-20210614173144699&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const postorderTraversal = (root) =&amp;gt; {
    const res = [];
    const stack = [root];
    
    while(stack.length &amp;gt; 0) {
        const node = stack.pop()
        node &amp;amp;&amp;amp; res.unshift(node.val)
        if(node?.left) {
            stack.push(node.left)
        }
        if(node?.right) {
            stack.push(node.right)
        }
    }
    return res
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之三数之和</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/</guid><description>15.三数之和    思路：      js /   @param {number[]} nums   @return {number[][]}  / var threeSum = function(nums) {     let ans = [];     const len = nums.len...</description><pubDate>Mon, 14 Jun 2021 15:28:27 GMT</pubDate><content:encoded>&lt;p&gt;15.三数之和&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614161328925.png&quot; alt=&quot;image-20210614161328925&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614161237056.png&quot; alt=&quot;image-20210614161237056&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    let ans = [];
    const len = nums.length;
    if(nums == null || len &amp;lt; 3) return ans;
    nums.sort((a, b) =&amp;gt; a - b); // 排序
    for (let i = 0; i &amp;lt; len ; i++) {
        if(nums[i] &amp;gt; 0) break; // 如果当前数字大于0，则三数之和一定大于0，所以结束循环
        if(i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i-1]) continue; // 去重
        let L = i+1;
        let R = len-1;
        while(L &amp;lt; R){
            const sum = nums[i] + nums[L] + nums[R];
            if(sum == 0){
                ans.push([nums[i],nums[L],nums[R]]);
                while (L&amp;lt;R &amp;amp;&amp;amp; nums[L] == nums[L+1]) L++; // 去重
                while (L&amp;lt;R &amp;amp;&amp;amp; nums[R] == nums[R-1]) R--; // 去重
                L++;
                R--;
            }
            else if (sum &amp;lt; 0) L++;
            else if (sum &amp;gt; 0) R--;
        }
    }        
    return ans;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之赎金信</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E8%B5%8E%E9%87%91%E4%BF%A1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E8%B5%8E%E9%87%91%E4%BF%A1/</guid><description>383.赎金信  给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串，判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成，返回 true ；否则返回 false。  (题目说明：为了不暴露赎金信字迹，要从杂志上搜索各个需要...</description><pubDate>Mon, 14 Jun 2021 14:57:24 GMT</pubDate><content:encoded>&lt;p&gt;383.赎金信&lt;/p&gt;
&lt;p&gt;给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串，判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成，返回 true ；否则返回 false。&lt;/p&gt;
&lt;p&gt;(题目说明：为了不暴露赎金信字迹，要从杂志上搜索各个需要的字母，组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614151820684.png&quot; alt=&quot;image-20210614151820684&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.哈希表&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;首先我们遍历一遍magazine的值，将其中的字符当成key存入对象中，每出现一次就将其加1，之后遍历ransomNode，如果其中的值不存在就返回false，存在就将对象中key对应的值-1，如果减完之后为0，则删除这个key，直到遍历结束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function(ransomNote, magazine) {
    const obj = {};
    for(let i = 0; i&amp;lt;magazine.length;i++){
        if(magazine[i] in obj){
            obj[magazine[i]]++;
        }else{
            obj[magazine[i]]=1;
        }
    }
    for(let j= 0; j&amp;lt;ransomNote.length; j++){
        if(ransomNote[j] in obj){
            obj[ransomNote[j]]--;
            if(obj[ransomNote[j]] === 0){
                delete obj[ransomNote[j]];
            }
        }else{
            return false;
        }
    }
    return true;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.一层for循环&lt;/p&gt;
&lt;p&gt;遍历ransomNode，只要magazine中有这个值，就把magazine中的这个值给删掉，每次遍历都去更新magazine的值，如果找不到就说明不能构成，遍历结束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function (ransomNote, magazine) {
    for (let i = 0; i &amp;lt; ransomNote.length; i++) {
        if (magazine.indexOf(ransomNote[i]) === -1) {
            return false;
        }
        magazine = magazine.replace(ransomNote[i], &quot;&quot;);
    }
    return true;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之四数之和Ⅱ</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C%E2%85%B1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C%E2%85%B1/</guid><description>454.四数字相加Ⅱ  给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ，使得 A[i] + B[j] + C[k] + D[l] = 0。  为了使问题简单化，所有的 A, B, C, D 具有相同的长度 N，且 0 ≤ N ≤ 500 。所有...</description><pubDate>Mon, 14 Jun 2021 14:48:40 GMT</pubDate><content:encoded>&lt;p&gt;454.四数字相加Ⅱ&lt;/p&gt;
&lt;p&gt;给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ，使得 A[i] + B[j] + C[k] + D[l] = 0。&lt;/p&gt;
&lt;p&gt;为了使问题简单化，所有的 A, B, C, D 具有相同的长度 N，且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间，最终结果不会超过 231 - 1 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210614145011013.png&quot; alt=&quot;image-20210614145011013&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;四个数之和咱们可以给他转换成2数之和，这里因为是4个数组，两两组合，之后用Map存储和值，因为既然要&lt;/p&gt;
&lt;p&gt;使得 A[i] + B[j] + C[k] + D[l] = 0,换个思路就是使得 A[i] + B[j]  = -C[k] - D[l]，AB一组遍历存键值，CD再去遍历找到键值相等的，那就是一组，或者多组，因为AB可能有多种情况，CD也是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number[]} A
 * @param {number[]} B
 * @param {number[]} C
 * @param {number[]} D
 * @return {number}
 */
var fourSumCount = function (A, B, C, D) {
    let h = new Map()
    let r = 0
    //遍历前两个参数，在map中以键值对方式添加所有可能的值
    //key值为-（a+b），当C D数组中的c+d=-（a+b）时候，说明组合起来会是0
    //key值存在的时候就+1，不存在就set为1
    for (var a of A) {
        for (var b of B) {
            if (h.get(0 - a - b)) {
                h.set(0 - a - b, h.get(0 - a - b) + 1)
            } else {
                h.set(0 - a - b, 1)
            }
        }
    }
    //C D中的值有匹配的话就在返回结果中加上key对应的value
    for (var c of C) {
        for (var d of D) {
            if (h.has(c + d)) {
                r += h.get(c + d)
            }
        }
    }
    return r
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之两数之和</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/</guid><description>1.两数之和  给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。  你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。  你可以按任意顺序返回答案。        思路：...</description><pubDate>Sun, 13 Jun 2021 17:18:10 GMT</pubDate><content:encoded>&lt;p&gt;1.两数之和&lt;/p&gt;
&lt;p&gt;给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。&lt;/p&gt;
&lt;p&gt;你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。&lt;/p&gt;
&lt;p&gt;你可以按任意顺序返回答案。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613172218604.png&quot; alt=&quot;image-20210613172218604&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;根据题意，如果我们使用暴破，会导致时间复杂度为 n^2 ，这样的代价无疑是很大的。&lt;/p&gt;
&lt;p&gt;所以我们很容易想到用哈希表来解决这个问题。&lt;/p&gt;
&lt;p&gt;我们遍历到数字 a 时，用 target 减去 a，就会得到 b，若 b 存在于哈希表中，我们就可以直接返回结果了。若b 不存在，那么我们需要将 a 存入哈希表，好让后续遍历的数字使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613172048421.png&quot; alt=&quot;image-20210613172048421&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613172111162.png&quot; alt=&quot;image-20210613172111162&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613172123173.png&quot; alt=&quot;image-20210613172123173&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613172134297.png&quot; alt=&quot;image-20210613172134297&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let obj = {};
    let i = 0;
    while(i &amp;lt; nums.length){
        let sum = target - nums[i];
        if(sum in obj){
            return [i, obj[sum]]
        }
        obj[nums[i]] = i;
        i++;
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之快乐数</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%BF%AB%E4%B9%90%E6%95%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%BF%AB%E4%B9%90%E6%95%B0/</guid><description>202.快乐数  编写一个算法来判断一个数 n 是不是快乐数。  「快乐数」定义为：  对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。 然后重复这个过程直到这个数变为 1，也可能是 无限循环 但始终变不到 1。 如果 可以变为  1，那么这个数就是快乐数。 如果 n 是快乐数就返回 ...</description><pubDate>Sun, 13 Jun 2021 17:02:36 GMT</pubDate><content:encoded>&lt;p&gt;202.快乐数&lt;/p&gt;
&lt;p&gt;编写一个算法来判断一个数 n 是不是快乐数。&lt;/p&gt;
&lt;p&gt;「快乐数」定义为：&lt;/p&gt;
&lt;p&gt;对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1，也可能是 无限循环 但始终变不到 1。
如果 可以变为  1，那么这个数就是快乐数。
如果 n 是快乐数就返回 true ；不是，则返回 false 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613171549533.png&quot; alt=&quot;image-20210613171549533&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路:&lt;/p&gt;
&lt;p&gt;1.哈希表&lt;/p&gt;
&lt;p&gt;每次都去拆分数字计算平方和，如果和为1，就是快乐数，如果不唯一就存在Map里头。&lt;/p&gt;
&lt;p&gt;后面计算发现，如果说不是快乐数得话，计算的值可能会回到之前计算的某个值，也就是Map里头，所以如果说计算的结果，map里头存在，那么说明不是快乐树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number} n
 * @return {boolean}
 */

function sum(n) {
    n = n + &apos;&apos;
    let sum = 0
    for (let num of n) {
        sum += num * num
    }
    return sum
}
var isHappy = function (n) {
    let res = sum(n)
    let obj = {}
    while (res != 1) {
        if (res in obj) return false
        obj[res] = 1
        res = sum(res)
    }
    return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.快慢指针&lt;/p&gt;
&lt;p&gt;根据题意，我们可以分析如下：&lt;/p&gt;
&lt;p&gt;找到快乐数
没有快乐数，形成环路，造成死循环。
其实分析是很容易的，接下来我们看看，如何解题。&lt;/p&gt;
&lt;p&gt;首先，我们肯定可以使用哈希表记录过程值，若找到 11，皆大欢喜。&lt;/p&gt;
&lt;p&gt;如果在找的过程中，哈希表中已存在当前数，则证明进入了环路，也就是死循环了！&lt;/p&gt;
&lt;p&gt;此时，我们就可以判断当前数不是一个快乐数了~&lt;/p&gt;
&lt;p&gt;但是，为了降低空间复杂度，我们选择使用快慢指针来解决，流程如下：&lt;/p&gt;
&lt;p&gt;创建一个慢指针，一次走一步，再创建一个快指针，一次走两步。
当快慢指针相遇，代表形参环路，该数不是快乐数。
若指针移动过程中，找到了 11，则当前数是一个快乐数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170644838.png&quot; alt=&quot;image-20210613170644838&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170717617.png&quot; alt=&quot;image-20210613170717617&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170728576.png&quot; alt=&quot;image-20210613170728576&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170747791.png&quot; alt=&quot;image-20210613170747791&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170758347.png&quot; alt=&quot;image-20210613170758347&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170816336.png&quot; alt=&quot;image-20210613170816336&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170827279.png&quot; alt=&quot;image-20210613170827279&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613170838148.png&quot; alt=&quot;image-20210613170838148&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613171129939.png&quot; alt=&quot;image-20210613171129939&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let getNext = function (n) {
    return n.toString().split(&apos;&apos;).map(i =&amp;gt; i ** 2).reduce((a, b) =&amp;gt; a + b);
}
let isHappy = function (n) {
    let slow = n;
    let fast = getNext(n);
    while(fast !== 1 &amp;amp;&amp;amp; fast !== slow){
        slow = getNext(slow);
        fast = getNext(getNext(fast));
    }
    return fast === 1;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之链表相交</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4/</guid><description>面试题 02.07.链表相交      解题思路：  1.假设两个链表相交于A，由于两个链表都是单向链表，所以A后面的所有节点都是两个链表的公共部分。现在要找它们第一个公共节点，如果可以从后往前找，则找到两个链表第一个不相同的节点，其后的节点即为所求。但链表的特点导致其更适合从前向后遍历，如果要从后...</description><pubDate>Sun, 13 Jun 2021 16:48:30 GMT</pubDate><content:encoded>&lt;p&gt;面试题 02.07.链表相交&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613164949653.png&quot; alt=&quot;image-20210613164949653&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解题思路：&lt;/p&gt;
&lt;p&gt;1.假设两个链表相交于A，由于两个链表都是单向链表，所以A后面的所有节点都是两个链表的公共部分。现在要找它们第一个公共节点，如果可以从后往前找，则找到两个链表第一个不相同的节点，其后的节点即为所求。但链表的特点导致其更适合从前向后遍历，如果要从后往前一个个比较，则每次都要从头扫描找到合适位置的节点再进行比较（最多扫描N趟，平均扫描N/2趟，其中N为链表长度），这样时间复杂度会很高，是平方级的时间复杂度。
要想从后往前一个个比较，又不需要每次从头扫描，可以考虑把两个链表的节点分别依次放入两个栈中，这样栈顶的元素便是其最后的节点，逐个出栈并进行比较，即可得到结果。不过这样做需要引入两个栈，而栈的空间复杂度为线性级。这相当于用空间换时间。
最后，再仔细分析一下为什么不能从头开始扫描两个链表并用于比较，这是因为两个链表的长度不一样，如果两个链表的长度一样，则由于其公共节点个数一样，所以不相同节点数目也一样，这样完全可以从头扫描并进行比较。但实际给定的两个链表并不一定长度相同，这时可以先让较长的链表走上k(k为长链表与短链表的差值)步，在依次比较两个链表的节点，这样便可以不使用复杂的数据结构，从而保证常量级的空间复杂度。同时，由于用这种方法只需扫描两个链表最多两趟，因此时间复杂度为线性级。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function (headA, headB) {
    let headALen = 0;
    let p = headA;
    while (p) {
        headALen++;
        p = p.next;
    }
    let headBLen = 0;
    let q = headB;
    while (q) {
        headBLen++;
        q = q.next;
    }
    if (headALen &amp;lt; headBLen) {
        p = headA;
        headA = headB;
        headB = p;
        [headALen, headBLen] = [headBLen, headALen];
    }
    while (headALen - headBLen) {
        headA = headA.next;
        headALen--;
    }
    while (headA &amp;amp;&amp;amp; headB) {
        if (headA === headB) {
            return headA;
        }
        headA = headA.next;
        headB = headB.next;
    }
    return null;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.两个指针最多走过headA链表长度 + headB链表长度的距离&lt;/p&gt;
&lt;p&gt;如果相交，会提前相遇在相交节点。此时返回相交节点。
如果不相交，则各自走过headA链表长度 + headB链表长度的距离，指向null。此时返回null.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    var p1 = headA, p2 = headB;
    while (p1 != p2) {
        p1 = p1 ? p1.next : headB;
        p2 = p2 ? p2.next : headA;
    }
    return p1;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之只出现一次的数字</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97/</guid><description>136.给定一个非空整数数组，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。  你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗？      解法：  1.哈希表  简单不接受，需要开辟新空间  2.逻辑运算符  js /   @param {numb...</description><pubDate>Sun, 13 Jun 2021 16:29:03 GMT</pubDate><content:encoded>&lt;p&gt;136.给定一个&lt;strong&gt;非空&lt;/strong&gt;整数数组，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。&lt;/p&gt;
&lt;p&gt;你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613163443078.png&quot; alt=&quot;image-20210613163443078&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;1.哈希表&lt;/p&gt;
&lt;p&gt;简单不接受，需要开辟新空间&lt;/p&gt;
&lt;p&gt;2.逻辑运算符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function (nums) {
    for (let i = 0, l = nums.length; i &amp;lt; l; i++) {
        nums[i + 1] = nums[i] ^ nums[i + 1];
    }
    return nums[nums.length - 1];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;逻辑 或非 运算符的特点就是，某个数或非本事就是 0，0或非任意数都是任意数&lt;/p&gt;
&lt;p&gt;再不开辟新空间的情况下，根据数组特点我们知道，除了那个一次出现的数据，其他都是成双的&lt;/p&gt;
&lt;p&gt;那我们把相邻两数 或非 运算的结果赋值给下一个，不断的或非下去，最终得到的最后一位数，就是出现一次的数&lt;/p&gt;
</content:encoded></item><item><title>算法之环形链表2</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A82/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A82/</guid><description>142.给定一个链表，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。  说明：不允许修改给定的链表。  进阶：  - 你是否可以使用 O(1) 空间解决此题？    解法：  1.哈希表  js var detectCycle = function(head) {   //建立一个...</description><pubDate>Sun, 13 Jun 2021 16:08:49 GMT</pubDate><content:encoded>&lt;p&gt;142.给定一个链表，返回链表开始入环的第一个节点。 如果链表无环，则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;**说明：**不允许修改给定的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进阶：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你是否可以使用 &lt;code&gt;O(1)&lt;/code&gt; 空间解决此题？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613161001882.png&quot; alt=&quot;image-20210613161001882&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解法：&lt;/p&gt;
&lt;p&gt;1.哈希表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var detectCycle = function(head) {
  //建立一个Set，如果有重复的，就说明是环。
  const memo = new Set()

  while(head){
    if(memo.has(head)){
      return head
    }else{
      memo.add(head)
    }
    head = head.next
  }

  return null;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.快慢指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var detectCycle = function(head) {
  let fast = head, slow = head;

  do{
    //检查是否有环 + 寻找相遇点 encounter point
    if(!fast || !fast.next) return null;
    fast = fast.next.next
    slow = slow.next
  }while(fast != slow)


  fast = head;

  //寻找入口 entrance
  //为什么这能寻找入口，请看下图
  while(fast != slow){
    fast = fast.next
    slow = slow.next
  }

  return fast;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复杂度分析
时间复杂度：O(N)
空间复杂度：O(1)
有一个快指针fast，慢指针slow；快指针一次走两步，慢指针一次走一步；在有环的情况下，他们俩会在encounter相遇。相遇的时候有(Nfast - 2Nslow) * L = Path + E
通过Lucifer的例子，我们得知(Nfast - 2Nslow) 只要是个整数，就不会影响encounter在环上的位置，所以我们能推导出:
Path = L - E (就是那俩蓝色的地方)
这时候fast再从head开始走，slow从encounter走，他们俩每个都走一步，遇见的时候就刚好在环的入口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1621542134-cQRGVb-LC142Floyd&apos;sCircleFinding.jpg&quot; alt=&quot;LC142Floyd&apos;sCircleFinding.jpg&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>算法之环形链表1</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A81/</guid><description>141.给定一个链表，判断链表中是否有环。  如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，我们使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。 如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行...</description><pubDate>Sun, 13 Jun 2021 15:58:09 GMT</pubDate><content:encoded>&lt;p&gt;141.给定一个链表，判断链表中是否有环。&lt;/p&gt;
&lt;p&gt;如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，我们使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。 如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行传递，仅仅是为了标识链表的实际情况。&lt;/p&gt;
&lt;p&gt;如果链表中存在环，则返回 &lt;code&gt;true&lt;/code&gt; 。 否则，返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;你能用 &lt;em&gt;O(1)&lt;/em&gt;（即，常量）内存解决此问题吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613160243446.png&quot; alt=&quot;image-20210613160243446&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解题思路：&lt;/p&gt;
&lt;p&gt;1.快慢指针&lt;/p&gt;
&lt;p&gt;两个人在圆形操场上的起点同时起跑，速度快的人一定会超过速度慢的人一圈。&lt;/p&gt;
&lt;p&gt;用一块一慢两个指针遍历链表，如果指针能够相逢，那么链表就有圈。&lt;/p&gt;
&lt;p&gt;用一块一慢两个指针遍历链表，如果指针能够相逢，就返回true&lt;/p&gt;
&lt;p&gt;遍历结束后，还没有相逢就返回false&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var hasCycle = function(head) {
    let p1 = head;
    let p2 = head;
    while(p1 &amp;amp;&amp;amp; p2 &amp;amp;&amp;amp; p2.next) {
        p1 = p1.next
        p2 = p2.next.next
        if(p1 === p2) {
            return true;
        }
    }
    return false;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.标记法&lt;/p&gt;
&lt;p&gt;标记法
给遍历过的节点打记号，如果遍历过程中遇到有记号的说明已环🤓&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const hasCycle = function(head) {
  while (head) {
    if (head.tag) {
      return true;
    }
    head.tag = true;
    head = head.next;
  }
  return false;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之删除链表的倒数第N个节点</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/</guid><description>19.给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。  进阶：你能尝试使用一趟扫描实现吗？    思路：  双指针的经典应用，如果要删除倒数第n个节点，让fast移动n步，然后让fast和slow同时移动，直到fast指向链表末尾。删掉slow所指向的节点就可以了。  - 定义f...</description><pubDate>Sun, 13 Jun 2021 15:35:20 GMT</pubDate><content:encoded>&lt;p&gt;19.给你一个链表，删除链表的倒数第 &lt;code&gt;n&lt;/code&gt; 个结点，并且返回链表的头结点。&lt;/p&gt;
&lt;p&gt;**进阶：**你能尝试使用一趟扫描实现吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153633327.png&quot; alt=&quot;image-20210613153633327&quot; /&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;双指针的经典应用，如果要删除倒数第n个节点，让fast移动n步，然后让fast和slow同时移动，直到fast指向链表末尾。删掉slow所指向的节点就可以了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义fast指针和slow指针，初始值为虚拟头结点，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613155602080.png&quot; alt=&quot;image-20210613155602080&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fast首先走n + 1步 ，为什么是n+1呢，因为只有这样同时移动的时候slow才能指向删除节点的上一个节点（方便做删除操作），如图：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613155614868.png&quot; alt=&quot;image-20210613155614868&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fast和slow同时移动，之道fast指向末尾，如题：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613155634908.png&quot; alt=&quot;image-20210613155634908&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除slow指向的下一个节点，如图：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613155643046.png&quot; alt=&quot;image-20210613155643046&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function (head, n) {
    let preNode = new ListNode();
    preNode.next = head;
    let fast = preNode;
    let slow = preNode;
    while (n--) {
        fast = fast.next;
    }
    while (fast.next) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return preNode.next;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之最小栈</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%80%E5%B0%8F%E6%A0%88/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%9C%80%E5%B0%8F%E6%A0%88/</guid><description>155. 设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。  push(x) —— 将元素 x 推入栈中。 pop() —— 删除栈顶的元素。 top() —— 获取栈顶元素。 getMin() —— 检索栈中的最小元素。   示例:  输入： [&quot;MinSta...</description><pubDate>Sun, 13 Jun 2021 15:24:44 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;示例:

输入：
[&quot;MinStack&quot;,&quot;push&quot;,&quot;push&quot;,&quot;push&quot;,&quot;getMin&quot;,&quot;pop&quot;,&quot;top&quot;,&quot;getMin&quot;]
[[],[-2],[0],[-3],[],[],[],[]]

输出：
[null,null,null,null,-3,null,0,-2]

解释：
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --&amp;gt; 返回 -3.
minStack.pop();
minStack.top();      --&amp;gt; 返回 0.
minStack.getMin();   --&amp;gt; 返回 -2.。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图解：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153021619.png&quot; alt=&quot;image-20210613153021619&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153039502.png&quot; alt=&quot;image-20210613153039502&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153308874.png&quot; alt=&quot;image-20210613153308874&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153321255.png&quot; alt=&quot;image-20210613153321255&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153341392.png&quot; alt=&quot;image-20210613153341392&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153354301.png&quot; alt=&quot;image-20210613153354301&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153406440.png&quot; alt=&quot;image-20210613153406440&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613153418555.png&quot; alt=&quot;image-20210613153418555&quot; /&gt;&lt;/p&gt;
&lt;p&gt;题解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * initialize your data structure here.
 */
var MinStack = function() {
    this.x_stack = [];
    this.min_stack = [Infinity];
};

MinStack.prototype.push = function(x) {
    this.x_stack.push(x);
    this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], x));
};

MinStack.prototype.pop = function() {
    this.x_stack.pop();
    this.min_stack.pop();
};

MinStack.prototype.top = function() {
    return this.x_stack[this.x_stack.length - 1];
};

MinStack.prototype.getMin = function() {
    return this.min_stack[this.min_stack.length - 1];
};

/**
 * Your MinStack object will be instantiated and called as such:
 * var obj = new MinStack()
 * obj.push(val)
 * obj.pop()
 * var param_3 = obj.top()
 * var param_4 = obj.getMin()
 */
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之两两交换链表中节点</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E8%8A%82%E7%82%B9/</guid><description>1.  给定一个链表，两两交换其中相邻的节点，并返回交换后的链表。  你不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。    题解：  js /   Definition for singly-linked list.   function ListNode(val, next) { ...</description><pubDate>Sun, 13 Jun 2021 15:20:43 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;给定一个链表，两两交换其中相邻的节点，并返回交换后的链表。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;你不能只是单纯的改变节点内部的值&lt;/strong&gt;，而是需要实际的进行节点交换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613152137809.png&quot; alt=&quot;image-20210613152137809&quot; /&gt;&lt;/p&gt;
&lt;p&gt;题解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    if(!head || !head.next){
        return head
    };
    let header = new ListNode();
    header.next = head
    let vnode = header;
    while(head &amp;amp;&amp;amp; head.next){
        const temp = head.next;
        head.next = head.next.next;
        temp.next = head;
        vnode.next = temp;
        vnode = head;
        head = head.next;
    }
    return header.next;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法之反转链表</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/</guid><description>206.给你单链表的头节点 head,请你反转链表，并返回反转后的链表    迭代解法：  js if(!head){         return head;     }     const virsualNode = new ListNode();     virsualNode.next = ...</description><pubDate>Sun, 13 Jun 2021 15:00:44 GMT</pubDate><content:encoded>&lt;p&gt;206.给你单链表的头节点 &lt;code&gt;head&lt;/code&gt;,请你反转链表，并返回反转后的链表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210613150413850.png&quot; alt=&quot;image-20210613150413850&quot; /&gt;&lt;/p&gt;
&lt;p&gt;迭代解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(!head){
        return head;
    }
    const virsualNode = new ListNode();
    virsualNode.next = head;
    while(head.next){
        let temp = head.next;
        head.next = head.next.next;
        temp.next = virsualNode.next;
        virsualNode.next = temp;
    }
    return virsualNode.next
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写Promise</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99promise/</guid><description>暂未整理，先附上代码  js class _Promise { constructor(excutor) {   this.state = &quot;pending&quot;;    this.resolveInfo = undefined;   this.rejectInfo = undefined;    th...</description><pubDate>Sun, 13 Jun 2021 13:20:58 GMT</pubDate><content:encoded>&lt;p&gt;暂未整理，先附上代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class _Promise {
constructor(excutor) {
  this.state = &quot;pending&quot;;

  this.resolveInfo = undefined;
  this.rejectInfo = undefined;

  this.resolveCb = [];

  this.rejectCb = [];

  const resolve = (value) =&amp;gt; {
    if (this.state === &quot;pending&quot;) {
      this.state = &quot;fullfilled&quot;;
      this.resolveInfo = value;
      this.resolveCb.forEach((fn) =&amp;gt; {
        fn(this.resolveInfo);
      });
    }
  };
  const reject = (value) =&amp;gt; {
    if (this.state === &quot;pending&quot;) {
      this.state = &quot;rejected&quot;;
      this.rejectInfo = value;
      this.rejectCb.forEach((fn) =&amp;gt; {
        fn(this.rejectInfo);
      });
    }
  };
  try {
    excutor(resolve, reject);
  } catch (error) {
    reject(error);
  }
}
then(onFullfilled, onRejected) {
  const innerPromise = new _Promise((resolve, reject) =&amp;gt; {
    if (this.state === &quot;fullfilled&quot;) {
      setTimeout(() =&amp;gt; {
        try {
          const returnValue =
            typeof onFullfilled === &quot;function&quot;
              ? onFullfilled(this.resolveInfo)
              : (value) =&amp;gt; value;
          _Promise.resolvePromise.apply(
            innerPromise,
            returnValue,
            resolve,
            reject
          );
        } catch (error) {
          reject(error);
        }
      }, 0);
    }
    if (this.state === &quot;rejected&quot;) {
      setTimeout(() =&amp;gt; {
        try {
          const returnValue =
            typeof onRejected === &quot;function&quot;
              ? onRejected(this.rejectInfo)
              : (value) =&amp;gt; value;
          _Promise.resolvePromise.apply(
            innerPromise,
            returnValue,
            resolve,
            reject
          );
        } catch (error) {
          reject(error);
        }
      }, 0);
    }
    if (this.state === &quot;pending&quot;) {
      this.resolveCb.push(() =&amp;gt; {
        setTimeout(() =&amp;gt; {
          try {
            const returnValue =
              typeof onFullfilled === &quot;function&quot;
                ? onFullfilled(this.resolveInfo)
                : (value) =&amp;gt; value;
            _Promise.resolvePromise.apply(
              innerPromise,
              returnValue,
              resolve,
              reject
            );
          } catch (error) {
            reject(error);
          }
        }, 0);
      });
      this.rejectCb.push(() =&amp;gt; {
        setTimeout(() =&amp;gt; {
          try {
            const returnValue =
              typeof onRejected === &quot;function&quot;
                ? onRejected(this.rejectInfo)
                : (value) =&amp;gt; value;
            _Promise.resolvePromise.apply(
              innerPromise,
              returnValue,
              resolve,
              reject
            );
          } catch (error) {
            reject(error);
          }
        }, 0);
      });
    }
  });
  return innerPromise;
}

static resolvePromise(newPromise, returnValue, resolve, reject) {
  if (newPromise === returnValue) {
    return reject(new Error(&quot;不能重复引用&quot;));
  }
  // 防止多次调用
  let called;

  if (
    returnValue instanceof _Promise &amp;amp;&amp;amp;
    typeof returnValue.then === &quot;function&quot;
  ) {
    returnValue.then(
      (innerResolveMsg) =&amp;gt; {
        if (called) return;
        called = true;
        resolvePromise(newPromise, innerResolveMsg, resolve, reject);
      },
      (innerRejectedMsg) =&amp;gt; {
        if (called) return;
        called = true;
        resolvePromise(newPromise, innerRejectedMsg, resolve, reject);
      }
    );
  } else {
    resolve(returnValue);
  }
}
}



&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前只实现了then方法，后续还有resolve,reject,catch, all, race, finally&lt;/p&gt;
</content:encoded></item><item><title>宏任务和微任务</title><link>https://nollieleo.github.io/posts/%E5%AE%8F%E4%BB%BB%E5%8A%A1%E5%92%8C%E5%BE%AE%E4%BB%BB%E5%8A%A1/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%AE%8F%E4%BB%BB%E5%8A%A1%E5%92%8C%E5%BE%AE%E4%BB%BB%E5%8A%A1/</guid><description>宏任务(macrotask)  在ECMAScript中，macrotask也被称为task  我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)， 每一个宏任务会从头到尾执行完毕，不会执行其他  由于JS引擎线程和GUI渲染线程是互斥的关系，浏...</description><pubDate>Thu, 10 Jun 2021 16:28:30 GMT</pubDate><content:encoded>&lt;h3&gt;宏任务(macrotask)&lt;/h3&gt;
&lt;p&gt;在ECMAScript中，&lt;code&gt;macrotask&lt;/code&gt;也被称为&lt;code&gt;task&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)， 每一个宏任务会从头到尾执行完毕，不会执行其他&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;JS引擎线程&lt;/code&gt;和&lt;code&gt;GUI渲染线程&lt;/code&gt;是互斥的关系，浏览器为了能够使&lt;code&gt;宏任务&lt;/code&gt;和&lt;code&gt;DOM任务&lt;/code&gt;有序的进行，会在一个&lt;code&gt;宏任务&lt;/code&gt;执行结果后，在下一个&lt;code&gt;宏任务&lt;/code&gt;执行前，&lt;code&gt;GUI渲染线程&lt;/code&gt;开始工作，对页面进行渲染&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;宏任务 -&amp;gt; GUI渲染 -&amp;gt; 宏任务 -&amp;gt; ...
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见的宏任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主代码块&lt;/li&gt;
&lt;li&gt;setTimeout&lt;/li&gt;
&lt;li&gt;setInterval&lt;/li&gt;
&lt;li&gt;setImmediate ()-Node&lt;/li&gt;
&lt;li&gt;requestAnimationFrame ()-浏览器&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;微任务(microtask)&lt;/h3&gt;
&lt;p&gt;ES6新引入了Promise标准，同时浏览器实现上多了一个&lt;code&gt;microtask&lt;/code&gt;微任务概念，在ECMAScript中，&lt;code&gt;microtask&lt;/code&gt;也被称为&lt;code&gt;jobs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们已经知道&lt;code&gt;宏任务&lt;/code&gt;结束后，会执行渲染，然后执行下一个&lt;code&gt;宏任务&lt;/code&gt;， 而微任务可以理解成在当前&lt;code&gt;宏任务&lt;/code&gt;执行后立即执行的任务&lt;/p&gt;
&lt;p&gt;当一个&lt;code&gt;宏任务&lt;/code&gt;执行完，会在渲染前，将执行期间所产生的所有&lt;code&gt;微任务&lt;/code&gt;都执行完&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;宏任务 -&amp;gt; 微任务 -&amp;gt; GUI渲染 -&amp;gt; 宏任务 -&amp;gt; ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见微任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;process.nextTick ()-Node&lt;/li&gt;
&lt;li&gt;Promise.then()&lt;/li&gt;
&lt;li&gt;catch&lt;/li&gt;
&lt;li&gt;finally&lt;/li&gt;
&lt;li&gt;Object.observe&lt;/li&gt;
&lt;li&gt;MutationObserver&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;简单区分宏任务与微任务&lt;/h3&gt;
&lt;p&gt;看了上述宏任务微任务的解释你可能还不太清楚，没关系，往下看，先记住那些常见的宏微任务即可&lt;/p&gt;
&lt;p&gt;我们通过几个例子来看，这几个例子思路来自掘金&lt;code&gt;云中君&lt;/code&gt;的文章参考链接【14】，通过渲染背景颜色来区分宏任务和微任务，很直观，我觉得很有意思，所以这里也用这种例子&lt;/p&gt;
&lt;p&gt;找一个空白的页面，在console中输入以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.body.style = &apos;background:black&apos;;
document.body.style = &apos;background:red&apos;;
document.body.style = &apos;background:blue&apos;;
document.body.style = &apos;background:pink&apos;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1.gif&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到上面动图背景直接渲染了粉红色，根据上文里讲浏览器会先执行完一个宏任务，再执行当前执行栈的所有微任务，然后移交GUI渲染，上面四行代码均属于同一次宏任务，全部执行完才会执行渲染，渲染时&lt;code&gt;GUI线程&lt;/code&gt;会将所有UI改动优化合并，所以视觉上，只会看到页面变成粉红色&lt;/p&gt;
&lt;p&gt;再接着看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.body.style = &apos;background:blue&apos;;
setTimeout(()=&amp;gt;{
    document.body.style = &apos;background:black&apos;
},200)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./2.gif&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上述代码中，页面会先卡一下蓝色，再变成黑色背景，页面上写的是200毫秒，大家可以把它当成0毫秒，因为0毫秒的话由于浏览器渲染太快，录屏不好捕捉，我又没啥录屏慢放的工具，大家可以自行测试的，结果也是一样，最安全的方法是写一个&lt;code&gt;index.html&lt;/code&gt;文件，在这个文件中插入上面的js脚本，然后浏览器打开，谷歌下使用控制台中&lt;code&gt;performance&lt;/code&gt;功能查看一帧一帧的加载最为恰当，不过这样录屏不好录所以。。。&lt;/p&gt;
&lt;p&gt;回归正题，之所以会卡一下蓝色，是因为以上代码属于两次&lt;code&gt;宏任务&lt;/code&gt;，第一次&lt;code&gt;宏任务&lt;/code&gt;执行的代码是将背景变成蓝色，然后触发渲染，将页面变成蓝色，再触发第二次宏任务将背景变成黑色&lt;/p&gt;
&lt;p&gt;再来看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.body.style = &apos;background:blue&apos;
console.log(1);
Promise.resolve().then(()=&amp;gt;{
    console.log(2);
    document.body.style = &apos;background:pink&apos;
});
console.log(3);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./3.gif&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行，所以 2 最后输出&lt;/p&gt;
&lt;p&gt;页面的背景色直接变成粉色，没有经过蓝色的阶段，是因为，我们在宏任务中将背景设置为蓝色，但在进行渲染前执行了微任务， 在微任务中将背景变成了粉色，然后才执行的渲染&lt;/p&gt;
&lt;h3&gt;微任务宏任务注意点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;浏览器会先执行一个宏任务，紧接着执行当前执行栈产生的微任务，再进行渲染，然后再执行下一个宏任务&lt;/li&gt;
&lt;li&gt;微任务和宏任务不在一个任务队列，不在一个任务队列
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;setTimeout&lt;/code&gt;是一个宏任务，它的事件回调在宏任务队列，&lt;code&gt;Promise.then()&lt;/code&gt;是一个微任务，它的事件回调在微任务队列，二者并不是一个任务队列&lt;/li&gt;
&lt;li&gt;以Chrome 为例，有关渲染的都是在渲染进程中执行，渲染进程中的任务（DOM树构建，js解析…等等）需要主线程执行的任务都会在主线程中执行，而浏览器维护了一套事件循环机制，主线程上的任务都会放到消息队列中执行，主线程会循环消息队列，并从头部取出任务进行执行，如果执行过程中产生其他任务需要主线程执行的，渲染进程中的其他线程会把该任务塞入到消息队列的尾部，消息队列中的任务都是宏任务&lt;/li&gt;
&lt;li&gt;微任务是如何产生的呢？当执行到script脚本的时候，js引擎会为全局创建一个执行上下文，在该执行上下文中维护了一个微任务队列，当遇到微任务，就会把微任务回调放在微队列中，当所有的js代码执行完毕，在退出全局上下文之前引擎会去检查该队列，有回调就执行，没有就退出执行上下文，这也就是为什么微任务要早于宏任务，也是大家常说的，每个宏任务都有一个微任务队列（由于定时器是浏览器的API，所以定时器是宏任务，在js中遇到定时器会也是放入到浏览器的队列中）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图解宏任务和微任务&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先执行一个宏任务，执行结束后判断是否存在微任务&lt;/p&gt;
&lt;p&gt;有微任务先执行所有的微任务，再渲染，没有微任务则直接渲染&lt;/p&gt;
&lt;p&gt;然后再接着执行下一个宏任务&lt;/p&gt;
&lt;h3&gt;完整的Event loop&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先，整体的script(作为第一个宏任务)开始执行的时候，会把所有代码分为&lt;code&gt;同步任务&lt;/code&gt;、&lt;code&gt;异步任务&lt;/code&gt;两部分&lt;/p&gt;
&lt;p&gt;同步任务会直接进入主线程依次执行&lt;/p&gt;
&lt;p&gt;异步任务会再分为宏任务和微任务&lt;/p&gt;
&lt;p&gt;宏任务进入到Event Table中，并在里面注册回调函数，每当指定的事件完成时，Event Table会将这个函数移到Event Queue中&lt;/p&gt;
&lt;p&gt;微任务也会进入到另一个Event Table中，并在里面注册回调函数，每当指定的事件完成时，Event Table会将这个函数移到Event Queue中&lt;/p&gt;
&lt;p&gt;当主线程内的任务执行完毕，主线程为空时，会检查微任务的Event Queue，如果有任务，就全部执行，如果没有就执行下一个宏任务&lt;/p&gt;
&lt;p&gt;上述过程会不断重复，这就是Event Loop，比较完整的事件循环&lt;/p&gt;
</content:encoded></item><item><title>Event Loop轮询处理线程</title><link>https://nollieleo.github.io/posts/event-loop%E8%BD%AE%E8%AF%A2%E5%A4%84%E7%90%86%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/event-loop%E8%BD%AE%E8%AF%A2%E5%A4%84%E7%90%86%E7%BA%BF%E7%A8%8B/</guid><description>首先要明白，   1. js代码里头 有同步的任务，是由JS引擎线程处理，也就是主线程，同步任务都在主线程(这里的主线程就是JS引擎线程)上执行 ，形成一个执行栈  2. js代码里头 有异步的任务，各种各样的异步任务，各种各样的异步任务在各个对应的线程中进行处理，比如settimeout异步任务就...</description><pubDate>Thu, 10 Jun 2021 16:12:17 GMT</pubDate><content:encoded>&lt;p&gt;首先要明白，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;js代码里头 有同步的任务，是由JS引擎线程处理，也就是主线程，同步任务都在主线程(这里的主线程就是JS引擎线程)上执行 ，形成一个&lt;code&gt;执行栈&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;js代码里头 有异步的任务，各种各样的异步任务，各种各样的异步任务在各个对应的线程中进行处理，比如settimeout异步任务就在定时触发器线程中处理，ajax，fetch这些的js库的本质的请求代码是由异步http请求线程处理的。&lt;/li&gt;
&lt;li&gt;主线程，之外还有一个 &lt;strong&gt;任务队列&lt;/strong&gt;，这个任务队列归 &lt;strong&gt;事件触发线程&lt;/strong&gt; 管， 只要异步任务有了运行结果 ，就在这个任务队列中加一个它的事件回调。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一旦&lt;code&gt;执行栈&lt;/code&gt;中的所有同步任务执行完毕(也就是JS引擎线程空闲了)，系统就会读取&lt;code&gt;任务队列&lt;/code&gt;，将可运行的异步任务(任务队列中的事件回调，只要任务队列中有事件回调，就说明可以执行)添加到执行栈中，开始执行&lt;/p&gt;
&lt;p&gt;这个过程可以简单概括为，上面这3个东西之间的交流，他们交流的中介，就是 &lt;strong&gt;EventLoop轮询处理线程&lt;/strong&gt; ， 既然叫轮询了，那么肯定是不断的循环的去交流和沟通&lt;/p&gt;
&lt;p&gt;看一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let setTimeoutCallBack = function() {
  console.log(&apos;我是定时器回调&apos;);
};
let httpCallback = function() {
  console.log(&apos;我是http请求回调&apos;);
}

// 同步任务
console.log(&apos;我是同步任务1&apos;);

// 异步定时任务
setTimeout(setTimeoutCallBack,1000);

// 异步http请求任务
ajax.get(&apos;/info&apos;,httpCallback);

// 同步任务
console.log(&apos;我是同步任务2&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JS是按照顺序从上往下依次执行的，可以先理解为这段代码时的执行环境就是主线程，也就是也就是当前&lt;strong&gt;执行栈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以下是顺序&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行&lt;code&gt;console.log(&apos;我是同步任务1&apos;)&lt;/code&gt; ，主线程中运行&lt;/li&gt;
&lt;li&gt;接着，执行到&lt;code&gt;setTimeout&lt;/code&gt;时会&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;移交给&lt;code&gt;定时触发器线程&lt;/code&gt; ，通知&lt;code&gt;定时触发器线程&lt;/code&gt; 1s 后将 &lt;code&gt;setTimeoutCallBack&lt;/code&gt; 这个回调交给&lt;code&gt;事件触发线程&lt;/code&gt;处理&lt;/li&gt;
&lt;li&gt;在 1s 后&lt;code&gt;事件触发线程&lt;/code&gt;会收到 &lt;code&gt;setTimeoutCallBack&lt;/code&gt; 这个回调并把它加入到&lt;code&gt;事件触发线程&lt;/code&gt;所管理的事件队列中等待执行&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;执行http请求&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;移交给&lt;code&gt;异步http请求线程&lt;/code&gt;发送网络请求&lt;/li&gt;
&lt;li&gt;请求成功后将 &lt;code&gt;httpCallback&lt;/code&gt; 这个回调交由事件触发线程处理，&lt;code&gt;事件触发线程&lt;/code&gt;收到 &lt;code&gt;httpCallback&lt;/code&gt; 这个回调后把它加入到&lt;code&gt;事件触发线程&lt;/code&gt;所管理的事件队列中等待执行&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;执行&lt;code&gt;console.log(&apos;我是同步任务2&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;主线程执行栈中执行完毕 ，JS引擎已经空闲了， 向&lt;code&gt;事件触发线程&lt;/code&gt;发起询问，询问&lt;code&gt;事件触发线程&lt;/code&gt;的事件队列中是否有需要执行的回调函数&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果有将事件队列中的回调事件加入执行栈中，开始执行回调&lt;/li&gt;
&lt;li&gt;如果事件队列中没有回调，&lt;code&gt;JS引擎线程&lt;/code&gt;会一直发起询问，直到有为止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图解Event Loop&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;图解Event Loop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先，执行栈开始顺序执行&lt;/p&gt;
&lt;p&gt;判断是否为同步，异步则进入异步线程，最终事件回调给事件触发线程的任务队列等待执行，同步继续执行&lt;/p&gt;
&lt;p&gt;执行栈空，询问任务队列中是否有事件回调&lt;/p&gt;
&lt;p&gt;任务队列中有事件回调则把回调加入执行栈末尾继续从第一步开始执行&lt;/p&gt;
&lt;p&gt;任务队列中没有事件回调则不停发起询问&lt;/p&gt;
&lt;p&gt;完整的图解Event loop可以再结合宏任务和微任务得出&lt;/p&gt;
</content:encoded></item><item><title>浏览器的渲染进程（renderer）</title><link>https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E6%B8%B2%E6%9F%93%E8%BF%9B%E7%A8%8Brenderer/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E6%B8%B2%E6%9F%93%E8%BF%9B%E7%A8%8Brenderer/</guid><description>简述渲染进程Renderer   页面的渲染，JS的执行，事件的循环，都在渲染进程内执行。   渲染进程Renderer的主要线程   GUI渲染线程  - 负责渲染浏览器界面，解析HTML，CSS，构建DOM树和RenderObject树，布局和绘制等    - 解析html代码(HTML代码本质...</description><pubDate>Thu, 10 Jun 2021 14:50:55 GMT</pubDate><content:encoded>&lt;h1&gt;简述渲染进程Renderer&lt;/h1&gt;
&lt;p&gt;页面的渲染，JS的执行，事件的循环，都在渲染进程内执行。&lt;/p&gt;
&lt;h1&gt;渲染进程Renderer的主要线程&lt;/h1&gt;
&lt;h4&gt;GUI渲染线程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;负责渲染浏览器界面，解析HTML，CSS，构建DOM树和RenderObject树，布局和绘制等
&lt;ul&gt;
&lt;li&gt;解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点，生成DOM树，也就是DOM Tree&lt;/li&gt;
&lt;li&gt;解析css，生成CSSOM(CSS规则树)&lt;/li&gt;
&lt;li&gt;把DOM Tree 和CSSOM结合，生成Rendering Tree(渲染树)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当我们修改了一些元素的颜色或者背景色，页面就会重绘(Repaint)&lt;/li&gt;
&lt;li&gt;当我们修改元素的尺寸，页面就会回流(Reflow)&lt;/li&gt;
&lt;li&gt;当页面需要Repaing和Reflow时GUI线程执行，绘制页面&lt;/li&gt;
&lt;li&gt;**回流(Reflow)&lt;strong&gt;比&lt;/strong&gt;重绘(Repaint)**的成本要高，我们要尽量避免Reflow和Repaint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GUI渲染线程与JS引擎线程是互斥的&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当JS引擎执行时GUI线程会被挂起(相当于被冻结了)&lt;/li&gt;
&lt;li&gt;GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;JS引擎线程（主线程）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;JS引擎线程就是JS内核，负责处理Javascript脚本程序(例如V8引擎)&lt;/li&gt;
&lt;li&gt;JS引擎线程负责解析Javascript脚本，运行代码&lt;/li&gt;
&lt;li&gt;JS引擎&lt;strong&gt;一直等待着任务队列中任务的到来&lt;/strong&gt;，然后加以处理
&lt;ul&gt;
&lt;li&gt;浏览器同时只能有一个JS引擎线程在运行JS程序，所以js是单线程运行的&lt;/li&gt;
&lt;li&gt;一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GUI渲染线程与JS引擎线程是互斥的，js引擎线程会阻塞GUI渲染线程
&lt;ul&gt;
&lt;li&gt;就是我们常遇到的JS执行时间过长，造成页面的渲染不连贯，导致页面渲染加载阻塞(就是加载慢)&lt;/li&gt;
&lt;li&gt;例如浏览器渲染的时候遇到script标签，就会停止GUI的渲染，然后js引擎线程开始工作，执行里面的js代码，等js执行完毕，js引擎线程停止工作，GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;事件触发线程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;属于浏览器而不是JS引擎，用来控制事件循环，并且管理着一个事件队列(task queue)&lt;/li&gt;
&lt;li&gt;当js执行碰到事件绑定和一些异步操作(如setTimeOut，也可来自浏览器内核的其他线程，如鼠标点击、AJAX异步请求等)，会&lt;strong&gt;走事件触发线程将对应的事件添加到对应的线程&lt;/strong&gt;中(比如定时器操作，便把定时器事件添加到定时器线程)，等异步事件有了结果，便把他们的回调操作添加到事件队列，等待js引擎线程空闲时来处理。&lt;/li&gt;
&lt;li&gt;当对应的事件符合触发条件被触发时，该线程会把事件添加到待处理队列的队尾，等待JS引擎的处理&lt;/li&gt;
&lt;li&gt;因为JS是单线程，所以这些待处理队列中的事件都得排队等待JS引擎处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;定时触发器线程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setInterval&lt;/code&gt;与&lt;code&gt;setTimeout&lt;/code&gt;所在线程&lt;/li&gt;
&lt;li&gt;浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的，如果处于阻塞线程状态就会影响记计时的准确)&lt;/li&gt;
&lt;li&gt;通过单独线程来计时并触发定时(计时完毕后，添加到事件触发线程的事件队列中，等待JS引擎空闲后执行)，这个线程就是定时触发器线程，也叫定时器线程&lt;/li&gt;
&lt;li&gt;W3C在HTML标准中规定，规定要求&lt;code&gt;setTimeout&lt;/code&gt;中低于4ms的时间间隔算为4ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;异步http请求线程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在XMLHttpRequest在连接后是通过浏览器新开一个线程请求&lt;/li&gt;
&lt;li&gt;将检测到状态变更时，如果设置有回调函数，异步线程就产生状态变更事件，将这个回调再放入事件队列中再由JavaScript引擎执行&lt;/li&gt;
&lt;li&gt;简单说就是当执行到一个http异步请求时，就把异步请求事件添加到异步请求线程，等收到响应(准确来说应该是http状态变化)，再把回调函数添加到事件队列，等待js引擎线程来执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;EventLoop轮询处理线程 （重中之重）&lt;/h3&gt;
&lt;p&gt;这里单独拿出一篇文章将这个&lt;/p&gt;
</content:encoded></item><item><title>进程和线程</title><link>https://nollieleo.github.io/posts/%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B/</guid><description>线程与进程   什么是进程？  首先， CPU懂吧？是计算机的核心，承当所有的计算任务   官方：进程是CPU资源分配的最小单位  按我的理解：  进程就是进行中的程序，可以独立运行并且拥有自己的资源空间的任务程序  意思就是：  1. 运行中的程序 2. 程序所用到的内存和系统资源  浏览器中的每...</description><pubDate>Thu, 10 Jun 2021 14:39:25 GMT</pubDate><content:encoded>&lt;h1&gt;线程与进程&lt;/h1&gt;
&lt;h2&gt;什么是进程？&lt;/h2&gt;
&lt;p&gt;首先， CPU懂吧？是计算机的核心，承当所有的计算任务&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;官方：&lt;strong&gt;进程&lt;/strong&gt;是CPU&lt;strong&gt;资源分配&lt;/strong&gt;的最小单位&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;按我的理解：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进程就是进行中的程序，可以独立运行并且拥有自己的资源空间的任务程序&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;意思就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行中的程序&lt;/li&gt;
&lt;li&gt;程序所用到的内存和系统资源&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;浏览器中的每个tab页都是一个进程&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;什么是线程？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;官方：&lt;strong&gt;线程&lt;/strong&gt;是CPU调度的最小单位&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;线程&lt;/strong&gt;是建立 &lt;strong&gt;进程&lt;/strong&gt;的基础上的一次程序运行单位&lt;/p&gt;
&lt;p&gt;通俗点就是：线程是程序中的一个执行流，&lt;strong&gt;一个进程中可以拥有多个执行流&lt;/strong&gt;，也就是多个线程&lt;/p&gt;
&lt;p&gt;线程分为两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单线程&lt;/strong&gt;：一个进程中只有一个执行流，就只有一个线程，叫做单线程&lt;/li&gt;
&lt;li&gt;多线程：顾名思义一个进程多个执行流，&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;进程与线程的区别&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;进程&lt;/strong&gt;是操作系统&lt;strong&gt;分配资源的&lt;/strong&gt;最小单位，&lt;strong&gt;线程&lt;/strong&gt;是&lt;strong&gt;程序执行&lt;/strong&gt;的最小单位&lt;/p&gt;
&lt;p&gt;一个进程由一个或多个线程组成，线程可以理解为是一个进程中代码的不同执行路线&lt;/p&gt;
&lt;p&gt;进程之间相互独立，但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)&lt;/p&gt;
&lt;h2&gt;多进程和多线程&lt;/h2&gt;
&lt;p&gt;**多进程：**多进程指的是在同一个时间里，同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的，比如大家可以在网易云听歌的同时打开编辑器敲代码，编辑器和网易云的进程之间不会相互干扰&lt;/p&gt;
&lt;p&gt;**多线程：**多线程是指程序中包含多个执行流，即在一个程序中可以同时运行多个不同的线程来执行不同的任务，也就是说允许单个程序创建多个并行执行的线程来完成各自的任务&lt;/p&gt;
&lt;h1&gt;JS为什么是单线程&lt;/h1&gt;
&lt;p&gt;JS的单线程，与它的用途有关。作为浏览器脚本语言，JavaScript的主要用途是与用户互动，以及操作DOM。这决定了它只能是单线程，否则会带来很复杂的同步问题。比如，假定JavaScript同时有两个线程，一个线程在某个DOM节点上添加内容，另一个线程删除了这个节点，这时浏览器应该以哪个线程为准？&lt;/p&gt;
&lt;p&gt;还有人说js还有Worker线程，对的，为了利用多核CPU的计算能力，HTML5提出Web Worker标准，允许JavaScript脚本创建多个线程，但是子线程是完 全受主线程控制的，而且不得操作DOM&lt;/p&gt;
&lt;p&gt;所以，这个标准并没有改变JavaScript是单线程的本质&lt;/p&gt;
</content:encoded></item><item><title>浏览器的进程种类</title><link>https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E8%BF%9B%E7%A8%8B%E7%A7%8D%E7%B1%BB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E8%BF%9B%E7%A8%8B%E7%A7%8D%E7%B1%BB/</guid><description>浏览器是多进程的，一个网页就是一个进程，所以你看谷歌开了那么多tab页，然后放着，就是玩，然后我电脑就炸了，因为老子CPU不行啊，就分配不了那么多内存空间给他用，就不行了呗     浏览器的进程种类   Browser进程  - 浏览器的主进程(负责协调、主控)，该进程只有一个 - 负责浏览器界面显...</description><pubDate>Thu, 10 Jun 2021 14:18:46 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;浏览器是多进程的，一个网页就是一个进程，所以你看谷歌开了那么多tab页，然后放着，就是玩，然后我电脑就炸了，因为老子CPU不行啊，就分配不了那么多内存空间给他用，就不行了呗&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;浏览器的进程种类&lt;/h1&gt;
&lt;h2&gt;Browser进程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;浏览器的主进程(负责协调、主控)，该进程只有一个&lt;/li&gt;
&lt;li&gt;负责浏览器界面显示，与用户交互。如前进，后退等&lt;/li&gt;
&lt;li&gt;负责各个页面的管理，创建和销毁其他进程&lt;/li&gt;
&lt;li&gt;将渲染(Renderer)进程得到的内存中的Bitmap(位图)，绘制到用户界面上&lt;/li&gt;
&lt;li&gt;网络资源的管理，下载等&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三方插件进程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每种类型的插件对应一个进程，当使用该插件时才创建&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GPU进程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;该进程也只有一个，用于3D绘制等等&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;*渲染进程(如此的重要)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;即通常所说的浏览器内核(Renderer进程，内部是多线程)&lt;/li&gt;
&lt;li&gt;每个Tab页面都有一个渲染进程，互不影响&lt;/li&gt;
&lt;li&gt;主要作用为页面渲染，脚本执行，事件处理等&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为什么浏览器需要多进程？&lt;/h1&gt;
&lt;p&gt;我们假设浏览器是单进程，那么某个Tab页崩溃了，就影响了整个浏览器，体验有多差&lt;/p&gt;
&lt;p&gt;同理如果插件崩溃了也会影响整个浏览器&lt;/p&gt;
&lt;p&gt;当然多进程还有其它的诸多优势，不过多阐述&lt;/p&gt;
&lt;p&gt;浏览器进程有很多，每个进程又有很多线程，都会占用内存&lt;/p&gt;
&lt;p&gt;这也意味着内存等资源消耗会很大，有点拿空间换时间的意思&lt;/p&gt;
</content:encoded></item><item><title>使用生成器写出斐波那契数列</title><link>https://nollieleo.github.io/posts/%E4%BD%BF%E7%94%A8%E7%94%9F%E6%88%90%E5%99%A8%E5%86%99%E5%87%BA%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E4%BD%BF%E7%94%A8%E7%94%9F%E6%88%90%E5%99%A8%E5%86%99%E5%87%BA%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97/</guid><description>下面是一个利用 Generator 函数和for...of循环，实现斐波那契数列的例子。    js function fibonacci() {     let [prev, curr] = [0, 1];     for (;;) {       yield curr;       [prev,...</description><pubDate>Mon, 07 Jun 2021 11:04:35 GMT</pubDate><content:encoded>&lt;p&gt;下面是一个利用 Generator 函数和&lt;code&gt;for...of&lt;/code&gt;循环，实现斐波那契数列的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function* fibonacci() {
    let [prev, curr] = [0, 1];
    for (;;) {
      yield curr;
      [prev, curr] = [curr, prev + curr];
    }
  }

  for (let n of fibonacci()) {
    if (n &amp;gt; 10000) break;
    console.log(n);
  }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>前端性能优化</title><link>https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</guid><description>前端性能优化最佳实践  本文主要考量客户端性能、服务器端和网络性能，内容框架来自 ，包含 7 个类别共 35 条前端性能优化最佳实践，在此基础上补充了一些相关或者更符合主流技术的内容。  同时，建议关注及时更新的 。  目录：  - 页面内容   -    -    -    -    -    -...</description><pubDate>Sun, 06 Jun 2021 16:13:08 GMT</pubDate><content:encoded>&lt;h1&gt;前端性能优化最佳实践&lt;/h1&gt;
&lt;p&gt;本文主要考量客户端性能、服务器端和网络性能，内容框架来自 &lt;a href=&quot;https://developer.yahoo.com/performance/rules.html&quot;&gt;Yahoo Developer Network&lt;/a&gt;，包含 7 个类别共 35 条前端性能优化最佳实践，在此基础上补充了一些相关或者更符合主流技术的内容。&lt;/p&gt;
&lt;p&gt;同时，建议关注及时更新的 &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/?hl=en&quot;&gt;Google 性能优化指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目录：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页面内容
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-http&quot;&gt;减少 HTTP 请求数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-dns&quot;&gt;减少 DNS 查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-redirect&quot;&gt;避免重定向&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-ajax-cache&quot;&gt;缓存 Ajax 请求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-postload&quot;&gt;延迟加载&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-preload&quot;&gt;预先加载&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-dom&quot;&gt;减少 DOM 元素数量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-domains&quot;&gt;划分内容到不同域名&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-iframe&quot;&gt;尽量减少 iframe 使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-404&quot;&gt;避免 404 错误&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;服务器
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-cdn&quot;&gt;使用 CDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-cache&quot;&gt;添加 Expires 或 Cache-Control 响应头&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-gzip&quot;&gt;启用 Gzip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-etag&quot;&gt;配置 Etag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-flush&quot;&gt;尽早输出缓冲&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-ajax-get&quot;&gt;Ajax 请求使用 GET 方法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-src&quot;&gt;避免图片 src 为空&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cookie
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#cookie-size&quot;&gt;减少 Cookie 大小&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#cookie-free&quot;&gt;静态资源使用无 Cookie 域名&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSS
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#css-head&quot;&gt;把样式表放在 `` 中&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#css-expression&quot;&gt;不要使用 CSS 表达式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#css-link&quot;&gt;使用 `` 替代 &lt;code&gt;@import&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#css-filter&quot;&gt;不要使用 filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JavaScript
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-bottom&quot;&gt;把脚本放在页面底部&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-extenal&quot;&gt;使用外部 JavaScript 和 CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-minify&quot;&gt;压缩 JavaScript 和 CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-duplicate&quot;&gt;移除重复脚本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-dom&quot;&gt;减少 DOM 操作&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-event&quot;&gt;使用高效的事件处理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图片
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#image-optimize&quot;&gt;优化图片&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#image-sprite&quot;&gt;优化 CSS Sprite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#image-scale&quot;&gt;不要在 HTML 中缩放图片&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#image-favicon&quot;&gt;使用体积小、可缓存的 favicon.ico&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;移动端
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#mobile-25kb&quot;&gt;保持单个文件小于 25 KB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#mobile-multipart&quot;&gt;打包内容为分段（multipart）文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前端性能的一个重要指标是&lt;strong&gt;页面加载时间&lt;/strong&gt;，不仅事关用户体验，也是搜索引擎排名考虑的一个因素。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;来自 Google 的数据表明，一个有 10 条数据 0.4 秒能加载完的页面，变成 30 条数据 0.9 秒加载完之后，流量和广告收入下降 90%。&lt;/li&gt;
&lt;li&gt;Google Map 首页文件大小从 100KB 减小到 70-80KB 后，流量在第一周涨了 10%，接下来的三周涨了 25%。&lt;/li&gt;
&lt;li&gt;亚马逊的数据表明：加载时间增加 100 毫秒，销量就下降 1%。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;以上数据更说明「加载时间就是金钱」，前端优化主要围绕提高加载速度进行。&lt;/p&gt;
&lt;h2&gt;页面内容&lt;/h2&gt;
&lt;h3&gt;减少 HTTP 请求数&lt;/h3&gt;
&lt;p&gt;Web 前端 80% 的响应时间花在图片、样式、脚本等资源下载上。浏览器对每个域名的连接数是有限制的，减少请求次数是缩短响应时间的关键。&lt;/p&gt;
&lt;p&gt;通过简洁的设计减少页面所需资源，进而减少 HTTP 请求，这是最直接的方式，前提是你的 Boss、设计师同事不打死你。所以，还是另辟蹊径吧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;合并 JavaScript、CSS 等文件；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器端（CDN）自动合并&lt;/li&gt;
&lt;li&gt;基于 Node.js 的文件合并工具一抓一大把&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;a href=&quot;http://alistapart.com/articles/sprites&quot;&gt;CSS Sprite&lt;/a&gt;：将背景图片合并成一个文件，通过&lt;code&gt;background-image&lt;/code&gt; 和 &lt;code&gt;background-position&lt;/code&gt; 控制显示；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.spritecow.com/&quot;&gt;Sprite Cow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.spritebox.net/&quot;&gt;Spritebox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;逐步被 Icon Font 和 SVG Sprite 取代。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.w3.org/TR/html401/struct/objects.html#h-13.6&quot;&gt;Image Map&lt;/a&gt;：合并图片，然后使用坐标映射不同的区域（&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Club_(dining_club)&quot;&gt;演示&lt;/a&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;缺点：仅适用于相连的图片；设置坐标过程乏味且易出错；可访性问题。不推荐使用这种过时的技术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Inline Assets：使用 &lt;a href=&quot;https://en.wikipedia.org/wiki/Data_URI_scheme&quot;&gt;Data URI scheme&lt;/a&gt; 将图片嵌入 HTML 或者 CSS 中；或者将 CSS、JS、图片直接嵌入 HTML 中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;会增加文件大小，也可能产生浏览器兼容及其他性能问题（有待整理补充）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;未来的趋势是使用内嵌 SVG。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-domains&quot;&gt;内容分片&lt;/a&gt;，将请求划分到不同的域名上。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;HTTP/2 通过多路复用大幅降低了多个请求的开销。通过数据分帧层，客户端和服务器之间只需要建立一个 TCP 连接，即可同时收发多个文件，而且，该连接在相当长的时间周期内保持打开（持久化），以便复用。&lt;/p&gt;
&lt;p&gt;HTTP/2 的新特性意味着上述优化实践不再适用，但考虑到客户端对 HTTP/2 的支持覆盖程度，还需根据实际数据权衡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;减少 DNS 查询&lt;/h3&gt;
&lt;p&gt;用户输入 URL 以后，浏览器首先要查询域名（hostname）对应服务器的 IP 地址，一般需要耗费 &lt;strong&gt;20-120 毫秒&lt;/strong&gt; 时间。DNS 查询完成之前，浏览器无法从服务器下载任何数据。&lt;/p&gt;
&lt;p&gt;基于性能考虑，ISP、局域网、操作系统、浏览器都会有相应的 DNS 缓存机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IE 缓存 30 分钟，可以通过注册表中 &lt;code&gt;DnsCacheTimeout&lt;/code&gt; 项设置；&lt;/li&gt;
&lt;li&gt;Firefox 混存 1 分钟，通过 &lt;code&gt;network.dnsCacheExpiration&lt;/code&gt; 配置；&lt;/li&gt;
&lt;li&gt;（TODO：补充其他浏览器缓存信息）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首次访问、没有相应的 DNS 缓存时，域名越多，查询时间越长。所以应尽量减少域名数量。但基于并行下载考虑，&lt;strong&gt;把资源分布到 2 个域名上（最多不超过 4 个）&lt;/strong&gt;。这是减少 DNS 查询同时保证并行下载的折衷方案。&lt;/p&gt;
&lt;h3&gt;避免重定向&lt;/h3&gt;
&lt;p&gt;HTTP 重定向通过 &lt;code&gt;301&lt;/code&gt;/&lt;code&gt;302&lt;/code&gt; 状态码实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端收到服务器的重定向响应后，会根据响应头中 &lt;code&gt;Location&lt;/code&gt; 的地址再次发送请求。重定向会影响用户体验，尤其是多次重定向时，用户在一段时间内看不到任何内容，只看到浏览器进度条一直在刷新。&lt;/p&gt;
&lt;p&gt;有时重定向无法避免，在糟糕也比抛出 404 好。虽然通过 &lt;a href=&quot;https://en.wikipedia.org/wiki/Meta_refresh&quot;&gt;HTML meta refresh&lt;/a&gt; 和 JavaScript 也能实现，但首选 HTTP &lt;code&gt;3xx&lt;/code&gt; 跳转，以保证浏览器「后退」功能正常工作（也利于 SEO）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最浪费的重定向经常发生、而且很容易被忽略：URL 末尾应该添加 &lt;code&gt;/&lt;/code&gt; 但未添加。比如，访问 &lt;code&gt;http://astrology.yahoo.com/astrology&lt;/code&gt; 将被 301 重定向到 &lt;code&gt;http://astrology.yahoo.com/astrology/&lt;/code&gt;（注意末尾的 &lt;code&gt;/&lt;/code&gt;）。如果使用 Apache，可以通过 &lt;code&gt;Alias&lt;/code&gt; 或 &lt;code&gt;mod_rewrite&lt;/code&gt; 或 &lt;code&gt;DirectorySlash&lt;/code&gt; 解决这个问题。&lt;/li&gt;
&lt;li&gt;网站域名变更：CNAME 结合 &lt;code&gt;Alias&lt;/code&gt; 或 &lt;code&gt;mod_rewrite&lt;/code&gt; 或者其他服务器类似功能实现跳转。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存 Ajax 请求&lt;/h3&gt;
&lt;p&gt;Ajax 可以提高用户体验。但「异步」不意味着「及时」，优化 Ajax 响应速度提高性能仍是需要关注的主题。&lt;/p&gt;
&lt;p&gt;最重要的的优化方式是&lt;strong&gt;缓存响应结果&lt;/strong&gt;，详见 &lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-cache&quot;&gt;添加 Expires 或 Cache-Control 响应头&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;以下规则也关乎 Ajax 响应速度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-gzip&quot;&gt;启用 Gzip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-dns&quot;&gt;减少 DNS 查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#javascript-minify&quot;&gt;压缩 JavaScript 和 CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#content-redirect&quot;&gt;避免重定向&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#server-etag&quot;&gt;配置 Etag&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;延迟加载&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;页面初始加载时哪些内容是绝对必需的&lt;/strong&gt;？不在答案之列的资源都可以延迟加载。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非首屏使用的数据、样式、脚本、图片等；&lt;/li&gt;
&lt;li&gt;用户交互时才会显示的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;遵循「渐进增强」理念开发的网站：JavaScript 用于增强用用户体验，但没有（不支持） JavaScript 也能正常工作，完全可以延迟加载 JavaScript。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;延迟渲染&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将首屏以外的 HTML 放在不渲染的元素中，如隐藏的 &lt;code&gt;，或者 `type` 属性为非执行脚本的 &lt;/code&gt; 标签中，减少初始渲染的 DOM 元素数量，提高速度。等首屏加载完成或者用户操作时，再去渲染剩余的页面内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;预先加载&lt;/h3&gt;
&lt;p&gt;预先加载利用浏览器空闲时间请求将来要使用的资源，以便用户访问下一页面时更快地响应。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无条件预先加载&lt;/strong&gt;：页面加载完成（&lt;code&gt;load&lt;/code&gt;）后，马上获取其他资源。以 google.com 为例，首页加载完成后会立即下载一个 Sprite 图片，此图首页不需要，但是搜索结果页要用到。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有条件预先加载&lt;/strong&gt;：根据用户行为预判用户去向，预载相关资源。比如 search.yahoo.com 开始输入时会有额外的资源加载。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Chrome 等浏览器的地址栏也有类似的机制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有「阴谋」的预先加载&lt;/strong&gt;：页面即将上线新版前预先加载新版内容。网站改版后由于缓存、使用习惯等原因，会有旧版的网站更快更流畅的反馈。为缓解这一问题，在新版上线之前，旧版可以利用空闲提前加载一些新版的资源缓存到客户端，以便新版正式上线后更快的载入（好一个「心机猿」:scream:）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「双十一」、「黑五」这类促销日来临之前，也可以预先下载一些相关资源到客户端（浏览器、App 等），有效利用浏览器缓存和本地存储，降低活动当日请求压力，提高用户体验。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;TODO: Prefetch 相关细节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/resource-hints/&quot;&gt;Resource Hints Spec&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;减少 DOM 元素数量&lt;/h3&gt;
&lt;p&gt;复杂的页面不仅下载的字节更多，JavaScript DOM 操作也更慢。例如，同是添加一个事件处理器，500 个元素和 5000 个元素的页面速度上会有很大区别。&lt;/p&gt;
&lt;p&gt;从以下几个角度考虑移除不必要的标记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否还在使用表格布局？&lt;/li&gt;
&lt;li&gt;塞进去更多的 `` 仅为了处理布局问题？也许有更好、更语义化的标记。&lt;/li&gt;
&lt;li&gt;能通过伪元素实现的功能，就没必要添加额外元素，如清除浮动。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;浏览器控制台中输入以下代码可以计算出页面中有多少 DOM 元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementsByTagName(&apos;*&apos;).length;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比标记良好的的网站，看看差距是多少。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么不使用表格布局？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更多的标签，增加文件大小；&lt;/li&gt;
&lt;li&gt;不易维护，无法适应响应式设计；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能考量&lt;/strong&gt;，默认的表格布局算法会产生大量重绘（参见&lt;a href=&quot;https://csspod.com/table-width-algorithms/&quot;&gt;表格布局算法&lt;/a&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;划分内容到不同域名&lt;/h3&gt;
&lt;p&gt;浏览器一般会限制每个域的并行线程（一般为 6 个，甚至更少），使用不同的域名可以最大化下载线程，但注意保持在 2-4 个域名内，以避免 DNS 查询损耗。&lt;/p&gt;
&lt;p&gt;例如，动态内容放在 &lt;code&gt;csspod.com&lt;/code&gt; 上，静态资源放在 &lt;code&gt;static.csspod.com&lt;/code&gt; 上。这样还可以禁用静态资源域下的 Cookie，减少数据传输，详见 &lt;a href=&quot;https://csspod.com/frontend-performance-best-practices/#cookie-free&quot;&gt;Cookie 优化&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;更多信息参考 &lt;a href=&quot;http://yuiblog.com/blog/2007/04/11/performance-research-part-4/&quot;&gt;Maximizing Parallel Downloads in the Carpool Lane&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;尽量减少 iframe 使用&lt;/h3&gt;
&lt;p&gt;使用 iframe 可以在页面中嵌入 HTML 文档，但有利有弊。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe&amp;gt; 优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以用来加载速度较慢的第三方资源，如广告、徽章；&lt;/li&gt;
&lt;li&gt;可用作&lt;a href=&quot;http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/&quot;&gt;安全沙箱&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;可以并行下载脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;iframe&amp;gt; 缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加载代价昂贵，即使是空的页面；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阻塞页面 &lt;code&gt;load&lt;/code&gt; 事件触发；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Iframe 完全加载以后，父页面才会触发 &lt;code&gt;load&lt;/code&gt; 事件。 Safari、Chrome 中通过 JavaScript 动态设置 iframe &lt;code&gt;src&lt;/code&gt; 可以避免这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺乏语义。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;避免 404 错误&lt;/h3&gt;
&lt;p&gt;HTTP 请求很昂贵，返回无效的响应（如 404 未找到）完全没必要，降低用户体验而且毫无益处。&lt;/p&gt;
&lt;p&gt;一些网站设计很酷炫、有提示信息的 404 页面，有助于提高用户体验，但还是浪费服务器资源。尤其糟糕的是外部脚本返回 404，不仅阻塞其他资源下载，浏览器还会尝试把 404 页面内容当作 JavaScript 解析，消耗更多资源。&lt;/p&gt;
&lt;h3&gt;补充规则：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义字符集，并放在 `` 顶部&lt;/strong&gt;。大多数浏览器会暂停页面渲染，直到找到字符集定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;服务器&lt;/h2&gt;
&lt;p&gt;服务器相关优化设置可参考 H5BP 相关项目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h5bp/server-configs-nginx&quot;&gt;Nginx HTTP server boilerplate configs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h5bp/server-configs-apache&quot;&gt;Apache HTTP server boilerplate configs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h5bp/server-configs-iis&quot;&gt;IIS Web.Config Boilerplates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用 CDN&lt;/h3&gt;
&lt;p&gt;网站 80-90% 响应时间消耗在资源下载上，&lt;strong&gt;减少资源下载时间是性能优化的黄金发则&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;相比分布式架构的复杂和巨大投入，静态内容分发网络（CDN）可以以较低的投入，获得加载速度有效提升。&lt;/p&gt;
&lt;h3&gt;添加 Expires 或 Cache-Control 响应头&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态内容&lt;/strong&gt;：将 &lt;code&gt;Expires&lt;/code&gt; 响应头设置为将来很远的时间，实现「永不过期」策略；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态内容&lt;/strong&gt;：设置合适的 &lt;code&gt;Cache-Control&lt;/code&gt; 响应头，让浏览器有条件地发起请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control&quot;&gt;Cache-Control&lt;/a&gt; 头在 HTTP/1.1 规范中定义，取代了之前用来定义响应缓存策略的头（例如 Expires、Pragma）。当前的所有浏览器都支持 Cache-Control，因此，使用它就够了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;鉴于静态内容和动态内容不同的缓存策略，实践中一般会把二者部署在不同的服务器（域名）以方便管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn&quot;&gt;HTTP 缓存 | Web Fundamentals - Google Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h5bp/server-configs&quot;&gt;H5BP - Server Configs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;启用 Gzip&lt;/h3&gt;
&lt;p&gt;Gzip 压缩通常可以减少 70% 的响应大小，对某些文件更可能高达 90%，比 Deflate 更高效。主流 Web 服务器都有相应模块，而且绝大多数浏览器支持 gzip 解码。所以，应该对 HTML、CSS、JS、XML、JSON 等文本类型的内容启用压缩。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;，图片和 PDF 文件不要使用 gzip。它们本身已经压缩过，再使用 gzip 压缩不仅浪费 CPU 资源，而且还可能增加文件体积。&lt;/p&gt;
&lt;p&gt;对于不支持的 Gzip 的用户代理，通过设置 Vary 响应头，返回为未压缩的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vary: *
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 Etag&lt;/h3&gt;
&lt;p&gt;Etag 通过文件版本标识，方便服务器判断请求的内容是否有更新，如果没有就响应 &lt;code&gt;304&lt;/code&gt;，避免重新下载。&lt;/p&gt;
&lt;p&gt;当然，启用 Etag 可能会导致其他问题，还需要根据具体情况做判断。（TODO：补充相关内容）&lt;/p&gt;
&lt;h3&gt;尽早输出（flush）缓冲&lt;/h3&gt;
&lt;p&gt;用户请求页面时，服务器通常需要花费 200 ~ 500 毫秒来组合 HTML 页面。在此期间，浏览器处于空闲、等待数据状态。使用PHP 中的 &lt;a href=&quot;http://php.net/flush&quot;&gt;flush()&lt;/a&gt; 函数，可以发送部分已经准备好的 HTML 到浏览器，以便服务器还在忙于处理剩余页面时，浏览器可以提前开始获取资源。&lt;/p&gt;
&lt;p&gt;可以考虑在 &lt;code&gt;之后输出一次缓冲，HTML head 一般比较容易生成，先发送以便浏览器开始获取&lt;/code&gt; 里引用的 CSS 等资源。&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Css, js --&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;?php flush(); ?&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;!-- content --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ajax 请求使用 GET 方法&lt;/h3&gt;
&lt;p&gt;浏览器执行 XMLHttpRequest POST 请求时分成两步，先发送 Header，再发送数据。而 GET 只使用一个 TCP 数据包发送数据，所以首选 GET 方法。&lt;/p&gt;
&lt;p&gt;根据 &lt;a href=&quot;https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html&quot;&gt;HTTP 规范&lt;/a&gt;，GET 用于获取数据，POST 则用于向服务器发送数据，所以 Ajax 请求数据时使用 GET 更符合规范（&lt;a href=&quot;http://www.w3schools.com/tags/ref_httpmethods.asp&quot;&gt;GET 和 POST 对比&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;IE 中最大 URL 长度为 2K，如果超出 2K，则需要考虑使用 POST 方法。&lt;/p&gt;
&lt;h3&gt;避免图片 src 为空&lt;/h3&gt;
&lt;p&gt;图片 &lt;code&gt;src&lt;/code&gt; 属性值为空字符串可能以下面两种形式出现：&lt;/p&gt;
&lt;p&gt;HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var img = new Image(); 
img.src = &quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然 &lt;code&gt;src&lt;/code&gt; 属性为空字符串，但浏览器仍然会向服务器发起一个 HTTP 请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IE 向页面所在的目录发送请求；&lt;/li&gt;
&lt;li&gt;Safari、Chrome、Firefox 向页面本身发送请求；&lt;/li&gt;
&lt;li&gt;Opera 不执行任何操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;以上数据较老，当下主流版本可能会有改变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;空 &lt;code&gt;src&lt;/code&gt; 产生请求的后果不容小觑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给服务器造成意外的流量负担，尤其时日 PV 较大时；&lt;/li&gt;
&lt;li&gt;浪费服务器计算资源；&lt;/li&gt;
&lt;li&gt;可能产生报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，浏览器如此实现也是根据 &lt;a href=&quot;https://www.ietf.org/rfc/rfc3986.txt&quot;&gt;RFC 3986 - Uniform Resource Identifiers&lt;/a&gt;，当空字符串作为 URI 出现时，被当成相对 URI，具体算法参见规范 5.2 节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考链接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nczonline.net/blog/2009/11/30/empty-image-src-can-destroy-your-site/&quot;&gt;Empty image src can destroy your site&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;空的 &lt;code&gt;href&lt;/code&gt; 属性也存在类似问题。用户点击空链接时，浏览器也会向服务器发送 HTTP 请求，可以通过 JavaScript 阻止空链接的默认的行为。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Cookie&lt;/h2&gt;
&lt;h3&gt;减少 Cookie 大小&lt;/h3&gt;
&lt;p&gt;Cookie 被用于身份认证、个性化设置等诸多用途。Cookie 通过 HTTP 头在服务器和浏览器间来回传送，减少 Cookie 大小可以降低其对响应速度的影响。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去除不必要的 Cookie；&lt;/li&gt;
&lt;li&gt;尽量压缩 Cookie 大小；&lt;/li&gt;
&lt;li&gt;注意设置 Cookie 的 domain 级别，如无必要，不要影响到 sub-domain；&lt;/li&gt;
&lt;li&gt;设置合适的过期时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更多细节参考 &lt;a href=&quot;http://yuiblog.com/blog/2007/03/01/performance-research-part-3/&quot;&gt;When the Cookie Crumbles&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HTTP/2 首部压缩在客户端和服务器端使用「首部表」来跟踪和存储之前发送的键值对，对于相同的数据，不再随每次请求和响应发送。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;静态资源使用无 Cookie 域名&lt;/h3&gt;
&lt;p&gt;静态资源一般无需使用 Cookie，可以把它们放在使用二级域名或者专门域名的无 Cookie 服务器上，降低 Cookie 传送的造成的流量浪费，提高响应速度。&lt;/p&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;h3&gt;把样式表放在 `` 中&lt;/h3&gt;
&lt;p&gt;把样式表放在 `` 中可以让页面渐进渲染，尽早呈现视觉反馈，给用户加载速度很快的感觉。&lt;/p&gt;
&lt;p&gt;这对内容比较多的页面尤为重要，用户可以先查看已经下载渲染的内容，而不是盯着白屏等待。&lt;/p&gt;
&lt;p&gt;如果把样式表放在页面底部，一些浏览器为减少重绘，会在 CSS 加载完成以后才渲染页面，用户只能对着白屏干瞪眼，用户体验极差。&lt;/p&gt;
&lt;h3&gt;不要使用 CSS 表达式&lt;/h3&gt;
&lt;p&gt;CSS 表达式可以在 CSS 里执行 JavaScript，仅 IE5-IE7 支持，IE8 标准模式已经废弃。&lt;/p&gt;
&lt;p&gt;CSS 表达式超出预期的频繁执行，页面滚动、鼠标移动时都会不断执行，带来很大的性能损耗。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IE7 及更低版本的浏览器已经逐渐成为历史，忘记它吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;使用 `` 替代 &lt;code&gt;@import&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;对于 IE 某些版本，&lt;code&gt;@import&lt;/code&gt; 的行为和 `` 放在页面底部一样。所以，不要用它。&lt;/p&gt;
&lt;h3&gt;不要使用 filter&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AlphaImageLoader&lt;/code&gt; 为 IE5.5-IE8 专有的技术，和 CSS 表达式一样，放进博物馆吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;p&gt;这里所说的不是 &lt;a href=&quot;https://www.w3.org/TR/filter-effects-1/&quot;&gt;CSS3 Filter&lt;/a&gt;，参考文章 &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/filters/understanding-css/&quot;&gt;Understanding CSS Filter Effects&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;JavaScript&lt;/h2&gt;
&lt;h3&gt;使用requestIdleCallback&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;兼容性不好，目前只有chrome支持&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;希望快速响应用户，让用户觉得快&lt;/p&gt;
&lt;p&gt;使开发者能够再主事件循环上执行后台和低优先级工作，而不会影响延迟关键事件。&lt;/p&gt;
&lt;p&gt;正常帧任务完成后没超过16ms，说明事件有富余，则执行它的回调函数&lt;/p&gt;
&lt;h3&gt;把脚本放在页面底部&lt;/h3&gt;
&lt;p&gt;浏览器下载脚本时，会阻塞其他资源并行下载，即使是来自不同域名的资源。因此，最好将脚本放在底部，以提高页面加载速度。&lt;/p&gt;
&lt;p&gt;一些特殊场景无法将脚本放到页面底部的，可以考虑 `` 的以下属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement#defer_property&quot;&gt;&lt;code&gt;defer&lt;/code&gt; 属性&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;HTML5 新增的 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement#async_property&quot;&gt;&lt;code&gt;async&lt;/code&gt; 属性&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用外部 JavaScript 和 CSS&lt;/h3&gt;
&lt;p&gt;外部 JavaScript 和 CSS 文件可以被浏览器缓存，在不同页面间重用，也能降低页面大小。&lt;/p&gt;
&lt;p&gt;当然，实际中也需要考虑代码的重用程度。如果仅仅是某个页面使用到的代码，可以考虑内嵌在页面中，减少 HTTP 请求数。另外，可以在首页加载完成以后，预先加载子页面的资源。&lt;/p&gt;
&lt;h3&gt;压缩 JavaScript 和 CSS&lt;/h3&gt;
&lt;p&gt;压缩代码可以移除非功能性的字符（注释、空格、空行等），减少文件大小，提高载入速度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;得益于 Node.js 的流行，开源社区涌现出许多高效、易用的前端优化工具，JavaScript 和 CSS 压缩类的，不敢说多如牛毛，多入鸡毛倒是一点不夸张，如 [UglifyJS 2] (https://github.com/mishoo/UglifyJS2)、&lt;a href=&quot;https://www.npmjs.com/package/csso&quot;&gt;csso&lt;/a&gt;、&lt;a href=&quot;https://www.npmjs.com/package/cssnano&quot;&gt;cssnano&lt;/a&gt; 等。&lt;/p&gt;
&lt;p&gt;对于内嵌的 CSS 和 JavaScript，也可以通过 &lt;a href=&quot;https://www.npmjs.com/package/htmlmin&quot;&gt;htmlmin&lt;/a&gt; 等工具压缩。&lt;/p&gt;
&lt;p&gt;这些项目都有 Gulp、Webpack 等流行构建工具的配套版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;移除重复脚本&lt;/h3&gt;
&lt;p&gt;重复的脚本不仅产生不必要的 HTTP 请求，而且重复解析执行浪费时间和计算资源。&lt;/p&gt;
&lt;h3&gt;减少 DOM 操作&lt;/h3&gt;
&lt;p&gt;JavaScript 操作 DOM 很慢，尤其是 DOM 节点很多时。&lt;/p&gt;
&lt;p&gt;使用时应该注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存已经访问过的元素；&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment&quot;&gt;DocumentFragment&lt;/a&gt; 暂存 DOM，整理好以后再插入 DOM 树；&lt;/li&gt;
&lt;li&gt;操作 className，而不是多次读写 &lt;code&gt;style&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;避免使用 JavaScript 修复布局。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用高效的事件处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;减少绑定事件监听的节点，如通过事件委托；&lt;/li&gt;
&lt;li&gt;尽早处理事件，在 &lt;code&gt;DOMContentLoaded&lt;/code&gt; 即可进行，不用等到 &lt;code&gt;load&lt;/code&gt; 以后。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 &lt;code&gt;resize&lt;/code&gt;、&lt;code&gt;scroll&lt;/code&gt; 等触发频率极高的事件，应该通过 debounce 等机制降低处理程序执行频率。&lt;/p&gt;
&lt;p&gt;TODO: 补充相关内容 http://demo.nimius.net/debounce_throttle/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;图片&lt;/h2&gt;
&lt;h3&gt;优化图片&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;YDN &lt;a href=&quot;https://developer.yahoo.com/performance/rules.html#opt_images&quot;&gt;列出的相关工具&lt;/a&gt; 缺乏易用性，建议参考以下工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/imagemin/imagemin&quot;&gt;imagemin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://imageoptim.com/mac&quot;&gt;ImageOptim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;TODO:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PNG 终极优化；&lt;/li&gt;
&lt;li&gt;Webp 相关内容；&lt;/li&gt;
&lt;li&gt;SVG 相关内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;PNG 终极优化：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.queness.com/post/2507/most-effective-method-to-reduce-and-optimize-png-images&quot;&gt;Most Effective Method to Reduce and Optimize PNG Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.smashingmagazine.com/2009/07/clever-png-optimization-techniques/&quot;&gt;Clever PNG Optimization Techniques&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优化 CSS Sprite&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;水平排列 Sprite 中的图片，垂直排列会增加图片大小；&lt;/li&gt;
&lt;li&gt;Spirite 中把颜色较近的组合在一起可以降低颜色数，理想状况是低于 256 色以适用 PNG8 格式；&lt;/li&gt;
&lt;li&gt;不要在 Spirite 的图像中间留有较大空隙。减少空隙虽然不太影响文件大小，但可以降低用户代理把图片解压为像素图的内存消耗，对移动设备更友好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不要在 HTML 中缩放图片&lt;/h3&gt;
&lt;p&gt;不要使用 `` 的 &lt;code&gt;width&lt;/code&gt;、&lt;code&gt;height&lt;/code&gt; 缩放图片，如果用到小图片，就使用相应大小的图片。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;很多 CMS 和 CDN 都提供图片裁切功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;使用体积小、可缓存的 favicon.ico&lt;/h3&gt;
&lt;p&gt;Favicon.ico 一般存放在网站根目录下，无论是否在页面中设置，浏览器都会尝试请求这个文件。&lt;/p&gt;
&lt;p&gt;所以确保这个图标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存在（避免 404）；&lt;/li&gt;
&lt;li&gt;尽量小，最好小于 1K；&lt;/li&gt;
&lt;li&gt;设置较长的过期时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;对于较新的浏览器，可以使用 PNG 格式的 favicon。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;参考链接：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://css-tricks.com/favicon-quiz/&quot;&gt;Favicons, Touch Icons, Tile Icons, etc. Which Do You Need?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图片相关补充&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设置图片的宽和高，以免浏览器按照「猜」的宽高给图片保留的区域和实际宽高差异，产生重绘。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;移动端&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;移动端优化相关内容有待进一步整理补充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a href=&quot;https://developer.yahoo.com/performance/rules.html#under25&quot;&gt;保持单个文件小于 25 KB&lt;/a&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;a href=&quot;https://developer.yahoo.com/performance/rules.html#multipart&quot;&gt;打包内容为分段（multipart）文档&lt;/a&gt;&lt;/h3&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.websiteoptimization.com/speed/tweak/psychology-web-performance/&quot;&gt;The Psychology of Web Performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>react相关面试知识点</title><link>https://nollieleo.github.io/posts/react%E7%9B%B8%E5%85%B3%E9%9D%A2%E8%AF%95%E7%9F%A5%E8%AF%86%E7%82%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E7%9B%B8%E5%85%B3%E9%9D%A2%E8%AF%95%E7%9F%A5%E8%AF%86%E7%82%B9/</guid><description>react 的生命周期，分别在哪个时候被执行   setState，哪些生命周期可以setState   函数组件和普通组件区别   什么是render props组件   fiber是什么   diff算法   VDom虚拟dom  这里，小编的理解是：虚拟DOM是真实DOM的内存表示，是一种编程...</description><pubDate>Sun, 06 Jun 2021 15:34:05 GMT</pubDate><content:encoded>&lt;h3&gt;react 的生命周期，分别在哪个时候被执行&lt;/h3&gt;
&lt;h3&gt;setState，哪些生命周期可以setState&lt;/h3&gt;
&lt;h3&gt;函数组件和普通组件区别&lt;/h3&gt;
&lt;h3&gt;什么是render props组件&lt;/h3&gt;
&lt;h3&gt;fiber是什么&lt;/h3&gt;
&lt;h3&gt;diff算法&lt;/h3&gt;
&lt;h3&gt;VDom虚拟dom&lt;/h3&gt;
&lt;p&gt;这里，小编的理解是：虚拟&lt;code&gt;DOM&lt;/code&gt;是真实&lt;code&gt;DOM&lt;/code&gt;的内存表示，是一种编程概念，一种模式。它的作用是判断&lt;code&gt;DOM&lt;/code&gt;是否改变、哪些部分需要被重新渲染。这样，不需要操纵真实的&lt;code&gt;DOM&lt;/code&gt;,同时极大的提高了&lt;code&gt;React&lt;/code&gt;的性能。&lt;/p&gt;
&lt;p&gt;虚拟&lt;code&gt;DOM&lt;/code&gt;使用&lt;code&gt;diff&lt;/code&gt;算法，当我们多次修改某一部分的内容时，首先在虚拟&lt;code&gt;DOM&lt;/code&gt;树从上至下进行同层比对（不影响真实&lt;code&gt;DOM&lt;/code&gt;），上层发生变化，下层重新渲染，直到最后修改完成，再在真实&lt;code&gt;DOM&lt;/code&gt;中渲染。&lt;/p&gt;
&lt;p&gt;使用虚拟&lt;code&gt;DOM&lt;/code&gt;的原因是，可以极大程度上减少&lt;code&gt;DOM&lt;/code&gt;节点的回流和重绘问题，节约资源，提升运行效率。&lt;/p&gt;
&lt;h4&gt;区别&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;虚拟&lt;code&gt;DOM&lt;/code&gt;不会进行重排和重绘；&lt;/li&gt;
&lt;li&gt;虚拟&lt;code&gt;DOM&lt;/code&gt;进行频繁的修改，然后一次性比较并修改真实&lt;code&gt;DOM&lt;/code&gt;中需要修改的部分，最后进行回流和重绘，有效的减少了过多&lt;code&gt;DOM&lt;/code&gt;节点回流和重绘资源消耗的问题；&lt;/li&gt;
&lt;li&gt;虚拟&lt;code&gt;DOM&lt;/code&gt;有效降低大面积（真实&lt;code&gt;DOM&lt;/code&gt;节点）的回流和重绘，因为最终与真实&lt;code&gt;DOM&lt;/code&gt;比较差异，可以局部渲染。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;React中的controlled component 和 uncontrolled component区别 （受控组件和不受控组件）&lt;/h3&gt;
&lt;h3&gt;react-router内部实现机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>js的精度运算</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E7%B2%BE%E5%BA%A6%E8%BF%90%E7%AE%97/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E7%B2%BE%E5%BA%A6%E8%BF%90%E7%AE%97/</guid><description>前言  0.1 + 0.2 是否等于 0.3 作为一道经典的面试题，已经广外熟知，说起原因，大家能回答出这是浮点数精度问题导致，也能辩证的看待这并非是 ECMAScript 这门语言的问题，今天就是具体看一下背后的原因。   数字类型  ECMAScript 中的 Number 类型使用 IEEE7...</description><pubDate>Sun, 06 Jun 2021 10:17:48 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;0.1 + 0.2 是否等于 0.3 作为一道经典的面试题，已经广外熟知，说起原因，大家能回答出这是浮点数精度问题导致，也能辩证的看待这并非是 ECMAScript 这门语言的问题，今天就是具体看一下背后的原因。&lt;/p&gt;
&lt;h2&gt;数字类型&lt;/h2&gt;
&lt;p&gt;ECMAScript 中的 Number 类型使用 IEEE754 标准来表示整数和浮点数值。所谓 IEEE754 标准，全称 IEEE 二进制浮点数算术标准，这个标准定义了表示浮点数的格式等内容。&lt;/p&gt;
&lt;p&gt;在 IEEE754 中，规定了四种表示浮点数值的方式：单精确度（32位）、双精确度（64位）、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度，也就是说，会用 64 位字节来储存一个浮点数。&lt;/p&gt;
&lt;h2&gt;浮点数转二进制&lt;/h2&gt;
&lt;p&gt;我们来看下 1020 用十进制的表示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1020 = &lt;strong&gt;1&lt;/strong&gt; * 10^3 + &lt;strong&gt;0&lt;/strong&gt; * 10^2 + &lt;strong&gt;2&lt;/strong&gt; * 10^1 + &lt;strong&gt;0&lt;/strong&gt; * 10^0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以 1020 用十进制表示就是 1020……(哈哈)&lt;/p&gt;
&lt;p&gt;如果 1020 用二进制来表示呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1020 = &lt;strong&gt;1&lt;/strong&gt; * 2^9 + &lt;strong&gt;1&lt;/strong&gt; * 2^8 + &lt;strong&gt;1&lt;/strong&gt; * 2^7 + &lt;strong&gt;1&lt;/strong&gt; * 2^6 + &lt;strong&gt;1&lt;/strong&gt; * 2^5 + &lt;strong&gt;1&lt;/strong&gt; * 2^4 + &lt;strong&gt;1&lt;/strong&gt; * 2^3 + &lt;strong&gt;1&lt;/strong&gt; * 2^2 + &lt;strong&gt;0&lt;/strong&gt; * 2^1 + &lt;strong&gt;0&lt;/strong&gt; * 2^0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以 1020 的二进制为 &lt;code&gt;1111111100&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那如果是 0.75 用二进制表示呢？同理应该是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为使用的是二进制，这里的 abcd……的值的要么是 0 要么是 1。&lt;/p&gt;
&lt;p&gt;那怎么算出 abcd…… 的值呢，我们可以两边不停的乘以 2 算出来，解法如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两边同时乘以 2&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1 + 0.5 = a * 2^0 + b * 2^-1 + c * 2^-2 + d * 2^-3... (所以 a = 1)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;剩下的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0.5 = b * 2^-1 + c * 2^-2 + d * 2^-3...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再同时乘以 2&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1 + 0 = b * 2^0 + c * 2^-2 + d * 2^-3... (所以 b = 1)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以 0.75 用二进制表示就是 0.ab，也就是 0.11&lt;/p&gt;
&lt;p&gt;然而不是所有的数都像 0.75 这么好算，我们来算下 0.1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...

0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + ...   (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + ...   (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + ...   (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + ...   (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + ...   (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + ...   (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + ...   (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + ...   (h = 1)
....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后你就会发现，这个计算在不停的循环，所以 0.1 用二进制表示就是 0.00011001100110011……&lt;/p&gt;
&lt;h2&gt;浮点数的存储&lt;/h2&gt;
&lt;p&gt;虽然 0.1 转成二进制时是一个无限循环的数，但计算机总要储存吧，我们知道 ECMAScript 使用 64 位字节来储存一个浮点数，那具体是怎么储存的呢？这就要说回 IEEE754 这个标准了，毕竟是这个标准规定了存储的方式。&lt;/p&gt;
&lt;p&gt;这个标准认为，一个浮点数 (Value) 可以这样表示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Value = sign * exponent * fraction&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看起来很抽象的样子，简单理解就是科学计数法……&lt;/p&gt;
&lt;p&gt;比如 -1020，用科学计数法表示就是:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;-1 * 10^3 * 1.02&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;sign 就是 -1，exponent 就是 10^3，fraction 就是 1.02&lt;/p&gt;
&lt;p&gt;对于二进制也是一样，以 0.1 的二进制 0.00011001100110011…… 这个数来说：&lt;/p&gt;
&lt;p&gt;可以表示为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1 * 2^-4 * 1.1001100110011……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中 sign 就是 1，exponent 就是 2^-4，fraction 就是 1.1001100110011……&lt;/p&gt;
&lt;p&gt;而当只做二进制科学计数法的表示时，这个 Value 的表示可以再具体一点变成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;V = (-1)^S * (1 + Fraction) * 2^E&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(如果所有的浮点数都可以这样表示，那么我们存储的时候就把这其中会变化的一些值存储起来就好了)&lt;/p&gt;
&lt;p&gt;我们来一点点看：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(-1)^S&lt;/code&gt; 表示符号位，当 S = 0，V 为正数；当 S = 1，V 为负数。&lt;/p&gt;
&lt;p&gt;再看 &lt;code&gt;(1 + Fraction)&lt;/code&gt;，这是因为所有的浮点数都可以表示为 1.xxxx * 2^xxx 的形式，前面的一定是 1.xxx，那干脆我们就不存储这个 1 了，直接存后面的 xxxxx 好了，这也就是 Fraction 的部分。&lt;/p&gt;
&lt;p&gt;最后再看 &lt;code&gt;2^E&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果是 1020.75，对应二进制数就是 1111111100.11，对应二进制科学计数法就是 1 * 1.11111110011 * 2^9，E 的值就是 9，而如果是 0.1 ，对应二进制是 1 * 1.1001100110011…… * 2^-4， E 的值就是 -4，也就是说，E 既可能是负数，又可能是正数，那问题就来了，那我们该怎么储存这个 E 呢？&lt;/p&gt;
&lt;p&gt;我们这样解决，假如我们用 8 位字节来存储 E 这个数，如果只有正数的话，储存的值的范围是 0 ~ 254，而如果要储存正负数的话，值的范围就是 -127~127，我们在存储的时候，把要存储的数字加上 127，这样当我们存 -127 的时候，我们存 0，当存 127 的时候，存 254，这样就解决了存负数的问题。对应的，当取值的时候，我们再减去 127。&lt;/p&gt;
&lt;p&gt;所以呢，真到实际存储的时候，我们并不会直接存储 E，而是会存储 E + bias，当用 8 个字节的时候，这个 bias 就是 127。&lt;/p&gt;
&lt;p&gt;所以，如果要存储一个浮点数，我们存 S 和 Fraction 和 E + bias 这三个值就好了，那具体要分配多少个字节位来存储这些数呢？IEEE754 给出了标准：&lt;/p&gt;
&lt;p&gt;[&lt;img src=&quot;./1.png&quot; alt=&quot;IEEE754&quot; /&gt;]&lt;/p&gt;
&lt;p&gt;在这个标准下：&lt;/p&gt;
&lt;p&gt;我们会用 1 位存储 S，0 表示正数，1 表示负数。&lt;/p&gt;
&lt;p&gt;用 11 位存储 E + bias，对于 11 位来说，bias 的值是 2^(11-1) - 1，也就是 1023。&lt;/p&gt;
&lt;p&gt;用 52 位存储 Fraction。&lt;/p&gt;
&lt;p&gt;举个例子，就拿 0.1 来看，对应二进制是 1 * 1.1001100110011…… * 2^-4， Sign 是 0，E + bias 是 -4 + 1023 = 1019，1019 用二进制表示是 1111111011，Fraction 是 1001100110011……&lt;/p&gt;
&lt;p&gt;对应 64 个字节位的完整表示就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0 01111111011 1001100110011001100110011001100110011001100110011010&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同理, 0.2 表示的完整表示是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0 01111111100 1001100110011001100110011001100110011001100110011010&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以当 0.1 存下来的时候，就已经发生了精度丢失，当我们用浮点数进行运算的时候，使用的其实是精度丢失后的数。&lt;/p&gt;
&lt;h2&gt;浮点数的运算&lt;/h2&gt;
&lt;p&gt;关于浮点数的运算，一般由以下五个步骤完成：对阶、尾数运算、规格化、舍入处理、溢出判断。我们来简单看一下 0.1 和 0.2 的计算。&lt;/p&gt;
&lt;p&gt;首先是对阶，所谓对阶，就是把阶码调整为相同，比如 0.1 是 &lt;code&gt;1.1001100110011…… * 2^-4&lt;/code&gt;，阶码是 -4，而 0.2 就是 &lt;code&gt;1.10011001100110...* 2^-3&lt;/code&gt;，阶码是 -3，两个阶码不同，所以先调整为相同的阶码再进行计算，调整原则是小阶对大阶，也就是 0.1 的 -4 调整为 -3，对应变成 &lt;code&gt;0.11001100110011…… * 2^-3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;接下来是尾数计算:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
 10.0110011001100110011001100110011001100110011001100111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们得到结果为 &lt;code&gt;10.0110011001100110011001100110011001100110011001100111 * 2^-3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;将这个结果处理一下，即结果规格化，变成 &lt;code&gt;1.0011001100110011001100110011001100110011001100110011(1) * 2^-2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;括号里的 1 意思是说计算后这个 1 超出了范围，所以要被舍弃了。&lt;/p&gt;
&lt;p&gt;再然后是舍入，四舍五入对应到二进制中，就是 0 舍 1 入，因为我们要把括号里的 1 丢了，所以这里会进一，结果变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.0011001100110011001100110011001100110011001100110100 * 2^-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本来还有一个溢出判断，因为这里不涉及，就不讲了。&lt;/p&gt;
&lt;p&gt;所以最终的结果存成 64 位就是&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0 01111111101 0011001100110011001100110011001100110011001100110100&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将它转换为10进制数就得到 &lt;code&gt;0.30000000000000004440892098500626&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因为两次存储时的精度丢失加上一次运算时的精度丢失，最终导致了 0.1 + 0.2 !== 0.3&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 十进制转二进制
parseFloat(0.1).toString(2);
=&amp;gt; &quot;0.0001100110011001100110011001100110011001100110011001101&quot;

// 二进制转十进制
parseInt(1100100,2)
=&amp;gt; 100

// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=&amp;gt; &quot;0.300000000000000044409&quot;
(0.3).toPrecision(21)
=&amp;gt; &quot;0.299999999999999988898&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>js的闭包</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E9%97%AD%E5%8C%85/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E9%97%AD%E5%8C%85/</guid><description>定义  MDN 对闭包的定义为：   闭包是指那些能够访问自由变量的函数。  那什么是自由变量呢？   自由变量是指在函数中使用的，但既不是函数参数也不是函数的局部变量的变量。  由此，我们可以看出闭包共有两部分组成：   闭包 = 函数 + 函数能够访问的自由变量  举个例子：   var a =...</description><pubDate>Sun, 06 Jun 2021 09:56:18 GMT</pubDate><content:encoded>&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;MDN 对闭包的定义为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;闭包是指那些能够访问自由变量的函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那什么是自由变量呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自由变量是指在函数中使用的，但既不是函数参数也不是函数的局部变量的变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由此，我们可以看出闭包共有两部分组成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;闭包 = 函数 + 函数能够访问的自由变量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = 1;

function foo() {
    console.log(a);
}

foo();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;foo 函数可以访问变量 a，但是 a 既不是 foo 函数的局部变量，也不是 foo 函数的参数，所以 a 就是自由变量。&lt;/p&gt;
&lt;p&gt;那么，函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……&lt;/p&gt;
&lt;p&gt;还真是这样的！&lt;/p&gt;
&lt;p&gt;所以在《JavaScript权威指南》中就讲到：从技术的角度讲，所有的JavaScript函数都是闭包。&lt;/p&gt;
&lt;p&gt;咦，这怎么跟我们平时看到的讲到的闭包不一样呢！？&lt;/p&gt;
&lt;p&gt;别着急，这是理论上的闭包，其实还有一个实践角度上的闭包，让我们看看汤姆大叔翻译的关于闭包的文章中的定义：&lt;/p&gt;
&lt;p&gt;ECMAScript中，闭包指的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从理论角度：所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此，因为函数中访问全局变量就相当于是在访问自由变量，这个时候使用最外层的作用域。&lt;/li&gt;
&lt;li&gt;从实践角度：以下函数才算是闭包：
&lt;ol&gt;
&lt;li&gt;即使创建它的上下文已经销毁，它仍然存在（比如，内部函数从父函数中返回）&lt;/li&gt;
&lt;li&gt;在代码中引用了自由变量&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来就来讲讲实践上的闭包。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;让我们先写个例子，例子依然是来自《JavaScript权威指南》，稍微做点改动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var scope = &quot;global scope&quot;;
function checkscope(){
    var scope = &quot;local scope&quot;;
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。&lt;/p&gt;
&lt;p&gt;这里直接给出简要的执行过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入全局代码，创建全局执行上下文，全局执行上下文压入执行上下文栈&lt;/li&gt;
&lt;li&gt;全局执行上下文初始化&lt;/li&gt;
&lt;li&gt;执行 checkscope 函数，创建 checkscope 函数执行上下文，checkscope 执行上下文被压入执行上下文栈&lt;/li&gt;
&lt;li&gt;checkscope 执行上下文初始化，创建变量对象、作用域链、this等&lt;/li&gt;
&lt;li&gt;checkscope 函数执行完毕，checkscope 执行上下文从执行上下文栈中弹出&lt;/li&gt;
&lt;li&gt;执行 f 函数，创建 f 函数执行上下文，f 执行上下文被压入执行上下文栈&lt;/li&gt;
&lt;li&gt;f 执行上下文初始化，创建变量对象、作用域链、this等&lt;/li&gt;
&lt;li&gt;f 函数执行完毕，f 函数上下文从执行上下文栈中弹出&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;了解到这个过程，我们应该思考一个问题，那就是：&lt;/p&gt;
&lt;p&gt;当 f 函数执行的时候，checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出)，怎么还会读取到 checkscope 作用域下的 scope 值呢？&lt;/p&gt;
&lt;p&gt;以上的代码，要是转换成 PHP，就会报错，因为在 PHP 中，f 函数只能读取到自己作用域和全局作用域里的值，所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)&lt;/p&gt;
&lt;p&gt;然而 JavaScript 却是可以的！&lt;/p&gt;
&lt;p&gt;当我们了解了具体的执行过程后，我们知道 f 执行上下文维护了一个作用域链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对的，就是因为这个作用域链，f 函数依然可以读取到 checkscopeContext.AO 的值，说明当 f 函数引用了 checkscopeContext.AO 中的值的时候，即使 checkscopeContext 被销毁了，但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中，f 函数依然可以通过 f 函数的作用域链找到它，正是因为 JavaScript 做到了这一点，从而实现了闭包这个概念。&lt;/p&gt;
&lt;p&gt;所以，让我们再看一遍实践角度上闭包的定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;即使创建它的上下文已经销毁，它仍然存在（比如，内部函数从父函数中返回）&lt;/li&gt;
&lt;li&gt;在代码中引用了自由变量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;闭包在计算机科学中也只是一个普通的概念，大家不要去想得太复杂。&lt;/p&gt;
&lt;h2&gt;必刷题&lt;/h2&gt;
&lt;p&gt;接下来，看这道刷题必刷，面试必考的闭包题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data = [];

for (var i = 0; i &amp;lt; 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案是都是 3，让我们分析一下原因：&lt;/p&gt;
&lt;p&gt;当执行到 data[0] 函数之前，此时全局上下文的 VO 为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当执行 data[0] 函数的时候，data[0] 函数的作用域链为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data[0]Context = {
    Scope: [AO, globalContext.VO]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;data[0]Context 的 AO 并没有 i 值，所以会从 globalContext.VO 中查找，i 为 3，所以打印的结果就是 3。&lt;/p&gt;
&lt;p&gt;data[1] 和 data[2] 是一样的道理。&lt;/p&gt;
&lt;p&gt;所以让我们改成闭包看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data = [];

for (var i = 0; i &amp;lt; 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当执行到 data[0] 函数之前，此时全局上下文的 VO 为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟没改之前一模一样。&lt;/p&gt;
&lt;p&gt;当执行 data[0] 函数的时候，data[0] 函数的作用域链发生了改变：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;匿名函数执行上下文的AO为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;data[0]Context 的 AO 并没有 i 值，所以会沿着作用域链从匿名函数 Context.AO 中查找，这时候就会找 i 为 0，找到了就不会往 globalContext.VO 中查找了，即使 globalContext.VO 也有 i 的值(值为3)，所以打印的结果就是0。&lt;/p&gt;
&lt;p&gt;data[1] 和 data[2] 是一样的道理。&lt;/p&gt;
</content:encoded></item><item><title>js的执行上下文以及执行上下文栈</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BB%A5%E5%8F%8A%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E6%A0%88/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BB%A5%E5%8F%8A%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E6%A0%88/</guid><description>js的执行上下文以及执行上下文栈   可执行代码  js的可执行代码（executable code）有哪些？只有可执行代码会创建执行上下文  1. 全局代码 2. 函数代码 3. eval代码   执行上下文  只有可执行代码会创建执行上下文  当执行到一个函数的时候这里会进行准备工作，  这个准...</description><pubDate>Sat, 05 Jun 2021 15:37:38 GMT</pubDate><content:encoded>&lt;h1&gt;js的执行上下文以及执行上下文栈&lt;/h1&gt;
&lt;h2&gt;可执行代码&lt;/h2&gt;
&lt;p&gt;js的可执行代码（executable code）有哪些？只有可执行代码会创建执行上下文&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;全局代码&lt;/li&gt;
&lt;li&gt;函数代码&lt;/li&gt;
&lt;li&gt;eval代码&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;执行上下文&lt;/h2&gt;
&lt;p&gt;只有可执行代码会创建执行上下文&lt;/p&gt;
&lt;p&gt;当执行到一个函数的时候这里会进行准备工作，&lt;/p&gt;
&lt;p&gt;这个准备工作用专业点的话讲就是 &lt;strong&gt;执行上下文&lt;/strong&gt;（execution context）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里一定是！函数执行的时候才会去创建执行上下文！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于每一个执行上下文，都有三个重要属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量对象（Variable object） 俗称VO&lt;/li&gt;
&lt;li&gt;作用域链（Scope chain）&lt;/li&gt;
&lt;li&gt;this&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些属性可以去对应的文章中看&lt;/p&gt;
&lt;h2&gt;执行上下文栈&lt;/h2&gt;
&lt;p&gt;js以执行上下文栈的方式去创建一个执行上下文栈，来进行各个上下文之间的管理&lt;/p&gt;
&lt;p&gt;在所有的情况下，执行js的代码，首先遇到的是全局代码这是毋庸置疑的&lt;/p&gt;
&lt;p&gt;这个时候相当于这个&lt;strong&gt;全局的执行上下文&lt;/strong&gt;就是在&lt;strong&gt;栈底&lt;/strong&gt;了&lt;/p&gt;
&lt;p&gt;首先我们先模拟一下，创建一个栈表示执行上下文栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照上面所说的，执行上下文栈底永远都有一个全局的执行上下文，这里用globalContext表示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack=[
    globalContext
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;按题分析&lt;/h2&gt;
&lt;h3&gt;解释执行上下文栈&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function fn3(){
    console.log(&apos;fn3&apos;)
}

function fn2(){
    console.log(&apos;fn2&apos;);
    fn3();
}

function fn1(){
    console.log(&apos;fn1&apos;);
    fn2();
}

fun1();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当执行一个函数的时候，就会创建一个执行上下文，并且压入执行上下文栈，当函数执行完毕的时候，就会将函数的执行上下文从栈中弹出。&lt;/p&gt;
&lt;p&gt;因此以上的代码执行原理是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//fn1()
ECStack.push(&amp;lt;fn1&amp;gt; function1Context);
// 这时候发现fun1中调用了fn2
ECStack.push(&amp;lt;fn2&amp;gt; function2Context);
// fn2中又调用了fn3
ECStack.puhs(&amp;lt;fn3&amp;gt; function3Context);
// 此时的ECStack为
// ECStack = [
// 	globalContext
//  function1Context
//	function2Context
//  function3Context
// ]

//fn3执行完了
ECStack.pop(); // function3Context
// fn2执行完了
ECStack.pop(); // function3Context
// fn1执行完了
ECStack.pop(); // function1Context

// javascript接着执行下面的代码，但是ECStack底层永远有个globalContext

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根据上下文栈以及作用域链以及VO或者AO分析&lt;/h3&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var scope = &quot;global scope&quot;;
function checkscope(){
    var scope = &quot;local scope&quot;;
    function f(){
        return scope;
    }
    return f();
}
checkscope();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;var scope = &quot;global scope&quot;;
function checkscope(){
    var scope = &quot;local scope&quot;;
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分别输出什么？&lt;/p&gt;
&lt;p&gt;对，都是local scope&lt;/p&gt;
&lt;p&gt;具体分析一下&lt;/p&gt;
&lt;p&gt;先分析第一道&lt;/p&gt;
&lt;h4&gt;分析第一道&lt;/h4&gt;
&lt;p&gt;执行过程如下&lt;/p&gt;
&lt;p&gt;1.无论如何先执行全局代码，创建全局执行上下文，全局上下文压入栈底&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [
    globalContext
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.初始化全局上下文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;globalContext = {
    VO: [global],
    Scope: [globalContext.VO],
    this: globalContext.VO
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.在第2步初始化的过程发现全局的作用域中定义了一个 checkscrope函数，这个函数的作用域链上就会先推入一个全局上下文对象中的Scope&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscope.[[scope]]=[
    globalContext.VO
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.执行 checkscope 函数，创建 checkscope 函数执行上下文，checkscope 函数执行上下文被压入执行上下文栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [
    globalContext,
    checkscopeContext,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5.checkscope函数执行上下文的初始化&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;复制函数的[[scope]]属性创建作用域链，放到checkscopeContext的[[scope]]中&lt;/li&gt;
&lt;li&gt;用arguments创建活动对象&lt;/li&gt;
&lt;li&gt;初始化活动对象，加入形参，内部函数声明，变量声明&lt;/li&gt;
&lt;li&gt;将活动对象压入 checkscope 作用域链顶端，也就是checkscope.VO&lt;/li&gt;
&lt;li&gt;函数f被声明，保存checkscope的作用域链到fn函数的[[scope]]当中&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;checkscopeContext = {
 AO:{
     arguments: {
         length: 0,
     },
     scope: undefined,
     f: refrence to function f(){}
 },
 Scope: [checkscopeContext.AO, globalContext.VO],
 this: undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	6. 执行函数f，创建f的执行上下文，f函数的执行上下文被压入栈中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack= [ 
	globalContext,
	checkscopeContext,
	fnContext
]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;f函数执行上下文初始化，和第5步一样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fnContext = {
    AO = {
    	arguments:{
    		length: 0
		}
	},
    Scope: [fnContext.AO, checkscopeContext.AO, globalContext.VO],
    this: undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;f函数执行之后，沿着&lt;strong&gt;作用域链&lt;/strong&gt;查找到scope的值，返回scope值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;f函数执行完，栈中pop出上下文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack.pop()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [
   	globalContext,
	checkscopeContext, 
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;checkscope函数执行完毕，checkscope执行上下文pop出栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack.pop()
ECStack = [
   	globalContext,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>js的作用域链</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E9%93%BE/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E9%93%BE/</guid><description>作用域链  当查找变量的时候，会先从当前上下文的变量对象中查找，如果没有找到，就会从父级(词法层面上的父级)执行上下文的变量对象中查找，一直找到全局上下文的变量对象，也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。  下面，以一个函数的创建和激活两个时期来讲解作用域链是如何创...</description><pubDate>Sat, 05 Jun 2021 15:34:14 GMT</pubDate><content:encoded>&lt;h2&gt;作用域链&lt;/h2&gt;
&lt;p&gt;当查找变量的时候，会先从当前上下文的变量对象中查找，如果没有找到，就会从父级(词法层面上的父级)执行上下文的变量对象中查找，一直找到全局上下文的变量对象，也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。&lt;/p&gt;
&lt;p&gt;下面，以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。&lt;/p&gt;
&lt;h2&gt;函数创建&lt;/h2&gt;
&lt;p&gt;函数的作用域在函数定义的时候就决定了。&lt;/p&gt;
&lt;p&gt;这是因为函数有一个内部属性 [[scope]]，当函数创建的时候，就会保存所有父变量对象到其中，你可以理解 [[scope]] 就是所有父变量对象的层级链，但是注意：[[scope]] 并不代表完整的作用域链！&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function foo() {
    function bar() {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数创建时，各自的[[scope]]为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数激活&lt;/h2&gt;
&lt;p&gt;当函数激活时，进入函数上下文，创建 VO/AO 后，就会将活动对象添加到作用链的前端。&lt;/p&gt;
&lt;p&gt;这时候执行上下文的作用域链，我们命名为 Scope：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Scope = [AO].concat([[Scope]]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，作用域链创建完毕。&lt;/p&gt;
&lt;h2&gt;捋一捋&lt;/h2&gt;
&lt;p&gt;以下面的例子为例，结合着之前讲的变量对象和执行上下文栈，我们来总结一下函数执行上下文中作用域链和变量对象的创建过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var scope = &quot;global scope&quot;;
function checkscope(){
    var scope2 = &apos;local scope&apos;;
    return scope2;
}
checkscope();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程如下：&lt;/p&gt;
&lt;p&gt;1.checkscope 函数被创建，保存作用域链到 内部属性[[scope]]&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscope.[[scope]] = [
    globalContext.VO
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.执行 checkscope 函数，创建 checkscope 函数执行上下文，checkscope 函数执行上下文被压入执行上下文栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [
    checkscopeContext,
    globalContext
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.checkscope 函数并不立刻执行，开始做准备工作，第一步：复制函数[[scope]]属性创建作用域链&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscopeContext = {
    Scope: checkscope.[[scope]],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.第二步：用 arguments 创建活动对象，随后初始化活动对象，加入形参、函数声明、变量声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    }，
    Scope: checkscope.[[scope]],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5.第三步：将活动对象压入 checkscope 作用域链顶端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6.准备工作做完，开始执行函数，随着函数的执行，修改 AO 的属性值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: &apos;local scope&apos;
    },
    Scope: [AO, [[Scope]]]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7.查找到 scope2 的值，返回后函数执行完毕，函数上下文从执行上下文栈中弹出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ECStack = [
    globalContext
];
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>js的静态作用域理解</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E9%9D%99%E6%80%81%E4%BD%9C%E7%94%A8%E5%9F%9F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E9%9D%99%E6%80%81%E4%BD%9C%E7%94%A8%E5%9F%9F/</guid><description>js的静态作用域理解  总所周知，js是词法作用域（静态作用域），函数的作用域在函数定义的时候就已经决定了。（这句话是重点，要考）     作用域    作用域是指程序源代码中定义变量的区域    作用域规定了如何查找变量，也就是确定当前执行代码对变量的访问权限。   JavaScript 采用词法...</description><pubDate>Sat, 05 Jun 2021 15:10:11 GMT</pubDate><content:encoded>&lt;h1&gt;js的静态作用域理解&lt;/h1&gt;
&lt;p&gt;总所周知，js是词法作用域（静态作用域），&lt;strong&gt;函数的作用域在函数定义的时候就已经决定了&lt;/strong&gt;。（这句话是重点，要考）&lt;/p&gt;
&lt;h2&gt;作用域&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;作用域是指程序源代码中定义变量的区域&lt;/p&gt;
&lt;p&gt;作用域规定了如何查找变量，也就是确定当前执行代码对变量的访问权限。&lt;/p&gt;
&lt;p&gt;JavaScript 采用词法作用域(lexical scoping)，也就是静态作用域。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;按题分析&lt;/h2&gt;
&lt;p&gt;如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var value = 0;
function sayHi(){
    console.log(value);
}
function hi(){
    const value = 2;
    sayHi();
}

hi();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终打印啥？&lt;/p&gt;
&lt;p&gt;对&lt;/p&gt;
&lt;p&gt;就是&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;错！&lt;/p&gt;
&lt;p&gt;是1哈哈哈哈哈&lt;/p&gt;
&lt;p&gt;为啥卧槽&lt;/p&gt;
&lt;h3&gt;执行过程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;首先可以看到全局定义了value， 声明了函数hi和sayHi&lt;/li&gt;
&lt;li&gt;这时候是最外层声明的函数，那么sayHi和hi的函数作用域在此时已经决定了！（作用域是一层层的嵌套的，具体这里内部处理方式看 js的作用域链那片文章&lt;/li&gt;
&lt;li&gt;这时候执行hi函数&lt;/li&gt;
&lt;li&gt;定义了一个value，然后什么都不做去执行sayHi&lt;/li&gt;
&lt;li&gt;这时候执行sayHi函数，要输出value，这时候会去查找他的局部是否有这个变量，发现没有&lt;/li&gt;
&lt;li&gt;这个时候往上找，这个上，很重要，就是sayHi函数在定义的时候的那个区域，这里指的就是全局&lt;/li&gt;
&lt;li&gt;全局找到了value，输出value&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;如果说是动态作用域的话，那这里执行sayHi就得看调用它是在哪个域中调用了&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>手写bind</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99bind/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99bind/</guid><description>手写bind   原理   bind() 方法会创建一个新函数。当这个新函数被调用时，bind() 的第一个参数将作为它运行时的 this，之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )  由此我们可以首先得出 bind 函数的两个特点：  1. 返回一个函数 2. 可以传...</description><pubDate>Thu, 03 Jun 2021 16:58:33 GMT</pubDate><content:encoded>&lt;h1&gt;手写bind&lt;/h1&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;bind() 方法会创建一个新函数。当这个新函数被调用时，bind() 的第一个参数将作为它运行时的 this，之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由此我们可以首先得出 bind 函数的两个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;返回一个函数&lt;/li&gt;
&lt;li&gt;可以传入参数&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;首先先在Function的原型上挂载一个自定义bind函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 模拟实现返回一个函数&lt;/h3&gt;
&lt;p&gt;正常的使用bind的时候是返回一个函数，这里返回的函数的引用(this)依旧指向的是调用bind的函数，而不是新得返回函数, 而且 考虑到绑定函数可能是有返回值 ，所以这块还是要处理下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj ={
    name: &apos;weng&apos;,
};

function sayName(){
    return this.name;
}

const a = sayName._bind(obj);
console.log(a()); //weng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以这块的this指向需要做处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function(context){
    const that = this;
    return function(){
      return that.call(context);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用that存储this，之后返回一个闭包函数&lt;/p&gt;
&lt;h3&gt;2. 传参数&lt;/h3&gt;
&lt;p&gt;在执行bind的时候，bind可以传参数，而且bind返回之后的函数也可以传参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj ={
    name: &apos;weng&apos;,
};

function sayName(age, address){
    return `${this.name}: ${age} : ${address}`;
}

const a = sayName.bind(obj, 23);

console.log(a(&apos;fujian&apos;)); // weng : 23 : fujian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里就相当于内部实现一个函数柯里化用来存储每次传进来的数据，但是没有柯里化那么牛逼，具体可以看实现函数柯里化那那篇文章&lt;/p&gt;
&lt;p&gt;具体实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function(context){
    const that = this;
    const outerArgs = Array.prototype.slice.call(arguments, 1); // 存下第一次传的参数， 截掉第一个
    return function(){
      const innerArgs = Array.prototype.slice.call(arguments);
      return that.call(context, ...outerArgs.concat(innerArgs)); // 把外层参数拼接到内层的参数类数组里头
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 构造函数的模拟实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;一个绑定函数也能使用new操作符创建对象：这种行为就像把原函数当成构造器。提供的 this 值被忽略，同时调用时的参数被提供给模拟函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说当 bind 返回的函数作为构造函数的时候，bind 时指定的 this 值会失效，但传入的参数依然生效。举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
    preName: &quot;weng&quot;,
};

function Bar(name, age) {
    this.name = name;
    this.age = age;
}

Bar.prototype.sayName = function(){
    console.log(this.name)
}

const MyBar = Bar.bind(obj, &quot;kaimin&quot;);

const o = new MyBar(12);

console.log(o);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210604110748385.png&quot; alt=&quot;image-20210604110748385&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以得知此时Bar函数的this指向的是o对象，就是相当于以new的形式调用了构造函数，这块的原理可以去 手写new那篇文章看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function (context) {
    const that = this;
    const outerArgs = Array.prototype.slice.call(arguments, 1);

    const returnFn = function () {
      const innerArgs = Array.prototype.slice.call(arguments);
      return that.call(
        this instanceof returnFn ? this : context,
        ...outerArgs.concat(innerArgs)
      );
    };
    returnFn.prototype = this.prototype;
    return returnFn;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以這行代碼来说&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  return that.call(
    this instanceof returnFn ? this : context,
    ...outerArgs.concat(innerArgs)
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当作为构造函数时，this 指向实例，此时&lt;code&gt;this instanceof returnFn&lt;/code&gt;结果为 true，将绑定函数的 this 指向该实例，可以让实例获得来自绑定函数的值
以上面的是 demo 为例，如果改成 &lt;code&gt;this instanceof returnFn? null : context&lt;/code&gt;，实例只是一个空对象，将 null 改成 this
当作为普通函数时，this 指向 window，此时结果为 false，将绑定函数的 this 指向 context&lt;/p&gt;
&lt;h3&gt;4. 构造函数效果的优化实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;但是在这个写法中，我们直接将 fBound.prototype = this.prototype，我们直接修改 fBound.prototype 的时候，也会直接修改绑定函数的 prototype。这个时候，我们可以通过一个空函数来进行中转：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function (context) {
    const that = this;
    const outerArgs = Array.prototype.slice.call(arguments, 1);

    const _Transit = function(){};

    const returnFn = function () {
      const innerArgs = Array.prototype.slice.call(arguments);
      return that.call(
        this instanceof returnFn ? this : context,
        ...outerArgs.concat(innerArgs)
      );
    };
    _Transit.prototype = this.prototype;
    returnFn.prototype = new _Transit();
    return returnFn;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里相当做了个私有变量（匿名构造函数类似的），外部是不能访问到的，所以如果后续做returnFn的原型修改，也只是改在了这个私有构造函数new出来的实例上的，这个实例的&lt;code&gt;[[Prototype]]&lt;/code&gt;指向的是真正的this原型。&lt;/p&gt;
&lt;h3&gt;5. 最后做一个日常判定this是否为函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;if (typeof this !== &quot;function&quot;) {
  throw new Error(&quot;Function.prototype.bind - what is trying to be bound is not callable&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.最终代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._bind = function (context) {
    if(typeof this !== &apos;function&apos;){
      // MDN上的
      throw new Error(&apos;Function.prototype.bind - what is trying to be bound is not callable&quot;&apos;)
    }
    const that = this;
    const outerArgs = Array.prototype.slice.call(arguments, 1);

    const _Transit = function(){};

    const returnFn = function () {
      const innerArgs = Array.prototype.slice.call(arguments);
      return that.call(
        this instanceof returnFn ? this : context,
        ...outerArgs.concat(innerArgs)
      );
    };
    _Transit.prototype = this.prototype;
    returnFn.prototype = new _Transit();
    return returnFn;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;let o = {
    address: &apos;fujian&apos;
}

function Person(name, age){
    this.name = name;
    this.age = age;
}

const NewPerson = Person._bind(o, &apos;weng&apos;);

const per1 = new NewPerson(23);

console.log(per1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终打印的是这样的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210604150133583.png&quot; alt=&quot;image-20210604150133583&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正常打印出来是这样的，差了一个标识的问题， 这个无所谓了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210604150802371.png&quot; alt=&quot;image-20210604150802371&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let o = {
    address: &apos;fujian&apos;
}

function sayHi(name, age){
    console.log(this.name = name, this.age = age, this.address);
}

const hi = sayHi.bind(o, &apos;weng&apos;);

hi(23); // weng 23 fujian
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写instanceof</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99instanceof/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99instanceof/</guid><description>手写instanceof   instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。   原理  instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。  就是找对象原型链上是否...</description><pubDate>Wed, 02 Jun 2021 17:45:33 GMT</pubDate><content:encoded>&lt;h1&gt;手写instanceof&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;instanceof&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;运算符&lt;/strong&gt;用于检测构造函数的 &lt;code&gt;prototype&lt;/code&gt; 属性是否出现在某个实例对象的原型链上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;instanceof&lt;/code&gt; 运算符用来检测 &lt;code&gt;constructor.prototype &lt;/code&gt;是否存在于参数 &lt;code&gt;object&lt;/code&gt; 的原型链上。&lt;/p&gt;
&lt;p&gt;就是找对象原型链上是否有某个构造函数的原型&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function _instanceof(obj, fn){
    let proto = obj.__proto__;
    let prototype = fn.prototype;
    while(true){
      if(prototype === null){
        return false;
      }
      if(proto === prototype){
        return true;
      }
      proto = prototype.__proto__;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先将实例的&lt;code&gt;[[Prototype]]&lt;/code&gt;指针赋值给proto对象，这里指向的是此对象构造函数的原型&lt;/p&gt;
&lt;p&gt;之后将构造函数的原型赋值给prototype，这里要排除原型指向null的情况；&lt;/p&gt;
&lt;p&gt;之后不断的循环，将原型的原型付给prototype，一层层向上查找，直到等于proto或者为null的情况下退出循环&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;Object.prototype.__proto__&lt;/code&gt;是指向null的，所以到这里还找不到，那就说明目标构造函数不在原型链上面&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function BasePerson(){}
function Person(){}

Person.prototype = new BasePerson(); // 这里使用继承

const a = new Person();

console.log(_instanceof(a, Person)) // true

console.log(_instanceof(a, BasePerson)) // true
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>手写call, apply</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99call-apply/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99call-apply/</guid><description>手写call，apply  这应该是面试最傻逼的题目了， 这类 native code ，用js怎么能实现的完美呢？？？  首先先理解一下call, apply具体做了些啥事情   原理   1. 调用形式  首先调用call或者apply的都是函数  js function Person(addr...</description><pubDate>Wed, 02 Jun 2021 11:04:59 GMT</pubDate><content:encoded>&lt;h1&gt;手写call，apply&lt;/h1&gt;
&lt;p&gt;这应该是面试最傻逼的题目了， 这类 native code ，用js怎么能实现的完美呢？？？&lt;/p&gt;
&lt;p&gt;首先先理解一下call, apply具体做了些啥事情&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;h3&gt;1. 调用形式&lt;/h3&gt;
&lt;p&gt;首先调用call或者apply的都是函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(address){
    this.name = &apos;weng&apos;;
    this.address = address;
}

const obj = {
    age: 23,
}

Person.call(obj, &apos;fujian&apos;); // 这里new

console.log(obj); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602111202474.png&quot; alt=&quot;image-20210602111202474&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 传入的参数&lt;/h3&gt;
&lt;p&gt;call和apply 的区别在于传参的写法不同： call 是一个一个参数传，而apply是以数组的形式传入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const args = [1,2,3,4];
Fn.call(obj, ...args);
Fn.apply(obj, args);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;首先先写一个_call的方法&lt;/p&gt;
&lt;p&gt;那这个_call要挂在哪里呢？对，就是Fn函数的原型上，那要怎么挂呢？直接原型挂&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;target指向对象，args就是其他参数&lt;/p&gt;
&lt;h3&gt;1. 判断是否是函数(this)来调用_call&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 判断传进来的target是否为基本数据类型&lt;/h3&gt;
&lt;p&gt;如果target是基本数据类型，是无法将函数附加到target上面的，通过instanceof可以顺着原型链找到Object的构造函数的数据类型才可以被附加函数。否者就直接把这个数据返回，因为我看call就是这样做的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
    if(!target instanceof Object){
        return target
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 对象是否是被冻结或者封闭的状态&lt;/h3&gt;
&lt;p&gt;这里的对象如果被冻结或者处于封闭的状态那么，后续调用函数给对象做赋值操作也会失效，所以这里还需要判断一层，具体的对象冻结和对象封闭可以看看这&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze&quot;&gt;Object.freeze()&lt;/a&gt; 和 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/seal&quot;&gt;Object.seal()&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
    if(!target instanceof Object){
        return target
    }
    if(Object.isFrozen(target) || Object.isSeal(target)){
        return target
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. target必须是目标对象，如果不传值，默认为window&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
    if(!(target instanceof Object){
        return target
    }
    if(Object.isFrozen(target) || Object.isSeal(target)){
        return target
    }
    target = target || window;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.  隐式绑定，改变构造函数的调用者间接改变 this 指向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
    if(!(target instanceof Object){
        return target
    }
    if(Object.isFrozen(target) || Object.isSeal(target)){
        return target
    }
    target = target || window;
    target.fn = this; // 隐式绑定，改变构造函数的调用者间接改变 this 指向
    let result = target.fn(...args)
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我看很多人用了 target.fn去将this指向这个参数，但是万一这个fn本身就在target当中声明了呢？&lt;/p&gt;
&lt;p&gt;这里我的想法是以一个符号作为键，保存这个键名，之后调用和赋值就不怕出问题了，因为声明新得符号的引用只是暂时的，也不可能存在重复的情况，之后删除也方便&lt;/p&gt;
&lt;p&gt;改善之后的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function(target, ...args){
    if(typeof this !== &apos;function&apos;){
        throw new Error(&apos;must be a function&apos;)
    }
    if(!(target instanceof Object){
        return target
    }
    if(Object.isFrozen(target) || Object.isSealed(target)){
        return target
    }
    target = target || window;
    const fn = Symbol(&apos;fn&apos;);
    target[fn] = this; // 隐式绑定，改变构造函数的调用者间接改变 this 指向
    let result = target[fn](...args);
    delete target[fn]; // 这里要把这玩意删了
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;h3&gt;正常传&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function Person(address) {
    this.name = &quot;weng&quot;;
    this.address = address;
}

let obj = {
    name: &quot;weng&quot;,
};
let arr = [1, 2, 3, 4];

let set = new Set([1, 2, 3, 4]);
let map = new Map([
    [&quot;key1&quot;, 1],
    [&quot;key2&quot;, 2],
]);

let stringObj = new String(&quot;2121&quot;);
let numberObj = new Number(1212);
let booleanObj = new Boolean(false);

Person._call(obj, &quot;fujian&quot;);
Person._call(arr, &quot;fujian&quot;);
Person._call(set, &quot;fujian&quot;);
Person._call(map, &quot;fujian&quot;);
Person._call(stringObj, &quot;fujian&quot;);
Person._call(numberObj, &quot;fujian&quot;);
Person._call(booleanObj, &quot;fujian&quot;);

console.log(obj);
console.log(arr);
console.log(set);
console.log(map);
console.log(stringObj);
console.log(numberObj);
console.log(booleanObj);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印出来的是酱紫的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602160711663.png&quot; alt=&quot;image-20210602160711663&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602161119496.png&quot; alt=&quot;image-20210602161119496&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;传基本数据类型数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const symb = Symbol(&quot;dsadas&quot;);
const num = 1212;
const str = &quot;1212&quot;;
const isNull = null;
const isUndefined = undefined;
const isBoolean = false;

Person._call(symb, &quot;fujian&quot;);
Person._call(num, &quot;fujian&quot;);

Person._call(str, &quot;fujian&quot;);
Person._call(isNull, &quot;fujian&quot;);
Person._call(isUndefined, &quot;fujian&quot;);
Person._call(isBoolean, &quot;fujian&quot;);

console.log(symb, num, str, isNull, isUndefined, isBoolean);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终打印出来的是酱紫的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602162033860.png&quot; alt=&quot;image-20210602162033860&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;传一个冻结或者封闭的对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const obj = Object.freeze({
    key: 1,
});
const obj2 = Object.seal({
    key: 2,
});

Person._call(obj, &quot;fujian&quot;);
Person._call(obj2, &quot;fujian&quot;);

console.log(obj);
console.log(obj2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印结果是这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602162721877.png&quot; alt=&quot;image-20210602162721877&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;直接调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Person.call();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那这时候作用对象在浏览器里头就是window了，构造函数里头的赋值操作可以在window对象里头体现。&lt;/p&gt;
&lt;h2&gt;最终代码&lt;/h2&gt;
&lt;p&gt;优化了一哈子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype._call = function (target, ...args) {
    if (typeof this !== &quot;function&quot;) {
      throw new Error(&quot;must be a function&quot;);
    }
    if (
      !(target instanceof Object) ||
      Object.isFrozen(target) ||
      Object.isSealed(target)
    ) {
      return target;
    }
    target = target || window;
    const fn = Symbol(&quot;fn&quot;);
    target[fn] = this; // 隐式绑定，改变构造函数的调用者间接改变 this 指向
    let result = target[fn](...args);
    delete target[fn];
    return result;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不晓得对不对反正自己感觉 &lt;strong&gt;一般&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>手写new</title><link>https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99new/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%89%8B%E5%86%99new/</guid><description>手写new操作符  要手写new的操作实现  必须先了解new的过程发生了什么  1. 创建一个新得对象（obj） 2.  将新对象的__proto__指向构造函数的prototype  3. 将构造函数的this执行这个新对象（obj） 4. 执行构造函数中的代码(为这个新对象添加属性和方法); ...</description><pubDate>Tue, 01 Jun 2021 15:27:54 GMT</pubDate><content:encoded>&lt;h1&gt;手写new操作符&lt;/h1&gt;
&lt;p&gt;要手写new的操作实现&lt;/p&gt;
&lt;p&gt;必须先了解new的过程发生了什么&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个新得对象（obj）&lt;/li&gt;
&lt;li&gt;将新对象的&lt;code&gt;__proto__&lt;/code&gt;指向构造函数的&lt;code&gt;prototype&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将构造函数的this执行这个新对象（obj）&lt;/li&gt;
&lt;li&gt;执行构造函数中的代码(为这个新对象添加属性和方法);&lt;/li&gt;
&lt;li&gt;返回这个新对象;（这里构造函数可能会显式返回值，得做处理，可以看看 构造函数返回值 那篇文章）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结就是需要实现以下的功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让实例可以访问到私有的属性&lt;/li&gt;
&lt;li&gt;实例可以访问到构造函数原型所在链上的属性（也就是原型链）&lt;/li&gt;
&lt;li&gt;针对构造函数显式返回的值做处理&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;p&gt;首先先写出一个叫做 _new的函数，类似构造函数的传参形式&lt;/p&gt;
&lt;p&gt;structFn: 函数&lt;/p&gt;
&lt;p&gt;args: 给构造函数传的参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function _new(structFn, ...args){
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.  第一个参数类型判断&lt;/h3&gt;
&lt;p&gt;structFn 指的就是构造函数，首先明白一点，new操作符后头只能够跟随函数，要先做类型判断(ES6中的class也是一样的)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function _new(structFn, ...args) {
    if (typeof structFn !== &quot;function&quot;) {
      throw &apos;structFn must be a function&apos;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建新对象，并且克隆构造函数的原型，将原型赋值给新对象的&lt;code&gt;[[Prototype]]&lt;/code&gt;指针&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function _new(structFn, ...args) {
    if (typeof structFn !== &quot;function&quot;) {
      throw &apos;structFn must be a function&apos;
    }
    let obj = new Object;
    obj.__proto__ = Object.create(structFn.prototype);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 执行构造函数中的初始化逻辑，并且将this指向新对象&lt;/h3&gt;
&lt;p&gt;这里声明res变量，将构造函数的this指向了新对象，并且将args剩余参数传入构造函数，保存执行构造函数之后返回值存在res&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function _new(structFn, ...args) {
    if (typeof structFn !== &quot;function&quot;) {
      throw &quot;structFn must be a function&quot;;
    }
    let obj = new Object();
    obj.__proto__ = Object.create(structFn.prototype);
    let res = fn.call(obj, ...args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 对构造函数执行之后的返回值做处理&lt;/h3&gt;
&lt;p&gt;因为根据构造函数的返回值情况来看，比较值得注重的就是返回一个引用类型的时候，和返回null 以及基本数据类型的时候区别，引用类型（比如new String() new Object() {} new Number() 这些的）就直接返回了，null以及基本数据类型就默认返回this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  function _new(structFn, ...args) {
    if (typeof structFn !== &quot;function&quot;) {
      throw &quot;structFn must be a function&quot;;
    }
    let obj = new Object();
    obj.__proto__ = Object.create(structFn.prototype);
    let res = fn.call(obj, ...args);
    const isObject = typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null;
    const isFunction = typeof res === &quot;function&quot;;
    return isFunction || isObject ? res : obj;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里通过isObject和isFunction来确定返回值的类型&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;首先检测一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  function _new(structFn, ...args) {
    if (typeof structFn !== &quot;function&quot;) {
      throw &quot;structFn must be a function&quot;;
    }
    let obj = new Object();
    obj.__proto__ = Object.create(structFn.prototype);
    let res = structFn.call(obj, ...args);
    const isObject = typeof res === &quot;object&quot; &amp;amp;&amp;amp; res !== null;
    const isFunction = typeof res === &quot;function&quot;;
    return isFunction || isObject ? res : obj;
  }

  function Person(name) {
    this.name = name;
  }

  const per1 = _new(Person, &apos;weng&apos;);

  const per2 = new Person(&apos;weng&apos;);

  console.log(per1);
  console.log(per2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里分别打印出使用 new操作符和_new函数的实例&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210601175133106.png&quot; alt=&quot;image-20210601175133106&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里会发现，我们自己写的_new操作函数得出的实例&lt;code&gt;[[Prototype]]&lt;/code&gt;指针的指向指向的是Person实例，通过Person实例链接到Person构造函数的原型的，这是因为这里包了一层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object.create(structFn.prototype)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于这里通过原型式继承，克隆了一份构造函数的原型对象，出发点是为了不在使用多余构造函数的情况下，实现数据共享, 原理其实如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function object(o){
    function Fn(){};
    Fn.prototype = o;
    return new Fn();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我觉得没必要，直接让，structFn.prototype也就是构造函数的原型直接等于新对象的&lt;code&gt;[[Prototype]]&lt;/code&gt;特性就行&lt;/p&gt;
&lt;p&gt;于是就改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obj.__proto__ = structFn.prototype;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后打印出其他相关的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(per1.__proto__);
console.log(per2.__proto__);
console.log(Object.getPrototypeOf(per1) === Object.getPrototypeOf(per2));
console.log(Person.prototype.isPrototypeOf(per1));
console.log(Person.prototype.isPrototypeOf(per2));

console.log(per1 instanceof Person);
console.log(per2 instanceof Person);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210602104007880.png&quot; alt=&quot;image-20210602104007880&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OK成功手写了一个new，接下来就再试试能不能写call, aplly, bind这些用c/c ++写的底层代码&lt;/p&gt;
</content:encoded></item><item><title>构造函数的返回值</title><link>https://nollieleo.github.io/posts/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC/</guid><description>构造函数的返回值  在使用new 操作符去执行构造函数的时候，构造函数的返回值会决定了最终构建出来的实例是长什么样子的  例如以下例子  js function Person(){   this.name = &apos;weng&apos;; }  const per1 = new Person();  consol...</description><pubDate>Tue, 01 Jun 2021 15:15:30 GMT</pubDate><content:encoded>&lt;h1&gt;构造函数的返回值&lt;/h1&gt;
&lt;p&gt;在使用new 操作符去执行构造函数的时候，构造函数的返回值会决定了最终构建出来的实例是长什么样子的&lt;/p&gt;
&lt;p&gt;例如以下例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(){
  this.name = &apos;weng&apos;;
}

const per1 = new Person();

console.log(per1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般情况下构造函数是不需要返回值的，默认是返回this，内部的这些行为都会指定到this&lt;/p&gt;
&lt;p&gt;但是有些面试题就要你整活，所以还是了解以下返回值对初始化后的实例的影响吧&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// return;                              // 返回 this
// return null;                         // 返回 this
// return this;                         // 返回 this
// return false;                        // 返回 this
// return &apos;hello world&apos;;                // 返回 this
// return 2;                            // 返回 this

// return {};                           // 返回 新建的 {}, person.name = undefined
// return [];                           // 返回 新建的 [], person.name = undefined
// return function(){};                 // 返回 新建的 function，抛弃 this, person.name = undefined
// return new Boolean(false);           // 返回 新建的 boolean，抛弃 this, person.name = undefined
// return new String(&apos;hello world&apos;);    // 返回 新建的 string，抛弃 this, person.name = undefined
// return new Number(32);               // 返回 新的 number，抛弃 this, person.name = undefined

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结一下，&lt;strong&gt;构造函数中直接 return 一个非引用类型值或者null，会直接返回this对象；如果返回一个复杂对象或是 new 关键字初始化的对象，会直接返回此对象，也就是引用类型值。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>继承之寄生式组合继承</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%AF%84%E7%94%9F%E5%BC%8F%E7%BB%84%E5%90%88%E7%BB%A7%E6%89%BF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%AF%84%E7%94%9F%E5%BC%8F%E7%BB%84%E5%90%88%E7%BB%A7%E6%89%BF/</guid><description>寄生式组合继承  在组合式继承的那篇文章说过，组合式继承，会将父类的构造函数调用两次。  本质上，子类原型最终式要包含超类的所有实例属性，子类的构造函数只要在执行适合重写原型就行了  再看看组合式继承的例子  js function SuperType(name){     this.name = ...</description><pubDate>Mon, 31 May 2021 16:53:22 GMT</pubDate><content:encoded>&lt;h1&gt;寄生式组合继承&lt;/h1&gt;
&lt;p&gt;在组合式继承的那篇文章说过，组合式继承，会将父类的构造函数调用两次。&lt;/p&gt;
&lt;p&gt;本质上，子类原型最终式要包含超类的所有实例属性，子类的构造函数只要在执行适合重写原型就行了&lt;/p&gt;
&lt;p&gt;再看看组合式继承的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function SuperType(name){
    this.name = name;
    this.numbers = [1,2,3,4];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个详细过程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.jpg&quot; alt=&quot;image-20210531170936146&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210531171003754.png&quot; alt=&quot;image-20210531171003754&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看出这里重复调用了SuperType函数。&lt;/p&gt;
&lt;p&gt;为了解决这个，引出了了寄生组合式继承&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;通过盗用构造函数继承属性，但使用混合式原型链继承的方法。基本思路是不通过调用父类构造函数给子类原型赋值，而是取得父类原型的一个副本。&lt;/p&gt;
&lt;p&gt;说到底就是使用寄生式继承来继承父类原型，然后将返回的新对象赋值给子类原型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function inheritPrototype(subType, superType){
    let prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 赋值对象
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;inheritPrototype函数接受两个参数，子构造函数和父类构造函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建父类原型的一个副本&lt;/li&gt;
&lt;li&gt;给返回的原型对象设置constructor属性设置为自身，解决原型覆盖丢失constructor的问题&lt;/li&gt;
&lt;li&gt;最后将新创建的对象赋值给子类型的原型&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里只调用了一次父类构造函数，避免了SubType原型上不必要的属性。&lt;/p&gt;
&lt;p&gt;而且原型键也保持不变，因此instanceof操作符和isPrototypeOf()方法正常有效。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;寄生式组合继承可以算是引用类型继承的最佳模式&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>继承之寄生式继承</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%AF%84%E7%94%9F%E5%BC%8F%E7%BB%A7%E6%89%BF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%AF%84%E7%94%9F%E5%BC%8F%E7%BB%A7%E6%89%BF/</guid><description>寄生式继承  寄生式继承背后的思路类似于寄生 构造函数和工厂模式  思路如下：  1. 创建一个实现继承的函数，这里只要是能返回对象的函数就行，例如 原型式继承的object函数（Object.create()） 2. 然后以某种方式增强对象，返回对象  例子：  js function creat...</description><pubDate>Mon, 31 May 2021 16:45:34 GMT</pubDate><content:encoded>&lt;h1&gt;寄生式继承&lt;/h1&gt;
&lt;p&gt;寄生式继承背后的思路类似于寄生 构造函数和工厂模式&lt;/p&gt;
&lt;p&gt;思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个实现继承的函数，这里只要是能返回对象的函数就行，例如 原型式继承的object函数（Object.create()）&lt;/li&gt;
&lt;li&gt;然后以某种方式增强对象，返回对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function create(o){
    let clone = object(o);
    clone.sayHi = function(){
        console.log(&apos;hello world&apos;);
    }
    return clone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;寄生式继承同样适合主要关注对象，而不在乎类型和构造函数的情况下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;寄生式继承存在问题&lt;/h1&gt;
&lt;p&gt;通过寄生式继承给对象添加函数会导致函数难以重用，与构造函数模式类似&lt;/p&gt;
</content:encoded></item><item><title>继承之原型式继承</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%8E%9F%E5%9E%8B%E5%BC%8F%E7%BB%A7%E6%89%BF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%8E%9F%E5%9E%8B%E5%BC%8F%E7%BB%A7%E6%89%BF/</guid><description>原型式继承  这是一种不涉及严格意义上构造函数的继承方式  这个思路就是，不需要自定义一个类型，就可以通过原型实现对象之间的信息共享  就仍然是为了实现对象之间的信息共享  目标函数如下  js function object(o){     function F(){}     F.prototy...</description><pubDate>Mon, 31 May 2021 16:18:56 GMT</pubDate><content:encoded>&lt;h1&gt;原型式继承&lt;/h1&gt;
&lt;p&gt;这是一种不涉及严格意义上构造函数的继承方式&lt;/p&gt;
&lt;p&gt;这个思路就是，不需要自定义一个类型，就可以通过原型实现对象之间的信息共享&lt;/p&gt;
&lt;p&gt;就仍然是为了实现对象之间的信息共享&lt;/p&gt;
&lt;p&gt;目标函数如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;object函数会创建一个 &lt;strong&gt;临时&lt;/strong&gt; 的构造函数在函数作用域内，退出函数就直接销毁的那种&lt;/li&gt;
&lt;li&gt;将传入的对象赋值给这个临时构造函数的原型，然后返回这个临时构造函数的实例&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实本质上是对这个传入的object进行一次浅复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

let person = {
    name: &quot;weng&quot;,
    numbers: [1, 2, 3, 4],
};
let person2 = object(person);
person2.name = &quot;kaimin&quot;;
person2.numbers.push(5);

let person3 = object(person);
person3.name = &quot;helloworld&quot;;
person2.numbers.push(6);

console.log(Object.getPrototypeOf(person));
console.log(Object.getPrototypeOf(person2));
console.log(Object.getPrototypeOf(person3));

console.log(person2.name);
console.log(person3.name);

console.log(person3.numbers);

console.log(person.name);
console.log(person.numbers);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是最终的打印结果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210531163110860.png&quot; alt=&quot;image-20210531163110860&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的person, person2, person3之间的数据都是共享的&lt;/p&gt;
&lt;h2&gt;Object.create()&lt;/h2&gt;
&lt;p&gt;ES5将这种原型式继承的概念规范化了。&lt;/p&gt;
&lt;p&gt;Object.create()接受两个参数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作为新对象原型的对象&lt;/li&gt;
&lt;li&gt;给新对象定义的额外属性对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有在传第一个参数的时候，Object.create与这里的object()方法一样&lt;/p&gt;
&lt;p&gt;Object.create()的第二个参数与Object.defineProperties第二个参数一样：每一个新增的属性都通过各自的描述符来描述。这种方式添加的的属性会遮蔽原型对象上的同名属性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上和原型模式一样的，只是不需要显式的去创建构造函数了。适用于需要在对象之间共享信息的场合&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>继承之组合继承</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E7%BB%84%E5%90%88%E7%BB%A7%E6%89%BF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E7%BB%84%E5%90%88%E7%BB%A7%E6%89%BF/</guid><description>组合继承（伪经典继承）  组合继承综合了原型链和盗用构造函数，将两者的优点集中起来了。  基本思路是：  1. 使用原型链继承原型上的属性和方法  2. 再通过盗用构造函数继承实例属性    如下例子：  js function SuperType(name){     this.name = na...</description><pubDate>Mon, 31 May 2021 11:14:09 GMT</pubDate><content:encoded>&lt;h1&gt;组合继承（伪经典继承）&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;组合继承&lt;/strong&gt;综合了原型链和盗用构造函数，将两者的优点集中起来了。&lt;/p&gt;
&lt;p&gt;基本思路是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用原型链继承原型上的属性和方法&lt;/li&gt;
&lt;li&gt;再通过盗用构造函数继承实例属性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function SuperType(name){
    this.name = name;
    this.numbers = [1,2,3,4];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

let instance1 = new SubType(&apos;weng&apos;, 23);
instance1.numbers.push(5);
console.log(instance1.numbers); // 1,2,3,4,5

instance1.sayName(); // weng
instance1.sayAge(); // 23

let instance2 = new SubType(&apos;helloworld&apos;, 24);
console.log(instance2.numbers); // 1,2,3,4

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用以上的方式创建的实例，可以实现自身的属性是独立的，方法可以是共享的&lt;/p&gt;
&lt;p&gt;组合继承是js中用的最多的继承模式&lt;/p&gt;
&lt;p&gt;而组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力&lt;/p&gt;
&lt;p&gt;可以执行一下语句检验一下，其实这篇文章和原型那块的例子一样，那边更解释了实例和这两个构造函数之间的关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(SubType.prototype);
console.log(SubType.prototype.isPrototypeOf(instance1));
console.log(SuperType.prototype.isPrototypeOf(instance1));
console.log(SuperType.prototype.isPrototypeOf(SubType.prototype))
console.log(Object.getPrototypeOf(instance1));
console.log(instance1.__proto__);
console.log(instance1.__proto__.constructor);
console.log(SubType.prototype.constructor);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210531160333624.png&quot; alt=&quot;image-20210531160333624&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;组合继承的问题&lt;/h1&gt;
&lt;p&gt;看以上的组合继承代码，你会发现这里的超类也就是SuperType被实例化了两次&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在SubType将其原型覆盖为SuperType构造函数new出来的实例的时候&lt;/li&gt;
&lt;li&gt;在SubType构造函数创建实例的时候，构造函数内部直接调用了SuperType，这里又初始化了一次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;造成的问题就是，SuperType创建出来的实例，也就是此时SubType的原型，会带有SuperType构造函数初始化的参数，并且，SubType构造函数创建出来的实例，也会带着SuperType的属性，这里就重复了。&lt;/p&gt;
</content:encoded></item><item><title>继承之盗用构造函数</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E7%9B%97%E7%94%A8%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E7%9B%97%E7%94%A8%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0/</guid><description>盗用构造函数（对象伪装）  为了解决原型中包含引用值的问题，引出盗用构造函数的一种方法  基本思路：在子类的构造函数中调用父类的构造函数，使用aplly()和call()方法以新创建的对象为上下文执行构造函数。  例如：  js function SuperType(){     this.numb...</description><pubDate>Mon, 31 May 2021 10:41:46 GMT</pubDate><content:encoded>&lt;h1&gt;盗用构造函数（对象伪装）&lt;/h1&gt;
&lt;p&gt;为了解决原型中包含引用值的问题，引出盗用构造函数的一种方法&lt;/p&gt;
&lt;p&gt;基本思路：在子类的构造函数中调用父类的构造函数，使用aplly()和call()方法以新创建的对象为上下文执行构造函数。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function SuperType(){
    this.numbers = [1,2,3,4];
}

function SubType(){
    SuperType.call(this);
}

let instance1 =  new SubType();
instance1.numbers.push(5);

console.log(instance1.numbers); // [1,2,3,4,5]

let instance2 = new SubType();
console.log(instance2.numbers); // [1,2,3,4];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上的盗用，即让SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这就是相当于在新的SubType对象上运行了SuperType构造函数中的所有初始化代码。结果就是每一个实例都有自己的numbers属性&lt;/p&gt;
&lt;h2&gt;1. 传递参数&lt;/h2&gt;
&lt;p&gt;相比于原型链的使用，盗用构造函数优点就是，可以在子类构造函数中向父类构造函数传参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function SuperType(name){
    this.name = name;
}

function SubType(){
    SuperType.call(this, &apos;weng&apos;);
    this.age = 23;
}

let instance = new SubType();
console.log(instance.name); // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了确保调用SuperType的时候覆盖了SubType自身定义的属性，最好先调用父函数之后再给子类添加额外属性&lt;/p&gt;
&lt;h2&gt;2. 盗用构造函数的问题&lt;/h2&gt;
&lt;p&gt;这个问题显而易见&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;也是必须再构造函数中定义方法，因此函数不能重用&lt;/li&gt;
&lt;li&gt;子类也不能访问父类原型上定义得方法，因此所有类型只能使用构造函数模式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于存在这些问题，盗用函数也不能单独使用，&lt;/p&gt;
&lt;p&gt;可以看看 继承之组合继承 这篇文章&lt;/p&gt;
</content:encoded></item><item><title>继承之原型链</title><link>https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%8E%9F%E5%9E%8B%E9%93%BE/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BB%A7%E6%89%BF%E4%B9%8B%E5%8E%9F%E5%9E%8B%E9%93%BE/</guid><description>继承之原型链  首先再明确一下构造函数，原型，实例之间关系  1. 每一个构造函数都有一个原型对象 2. 原型对象有个属性叫做constructor指回构造函数 3. 实例是用构造函数创建出来的，它有一个指针[[Prototype]]指向其构造函数的原型，也可以说是实例的原型  按照上面的说法，假设...</description><pubDate>Mon, 31 May 2021 09:53:23 GMT</pubDate><content:encoded>&lt;h1&gt;继承之原型链&lt;/h1&gt;
&lt;p&gt;首先再明确一下构造函数，原型，实例之间关系&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每一个构造函数都有一个原型对象&lt;/li&gt;
&lt;li&gt;原型对象有个属性叫做constructor指回构造函数&lt;/li&gt;
&lt;li&gt;实例是用构造函数创建出来的，它有一个指针&lt;code&gt;[[Prototype]]&lt;/code&gt;指向其构造函数的原型，也可以说是实例的原型&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;按照上面的说法，假设，如果一个原型是另一个类型的实例，那就意味着这个原型本身有一个内部指针指向他的构造函数的原型，也就是它的原型，相应的另外一个原型也有一个指针指向另外一个构造函数。所以 在实例和原型之间构造了一条原型链&lt;/p&gt;
&lt;p&gt;实现一条原型链的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

// 这里直接继承SuperType，将原型指向SuperType的实例
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}
let instance = new SubType();
console.log(instance.getSuperValue()); // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下是他们的原型链图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210531100430978.png&quot; alt=&quot;image-20210531100430978&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个例子继承的关键就是在于，将SubType的默认原型设置为了SubType的实例，这样SubType的实例不仅能从superType的实例中继承属性方法，而且还和SuperType挂钩了&lt;/p&gt;
&lt;p&gt;这个时候&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;instance内部的&lt;code&gt;[[Prototype]]&lt;/code&gt;指向SubType.prototype&lt;/li&gt;
&lt;li&gt;SubType.prototype通过内部的&lt;code&gt;[[Prototype]]&lt;/code&gt;指向了SuperType.prototype&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用或者访问实例的一个属性，这个搜索过程是逐级向上的，如果实例上没有找到属性或者方法就回去原型上找，如果还没有，就去原型的原型上找，直到一直找到&lt;code&gt;Object.prototype.__proto__&lt;/code&gt;为null的时候，就是一直找到原生对象的原型指向的原型（原型链末端）&lt;/p&gt;
&lt;p&gt;上面的例子实际的原型链是如下图所示的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210531101503489.png&quot; alt=&quot;image-20210531101503489&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;原型链存在的问题&lt;/h1&gt;
&lt;p&gt;原型链毕竟也是由原型搭起来的，这个时候就包含了原型存在的问题&lt;/p&gt;
&lt;p&gt;在原型那篇文章说过&lt;/p&gt;
&lt;h2&gt;问题一&lt;/h2&gt;
&lt;p&gt;原型中包含引用值的时候，会在实例间共享&lt;/p&gt;
&lt;p&gt;所以为什么通常一些属性是定义在构造函数中，而不是直接存在原型上&lt;/p&gt;
&lt;h2&gt;问题二&lt;/h2&gt;
&lt;p&gt;子类型在实例化的时候不能够给父类型的构造函数传参数。&lt;/p&gt;
&lt;p&gt;实际上我们还无法在不影响所有对象实例的情况下把参数传给父类的构造函数。&lt;/p&gt;
&lt;p&gt;所以原型链基本不会被单独使用&lt;/p&gt;
&lt;p&gt;这个时候就衍生出了解决原型链存在问题的几种继承方案&lt;/p&gt;
&lt;p&gt;例如 盗用构造函数 组合继承 原型式继承 寄生式继承 寄生组合式继承&lt;/p&gt;
</content:encoded></item><item><title>算法之滑动窗口</title><link>https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%97%E6%B3%95%E4%B9%8B%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/</guid><description>leetcode第209道题目    题目是      题解    js /   @param {number} target   @param {number[]} nums   @return {number}  / var minSubArrayLen = function (target, ...</description><pubDate>Sun, 30 May 2021 20:58:43 GMT</pubDate><content:encoded>&lt;p&gt;leetcode第209道题目&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-size-subarray-sum/&quot;&gt;链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目是&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210530205955854.png&quot; alt=&quot;image-20210530205955854&quot; /&gt;&lt;/p&gt;
&lt;p&gt;题解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210530210433266.png&quot; alt=&quot;image-20210530210433266&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function (target, nums) {
    let left = 0;
    let right = 0;
    let minL = Infinity;
    let sum = 0;
    while (right &amp;lt; nums.length) {
        sum += nums[right];
        while (sum &amp;gt;= target) {
            minL = Math.min(minL, right - left + 1);
            sum -= nums[left];
            left++;
            if (sum &amp;lt; target) {
                break;
            }
        }
        right++;
    }
    return minL === Infinity ? 0 : minL
};

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>创建对象之原型模式</title><link>https://nollieleo.github.io/posts/%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E4%B9%8B%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E4%B9%8B%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F/</guid><description>原型模式  每一个函数都会创建一个prototype的属性  原型方式  js function Person(){}  Person.prototype.name = &apos;weng&apos;; Person.prototype.age = 23; Person.sayName = function(){  ...</description><pubDate>Sun, 30 May 2021 13:42:33 GMT</pubDate><content:encoded>&lt;h1&gt;原型模式&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;每一个函数都会创建一个prototype的属性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原型方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(){}

Person.prototype.name = &apos;weng&apos;;
Person.prototype.age = 23;
Person.sayName = function(){
    console.log(this.name);
}
let per1 = new Person();
ler per2 = new Person();

per1.sayName(); // weng


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1.与构造函数的不同&lt;/h2&gt;
&lt;p&gt;原型模式是直接再构造函数的prototype属性上加对象和相应的属性（也就是再构造函数的原型对象上面加属性）&lt;/p&gt;
&lt;p&gt;与构造函数不同的是，使用这种原型模式定义的属性和方法，之后创建出来的实例，都是一起共享这些属性的，因此&lt;/p&gt;
&lt;p&gt;上述的per1和per2的sayName()函数指向的是同一个指针, 他们的原型也同样都是Person构造函数指向的原型对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(per1.sayName = per2.sayName);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 理解原型&lt;/h2&gt;
&lt;p&gt;自定义构造函数的时候，原型对象，也就是构造函数的原型对象（prototype指向的对象），会自动获得一个constructor的属性，这个属性是重新指向自定义的构造函数，如果没有父类的话，其他的所有方法都是继承于js内部的Object对象的所有方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Person.prototype.constructor === Person; // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，根据上述的Person自定义构造函数创建一个实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let per1 = new Person()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个过程是这样的&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;per1实例（对象）内部的[[Prototype]]指针就会被赋值为Person构造函数的原型对象（也就是Person.prototype）&lt;/li&gt;
&lt;li&gt;per1可以访问Person的原型上所有属性&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;js脚本中是没有访问[[Prototype]]特性的标准方式，但是谷歌浏览器火狐还有safari会在每一个对象上暴露&lt;code&gt;__proto__&lt;/code&gt;属性，这个属性可以直接访问构造函数的原型也就是这个实例的原型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;但是，实例是和构造函数没有直接的联系的，只和构造函数的原型又关联&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;console.log(Person.prototype === per1.__proto__); // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Object的原型的原型是null&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__.constructor === Object); //true
console.log(Person.prototype.__proto__.__proto__ === null);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210530153200440.png&quot; alt=&quot;1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图可以看出实例，构造函数，构造函数的原型之间的关系&lt;/p&gt;
&lt;p&gt;这里比较&lt;strong&gt;重要&lt;/strong&gt;的点在于&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造函数的原型（Person.prototype）的constructor属性是指回Person构造函数的&lt;/li&gt;
&lt;li&gt;实例和构造函数之间没有直接联系&lt;/li&gt;
&lt;li&gt;所有通过同一个构造函数构建出来的实例，其中&lt;code&gt;[[Prototype]]&lt;/code&gt;指针是指向构造函数的原型。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 检查实例原型&lt;/h2&gt;
&lt;h3&gt;1. instanceof&lt;/h3&gt;
&lt;p&gt;由实例直接调用，去判断实例的原型链中是否有&lt;strong&gt;某个构造函数的原型&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里一定要注意，是判断某个构造函数的原型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;console.log(per1 instanceof Person); // true
console.log(per1 instanceof Object); // true;
console.log(Person.prototype instanceof Object); // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如第二大点的图1可以看清这样的继承关系，继承关系另一篇文章再说&lt;/p&gt;
&lt;h3&gt;2. isPrototypeOf&lt;/h3&gt;
&lt;p&gt;这个方法是为了确定两个对象之间原型的关系&lt;/p&gt;
&lt;p&gt;因为如第2大点所说，不是所有的实现都对外暴露的&lt;code&gt;[[Prototype]]&lt;/code&gt;的指针&lt;/p&gt;
&lt;p&gt;所以可以直接在原型上去调用这个方法检测某个对象是否的&lt;code&gt;[[Prototype]]&lt;/code&gt;的指针是否指向它，因此也能够检测出两个对象之间的关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Person.prototype.isPrototypeOf(per1)); // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Object.getPrototypeOf()&lt;/h3&gt;
&lt;p&gt;这个方法会返回参数的内部&lt;code&gt;[[Prototype]]&lt;/code&gt;的指针（这个&lt;code&gt;[[Prototype]]&lt;/code&gt;也其实就是参数的特性，可以去一篇叫做对象的属性特性详解的文章查看）&lt;/p&gt;
&lt;p&gt;这个函数传入一个 实例， 返回这个实例的原型对象，也就是它的构造函数的原型对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Object.getPrototypeOf(per1)===Person.prototype);
Object.getPrototypeOf(per1).name; // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. *Object.setPrototypeOf()&lt;/h3&gt;
&lt;p&gt;这个方法可以给实例的&lt;code&gt;[[Prototype]]&lt;/code&gt;指针写入一个新的值，这样可以重写一个实例的原型继承关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = {
    name: &apos;weng&apos;;
    age:23
};
let b = {
    name: &apos;kaimin&apos;
}

Object.setPrototypeof(b, a);

console.log(b.name) // kaimin
console.log(b.age) // 23
console.log(Object.getPrototypeOf(b) === a); // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里不推荐使用这种覆盖实例原型指向的方式改变原型继承的关系，会严重的影响代码的性能。&lt;/p&gt;
&lt;p&gt;Mozilla文档中说的“在所有浏览器和js的引擎中，修改继承关系的影响都是微妙而且深远的。这种影响并不是执行以上这个代码那么简单，而是会涉及到所有访问了那些修改过 [[Prototype]]指向的实例的代码”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为有上述的弊端，可以看另外一篇文章，继承之原型式继承 中讲到的创建新对象的形式，也就是类似于&lt;code&gt;Object.create()&lt;/code&gt;来创建一个新对象，同时给他指定一个原型，这块继承会在继承文章中详细解释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = {
    name: &apos;weng&apos;;
    age:23
};

let b = Object.create(a);
b.name = &apos;kaimin&apos;;
console.log(b.name) // kaimin
console.log(b.age) // 23
console.log(Object.getPrototypeOf(b)) // a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Object.create这种形式其实是以匿名构造函数的形式，指定这个匿名函数的原型为你想要的那个实例，因为这里咱们只是在乎这个新创建的实例（这里指的是b）他的&lt;code&gt;[[Prototype]]&lt;/code&gt;的指向是否是指向所需对象（这里指的是a）的，完全可以跳过显示创建构造函数的形式，使用这个方法不仅仅不会改变原来实例（这里指的是a）的&lt;code&gt;[[Prototype]]&lt;/code&gt;指向，而且又让新实例的原型指向了a，就很棒，这里具体还是去继承那边文章看&lt;/p&gt;
&lt;h2&gt;4.原型的层级&lt;/h2&gt;
&lt;p&gt;例如以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function BasePerson(){
    this.country = &apos;China&apos;;
    this.age = 23;
}

function Person(){
    this.name = &apos;weng&apos;;
}

Person.prototype = new BasePersn();

const per1 = new Person();
per1.address = &apos;fujian&apos;;

console.log(per1.age) // 23
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码的各个关系如下图所示；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210530163634192.png&quot; alt=&quot;image-20210530163634192&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里比较重要的是&lt;code&gt;Person.prototype&lt;/code&gt;，这里手动给他赋值给了new BasePerson()构造出来的实例，后面又手动的给把他的constructor属性赋值给了Person，这里为什么呢？本文最后会说明&lt;/p&gt;
&lt;p&gt;之后呢，Person的原型对象中有了BasePerson构造函数内部初始化的时候的一些参数&lt;/p&gt;
&lt;p&gt;按照这上面的层级关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;per1.name; // weng
per1.address; // fujian
per1.age; // 23
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实都是可以访问到的&lt;/p&gt;
&lt;p&gt;在通过实例访问属性的时候，会按这个属性的名称开始搜索，一层层往上走，一开始搜索实例本身是否有这些属性&lt;/p&gt;
&lt;p&gt;，如果有就返回，如果没有继续通过 &lt;code&gt;[[Prototype]]&lt;/code&gt;的指针向上搜索原型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;constructor只存在于原型对象上面，其实也可以通过访问实例的原型指针来访问这个constructor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果有存在同名属性的话，实力上创建的会覆盖原型上的，但不至于改变原型上的同名属性，只是给他盖住看不见了&lt;/p&gt;
&lt;h3&gt;1.删除实例中的属性&lt;/h3&gt;
&lt;p&gt;delete操作符号可以删除实例上面定义的属性值，调用时候，这个实例的原型上的值不会被删除&lt;/p&gt;
&lt;p&gt;要删除的话得直接在原型上做操作&lt;/p&gt;
&lt;p&gt;例如上面的代码构建的实例per1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete per1.address;
console.log(per1.address) // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过删除实例上的属性，可能会暴露出原型上的同名属性，这里的话具体而论，那怎么判断这个属性是在原型上还是在实例上的呢？&lt;/p&gt;
&lt;h3&gt;2. 判断属性在原型上还是在实例上&lt;/h3&gt;
&lt;h4&gt;1. hasOwnProperty()&lt;/h4&gt;
&lt;p&gt;这个方法可以用来判断某个属性是否在实例上面（这个函数不进入实例的原型中去搜索指定属性）&lt;/p&gt;
&lt;p&gt;这个方法直接在实例上调用，继承至Object构造函数的原型对象，如果害怕实例的中有同名方法的话可以使用Object.prototype.hasOwnProperty.call(this)来调用&lt;/p&gt;
&lt;p&gt;比如上面的per1实例在没有删除addrss的情况下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;per1.hasOwnProperty(&apos;address&apos;); // true;
per1.hasOwnProperty(&apos;name&apos;); // true;

per1.hasOwnProperty(&apos;age&apos;); // false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;per1.age实际上返回的是这个实例原型上的age，hasOwnProperty是访问不到的&lt;/p&gt;
&lt;p&gt;那如果要确定是否原型上有这个值呢？不用per1[&apos;....&apos;]来判断&lt;/p&gt;
&lt;h4&gt;2. in&lt;/h4&gt;
&lt;p&gt;in操作符有两种使用方式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在for-in中使用&lt;/li&gt;
&lt;li&gt;单独使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里先说说单独使用in，下面得第5点说for-in&lt;/p&gt;
&lt;p&gt;in操作符号可以通过对象访问指定属性时候返回true，&lt;strong&gt;无论是在原型上还是在实例上面，都可以访问的到&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如上面的per1对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;name&apos; in per1); // true
console.log(&apos;address&apos; in per1); // true
console.log(&apos;age&apos; in per1); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 确定实例的某个属性不在实例上，只在原型上&lt;/h3&gt;
&lt;p&gt;结合第4大点的第二小点的两个操作符，可以确定一个函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) &amp;amp;&amp;amp; (name in object);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 获取实例的属性&lt;/h3&gt;
&lt;h4&gt;1. Object.keys()获取实例上的属性（仅仅是实例上可枚举属性）&lt;/h4&gt;
&lt;h4&gt;2. for-in遍历实例以及其原型上的所有属性（仅仅是可枚举属性）&lt;/h4&gt;
&lt;h4&gt;3.Object.getOwnPropertyNames()获取仅仅是实例上的属性（是否可枚举都能获取）&lt;/h4&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;正常来说，一个实例的原型对象上的constructor属性都是不可以枚举的，是不能够通过1，2两个方法获取的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // [...., &apos;constructor&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. Object.getOwnPropertySymbols() 仅仅针对符号针对实例属性（是否枚举都可以）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let k1 = Symbol(&apos;k1&apos;);
let o = {
    [k1]: &apos;helloworld&apos;
}
console.log(o);
// [Symbol(k1)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 属性的枚举顺序&lt;/h2&gt;
&lt;p&gt;以上可以总结几种获取实例或者实例原型上的一些属性的方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;for-in&lt;/li&gt;
&lt;li&gt;Object.keys()&lt;/li&gt;
&lt;li&gt;Object.getOwnPropertyNames()&lt;/li&gt;
&lt;li&gt;Object.getOwnPropertySymbols();&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里涉及到获取属性的顺序，还有一个Object.assign这类浅层复制对象的方式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;for-in以及Object.keys()获取到的属性值的顺序是确定的&lt;/strong&gt;，取决于js的引擎&lt;/p&gt;
&lt;p&gt;Object.getOwnPropertyNames()，Object.getOwnPropertySymbols(); 以及Object.assign得出来的属性的枚举顺序都是确定的。&lt;/p&gt;
&lt;p&gt;按照以下规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先升序枚举数值键&lt;/li&gt;
&lt;li&gt;以插入顺序枚举字符串和符号键&lt;/li&gt;
&lt;li&gt;对象字面量中定义的键，以逗号分隔的顺序插入&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let k1 = Symbol(&apos;k1&apos;), k2 = Symbol(&apos;k2&apos;);
let o = {
    1: 1,
    first:&apos;first&apos;,
    [k1]:&apos;hello&apos;,
    second:&apos;second&apos;,
    0:0
}
o[k2] = &apos;world&apos;;
o[3] = 3;
o.third = &apos;third&apos;;
o[2] = 2;

console.log(Object.getOwnPropertyNames(0));
// [&quot;0&quot;,&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;first&quot;,&quot;second&quot;,&quot;third&quot;];

console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1),Symbol(k2)];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 原型中存在的问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;弱化了向构造函数传递初始化参数的能力，会导致所有实例默认都取得相同的属性值&lt;/li&gt;
&lt;li&gt;原型上引用值属性的问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一点是显而易见的&lt;/p&gt;
&lt;p&gt;第二点中举个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(){}
Person.prototype.arr = [1,2,3,4];
let per1 = new Person();
let per2 = new Person();
per1.arr.push(5);

console.log(per2.arr) // [1,2,3,4, 5];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引用类型的arr定义在原型上，这时候对per1上的arr属性进行修改，因为per1实例上不存在arr的属性，那么会找到原型中的arr，这时候通过arr.push是直接作用在原型中的arr的，所以，引用类型属性之间的共享特性导致per2.arr访问的也是原型上的arr，意思就是引用类型访问的都是一个指针，就特么和对象一样。&lt;/p&gt;
&lt;h1&gt;本篇文章中留下来的疑问&lt;/h1&gt;
&lt;h2&gt;1. constructor手动赋值的情况&lt;/h2&gt;
&lt;p&gt;以上第4大点中的层级关系代码中，手动将原型中的constructor赋值给了Person&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function BasePerson(){
    this.country = &apos;China&apos;;
    this.age = 23;
}

function Person(){
    this.name = &apos;weng&apos;;
}

Person.prototype = new BasePersn();
Person.prototype.constructor = Person;

const per1 = new Person();
per1.address = &apos;fujian&apos;;

console.log(per1.age) // 23
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为在这个例子中，Person.prototype被手动设置为一个BasePerson构建函数构建出来的新实例，这个过程相当于重写了Person构造函数的原型，这样重写之后，Person.prototype就不再指向自身的Person，由本文开头说的一样，函数构建的时候会默认创建原型，也就是prototype对象，也会自动给原型的contructor赋值，这个写法完全覆盖了默认的prototype，造成了constructor不再指向自身的构造函数，而是指向了Object构造函数Object(){}&lt;/p&gt;
&lt;p&gt;这个时候就不在能够通过constructor属性来识别是什么类型了，还是得用instanceof&lt;/p&gt;
&lt;p&gt;再比如下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(){
    
}
let f = new Person();
Person.prototype = {
    name:&apos;weng&apos;,
    sayName(){
        console.log(this.name);
    }
}

f.sayName(); // 报错，sayName is not a function

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是为什么呢？&lt;/p&gt;
&lt;p&gt;由于f实例是在重写Person原型之前就已经构建出来了的，它的[[Prototype]]指针指向的原型对象是一开始Person构造函数所指向的，那本身就不存在sayName这个方法，这时候Person的原型被覆盖了，和f一点关系都没有；&lt;/p&gt;
&lt;p&gt;这就解释了为什么不能用实例访问constructor来判断类型标识了得用intanceof&lt;/p&gt;
&lt;p&gt;那这时候来解决上面手动赋值constructor的问题&lt;/p&gt;
&lt;p&gt;如果constructor的值很重要，可以在上述代码中加入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(){
    
}
let f = new Person();
Person.prototype = {
    constructor: Person,
    name:&apos;weng&apos;,
    sayName(){
        console.log(this.name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可&lt;/p&gt;
&lt;p&gt;但是又存在个问题&lt;/p&gt;
&lt;p&gt;咱们知道原型上的constructor的属性是不可枚举的，也就是constructor本身这个属性的特性&lt;code&gt;[[Enumberable]]&lt;/code&gt;特性是false，但是这样定义的constructor属性是可以枚举的，那这个时候可以看看 那篇 对象属性特性的文章，然后定义这个属性&lt;/p&gt;
</content:encoded></item><item><title>创建对象之构造函数模式</title><link>https://nollieleo.github.io/posts/%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E4%B9%8B%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E4%B9%8B%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E6%A8%A1%E5%BC%8F/</guid><description>ES中的构造函数是用来创建特点类型的对象的，像Object和Array这样的原生构造函数，直接再环境中能用。  当然也可以自定义构造函数  就得自定义属性和方法了   定义构造函数  例如下面是一个工厂模式的函数，将他改造成构造函数形式  js function createPerson(name,...</description><pubDate>Fri, 28 May 2021 21:10:48 GMT</pubDate><content:encoded>&lt;p&gt;ES中的构造函数是用来创建特点类型的对象的，像Object和Array这样的原生构造函数，直接再环境中能用。&lt;/p&gt;
&lt;p&gt;当然也可以自定义构造函数&lt;/p&gt;
&lt;p&gt;就得自定义属性和方法了&lt;/p&gt;
&lt;h2&gt;定义构造函数&lt;/h2&gt;
&lt;p&gt;例如下面是一个工厂模式的函数，将他改造成构造函数形式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function createPerson(name, age){
    let o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
        console.log(this.name);
    }
    return o;
}

// 改成构造函数

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name)
    }
}
let per1 = new Person(&apos;weng&apos;, 20);
per1.sayName(); // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以这样定义和new构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let Person = function(){
    .....
}

let person1 = new Person; // 不传参可以不加括号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然构造函数也是函数，调用方式也很多&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person(&apos;weng&apos;, &apos;23&apos;); // 这种也可以，但是这样构建出来的实例会被添加到window上去
window.sayName() // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在另一个对象作用域中调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let o = new Object();
Person.call(o, &apos;weng&apos;, 23);
o.sayName() // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里要记住，调用一个函数没有明确的指定this的情况下（即没有作为对象的方法调用，或没有使用call或者apply转换this指向的话，this始终指向Global对象（浏览器中式window）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;构造函数与工厂模式区别&lt;/h2&gt;
&lt;p&gt;在上面例子中只是用Person()构造函数替代了createPerson()工厂函数。实际上内部是和createPerson一样的。&lt;/p&gt;
&lt;p&gt;区别在于&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有显示的去new一个object&lt;/li&gt;
&lt;li&gt;属性和方法都直接赋值了this&lt;/li&gt;
&lt;li&gt;没有return&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 以new操作符去调用构造函数&lt;/h3&gt;
&lt;p&gt;会执行以下操作&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在内存中创建一个新对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构造函数内部的this被赋值为这个新对象（就this指向了第一步的新对象）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行构造函数内部代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果构造函数返回非空对象，则返回该对象；否者返回刚创建的新对象&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里如果返回值是一个非空对象，那这个对象的行为会默认覆盖构造函数在第四步对创建的内存对象进行操作得出的行为，如果返回其他类型值是不会的；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这块设计到原型和继承方面的东西，在原型继承那边文章可以看&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 自定义构造函数确保实例被标识为特定类型&lt;/h3&gt;
&lt;p&gt;相比于工厂模式，这是一个很大的好处&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const person1 = new Person();

console.log(person1.contructor == Person); // true
console.log(person1 instanceof Person) // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建出来的person1的protype会指向它的原型对象，这个原型对象里头有个属性叫做contructor是回指向Person构造函数的，可以用person.contructor和person1 instanceof Person来判断&lt;/p&gt;
&lt;p&gt;相当于有个标识，告诉这个实例是哪个构造函数创建出来的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但是一般来说用instanceof来判断对象类型是更可靠的方式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;构造函数的问题&lt;/h2&gt;
&lt;p&gt;问题很明显&lt;/p&gt;
&lt;p&gt;就是在实例上定义的方法会在创建每个实例的时候都会再去创建一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let per1 = new Person();
let per2 = new Person();
console.log(per1.sayName === per2.sayName) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为都是调用同一种行为的函数，可能有些时候参数不同，但是行为一样的就没必要再去创建一个&lt;/p&gt;
&lt;p&gt;这个时候会考虑把公共函数提出去到外部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function sayName(){
    console.log(this.name)
}
function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样解决了不重复创建new Function的问题，因为每个实例sayName指针都是指向外部同一个函数；&lt;/p&gt;
&lt;p&gt;但是这样缺陷也很明显啊&lt;/p&gt;
&lt;p&gt;全局作用域乱了，如果内部有很多方法，那不得再全局再创建多个函数&lt;/p&gt;
&lt;p&gt;所以这时候需要原型模式，另一篇文章看&lt;/p&gt;
</content:encoded></item><item><title>js的类型转换</title><link>https://nollieleo.github.io/posts/js%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/</guid><description>自我检测    js [] == ![] // - true   D部分有答案   显式类型转换和隐式类型转换  强制转换经常发生在动态类型语言运行时。我们经常会写类型转换，如：    js var a=1  var b=a+&apos;&apos; // 隐式 &apos;1&apos; var c=String(a) // 显式 &apos;1...</description><pubDate>Fri, 28 May 2021 10:55:50 GMT</pubDate><content:encoded>&lt;h2&gt;自我检测&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;[] == ![] // -&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;D部分有答案&lt;/p&gt;
&lt;h2&gt;显式类型转换和隐式类型转换&lt;/h2&gt;
&lt;p&gt;强制转换经常发生在动态类型语言运行时。我们经常会写类型转换，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a=1 
var b=a+&apos;&apos; // 隐式 &apos;1&apos;
var c=String(a) // 显式 &apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的隐式和显式是相对于开发者而言的。可以从代码中看出来类型转换的是显式，反则为隐式。&lt;/p&gt;
&lt;h2&gt;A.抽象值操作&lt;/h2&gt;
&lt;h3&gt;1.ToString&lt;/h3&gt;
&lt;p&gt;非字符串-&amp;gt;字符串。&lt;/p&gt;
&lt;h4&gt;基本类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;null -&amp;gt; &apos;null&apos;
undefined -&amp;gt; &apos;undefined&apos;
true -&amp;gt; &apos;true&apos;
1 -&amp;gt; &apos;1&apos;
1 * 1 000 000 000 000 000 000 000 -&amp;gt; &apos;1e+21&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;复杂类型&lt;/h4&gt;
&lt;p&gt;当对象有自己的&lt;code&gt;toString()&lt;/code&gt;方法，字符串化时就会调用该方法，使用其返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj={
    a:&apos;test&apos;,
    toString(){
        return &apos;yeah~~&apos;
    }
}
//没有自定义的toString()方法应该返回[object Object]111，
console.log(obj+&apos;111&apos;) // yeah~~111
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;JSON字符串化&lt;/h4&gt;
&lt;p&gt;对于大多数简单值来说，&lt;code&gt;JSON.stringify()&lt;/code&gt;和&lt;code&gt;toString()&lt;/code&gt;的效果基本相同，序列化的结果总是字符串。有一个比较特殊的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JSON.stringify(&apos;hello&apos;) // &quot;&quot;hello&quot;&quot;  含有双引号的字符串
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于undefined、function、symbol来说会返回undefined，在数组中返回null、在对象中自动忽略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JSON.stringify(undefined) // undefined
JSON.stringify(function(){}) // undefined
JSON.stringify([function(){},2]) // &quot;[null,2]&quot;
JSON.stringify({a:function(){},b:2}) // &quot;{&quot;b&quot;:2}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const obj={
    a:&apos;test&apos;,
    toJSON(){
        return &apos;yeah~~&apos;
    }
}
console.log(JSON.stringify(obj))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;yeah~~&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.ToNumber&lt;/h3&gt;
&lt;h4&gt;基本类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;true -&amp;gt; 1
false -&amp;gt; 0
undefined -&amp;gt; NaN
null -&amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理字符串失败时返回NaN。&lt;/p&gt;
&lt;h4&gt;复杂类型&lt;/h4&gt;
&lt;p&gt;对象（包括数组），先被转换为相应的基本类型值，如果返回的是非数字的基本类型值，则按照上面的规则强制转换为数字。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将值转换为相应的基本类型值，先检查该值是否有valueOf()方法，有并且返回基本类型值，则使用该值进行强制类型转换；没有就使用toString()的返回值进行强制转换。如果以上都不返回基类型值，产生TypeError错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const obj={
    toString(){
        return &apos;1&apos;
    }
}
console.log({}) // NaN
console.log(Number(obj)) // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;使用&lt;code&gt;Object.create(null)&lt;/code&gt;创建的对象，无法进行强制转换！是因为其&lt;code&gt;[[Prototype]]&lt;/code&gt;为空，没有&lt;code&gt;valueOf()&lt;/code&gt;和&lt;code&gt;toString()&lt;/code&gt;方法。&lt;/p&gt;
&lt;h3&gt;3.ToBoolean&lt;/h3&gt;
&lt;h5&gt;假值（falsy value）&lt;/h5&gt;
&lt;p&gt;js中的值可被分为两类：可被强制转换为false的值，和其他（可以被强制转换为true的值）。&lt;/p&gt;
&lt;p&gt;以下这些为假值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;undefined
null
fasle
+0 -0 NaN
&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然没有明确规定，我们可以默认除了这些值以外的所有值为真值。&lt;/p&gt;
&lt;h2&gt;B.显式强制类型转换&lt;/h2&gt;
&lt;h3&gt;字符串和数字之间的显式转换&lt;/h3&gt;
&lt;p&gt;一般通过&lt;code&gt;String()&lt;/code&gt;和&lt;code&gt;Number()&lt;/code&gt;这两个内建函数实现的。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String(1) // &quot;1&quot;
Number(&apos;1.25&apos;) // 1.25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过一元运算符以及&lt;code&gt;toString()&lt;/code&gt;也被认为是显示强制类型转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+&apos;25&apos; // 25
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;日期显示转换为数字&lt;/h4&gt;
&lt;p&gt;一元运算符有一个常用的用途是，将Date对象强制转换为Unix时间戳，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+new Date() // 1516625381333
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也可以使用更显式的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Date().getTime() // 1516625518125
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最好还是使用&lt;code&gt;Date.now()&lt;/code&gt;来获得当前的时间戳。&lt;/p&gt;
&lt;h4&gt;位操作符~&lt;/h4&gt;
&lt;p&gt;~运算符，按位非，反转操作符的比特位。位操作符会强制操作数使用32位格式，通过ToInt32实现（ToInt32先执行ToNUmber强制转换，之后再执行ToInt32）。如果你不太明白他的运算机制，请记住一个公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~4 -&amp;gt; -5
~x  =&amp;gt;  -(x+1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;~在日常开发中很少会用到，但在我们处理&lt;code&gt;indexOf()&lt;/code&gt;时，可以将结果强制转换为真/假值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const str=&apos;hello&apos;
str.indexOf(&apos;a&apos;) // -1
~str.indexOf(&apos;a&apos;) //0  -&amp;gt; 假值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;~~x还可以用来截除小数部分，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~~-22.8 -&amp;gt; -22
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;显式解析数字字符串&lt;/h3&gt;
&lt;h4&gt;解析和转换的区别&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;parseInt()&lt;/code&gt;将字符串解析为数字，它与&lt;code&gt;Number&lt;/code&gt;的作用并不一样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;parseInt只能解析字符串，传入其他类型参数，如true、function(){}等，返回NaN。&lt;/li&gt;
&lt;li&gt;parseInt可以解析含有非数字字符的字符串，如&lt;code&gt;parseInt(&apos;2px&apos;)&lt;/code&gt;将会解析为2，Number则会返回NaN。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于parseInt有一个经典的例子，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parseInt(1/0,19) -&amp;gt; 18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为1/0为Infinity，先被转化为字符串&lt;code&gt;&apos;Infinity&apos;&lt;/code&gt;，第一个字符为i，在js中有效数字为09和0i，所以之后的n不会被解析，只解析到i为止，i为第18位，所以输出为18.&lt;/p&gt;
&lt;h3&gt;显式转换为布尔值&lt;/h3&gt;
&lt;p&gt;和上面的Number(),String()一样，Boolean()为显式的ToBoolean强制类型转换。但这个在开发中并不常用，通常使用&lt;code&gt;!!&lt;/code&gt;来进行强制类型转换。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;if()...&lt;/code&gt;上下文中，如没有使用&lt;code&gt;Boolean()&lt;/code&gt;或&lt;code&gt;!!&lt;/code&gt;转成布尔值，则会进行隐式转换。但还是建议使用显式转换，让代码可读性更高。&lt;/p&gt;
&lt;h2&gt;C.隐式强制类型转换&lt;/h2&gt;
&lt;h3&gt;1.字符串和数字之间的隐式转换&lt;/h3&gt;
&lt;h4&gt;+/-操作符&lt;/h4&gt;
&lt;p&gt;+如何判断是进行字符串拼接，还是数值加法呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;+的其中一个操作符为字符串（或是通过ToPrimitive抽象操作后转换为字符串的值）则进行字符串拼接，否则执行数字加法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以，通常上我们将空字符串与数值进行拼接，将其转换为字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a=&apos;2&apos;
const b=a-0
b // -&amp;gt; 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;-&lt;/code&gt;也可将a强制转换为数字，或者使用&lt;code&gt;a*1&lt;/code&gt;或&lt;code&gt;a/1&lt;/code&gt;，因为这两个运算符只适用于数字，所以比较少见。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a=[1]
const b=[3]
a-b // -&amp;gt; -2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.隐式类型转换为布尔值&lt;/h3&gt;
&lt;p&gt;在以下情况中，非布尔值会被隐式转换为布尔值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;if()中的判断表达式&lt;/li&gt;
&lt;li&gt;for(;;)中的条件判断表达式&lt;/li&gt;
&lt;li&gt;while(...)和do..while(..)循环中的条件表达式&lt;/li&gt;
&lt;li&gt;? : 中的条件判断表达式&lt;/li&gt;
&lt;li&gt;逻辑运算符 || 和 &amp;amp;&amp;amp; 左边的操作数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但&amp;amp;&amp;amp;和||返回的值并不一定是布尔值，而是两个操作书中其中的一个。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;123||&apos;hello&apos; // 123
42&amp;amp;&amp;amp;&apos;abc&apos; // &apos;abc&apos;
null || &apos;hello&apos; // -&amp;gt;&apos;hello&apos;
null &amp;amp;&amp;amp; &apos;hello&apos; // -&amp;gt;null
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.Symbol的强制类型转换&lt;/h3&gt;
&lt;p&gt;ES6允许从符号到字符串得显示类型转换，但使用隐式转换会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const s1=Symbol(&apos;test&apos;)
String(s1) -&amp;gt; &quot;Symbol(test)&quot;
&apos;&apos;+s1 -&amp;gt; Uncaught TypeError: Cannot convert a Symbol value to a string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，Symbol类型也不能被转换为数字（无论是显式还是隐式），但可以被转换为布尔值。&lt;/p&gt;
&lt;h2&gt;D.宽松相等（ == ）和严格相等（ === ）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;==&lt;/code&gt;允许在相等比较中进行强制类型转换，但&lt;code&gt;===&lt;/code&gt;则不允许。&lt;/p&gt;
&lt;h3&gt;宽松相等的转换规则（==）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对于基本类型：两个值的类型相同，则比较是否相等。
除了NaN（NaN是js中唯一不等于自身的值）和+0/-0（+0 === -0）。类型不同的两个值参考第三条。&lt;/li&gt;
&lt;li&gt;对于对象（包括函数和数组）：他们指向同一引用时，即视为相等，不发生强制转换。&lt;/li&gt;
&lt;li&gt;在比较两个不同类型的值时，会发生隐式类型转换，将其转为相同的类型后再比较。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;字符串和数字之间的相等比较&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const a=&apos;12&apos;
const b=12
a==b //true
a===b //false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则为：&lt;code&gt;==&lt;/code&gt;两边，哪边为数值类型，则另一边转为数值类型。&lt;/p&gt;
&lt;h4&gt;其它类型和布尔类型之间的相等比较&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const a=&apos;12&apos;
const b=true
a==b // false  a为真值，为什么返回false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为在&lt;code&gt;==&lt;/code&gt;两边，哪边为布尔类型，哪边转为数值类型！！
同样，&lt;code&gt;a==false&lt;/code&gt;也会返回&lt;code&gt;false&lt;/code&gt;，因为这里的布尔值会被强制转换为数字0.&lt;/p&gt;
&lt;h4&gt;null和undefined之间的相等比较&lt;/h4&gt;
&lt;p&gt;只要记住：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;null == undefined //true
null === undefined //false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对象和非对象之间的相等比较&lt;/h4&gt;
&lt;p&gt;对于布尔值和对象之间的比较，先把布尔值转换为数值类型。
数值或字符串与对象之间的比较，对象先会调用&lt;code&gt;ToPromitive&lt;/code&gt;抽象操作，之后再转为数值进行比较。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a=12
const b=[12]
a==b //true
b-&amp;gt;&apos;12&apos;-&amp;gt;12

const c=Object(null)
c==null //fasle 这里c被转换为空对象{}

const d=Object(undefined)
d==undefined // fasle 这里d被转换为空对象{}

const e=Object(NaN)
e==NaN // fasle 这里e被转换为Number(NaN) -&amp;gt; NaN 但NaN不等于自身，所以为false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;几个典型的坑&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 小坑
&quot;0&quot; == false // -&amp;gt; true  这里false先被转为0，&quot;0&quot;也会转为0，所以为true
&quot;0&quot; == &quot;&quot; // -&amp;gt; false 两个都是字符串类型，直接比较
0 == &apos;&apos; // -&amp;gt; true 空字符串直接转为0
false == [] // -&amp;gt; true false先转为0；[]空数组转为&apos;&apos;，之后ToNumber操作转为0

// 大坑
[] == ![] // -&amp;gt; true []  这里![]先被强制转换为false，变成[]与fasle的比较，之后fasle-&amp;gt;0；[]-&amp;gt;&apos;&apos;-&amp;gt;0，所以为true。
2=[2] // -&amp;gt; true [2]-&amp;gt;&apos;2&apos;-&amp;gt;2 所以为true
&apos;&apos;==[null] // true [null]-&amp;gt;&apos;&apos;
0==&apos;\n&apos; // -&amp;gt; true &apos;\n&apos;-&amp;gt;&apos;&apos;-&amp;gt;0
&apos;true&apos;==true // -&amp;gt; false true-&amp;gt;0;&apos;true&apos;-&amp;gt;NaN，所以为false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你还是一头雾水的话，请仔细阅读D部分这几种相互比较的规则和C部分的隐式类型转换。只要记住，遇到两个不同类型的值，转换优先顺序为布尔值&amp;gt;对象&amp;gt;字符串&amp;gt;数字；每一步的转换到相同类型的值即停止转换，进行比较判断。&lt;/p&gt;
&lt;h2&gt;E.抽象关系比较&lt;/h2&gt;
&lt;p&gt;出现非字符串就先转为数字类型；如果两者都为字符串，按照字母顺序来比较，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&apos;22&apos;]&amp;lt;[&apos;023&apos;] // -&amp;gt; false 这里并不转为数字，0在字母顺序上小于2，所以为false
22&amp;lt;[&apos;023&apos;] // -&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于对象来说，也同样是转换成字符串，再进行比较，如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a={a:1}
const b={a:2}
a&amp;gt;b // -&amp;gt; false
a&amp;lt;b // -&amp;gt; false
a==b // -&amp;gt; false
a&amp;lt;=b // -&amp;gt; true
a&amp;gt;=b // -&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子比较奇怪，虽然他们转成字符串都为&lt;code&gt;[Object Object]&lt;/code&gt;，但两个对象的比较并不是转为字符串，而是看他们的引用是否指向同一值。这里&lt;code&gt;&amp;lt;=&lt;/code&gt;被处理为&lt;code&gt;!&amp;gt;&lt;/code&gt;，所以为&lt;code&gt;true&lt;/code&gt;； &lt;code&gt;&amp;gt;=&lt;/code&gt;同理。&lt;/p&gt;
</content:encoded></item><item><title>面试需要知道点</title><link>https://nollieleo.github.io/posts/%E9%9D%A2%E8%AF%95%E9%9C%80%E8%A6%81%E7%9F%A5%E9%81%93%E7%82%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%9D%A2%E8%AF%95%E9%9C%80%E8%A6%81%E7%9F%A5%E9%81%93%E7%82%B9/</guid><description>1.JS 类型有哪些？   2.TCP,HTTP,CDN   3.大数相加、相乘算法题，可以直接使用 bigint，当然再加上字符串的处理会更好。   4.NaN 如何判断    5.instanceof 原理 ✔   6.手写 instanceof ✔   7.类型转换隐式转换和强制转换 ✔   ...</description><pubDate>Fri, 28 May 2021 10:28:41 GMT</pubDate><content:encoded>&lt;h3&gt;1.JS 类型有哪些？&lt;/h3&gt;
&lt;h3&gt;2.TCP,HTTP,CDN&lt;/h3&gt;
&lt;h3&gt;3.大数相加、相乘算法题，可以直接使用 &lt;code&gt;bigint&lt;/code&gt;，当然再加上字符串的处理会更好。&lt;/h3&gt;
&lt;h3&gt;4.NaN&lt;code&gt; 如何判断&lt;/code&gt;&lt;/h3&gt;
&lt;h3&gt;5.instanceof` 原理 ✔&lt;/h3&gt;
&lt;h3&gt;6.手写 &lt;code&gt;instanceof&lt;/code&gt; ✔&lt;/h3&gt;
&lt;h3&gt;7.类型转换隐式转换和强制转换 ✔&lt;/h3&gt;
&lt;h3&gt;8.this, 箭头函数&lt;/h3&gt;
&lt;h3&gt;9.闭包 ✔&lt;/h3&gt;
&lt;h3&gt;10.new, 手写new，new做了哪些事？new 返回不同类型的时候是什么表现 ✔&lt;/h3&gt;
&lt;h3&gt;11.作用域，作用域链，全局作用域，函数作用域，块级作用域 ✔&lt;/h3&gt;
&lt;h3&gt;12.&lt;strong&gt;原型，原型链，原型继承&lt;/strong&gt;， js中是如何实现继承的，通过原型实现的继承和class有什么区别，手写任意一种原型继承 ✔&lt;/h3&gt;
&lt;h3&gt;13.深拷贝，浅拷贝✔&lt;/h3&gt;
&lt;h3&gt;14.Promise**  &lt;code&gt;all&lt;/code&gt;、&lt;code&gt;race&lt;/code&gt; ， 使用all实现并行需求， 手写all的实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;页面上有三个按钮，分别为 A、B、C，点击各个按钮都会发送异步请求且互不影响，每次请求回来的数据都为按钮的名字。 请实现当用户依次点击 A、B、C、A、C、B 的时候，最终获取的数据为 ABCACB。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;15.async, await&lt;/h3&gt;
&lt;h3&gt;16.浏览器渲染也页面过程&lt;/h3&gt;
&lt;h4&gt;含义&lt;/h4&gt;
&lt;p&gt;浏览器缓存（Browser Caching）是为了加速浏览，浏览器在用户磁盘上岁最近请求过的文档进行存储，当用户再次访问这个文档时，浏览器会从本地磁盘显示此文档，从而提升页面加载速率。&lt;/p&gt;
&lt;h4&gt;cache的作用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;减少延迟，让网站的运行速度更快，带来更好的用户体验；&lt;/li&gt;
&lt;li&gt;避免网络拥塞，减少请求量，减少输出带宽；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实现手段&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;cache-control&lt;/code&gt;中的&lt;code&gt;max-age&lt;/code&gt;是实现内容&lt;code&gt;cache&lt;/code&gt;的重要手段，常用的策略有以下三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max-age&lt;/code&gt;和&lt;code&gt;ETag&lt;/code&gt;的组合；&lt;/li&gt;
&lt;li&gt;仅&lt;code&gt;max-age&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-age&lt;/code&gt;和&lt;code&gt;Last-Modified&lt;/code&gt;（If-Modified-Since）的组合；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;拓展&lt;/h4&gt;
&lt;p&gt;在此，拓展一下有关&lt;strong&gt;强制缓存&lt;/strong&gt;（200）和&lt;strong&gt;协商缓存&lt;/strong&gt;（304）部分的内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强制缓存（也称强缓存）&lt;/strong&gt;，指当浏览器去请求某个文件的时候，服务端就在&lt;code&gt;respone header&lt;/code&gt;里面对该文件做了缓存配置。强制缓存不会向服务器发送请求，直接从缓存中读取资源，在&lt;code&gt;chrome&lt;/code&gt;控制台的&lt;code&gt;network&lt;/code&gt;选项中可以看到该请求返回&lt;code&gt;200&lt;/code&gt;的状态码;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协商缓存&lt;/strong&gt;：强制缓存给资源设置了过期时间，在未过期时，可以说是给用户自给自足用的。但是当资源过期时，就需要去请求服务器，这时候请求服务器的过程就可以设置成协商缓存。协商缓存就是需要客户端和服务端进行交互的。协商缓存将缓存信息中的&lt;code&gt;Etag&lt;/code&gt;和&lt;code&gt;Last-Modified&lt;/code&gt;通过请求发给服务器，由服务器校验，返回状态码&lt;code&gt;304&lt;/code&gt;时，浏览器就可以直接使用缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;共同点&lt;/strong&gt;： 都是从客户端中读取数据；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;区别&lt;/strong&gt;： 强缓存不会发出请求，协商缓存会发出请求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存中header（头部）的参数：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（1）、强制缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expires&lt;/strong&gt;（常用）：&lt;code&gt;response header&lt;/code&gt;里的过期时间，浏览器再次加载资源时，如果在这个过期时间内，则命中强缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache-Control&lt;/strong&gt;（常用）：当值设为&lt;code&gt;max-age=120&lt;/code&gt;时，则代表在这个请求正确返回时间（浏览器也会记录下来）的&lt;code&gt;2&lt;/code&gt;分钟内再次加载资源，就会命中强缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;no-cache&lt;/strong&gt;：不使用本地缓存。需要使用缓存协商，来验证是否过期；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;no-store&lt;/strong&gt;：不可缓存；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;public&lt;/strong&gt;：客户端和代理服务器都可缓存；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;private&lt;/strong&gt;：只能有客户端缓存；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（2）、协商缓存&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Etag&lt;/strong&gt;：即文件&lt;code&gt;hash&lt;/code&gt;，每个文件唯一。&lt;code&gt;web&lt;/code&gt;服务器响应请求时，告诉浏览器当前资源在服务器的唯一标识（生成规则由服务器决定）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If-None-Match&lt;/strong&gt;：当资源过期时（使用&lt;code&gt;Cache-Control&lt;/code&gt;标识的&lt;code&gt;max-age&lt;/code&gt;），发现资源具有&lt;code&gt;Etag&lt;/code&gt;声明，则再次向&lt;code&gt;web&lt;/code&gt;服务器请求时带上头&lt;code&gt;If-None-Match&lt;/code&gt; （Etag的值）。&lt;code&gt;web&lt;/code&gt;服务器收到请求后发现有头&lt;code&gt;If-None-Match&lt;/code&gt; 则与被请求资源的相应校验串进行比对，决定是否命中协商缓存；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last-Modify/If-Modify-Since&lt;/strong&gt;：文件的修改时间，精确到秒。浏览器第一次请求一个资源的时候，服务器返回的&lt;code&gt;header&lt;/code&gt;中会加上&lt;code&gt;Last-Modify&lt;/code&gt;，&lt;code&gt;Last-modify&lt;/code&gt;是一个时间标识该资源的最后修改时间；当浏览器再次请求该资源时，&lt;code&gt;request&lt;/code&gt;的请求头中会包含&lt;code&gt;If-Modify-Since&lt;/code&gt;，该值为缓存之前返回的&lt;code&gt;Last-Modify&lt;/code&gt;。服务器收到&lt;code&gt;If-Modify-Since&lt;/code&gt;后，根据资源的最后修改时间判断是否命中缓存；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🌈🌈&lt;strong&gt;注意&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Etag&lt;/code&gt;要优于&lt;code&gt;Last-Modified&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在优先级上，服务器校验优先考虑&lt;code&gt;Etag&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在性能上，&lt;code&gt;Etag&lt;/code&gt;要逊于&lt;code&gt;Last-Modified&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;17.如何性能优化&lt;/h3&gt;
&lt;h3&gt;18.CDN 优化有哪些， CDN是什么&lt;/h3&gt;
&lt;h3&gt;19.webpack 插件原理，如何写一个插件&lt;/h3&gt;
&lt;h3&gt;20.手写 bind ✔、reduce&lt;/h3&gt;
&lt;h3&gt;21.防抖截流 ✔&lt;/h3&gt;
&lt;h3&gt;22.遍历树，求树的最大层数。求某层最多的节点数&lt;/h3&gt;
&lt;h3&gt;23.前端模块化的理解&lt;/h3&gt;
&lt;h3&gt;24.隐式转换&lt;/h3&gt;
&lt;h3&gt;25.数字在计算机怎么储存的&lt;/h3&gt;
&lt;h3&gt;26.&lt;strong&gt;知道什么是事件委托吗？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;概念&lt;/strong&gt;： &lt;strong&gt;事件冒泡&lt;/strong&gt;是指嵌套最深的元素触发一个事件，然后这个事件顺着嵌套顺序在父元素上触发。而&lt;strong&gt;事件委托&lt;/strong&gt;，是利用事件冒泡原理，让自己所触发的事件，让其父元素代替执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;冒泡阻止方式&lt;/strong&gt;：使用&lt;code&gt;event.cancelBubble = true&lt;/code&gt;或者&lt;code&gt;event.stopPropgation()&lt;/code&gt;（低于IE9）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;默认事件阻止方式&lt;/strong&gt;： &lt;code&gt;e.preventDefault(); &lt;/code&gt;或&lt;code&gt;return false;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;27.window的onload事件和domcontentloaded谁先谁后？&lt;/h3&gt;
&lt;h3&gt;28.macrotask 和 microtask&lt;/h3&gt;
&lt;h3&gt;29.浏览器缓存,HTTP缓存  对比缓存？强缓存？对应请求头&lt;/h3&gt;
&lt;h3&gt;30.commonjs和esmodule&lt;/h3&gt;
&lt;h3&gt;31.xss攻击和csrf攻击&lt;/h3&gt;
&lt;h3&gt;32.时针和分针的夹角&lt;/h3&gt;
&lt;h3&gt;33.Javascript的事件流模型都有什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;“事件冒泡”&lt;/strong&gt;： 当触发一个节点的事件时，会从当前节点开始，依次触发其祖先节点的同类型事件，直到&lt;code&gt;DOM&lt;/code&gt;根节点。（逐级向上）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“事件捕获”&lt;/strong&gt;： 当触发一个节点的事件时，会从&lt;code&gt;DOM&lt;/code&gt;根节点开始，依次触发其祖先节点的同类型事件，直到当前节点自身。（逐级向下）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“DOM事件流”&lt;/strong&gt;： &lt;code&gt;dom&lt;/code&gt;同时支持两种事件模型，但捕获性事件先开始，从&lt;code&gt;document&lt;/code&gt;开始也结束于&lt;code&gt;document&lt;/code&gt;，&lt;code&gt;dom&lt;/code&gt;模型的独特之处在于文本也可以触发事件。简单的说分为三个阶段：&lt;strong&gt;事件捕捉&lt;/strong&gt;， &lt;strong&gt;目标阶段&lt;/strong&gt;， &lt;strong&gt;事件冒泡&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;34.js延迟加载的方式有哪些？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt;属性（页面&lt;code&gt;load&lt;/code&gt;后执行）：脚本会被延迟到整个页面都解析完毕之后再执行。若是设置了&lt;code&gt;defer&lt;/code&gt;属性，就等于告诉浏览器立即下载，但是会延迟执行。注意&lt;code&gt;defer&lt;/code&gt;属性只适用于外部脚本文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 属性（页面&lt;code&gt;load&lt;/code&gt;前执行）：为了不让页面等待脚本下载和执行，异步加载页面和其他内容。&lt;code&gt;async&lt;/code&gt;同样也只适用于外部文件（不会影响页面加载，但是不能控制加载的顺序）。&lt;/p&gt;
&lt;p&gt;动态创建&lt;code&gt;DOM&lt;/code&gt;方式；&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;jQuery&lt;/code&gt;的&lt;code&gt;getScript()&lt;/code&gt;方法；&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;setTimeout&lt;/code&gt;延迟方法；&lt;/p&gt;
&lt;p&gt;让&lt;code&gt;js&lt;/code&gt;文件最后加载。&lt;/p&gt;
&lt;h3&gt;35.Cookie在客户机上是如何存储的&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cookies&lt;/code&gt;是服务器暂时放在我们电脑里的文本文件，好让服务器来辨认我们的计算机。&lt;/p&gt;
&lt;p&gt;当我们在浏览网站的时候，&lt;code&gt;web&lt;/code&gt;服务器会先发送小部分资料放在我们的计算机中，&lt;code&gt;cookies&lt;/code&gt;会帮助我们，将我们在网站上打印的文字或一些选择记录下来，当我们再次访问同一个网站，&lt;code&gt;web&lt;/code&gt;服务器会先检查有没有它上次留下的&lt;code&gt;cookies&lt;/code&gt;资料。&lt;/p&gt;
&lt;p&gt;若有，会依据&lt;code&gt;cookies&lt;/code&gt;里面的内容来判断使用者，从而给我们推出相应的网页内容。&lt;/p&gt;
&lt;h3&gt;36.DOM和BOM是什么&lt;/h3&gt;
&lt;p&gt;首先我们需要知道：&lt;strong&gt;javascript是由ECMAScript，DOM，BOM三部分构成的&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ECMAScript&lt;/code&gt;是一种语言，是对规定的语法，操作，关键字，语句的一个描述，&lt;code&gt;javascript&lt;/code&gt;实现了&lt;code&gt;ECMAScript&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOM&lt;/code&gt;是文档对象模型，包括了获取元素，修改样式以及操作元素等三方面的内容，也是通常我们用的最多的操作，其提供了很多兼容性的写法；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BOM&lt;/code&gt;是浏览器对象模型，包括浏览器的一些操作，&lt;code&gt;window.onload&lt;/code&gt;, &lt;code&gt;window.open&lt;/code&gt;等还有浏览器时间，监听窗口的改变&lt;code&gt;onresize&lt;/code&gt;，监听滚动事件&lt;code&gt;onscroll&lt;/code&gt;等；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;37.如何实现多个标签之间的通信&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调用 localStorage&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）、在一个标签内使用&lt;code&gt;localStorage&lt;/code&gt;。&lt;code&gt;setItem(key, value)&lt;/code&gt;添加（删除或修改）内容；&lt;/p&gt;
&lt;p&gt;（2）、在另一个标签页面监听&lt;code&gt;storage&lt;/code&gt;事件；&lt;/p&gt;
&lt;p&gt;（3）、得到&lt;code&gt;localStorage&lt;/code&gt;存储的值，即可实现不用页面之间的通信。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调用 cookie+setInterval()&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）、将要传递的信息存储在&lt;code&gt;cookie&lt;/code&gt;中，可以设置定时读取&lt;code&gt;cookie&lt;/code&gt;的信息，即可随时获取想要传递的信息。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用 Webworker&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）、&lt;code&gt;webworker&lt;/code&gt;作为浏览器的一个新特性，可以提供一个额外的线程来执行一些js代码，并且对浏览器用户界面不影响；&lt;/p&gt;
&lt;p&gt;（2）、普通的&lt;code&gt;Webworker&lt;/code&gt;用 &lt;code&gt;new worker()&lt;/code&gt;即可创建，这种&lt;code&gt;webworker&lt;/code&gt;是当前页面专有的。然后还有种共&lt;code&gt;享worker(SharedWorker)&lt;/code&gt;，这种是可以多个标签页、&lt;code&gt;iframe&lt;/code&gt;共同使用；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用 SharedWorker&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）、&lt;code&gt;SharedWorker&lt;/code&gt;可以被多个&lt;code&gt;window&lt;/code&gt;共同使用，但必须保证这些标签页都是同源的(相同的协议，主机和端口号)；&lt;/p&gt;
&lt;h3&gt;38. js垃圾回收的几种方式&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;javascript&lt;/code&gt;具有自动垃圾回收机制，垃圾器回收会按照固定的时间间隔周期性的执行。&lt;/p&gt;
&lt;p&gt;常见的垃圾回收机制有两种： &lt;strong&gt;标记清除&lt;/strong&gt;，&lt;strong&gt;引用计数&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;标记清除&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;： 当变量进入环境时，将这个变量标记为“进入环境”。当变量离开时，则将其标记为“离开环境”。标记“离开环境”的就回收内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垃圾回收器，在运行的时候会给存储在内存中的所有变量都加上标记；&lt;/li&gt;
&lt;li&gt;去掉环境中的变量以及被环境中的变量引用的变量的标记；&lt;/li&gt;
&lt;li&gt;再被加上标记的会被视为准备删除的变量；&lt;/li&gt;
&lt;li&gt;垃圾回收器完成内存清除工作，销毁那些带标记的值并回收他们所占用的内存空间；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;引用计数&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：  跟踪记录每个值被引用的次数，声明一个变量，并将引用 类型赋值给这个变量，则这个值的引用次数&lt;code&gt;+1&lt;/code&gt;，当变量的值变成了另一个，则这个值的引用次数&lt;code&gt;-1&lt;/code&gt;，当值的引用次数为&lt;code&gt;0&lt;/code&gt;的时候，就回收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明了一个变量并将一个引用类型的值赋值给这个变量，这个引用值的引用次数就是&lt;code&gt;1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;同一个值又被&lt;strong&gt;赋值给&lt;/strong&gt;另一个变量，这个引用类型值此时的引用次数&lt;code&gt;+1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;当包含这个引用类型值的变量又被&lt;strong&gt;赋值成&lt;/strong&gt;另一个值，那么这个引用呢性值的引用次数&lt;code&gt;-1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;当引用次数变成0时，说明目前无法访问此值；&lt;/li&gt;
&lt;li&gt;当垃圾收集器下一次运行时，它会释放引用次数是&lt;code&gt;0&lt;/code&gt;的值所占的内存；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;39.在浏览器中输入URL到整个页面显示在用户面前时这个过程中到底发生了什么&lt;/h3&gt;
&lt;p&gt;（1）、&lt;strong&gt;DNS解析&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查浏览器自身的&lt;code&gt;DNS&lt;/code&gt;缓存；&lt;/li&gt;
&lt;li&gt;若没有，搜索操作系统自身的&lt;code&gt;DNS&lt;/code&gt;缓存；&lt;/li&gt;
&lt;li&gt;若没有，尝试读取&lt;code&gt;hosts&lt;/code&gt;文件；&lt;/li&gt;
&lt;li&gt;若没有，可向本地配置的首选&lt;code&gt;DNS&lt;/code&gt;服务器发起请求；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.win&lt;/code&gt;系统若没有好到，可以操作系统查找&lt;code&gt;NetBIOS name cache&lt;/code&gt;或查询&lt;code&gt;wins&lt;/code&gt;服务器或广播查找或读取LMHOSTS文件；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（若以上都没有，则解析失败）&lt;/p&gt;
&lt;p&gt;（2）、&lt;strong&gt;TCP三次握手&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-gold-cdn.xitu.io/2020/6/9/1729712af898c1c3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（3）、&lt;strong&gt;浏览器向服务器发送http请求&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一旦建立了&lt;code&gt;TCP&lt;/code&gt;链接，&lt;code&gt;web&lt;/code&gt;浏览器就会向&lt;code&gt;web&lt;/code&gt;服务器发送请求命令。&lt;/p&gt;
&lt;p&gt;（4）、&lt;strong&gt;浏览器发送请求头信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;浏览器发送其请求命令之后，还要以头信息的形式想&lt;code&gt;web&lt;/code&gt;服务器发送一些别的信息，之后浏览器发送了一空白行开通知服务器，它已经结束了该头信息的发送。&lt;/p&gt;
&lt;p&gt;（5）、&lt;strong&gt;服务器处理请求&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务器收到&lt;code&gt;http&lt;/code&gt;请求之后，确定用相应的一眼来处理请求。读取参数并进行逻辑操作后，生成指定的数据。&lt;/p&gt;
&lt;p&gt;（6）、&lt;strong&gt;服务器做出响应&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;客户端向服务器发送请求后，服务端向客户端做出应答。&lt;/p&gt;
&lt;p&gt;（7）、&lt;strong&gt;服务器发送应答头信息&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;正如客户端会随同请求发送关于自身的信息一样，服务器也会随同应答向用户发送关于它自己的数据以及被请求的文档。&lt;/p&gt;
&lt;p&gt;（8）、&lt;strong&gt;服务器发送数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;web&lt;/code&gt;服务器向浏览器发送信息后，它会发送一个空白行来表示头信息的发送到此结束。接着，它会以&lt;code&gt;Content-Type&lt;/code&gt;应答头信息所描述的格式发送用户所请求的实际数据。&lt;/p&gt;
&lt;p&gt;（9）、 &lt;strong&gt;TCP关闭（四次挥手）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般情况下，一旦&lt;code&gt;web&lt;/code&gt;服务器向浏览器发送了请求数据，它就要关闭&lt;code&gt;tcp&lt;/code&gt;链接。如果浏览器或服务器在其头信息加入了&lt;code&gt;Connection:keep-alive &lt;/code&gt;,则会保持长连接状态，也就是TCP链接在发送后仍保持打开状态，浏览器可以继续通过仙童的链接发送请求。&lt;/p&gt;
&lt;p&gt;（优点：保持链接节省了为每个请求建立新链接所属的时间，还节约了网络宽带 ）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-gold-cdn.xitu.io/2020/6/9/1729712d216c4dee?imageView2/0/w/1280/h/960/format/webp/ignore-error/1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>js对象属性详解</title><link>https://nollieleo.github.io/posts/js%E5%AF%B9%E8%B1%A1%E5%B1%9E%E6%80%A7%E8%AF%A6%E8%A7%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E5%AF%B9%E8%B1%A1%E5%B1%9E%E6%80%A7%E8%AF%A6%E8%A7%A3/</guid><description>ECMA-262使用一些内部特性来描述属性的特征。这些特征是由为js实现引擎的规范定义的，开发者在js中不能直接访问这些特性，描述内部特性，一般会把这个特性括起来类似这样[[Enumerable]]   属性的类型  属性分两种   1. 数据属性 2. 访问器属性   1. 数据属性  数值属性包...</description><pubDate>Thu, 27 May 2021 20:21:37 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;ECMA-262使用一些内部特性来描述属性的特征。这些特征是由为js实现引擎的规范定义的，开发者在js中不能直接访问这些特性，描述内部特性，一般会把这个特性括起来类似这样&lt;code&gt;[[Enumerable]]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;属性的类型&lt;/h1&gt;
&lt;p&gt;属性分两种&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据属性&lt;/li&gt;
&lt;li&gt;访问器属性&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. 数据属性&lt;/h2&gt;
&lt;p&gt;数值属性包含一个保存数据值的位置。&lt;/p&gt;
&lt;p&gt;以下有4种特性描述他们的行为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[[Configurable]]&lt;/code&gt;： 表示属性以下特性。（默认情况下是true）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否可以通过 &lt;strong&gt;delete删除&lt;/strong&gt;并且&lt;strong&gt;重新定义&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以修改他的特性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否可以把它&lt;strong&gt;改为访问器属性&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[[Enumberable]]&lt;/code&gt;：表示属性。（默认情况是true）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否可以通过 &lt;code&gt;for-in&lt;/code&gt;循环返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[[Writable]]&lt;/code&gt;：表示这个属性的值是否可以被修改呢，默认情况为true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[[Value]]&lt;/code&gt;：包含属性实际的值。（这个就是数据属性保存数据值的位置，值会从这个位置读取，以及写入）默认值是&lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 修改属性的默认特性&lt;/h3&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let person = {
	name: &apos;weng&apos;
}
// 这里name属性的特性[[Value]]就会被赋值为 weng
// 而其他的特性默认都是为true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如要 修改特性需要用到对象的 &lt;code&gt;Object.defineProperty&lt;/code&gt;方法&lt;/p&gt;
&lt;p&gt;这个方法接受三个参数&lt;/p&gt;
&lt;p&gt;要给添加属性的对象，属性名称， 一个描述符对象（就是相关特性的描述属性，configurable，writable, enumerable, value）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let person = {};
Object.defineProperty(person, &quot;name&quot;, {
    writable: false,
    value: &apos;weng&apos;
});
// 这里设置了`[[Writable]]`属性为false，表示这个属性不能再被修改了
console.log(person.name) // weng
person.name = &apos;helloworld&apos;;  // 修改无效
console.log(person.name) // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上述这种情况，在严格模式之下尝试修改属性会抛出错误&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建一个不可配置的属性如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let person = {};
Object.defineProperty(person, &apos;name&apos;, {
    configurable: false,
    value: &apos;weng&apos;
})
// 这里把对象的[[Configurable]]特性设置为false
delete person.name;  // 删除无效
console.log(person.name) // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里如果修改了[[Configurable]]特性为不可配置，之后都不能再变回可配置的了，如果再调用Object.defineProperty把这个特性设置为true会报错&lt;/p&gt;
&lt;p&gt;并且设置这个特性为false，如果没有定义其他特性，那么其他特性都默认为false&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 访问器属性&lt;/h2&gt;
&lt;p&gt;访问器属性是不包含数据值的。&lt;/p&gt;
&lt;p&gt;他们包含一个（getter）函数和一个（setter）函数；&lt;/p&gt;
&lt;p&gt;读取属性时候调取 getter 决定了应该怎么返回值，&lt;/p&gt;
&lt;p&gt;设置属性的时候调用 setter，setter函数告诉你怎么对数据做出修改&lt;/p&gt;
&lt;p&gt;有四个属性描述他们的行为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[[Configurable]]&lt;/code&gt;：默认情况下为true
&lt;ul&gt;
&lt;li&gt;是否可通过delete删除并重新定义&lt;/li&gt;
&lt;li&gt;是否可以修改它的特性&lt;/li&gt;
&lt;li&gt;是否可以把他改为数据属性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[[Enumerable]]&lt;/code&gt;： 默认情况下为true
&lt;ul&gt;
&lt;li&gt;表示属性是否可以通过 &lt;code&gt;for-in&lt;/code&gt;循环-&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[[Get]]&lt;/code&gt;： 获取函数，在读取属性时候调用。默认值为undefined&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[[Set]]&lt;/code&gt;： 设置函数，在写入属性时候调用。默认值为undefined&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和数据属性一样，访问器的属性也是不能直接定义的，必须使用Object.defineProperty()&lt;/p&gt;
&lt;p&gt;如下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const book = {
    year_: 2021, // 表示私有属性不能被外部访问
    edition: 1
}
Object.defineProperty(book, &quot;year&quot;, {
    get(){
        return this.year_;
    }
    set(newValue){
    	if(newValue &amp;gt; 2021){
            this.year_ = newValue;
            this.edition += newValue - 2021
        }
	}
})

book.year = 2022;
console.log(book.edition) // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上是访问器属性的经典使用场景&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;获取函数和设置函数不一定都需要定义&lt;/p&gt;
&lt;p&gt;只定义获取函数意味着属性是只读的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然可以以上的Object.defineProperty只能定义单个属性，可以使用Object.defineProperties来定义多个属性值以及特性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let book = {}
Object.defineProperties(book, {
    year_:{
        value: 2021
    },
    edition:{
        value: 1
    },
    year: {
        get(){
            ....
        }
        set(){
    		....
		}
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;读取属性的特性&lt;/h1&gt;
&lt;h2&gt;1. Object.getOwnPropertyDescriptor()&lt;/h2&gt;
&lt;p&gt;使用 Object.getOwnPropertyDescriptor()方法可以获取指定属性的属性描述符。&lt;/p&gt;
&lt;p&gt;接受两个参数&lt;/p&gt;
&lt;p&gt;属性所在对象，以及要取得其描述符号的属性名（数据属性，和访问器属性）。返回一个对象&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let book = {};
Object.defineProperties(book, {
    year_:{
        value: 2021
    },
    edition:{
        value: 1
    },
    year: {
        get(){
            ....
        }
        set(){
    		....
		}
    }
})
let desc = Object.getOwnPropertyDescriptor(book,&quot;year_&quot;);
let descYear = Object.getOwnPropertyDescriptor(book, &quot;year&quot;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;desc的值如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210527212616268.png&quot; alt=&quot;image-20210527212616268&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210527212733518.png&quot; alt=&quot;image-20210527212733518&quot; /&gt;&lt;/p&gt;
&lt;p&gt;descYear值如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210527212917495.png&quot; alt=&quot;image-20210527212917495&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210527213001469.png&quot; alt=&quot;image-20210527213001469&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. Object.getOwnPropertyDescriptors()&lt;/h2&gt;
&lt;p&gt;es2017新增的静态方法，这个方法会在每个自有属性上调用Object.defineProperties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(Object.getOwnPropertyDescriptors(book));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印出来的如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20210527214742659.png&quot; alt=&quot;image-20210527214742659&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：这两个方法只对实例属性有效果，是不能取得原型属性上的描述符，如果需要取得原型属性上的描述符，就得直接再原型对象上面直接调用这两个方法&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>如何对两个超大数字字符串做相加操作</title><link>https://nollieleo.github.io/posts/%E5%A6%82%E4%BD%95%E5%AF%B9%E4%B8%A4%E4%B8%AA%E8%B6%85%E5%A4%A7%E6%95%B0%E5%AD%97%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%81%9A%E7%9B%B8%E5%8A%A0%E6%93%8D%E4%BD%9C/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%A6%82%E4%BD%95%E5%AF%B9%E4%B8%A4%E4%B8%AA%E8%B6%85%E5%A4%A7%E6%95%B0%E5%AD%97%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%81%9A%E7%9B%B8%E5%8A%A0%E6%93%8D%E4%BD%9C/</guid><description>如何对两个超大数字字符串做相加操作  js var a = &apos;10000000000000000000000&apos; var b = &apos;456789345678945678945678&apos; function add(a,b)     js function add(a,b){   /...</description><pubDate>Wed, 26 May 2021 16:23:49 GMT</pubDate><content:encoded>&lt;h1&gt;如何对两个超大数字字符串做相加操作&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;var a = &apos;10000000000000000000000&apos;
var b = &apos;456789345678945678945678&apos;
function add(a,b)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;function add(a,b){
  // 获取各自长度
  a = a.split(&apos;&apos;), b = b.split(&apos;&apos;);
  let sum=[],go=0;
  while(a.length || b.length){
    // 通过pop每次取一个
    let num1 = parseInt(a.pop()) || 0;
    let num2 = parseInt(b.pop()) || 0;
    // 两值相加，如果有进位就 + go
    let tmp = num1 + num2 + go;
     if(tmp &amp;gt; 9){
         go = 1;
         // 取余数
         tmp %= 10;
     }else{
         go = 0;
     }
     // array.unshift(item)表示在数组array的最前面插入
     sum.unshift(tmp)
  }
  if(go) sum.unshift(1);
  return sum.join(&apos;&apos;);
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>从页面 A 打开一个新页面 B，B 页面关闭（包括意外崩溃），如何通知 A 页面？</title><link>https://nollieleo.github.io/posts/%E9%A1%B5%E9%9D%A2%E9%80%9A%E4%BF%A1%E4%BB%8E%E9%A1%B5%E9%9D%A2-a-%E6%89%93%E5%BC%80%E4%B8%80%E4%B8%AA%E6%96%B0%E9%A1%B5%E9%9D%A2-bb-%E9%A1%B5%E9%9D%A2%E5%85%B3%E9%97%AD%E5%8C%85%E6%8B%AC%E6%84%8F%E5%A4%96%E5%B4%A9%E6%BA%83%E5%A6%82%E4%BD%95%E9%80%9A%E7%9F%A5-a-%E9%A1%B5%E9%9D%A2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%A1%B5%E9%9D%A2%E9%80%9A%E4%BF%A1%E4%BB%8E%E9%A1%B5%E9%9D%A2-a-%E6%89%93%E5%BC%80%E4%B8%80%E4%B8%AA%E6%96%B0%E9%A1%B5%E9%9D%A2-bb-%E9%A1%B5%E9%9D%A2%E5%85%B3%E9%97%AD%E5%8C%85%E6%8B%AC%E6%84%8F%E5%A4%96%E5%B4%A9%E6%BA%83%E5%A6%82%E4%BD%95%E9%80%9A%E7%9F%A5-a-%E9%A1%B5%E9%9D%A2/</guid><description>从页面 A 打开一个新页面 B，B 页面关闭（包括意外崩溃），如何通知 A 页面？  首先能够拆解一下这个题意  1. B手动关闭，如何通知A页面 2. B意外关闭，被线程杀死奔溃的时候如何通知A页面     1. B页面正常关闭的时候  1. 首先要回答出页面关闭时会触发的事...</description><pubDate>Wed, 26 May 2021 16:23:49 GMT</pubDate><content:encoded>&lt;h1&gt;从页面 A 打开一个新页面 B，B 页面关闭（包括意外崩溃），如何通知 A 页面？&lt;/h1&gt;
&lt;p&gt;首先能够拆解一下这个题意&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;B手动关闭，如何通知A页面&lt;/li&gt;
&lt;li&gt;B意外关闭，被线程杀死奔溃的时候如何通知A页面&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. B页面正常关闭的时候&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; 首先要回答出页面关闭时会触发的事件是什么？&lt;/p&gt;
&lt;p&gt;页面关闭时先执行&lt;code&gt;window.onbeforeunload&lt;/code&gt;，然后执行&lt;code&gt;window.onunload&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们可以在 &lt;code&gt;window.onbeforeunload&lt;/code&gt; 或 &lt;code&gt;window.onunload&lt;/code&gt; 里面设置回调。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; 然后回答如何传参？&lt;/p&gt;
&lt;p&gt;最先想到的是：用 &lt;code&gt;window.open&lt;/code&gt; 方法跳转到一个已经打开的页面（A页面），url 上可以挂参传递信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 chrome 浏览器下会报错&lt;/strong&gt;&lt;code&gt;“Blocked popup during beforeunload.”&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在 MDN 里找到了解释：HTML规范指出在此事件中调用window.alert()，window.confirm()以及window.prompt()方法，可能会失效。&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Events/beforeunload&quot;&gt;Window: beforeunload event&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在火狐浏览器下不会报错&lt;/strong&gt;，可以正常打开 A 页面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; 成功传参后，A 页面是如何监听 URL 的？&lt;/p&gt;
&lt;p&gt;onhashchange 是为您排忧解难。&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Window/hashchange_event&quot;&gt;Window: hashchange event&lt;/a&gt;：当URL的片段标识符更改时，将触发hashchange事件 (跟在＃符号后面的URL部分，包括＃符号)&lt;/p&gt;
&lt;p&gt;如果你传参是以 A.html?xxx 的形式，那就需要监听 window.location.href。&lt;/p&gt;
&lt;p&gt;// 页面A&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;这是 A 页面&amp;lt;/div&amp;gt;
    &amp;lt;button onclick=&quot;toB()&quot;&amp;gt;点击打开 B 页面&amp;lt;/button&amp;gt;
    &amp;lt;script&amp;gt;
        window.name = &apos;A&apos; // 设置页面名
        function toB() {
            window.open(&quot;B.html&quot;, &quot;B&quot;) // 打开新页面并设置页面名
        }
        window.addEventListener(&apos;hashchange&apos;, function () {// 监听 hash
            alert(window.location.hash)
        }, false);
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;// 页面B&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;这是 B 页面&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
        window.onbeforeunload = function (e) {
            window.open(&apos;A.html#close&apos;, &quot;A&quot;) // url 挂参 跳回到已打开的 A 页面
            return &apos;确定离开此页吗？&apos;;
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实传参也可以通过&lt;strong&gt;本地缓存传参&lt;/strong&gt;，A 页面设置监听,在 B 页面设置 loacalStorage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A.html
window.addEventListener(&quot;storage&quot;, function (e) {// 监听 storage
    alert(e.newValue);
});
// B.html
window.onbeforeunload = function (e) {
    localStorage.setItem(&quot;name&quot;,&quot;close&quot;);
    return &apos;确定离开此页吗？&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 奔溃的情况下&lt;/h2&gt;
&lt;p&gt;这个好鸡儿恼火&lt;/p&gt;
</content:encoded></item><item><title>window.open(导航与打开新窗口)</title><link>https://nollieleo.github.io/posts/windo-open-%E5%AF%BC%E8%88%AA%E4%B8%8E%E6%89%93%E5%BC%80%E6%96%B0%E7%AA%97%E5%8F%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/windo-open-%E5%AF%BC%E8%88%AA%E4%B8%8E%E6%89%93%E5%BC%80%E6%96%B0%E7%AA%97%E5%8F%A3/</guid><description>导航与打开新窗口  window.open()  1. 用于导航到指定的URL 2. 打开新浏览器窗口  接受4个参数，指定URL，目标窗口（名字），特性字符串，表示新窗口在浏览器历史记录中是否替代当前加载页面的布尔值（一般在不打开新窗口的时候使用）  js window.open(&quot;https:/...</description><pubDate>Wed, 26 May 2021 15:59:48 GMT</pubDate><content:encoded>&lt;h1&gt;导航与打开新窗口&lt;/h1&gt;
&lt;p&gt;window.open()&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用于导航到指定的URL&lt;/li&gt;
&lt;li&gt;打开新浏览器窗口&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接受4个参数，&lt;strong&gt;指定URL&lt;/strong&gt;，&lt;strong&gt;目标窗口&lt;/strong&gt;（名字），&lt;strong&gt;特性字符串&lt;/strong&gt;，表示新窗口在浏览器历史记录中&lt;strong&gt;是否替代当前加载页面&lt;/strong&gt;的布尔值（一般在不打开新窗口的时候使用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.open(&quot;https://www.baidu.com/&quot;, &quot;baidu&quot;);
// 与&amp;lt;a href=&quot;https://www.baidu.com/&quot; target=&quot;baidu&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有一个窗口名叫baidu，则这个窗口就会打开这URL；否者就会打开一个新窗口并将其命名为baidu&lt;/p&gt;
&lt;p&gt;第二个参数也可以是如下几个&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;_self&lt;/li&gt;
&lt;li&gt;_parent&lt;/li&gt;
&lt;li&gt;_top&lt;/li&gt;
&lt;li&gt;_blank&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. 弹出窗口&lt;/h2&gt;
&lt;p&gt;window.open会返回一个对象，可以通过调用他的api去操作打开的这个新窗口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const openWindow = window.open(&apos;https://www.baidu.com/&apos;,&apos;baidu&apos;,&apos;height=400,width=500,top=10,left=10,resizable=yes&apos;);

// 移动打开的窗口
openWindow.moveTo(100, 100);
// 关闭打开的窗口
openWindow.close();

//关闭之后这个对象仍然存在,可以判断它closed属性
window.closed // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹出的窗口可以关闭自己&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建的窗口window对象上有个属性&lt;code&gt;opener&lt;/code&gt;,指向他打开的窗口&lt;/p&gt;
&lt;p&gt;这个属性只在弹出窗口的最上层window对象有定义,就是指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opendWidon.opener === window // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为有了这个指针,表示的新开的窗口不能独立运行在进程当中,如果需要则需要把opener属性设置为null,与主页面切断链接之后,这个链接是不可以在恢复的&lt;/p&gt;
&lt;h2&gt;2.判断弹窗是否被屏蔽了&lt;/h2&gt;
&lt;p&gt;浏览器可能存在弹窗屏蔽程序,所以可以通过检测window.open()返回的值是否为null就能确定弹窗是否被屏蔽了&lt;/p&gt;
&lt;p&gt;但是有时候window.open都会报错&lt;/p&gt;
&lt;p&gt;所以用一层trycatch包裹会好一点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let blocked = false;
try{
    let openWindow = window.open(&apos;https://baidu.com&apos;,&quot;_blank&quot;);
    if(openWindow === null){
        blocked = true;
    }
} catch(e){
    blocked = true
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>fetch配置请求头下载文件（文件流显示进度）</title><link>https://nollieleo.github.io/posts/fetch%E9%85%8D%E7%BD%AE%E8%AF%B7%E6%B1%82%E5%A4%B4%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6%E6%96%87%E4%BB%B6%E6%B5%81%E6%98%BE%E7%A4%BA%E8%BF%9B%E5%BA%A6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/fetch%E9%85%8D%E7%BD%AE%E8%AF%B7%E6%B1%82%E5%A4%B4%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6%E6%96%87%E4%BB%B6%E6%B5%81%E6%98%BE%E7%A4%BA%E8%BF%9B%E5%BA%A6/</guid><description>上代码，到时候再写为什么这么做，因为懒  js import {   WritableStream, } from &apos;web-streams-polyfill/ponyfill&apos;;   import StreamSaver from &apos;streamsaver&apos;;  const handleFileD...</description><pubDate>Wed, 14 Apr 2021 10:24:33 GMT</pubDate><content:encoded>&lt;p&gt;上代码，到时候再写为什么这么做，因为懒&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  WritableStream,
} from &apos;web-streams-polyfill/ponyfill&apos;;  
import StreamSaver from &apos;streamsaver&apos;;

const handleFileDownLoad = async (url, username, password, filename) =&amp;gt; {
    const tempHeader = new Headers();
    tempHeader.append(&apos;Authorization&apos;, `Basic ${Base64.encode(`${username}:${password}`)}`);
    fetch(`${url}?pipelineDownLoad=true`, {
      method: &apos;GET&apos;,
      headers: tempHeader,
      // mode: &apos;no-cors&apos;,
      // redirect: &apos;manual&apos;,
    })
      .then((response) =&amp;gt; {
        console.log(response);
        if (!window.WritableStream) {
          StreamSaver.WritableStream = WritableStream;
          window.WritableStream = WritableStream;
        }
        const fileStream = StreamSaver.createWriteStream(filename, {
          writableStrategy: true,
          readableStrategy: true,
        });
        const readableStream = response.body;
        if (window.WritableStream &amp;amp;&amp;amp; readableStream?.pipeTo) {
          return readableStream.pipeTo(fileStream).then(() =&amp;gt; {
            message.success(&apos;下载成功&apos;);
          });
        }

        const writer = fileStream.getWriter();
        window.writer = writer;

        const reader = response.body?.getReader();
        const pump = () =&amp;gt; reader?.read()
          .then((res) =&amp;gt; (res.done
            ? writer.close()
            : writer.write(res.value).then(pump)));
        pump();
        // message.success(&apos;下载成功&apos;);
        return true;
      }).catch((error) =&amp;gt; {
        throw new Error(error);
      });
  };
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>js数据类型以及typeOf操作符号</title><link>https://nollieleo.github.io/posts/js%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%BB%A5%E5%8F%8Atypeof%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%8F%B7/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%BB%A5%E5%8F%8Atypeof%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%8F%B7/</guid><description>ES6有6中 简单数据类型（原始类型）和一种复杂数据类型   简单：Undifined, Null, Boolean, Number, String, Symbol   复杂：object  所有的值都可以用以上7种来表示   typeof操作符  ES的类型系统是松散的，typeof可以确定任意变...</description><pubDate>Sun, 14 Mar 2021 16:23:49 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;ES6有6中 &lt;strong&gt;简单数据类型&lt;/strong&gt;（&lt;strong&gt;原始类型&lt;/strong&gt;）和一种复杂数据类型&lt;/p&gt;
&lt;p&gt;简单：Undifined, Null, Boolean, Number, String, Symbol&lt;/p&gt;
&lt;p&gt;复杂：object&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所有的值都可以用以上7种来表示&lt;/p&gt;
&lt;h2&gt;typeof操作符&lt;/h2&gt;
&lt;p&gt;ES的类型系统是松散的，typeof可以确定任意变量的数据类型。&lt;/p&gt;
&lt;p&gt;typeof对一个值使用会返回的字符串之一&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;undefined&quot; 表示值未定义&lt;/li&gt;
&lt;li&gt;&quot;boolean&quot; 布尔值&lt;/li&gt;
&lt;li&gt;&quot;string&quot; 字符串&lt;/li&gt;
&lt;li&gt;&quot;number&quot; 数值&lt;/li&gt;
&lt;li&gt;&quot;object&quot; 表示为对象（而不是函数）或者null&lt;/li&gt;
&lt;li&gt;&quot;function&quot;表示函数&lt;/li&gt;
&lt;li&gt;&quot;symbol&quot;表示符号&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;函数在es中被认为是对象，但是不代表一种数据类型&lt;/p&gt;
&lt;p&gt;因此typeof可以很好的区别它和其他对象的区别&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;类型&lt;/h2&gt;
&lt;h3&gt;undefined&lt;/h3&gt;
&lt;p&gt;只有一个值，就是特殊值&lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当用let或者var 声明了变量但是没有初始化（给一个初始值）的时候，这个变量就相当于赋予了undefined值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let msg;
console.log(msg);// undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不需要显式的去给值赋值一个undefined，因为系统自动都会给未初始化的值赋值undefined&lt;/p&gt;
&lt;p&gt;*注意:&lt;/p&gt;
&lt;p&gt;使用typeof，未声明的变量和未初始化的变量返回都是&lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typeof a; // undefined
let b;
typeof b; // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;NULL&lt;/h3&gt;
&lt;p&gt;null类型也是同样只有一个值，特殊值Null；&lt;/p&gt;
&lt;p&gt;null逻辑上讲，表示的是一个&lt;strong&gt;空对象指针&lt;/strong&gt;，毕竟typeof null的时候是‘object&apos;;&lt;/p&gt;
&lt;p&gt;因此在定义将来要保存对象值得变量得时候，建议使用null初始化&lt;/p&gt;
&lt;p&gt;undefined值是由null值派生而来的，因此ECMA-262将他们定义为表面上相等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(null == undefined)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;*==操作符会对数据两头得数据类型进行转换，这个单独写一篇讲&lt;/p&gt;
&lt;h3&gt;Boolean&lt;/h3&gt;
&lt;p&gt;boolean就两值，&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;虽然只有两个值，但是可以通过特定得**Boolean()**转型函数将其他类型得值转换为布尔值&lt;/p&gt;
&lt;h4&gt;1.Boolean()转型&lt;/h4&gt;
&lt;p&gt;不同类型值转换为布尔值得转换规则如下&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据类型&lt;/th&gt;
&lt;th&gt;转为true&lt;/th&gt;
&lt;th&gt;转为false&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;非空字符串&lt;/td&gt;
&lt;td&gt;&quot;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Undefined&lt;/td&gt;
&lt;td&gt;N/A(不存在)&lt;/td&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;非0得数值（Infinity）&lt;/td&gt;
&lt;td&gt;0, NaN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object&lt;/td&gt;
&lt;td&gt;任何对象&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;*像一些if等流控制语句会自动得将其他类型值转换为boolean类型值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a = &apos;2121&apos;;
if(a){
    console.log(hello);
}
// hello;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;*Number&lt;/h3&gt;
&lt;p&gt;最有意思得数据类型了&lt;/p&gt;
&lt;p&gt;Number类型使用得是&lt;strong&gt;IEEE 754&lt;/strong&gt;格式表示整数和浮点值&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;十进制&lt;/li&gt;
&lt;li&gt;八进制(0开头，数字0~7)&lt;/li&gt;
&lt;li&gt;十六进制（0x开头，数字0~9，字母A~F）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;let a1 = 55; // 55
let a2 = 070; // 八进制的 56
let a3 = 098; // 无效八进制,自动转为十进制98
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 浮点数&lt;/h4&gt;
&lt;p&gt;表示：1.1， 1.2 ， 3.125e7， 3.125e-8&lt;/p&gt;
&lt;p&gt;*浮点数精确度问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(0.1 + 0.2==0.3){
    console.log(&apos;hello&apos;) 
}
// 这里的0.1 + 0.2 是不等于0.3的
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 值得范围&lt;/h4&gt;
&lt;p&gt;内存限制，ES不能表示这世界上所有得&lt;/p&gt;
&lt;p&gt;最小值存在：&lt;code&gt;Number.MIN_VALUE&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;最大值存在：&lt;code&gt;Number.MAX_VALUE&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;如果两个值得运算超过这两个极限值，则会是Infinity表示&lt;/p&gt;
&lt;h4&gt;3. NaN&lt;/h4&gt;
&lt;p&gt;特殊得数值：NaN，意思就是不是一个数值；&lt;/p&gt;
&lt;p&gt;用于本来是要返回数值得操作失败了（不抛错）&lt;/p&gt;
&lt;h5&gt;1）涉及任何NaN得操作都会返回NaN&lt;/h5&gt;
&lt;h5&gt;2）console(NaN == NaN)&lt;/h5&gt;
&lt;p&gt;为false&lt;/p&gt;
&lt;h5&gt;3）isNaN()函数&lt;/h5&gt;
&lt;p&gt;判断传入得值是否 ”不是数值“&lt;/p&gt;
&lt;p&gt;该函数会将传入得值尝试去转换为数值，任何不能转换为数值得值都会导致这个函数返回true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isNaN(NaN) // true
isNaN(10) // false
isNaN(&apos;10&apos;) // false
isNaN(&apos;blue&apos;) //true
isNaN(true) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;isNaN可以用于测试对象，首先会去调用对象得valueOf()方法，确定返回得值是否可以转换为数值，如果不行再调用toString()方法并测试返回值；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4. 数值转换&lt;/h4&gt;
&lt;p&gt;有3个函数可以将非数值型数据转为数值&lt;/p&gt;
&lt;p&gt;Number(), parseInt(), parseFloat()&lt;/p&gt;
&lt;h5&gt;1) Number()&lt;/h5&gt;
&lt;p&gt;转型函数可以用于任意类型得数据&lt;/p&gt;
&lt;p&gt;转型规则如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;布尔值，true为1，false为0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数值，直接返回数值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;null，返回0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;undefined&lt;/strong&gt;，返回&lt;strong&gt;NaN&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;包含数值字符串，包括前面带着+，- 号得情况，则转换为一个十进制数值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Number(&quot;1&quot;) // 1
Number(&quot;123abc&quot;) // 123
Number(&quot;0111&quot;) // 111
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串包含浮点值字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Number(&quot;1.1&quot;) // 1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串包含有效得十六进制格式，则转换为该十六进制对应得十进制数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Number(&quot;0xA&quot;) // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空字符串，则返回0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除了以上情况外，返回NaN&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象&lt;/p&gt;
&lt;p&gt;调用对象得valueOf()方法，并且按照上述规则转换返回得值。如果转换结果是NaN，则再调用对象得toString()方法，再按规则去转换&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Number(&quot;hello world&quot;) // NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;一元加操作符也遵循Number()转换规则&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;2) ParseInt()&lt;/h5&gt;
&lt;p&gt;此函数更加专注于字符串是否包含数值模式&lt;/p&gt;
&lt;p&gt;如果第一个字符不是数值字符， + -符号，则立即返回NaN&lt;/p&gt;
&lt;p&gt;空字符串也立即返回NaN&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parseInt(&quot;123&quot;) //123
parseInt(&quot;123abc&quot;) // 123
parseInt(&quot;&quot;) // NaN
parseInt(&quot;0xf&quot;) // 15
parseInt(&quot;07&quot;) // 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也接受第二个参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parseInt(&quot;0xAF&quot;,16) // 175
parseInt(&quot;AF&quot;,16) // 175
parseInt(&quot;AF&quot;) //NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3) ParseFloat()&lt;/h5&gt;
&lt;p&gt;和parseInt相似&lt;/p&gt;
&lt;h3&gt;String&lt;/h3&gt;
&lt;p&gt;字符串数据类型表示零或者多个16位得Unicode字符序列&lt;/p&gt;
&lt;h5&gt;1. 字符字面量&lt;/h5&gt;
&lt;p&gt;用于表示非打印字符或者其他用途字符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\t 
\n
\b
\r
\f
\\
\&apos;
\&quot;
\`
\xnn
\unnn
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果字符串包含双字节字符，length就不好确定了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;2. 字符串特点&lt;/h5&gt;
&lt;p&gt;es中得字符串是不可变的，一旦创建，值就不能再变化，要修改其中一个字符串值，必须先销毁原来得字符串再将包含新值的另一个字符串保存到该变量&lt;/p&gt;
&lt;h5&gt;3. 转换为字符串（toString &amp;amp; String()）&lt;/h5&gt;
&lt;p&gt;适用于 数值，字符串，布尔值，对象（字符串的toString只是简单的返回自身的一个副本），&lt;strong&gt;null和undefined没有这个方法&lt;/strong&gt;&lt;/p&gt;
&lt;h6&gt;1）toString()&lt;/h6&gt;
&lt;p&gt;toString再对数值类型进行转换的时候可以传入参数，其他的类型不允许&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let num = 10;
num.toString(); // &quot;10&quot;
num.toString(2); // &quot;1010&quot;
num.toString(8); // &quot;12&quot;
num.toString(16); // &quot;a&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2)   String()&lt;/h6&gt;
&lt;p&gt;如果不确定一个值是否为null或者undefined可以使用String()转型函数&lt;/p&gt;
&lt;p&gt;转换规则↓&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果值有toString()的方法，则调用该方法返回结果&lt;/li&gt;
&lt;li&gt;如果只是null返回&quot;null&quot;，undefined返回&quot;undefined&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;用加号给一值加上一个空字符串&quot;&quot;也可以将其转换为字符串&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;4. 模板字面量&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const name = &apos;lihua&apos;;
console.log(`hello ${name}`); // hello lihua
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;模板字面量保留换行符号,  空格&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const one = &apos;first line \nsecond line&apos;; 
// first line
// second line
const two = `first line
second line`;
// first line
// second line
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模板字面量不是字符串而是一种特殊的js句法表达式，只不过求值之后是字符串&lt;/p&gt;
&lt;p&gt;所有的插入值最后都会使用&lt;code&gt;toString()&lt;/code&gt;强制转型维字符串，适用与任何的js表达式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let foo =  {
    toString: ()=&amp;gt; &apos;world&apos;
};
console.log(`hello ${foo}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板字面量标签函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = 6;
let b = 5;
function simpleTag(strings, aValExpression, bValExpression, sum){
    console.log(strings);
    console.log(aValExpression);
	console.log(bValExpression);
    console.log(sum);
	return &apos;motherfucker&apos;;
}

let resUntagged = `${a} + ${b} = ${a + b}`;
let resTagged = simpleTag`${a} + ${b} = ${a + b}`;
// [&quot;&quot;, &quot;+&quot;, &quot;=&quot;, &quot;&quot;]
// 6 
// 5
// 11

console.log(resUntagged); // 6 + 9 = 15
console.log(resTagged); // motherfucker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表达式的参数一般可变，所以用...rest 传入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = 6;
let b = 5;
function simpleTag(strings, ...rest){
    console.log(strings);
    for(const express of rest){
        console.log(express);
    }
	return &apos;motherfucker&apos;;
}

let resTagged = simpleTag`${a} + ${b} = ${a + b}`;
// [&quot;&quot;, &quot;+&quot;, &quot;=&quot;, &quot;&quot;]
// 6 
// 5
// 11

console.log(resTagged); // motherfucker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要通过一个标签函数得到原始字符串的话&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = 6;
let b = 9;

function zipTag(strings, ...rest){
    return strings[0] + rest.map((e, i)=&amp;gt;`${e}${strings[i+1]}`).join(&apos;&apos;);
}

console.log(zipTag`${a} + ${b} = ${a + b}`); // 6 + 9 = 15
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原始字符串获取, 可以使用String.raw标签函数，从而获取到不是被转换后的字符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(`hello\nworld`);
// hello
// world

console.log(String.raw`hello\nworld`) // &quot;hello\nworld&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;但是对于实际的换行符号是不可以的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Symbol&lt;/h3&gt;
&lt;p&gt;符号属性是对内存中符号的一个引用&lt;/p&gt;
&lt;h4&gt;1.基本用法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let sym = Symbol();
let symV = Symbol(&apos;V&apos;);
typeof sym; // symbol
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里不可以使用像Boolean, String, Number那样使用构造函数初始化包装对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;let sym = new Symbol(); // TypeError: Symbol is not a constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.创建全局符号注册表&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Symbol.for()&lt;/code&gt;全局创建符号实现 共享 重用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let globleSym = Symbol.for(&quot;mine&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次调用Symbol.for()会去全局注册一个符号，如果再次调用则直接从注册表中检查，并且返回该符号实例；&lt;/p&gt;
&lt;p&gt;全局注册和使用普通方式定义的符号实例不等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = Symbol.for(&apos;hello&apos;);

let b = Symbol.for(&apos;hello&apos;);

let c= Symbol(&apos;hello&apos;);

a === b // true
a === c // false
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;传给Symbol.for()函数的任何值都会被转换为字符串&lt;/p&gt;
&lt;p&gt;let empty = Symbol.for();&lt;/p&gt;
&lt;p&gt;empty // Symbol(undefined);&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以使用 &lt;code&gt;Symbol.keyFor()&lt;/code&gt;来查询&lt;strong&gt;全局注册表&lt;/strong&gt;，这个方法接受符号，返回该全局符号对应的字符串键（如果没有则返回undefined, 如果传入的不是符号则会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let s = Symbol.for(&apos;foo&apos;);
console.log(Symbol.keyFor(s)); // foo	

let a = Symbol(&apos;bar&apos;);
console.log(Symbol.keyFor(a)); // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 使用符号作为属性&lt;/h4&gt;
&lt;p&gt;凡是可以使用字符串或数值作属性的地方都可以使用符号，包扩了&lt;code&gt;Object.defineProperty() &lt;/code&gt; / &lt;code&gt;Object.defineProperties()&lt;/code&gt;定义属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let s1 = Symbol(&apos;hello&apos;), s2 = Symbol(&apos;world&apos;), s3 = Symbol(&apos;beauty&apos;);
let o = {
    [s1]: &apos;1212&apos;
};
console.log(o); // {Symbol(hello): 1212}

Object.defineProperties(o, {
    [s2]: &apos;ssss&apos;,
    [s3]: &apos;2121&apos;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Object.getOwnPropertyNames()返回对象实例的常规属性数组&lt;/li&gt;
&lt;li&gt;Object.getOwnPropertySymbols()返回对象实例的符号属性数组&lt;/li&gt;
&lt;li&gt;Object.getOwnPropertyDescriptors()返回同时包含常规和符号属性描述符的对象&lt;/li&gt;
&lt;li&gt;Reflect.ownKeys()会返回两种类型的键&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.常用的内置符号&lt;/h4&gt;
&lt;p&gt;ES6引入了一些常用的内置符号，用于暴露语言内部的行为，开发者可以直接访问，重写或者模拟这些行为&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;改变原生行为。比如for-of循环会在相关对象上使用Symbol.iterator属性，那么就可以通过在自定义的对象上面重新定义这个[Symbol.iterator]从而改变for-of遍历对象时候的行为&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;1） Symbol.asyncIterator （异步迭代器符号）(ES2018)&lt;/h5&gt;
&lt;p&gt;这个符号作为一个属性，标识一个方法，该方法返回对象默认的AsyncIterator，由for-await-of语句使用，也就是异步迭代API函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo{
    async *[Symbol.asyncIterator](){}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator()]);

// AsyncGenerator(&amp;lt;suspended&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此符号函数生成的对象应该显式通过next()方法陆续返回Promise实例，也可以隐式通过异步生成器函数返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Emitter { 
	constructor(max){
        this.max = max;
        this.index = 0;
    }
    async *[Symbol.asyncIterator](){
        while(this.index&amp;lt; this.max){
            yield new Promise((resolve)=&amp;gt;resolve (this.index++));
        }
    }
}

async function count(number){
    let emitter = new Emitter(number);
    for await(const x of emitter){
        console.log(x)
    }
}

count(5);
// 0
// 1
// 2
// 3
// 4
// 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2) Symbol.hasInstance&lt;/h5&gt;
&lt;p&gt;标识一个方法，决定一个构造器对象是否认可一个对象式它的实例。由&lt;code&gt;instanceof&lt;/code&gt; 操作符使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Foo(){}
let f = new Foo();
console.log(f instanceof Foo); // true
console.log(Foo[Symbol.hasInstance](f)) // true;

class FlaseInstance(){
    static [Symbol.hasInstance](){
        return false
    }
}

let a = new FalseInstance();
console.log(a intanceof FalseInstance) //false
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3) Symbol.isConcatSpreadable&lt;/h5&gt;
&lt;p&gt;标识一个布尔值，如果是true以为着对象改用Array.prototype.concat()打平其数组元素。&lt;/p&gt;
&lt;p&gt;ES6中的&lt;code&gt;Array.prototype.concat()&lt;/code&gt;方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。&lt;/p&gt;
&lt;p&gt;如值为false，则会导致整个对象被追加到数组末尾，其他不是类数组对象的对象在true情况下会被忽略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let init = [&apos;foo&apos;];

let arr = [&apos;bar&apos;];
console.log(arr[Symbol.isConcatSpreadable]) // undefined
console.log(init,concat(arr)) // [&apos;foo&apos;, &apos;bar&apos;]
arr[Symbol.isConcatSpreadable] = false;
console.log(init.concat(arr)) // [&apos;foo&apos;, Array(1)];

let likeObj = { length:1, 0: &apos;bar&apos;};
console.log(likeObj[Symbol.isConcatSpreadable]); //undefined
console.log(init.cancat(likeObj)) // [&apos;foo&apos;, {...}];
likeObj[Symbol.isConcatSpreadable] = true;
console.log(init.concat(likeObj)) // [&apos;foo&apos;, &apos;bar&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4) Symbol.iterator&lt;/h5&gt;
&lt;p&gt;标识一个方法，返回对象默认迭代器，由&lt;code&gt;for-of&lt;/code&gt;语句使用&lt;/p&gt;
&lt;p&gt;这与上面的Symbol.asyncIterator相类似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Bar {
	constructor(max){
		this.max = max;
		this.index = 0;
	}
	*[Symbol.iterator](){
		while(this.index &amp;lt; this.max){
			yield this.index++;
		}
	}
}

function count(){
	let bar = new Bar(5);
	for(const x of bar){
		console.log(x)
	}
}
// 0
// 1
// 2
// 3
// 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5) Symbol.match&lt;/h5&gt;
&lt;p&gt;标识一个正则表达式的方法，该方法用正则表达式去匹配字符串；&lt;/p&gt;
&lt;p&gt;由&lt;code&gt;String.prototype.match()&lt;/code&gt;方法使用，使用以Symbol.match为键的函数来对这个正则表达式求值。正则表达式的原型上默认有这个函数的定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(RegExp.prototype[Symbol.match]);
// f [Symbol.match]() { [native code] }
console.log(&apos;foobar&apos;.match(/bar/));
// [&quot;bar&quot;, index: 3, input: &quot;foobar&quot;, groups: undefined]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给这个方法传入非正则的表达式值会导致该值呗转换为RegExp对象，如果想要改变这行为，允许方法直接使用参数，则重新定义Symbol.match&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class FooMatcher {
     static [Symbol.match](target){
         return target.includes(&apos;foo&apos;)
     }
}

console.log(&apos;foobar&apos;.match(FooMacther)) // true;

class StringMatcher{
    constructor(str){
        this.str = str;
    }
    [Symbol.match](target){
        return target.includes(this.str);
    }
}

console.log(&apos;foobar&apos;.match(new StringMacther(&apos;foo&apos;))) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;6) Symbol.replace&lt;/h5&gt;
&lt;p&gt;标识一个正则表达式的方法，替换一个字符串中匹配的子串；&lt;/p&gt;
&lt;p&gt;由&lt;code&gt;String.prototype.replace()&lt;/code&gt;方法会使用以Symbol.replace为键的函数来对正则表达式求值, 和上面的match一样，为了阻止默认的非正则表达式值被强行转换为RegExp对象，可以覆盖默认行为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class FooReplacer {
    static [Symbol.replace](target, replacememt) {
        return target,split(&apos;foo&apos;).join(replacement);
    }
}
console.log(&apos;barfoobaz&apos;.replace(FooReplacer, &apos;quz&apos;));
// barquxbaz

class StringReplacer {
    constructor(str){
        this.str = str;
    }
    [Symbol.replace](target, replacement){
        return target.split(this.str).join(replacement);
    }
}
console.log(&apos;barfoobaz&apos;.replace(new StringReplacer(&apos;foo&apos;), &apos;quz&apos;));
// barfoobaz
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;7）Symbol.search&lt;/h5&gt;
&lt;p&gt;标识一个正则的方法，返回字符串中匹配正则表达式的索以。&lt;/p&gt;
&lt;p&gt;由&lt;code&gt;String.prototype.search()&lt;/code&gt;方法使用。String.prototype.search()方法会使用以Symbol.search为键的函数来对正则表达式进行求值, 同样和上述两个方法一样，为了阻止默认的行为，可以覆盖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;foobar&apos;.search(/bar/)); //3

class StringSearcher {
	constructor(str){
		this.str = str;
	}
	[Symbol.search](target){
		return target.indexOf(this.str);
	}
}

console.log(&apos;foobar&apos;.search(new StringSeacher(&apos;foo&apos;))); // 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;8) Symbol.species&lt;/h5&gt;
&lt;p&gt;标识一个属性。表示一个函数值，该函数作为创建派生对象的构造函数。&lt;/p&gt;
&lt;p&gt;用Symbol.specied定义静态的获取器（getter）方法，可覆盖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Bar extends Array {}
class Baz extends Array {
    static get [Symbol.species](){
        return Array;
    }
}

let bar = new Bar();
console.log(bar instanceof Array) // true;
console.log(bar instanceof Bar) // true;

let baz = new Baz()
console.log(baz instanceof Array) // true
console.log(baz instanceof Baz) // true
baz = baz.concat(&apos;baz&apos;);
console.log(baz instanceof Array) // true
console.log(baz instanceof Baz) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;9) Symbol.toPrimitive&lt;/h5&gt;
&lt;p&gt;标识一个属性标识一个方法，该方法将对象转换为相应的原始值。有ToPrimitive抽象操作使用&lt;/p&gt;
&lt;p&gt;很多内置操作都会尝试强制将对象转换为原始的值，包括字符串数值和未指定的原始类型。对于一个自定义对象实例，通过这个实例的Symbol.toPrimitive属性覆盖默认行为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo{}
let foo = new Foo();
console.log(3 + foo); // 3[object Object]
console.log(3 - foo); // NaN
console.log(String(foo)); // [object Object]

class Baz{
    constructor(){
        this[Symbol.toPrimitive] = function(hint){
            switch(hint){
				case &apos;number&apos;:
                    return 3;
                case &apos;string&apos;:
                    return &apos;string bar&apos;;
                case &apos;default&apos;:
                default:
                    return &apos;default bar&apos;;
            }
        }
    }
}

let bar = new Bar();

console.log(3 + bar) // 3default bar
console.log(3 - bar) // 0
console.log(String(3)) // string bar
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;10）Symbol.toStringTag&lt;/h5&gt;
&lt;p&gt;标识一个属性表示一个字符串，该字符串用再创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用&lt;/p&gt;
&lt;p&gt;通过toString()方法获取对象标识时候，会自动检索由Symbol.toStringTag指定的实例表示符号，默认为&quot;Object&quot;。&lt;/p&gt;
&lt;p&gt;内置类型以及指定了这个值，自定义类实力还是需要明确定义的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let s = new Set();

console.log(s) // Set(0){}
console.log(s.toString()); // [object Set];
console.log(s[Symbol.toStringTag]); // Set

class Baz{
    constructor(){
        this[Symbol.toStringTag] = &quot;Bar&quot;;
    }
}

let baz = new Baz(); // Baz {}
console.log(baz.toString()) // [object Baz]
console.log(baz[Symbol.toStringTag]) // Baz


&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>var, let, const声明变量</title><link>https://nollieleo.github.io/posts/var-let-const%E5%A3%B0%E6%98%8E%E5%8F%98%E9%87%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/var-let-const%E5%A3%B0%E6%98%8E%E5%8F%98%E9%87%8F/</guid><description>ECMAscript中变量是松散类型的，意思就是可以用于保存任何类型的数据   有3个关键字可以声明变量 var, const, let   var 在所有版本ECMAscript中都能用，let const只能在ES6中使用   var 关键字  js var a = 11212	    1. v...</description><pubDate>Sun, 14 Mar 2021 15:39:55 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;ECMAscript中变量是松散类型的，意思就是可以用于保存任何类型的数据&lt;/p&gt;
&lt;p&gt;有3个关键字可以声明变量 var, const, let&lt;/p&gt;
&lt;p&gt;var 在所有版本ECMAscript中都能用，let const只能在ES6中使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;var 关键字&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;var a = 11212	
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. var 声明作用域&lt;/h3&gt;
&lt;p&gt;使用var操作符号定义的变量会成为&lt;strong&gt;包含它的函数&lt;/strong&gt;的局部变量，会被拿到函数或全局作用域得顶部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function hello(){
	var a = &apos;hi&apos;; // 局部变量
}
hello();
console.log(a); // 出错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里声明了a变量，函数使用完退出之后就销毁了这个a变量&lt;/p&gt;
&lt;p&gt;但是有一种情况，直接在函数中声明全局变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function hello(){
    a=&apos;hi&apos;;
}
hello();
console.log(a); // hi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用hello()函数之后全局注入了一个a的变量，这个变量&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然可以通过省略var操作符定义全局变量，但是不推荐。局部作用域中定义全局变量很难维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. var声明提式（变量提升hoist）&lt;/h3&gt;
&lt;p&gt;如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function foo(){
    console.log(a);
    var a = &apos;1212&apos;;
}
foo(); // undifined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a变量在定义之前就已经被访问，但是关键字会自动提升到函数作用域的顶部&lt;/p&gt;
&lt;p&gt;等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function foo(){
    var a;
    console.log(a);
    a = &apos;1212&apos;;
}
foo();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 全局变量声明&lt;/h3&gt;
&lt;p&gt;在全局中使用var来声明变量，这个变量会成为window对象的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name = &apos;weng&apos;;
console.log(window.name) // weng
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;let声明&lt;/h2&gt;
&lt;h3&gt;1. let的块作用域&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;let和var的作用差不多，但是有很大区别。最明显的是Let声明的范围是块作用域， 而var声明的范围是函数作用域&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;var&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(true){
    var name = &apos;a&apos;;
    console.log(name); // a
}
console.log(name); // a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;let&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(true){
    let name = &apos;a&apos;;
    console.log(name); // a
}
console.log(name); // ReferenceError:name 没有定义
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用let声明的变量之所以不能在if块外部被引用，是因为它的作用域仅限于该块的内部&lt;/p&gt;
&lt;p&gt;块作用域是函数作用域的子集， 函数作用域 &amp;gt; 块作用域&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因此适用于var的作用域也适用于let作用域&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;当然let也不允许同一个块作用域中出现冗余的声明，这样会报错&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var name;
var name;

let age;
let age; // SntaxError;标识符age已经声明过了
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;js引擎会记录用于变量声明的标识符以及其所在的块作用域，嵌套使用相同的标识符不会报错，因为在同一块中没有重复声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let age =30;
console.log(age);
function hello(){
	let age = 20;
    console.log(age); // 20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 暂时性死区（let不存在变量提升）&lt;/h3&gt;
&lt;p&gt;let 与var的另外一个重要区别就是let声明的变量不会在所对应的块作用域中被提升&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(name); // ReferenceError; age未被定义
let age = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;let和var同理，在访问name变量的时候js会去找块后面的let声明，只是var声明的变量可以提前访问，let不能够提前访问；&lt;/p&gt;
&lt;p&gt;在let声明之前执行瞬间被称作“暂时性死区”&lt;/p&gt;
&lt;h3&gt;3.全局变量声明&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let age = 26;
console.log(window.age) // undifined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;let全局声明的变量并不会成为window对象的属性这点与var有区别&lt;/p&gt;
&lt;p&gt;不过let声明在全局作用域中发生，相对应的变量会在页面的声明周期中续存&lt;/p&gt;
&lt;h3&gt;4.for循环中的let声明&lt;/h3&gt;
&lt;p&gt;在let出现之前，for循环体定义的迭代变量会渗透到循环体的外部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(var i =0;i&amp;lt;9;i++){
    // ....
}
console.log(i); // 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用let之后这个问题就消失了；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(let i = 0;i&amp;lt;9;i++){
    // ....
}
console.log(i); // ReferenceError;i 没有定义
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用var的时候，最常见的问题就是对迭代变量的奇特声明和修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(var i = 0;i&amp;lt;5;i++){
    setTimeout(()=&amp;gt;console.log(i),0);
}
// 你以为的输出0，1，2，3，4，5
//实际 5，5，5，5，5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以这样是因为在退出循环的时候，迭代变量保存的是导致循环退出的值：5.&lt;/p&gt;
&lt;p&gt;之后执行异步逻辑的时候，所有的i都是同一个变量，因此都是同一个最终值&lt;/p&gt;
&lt;p&gt;而使用ley声明迭代变量的时候，js引擎会在后台为每个迭代循环声明一个新的迭代变量，也就是每个内部的异步函数引用的都是不同的变量实例，所以console.log()输出的是我们期望的值&lt;/p&gt;
&lt;h2&gt;const声明&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;const和let相同，唯一区别就是它声明的变量时必须初始话一个值，并且后续尝试修改声明的变量会导致报错&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;*注意&lt;/p&gt;
&lt;p&gt;const声明得限制只适用于它指向的变量的引用，换句话说，如果const变量的引用指向一个对象，那么修改这个对象内部的属性，并不违反const的限制&lt;/p&gt;
&lt;p&gt;一般const用在for..of for.. in 语句中&lt;/p&gt;
</content:encoded></item><item><title>fr单位配合grid布局</title><link>https://nollieleo.github.io/posts/fr%E5%8D%95%E4%BD%8D%E9%85%8D%E5%90%88grid%E5%B8%83%E5%B1%80/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/fr%E5%8D%95%E4%BD%8D%E9%85%8D%E5%90%88grid%E5%B8%83%E5%B1%80/</guid><description>在网格布局中的运用   网格布局支持弹性尺寸（flex-size），这是一个很好的自适应布局技术。  fr是一个相对尺寸单位，表示剩余空间做等分，此项分配到的百分比(如果只有一个项使用此单位，那就占剩余空间的100%，所以多个项联合使用更有意义)    弹性尺寸使用fr尺寸单位，其来自 “fract...</description><pubDate>Fri, 12 Mar 2021 11:09:25 GMT</pubDate><content:encoded>&lt;p&gt;在网格布局中的运用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网格布局支持弹性尺寸（flex-size），这是一个很好的自适应布局技术。
fr是一个相对尺寸单位，表示剩余空间做等分，此项分配到的百分比(如果只有一个项使用此单位，那就占剩余空间的100%，所以多个项联合使用更有意义)&lt;/p&gt;
&lt;p&gt;弹性尺寸使用fr尺寸单位，其来自 “fraction” 或 “fractional unit” 单词的前两个字母，表示整体空间的一部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结合grid布局可以实现等分行列的烦恼&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  .grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px 20px;
    width: 500px;
    padding: 10px;
    background: bisque;
  }
  .grid-item {
    background-color: aquamarine;
  }
&amp;lt;/style&amp;gt;
&amp;lt;div class=&quot;grid&quot;&amp;gt;
  &amp;lt;div class=&quot;grid-item&quot;&amp;gt;
    &amp;lt;p&amp;gt;12121&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;grid-item&quot;&amp;gt;
    &amp;lt;p&amp;gt;12121&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;grid-item&quot;&amp;gt;
    &amp;lt;p&amp;gt;12121&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;坑&lt;/h3&gt;
&lt;p&gt;如果内部的元素内容很多就会容易造成不等分的情况&lt;/p&gt;
&lt;p&gt;如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;image-20210312113529373&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方案&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grid-template-columns: repeat(3, minmax(10px,1fr));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给了10px的最小宽度值。解决了最小宽度不确定导致的溢出问题、同时10px最小宽度比起0px避免了元素直接消失，当问题出现时可减小调试成本。&lt;/p&gt;
</content:encoded></item><item><title>箭头函数理解</title><link>https://nollieleo.github.io/posts/%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0%E7%90%86%E8%A7%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0%E7%90%86%E8%A7%A3/</guid><description>箭头函数  es6语法  js // ES6语法 const fn = v = v;    一. 特点   1. 语法简介  箭头函数省去了function关键字，用=代替function，圆括号代表参数部分，当只有一个参数时，圆括号可省略，当只有一行返回语句时，return和花括号{}都可以省略。...</description><pubDate>Mon, 08 Mar 2021 10:19:44 GMT</pubDate><content:encoded>&lt;h2&gt;箭头函数&lt;/h2&gt;
&lt;p&gt;es6语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ES6语法
const fn = v =&amp;gt; v;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一. 特点&lt;/h3&gt;
&lt;h4&gt;1. 语法简介&lt;/h4&gt;
&lt;p&gt;箭头函数省去了&lt;code&gt;function&lt;/code&gt;关键字，用&lt;code&gt;=&amp;gt;&lt;/code&gt;代替function，圆括号代表参数部分，当只有一个参数时，圆括号可省略，当只有一行返回语句时，&lt;code&gt;return&lt;/code&gt;和花括号&lt;code&gt;{}&lt;/code&gt;都可以省略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 求两数之和
const fn = (a, b) =&amp;gt; {return a + b;}; // 等价于 const fn = (a, b) =&amp;gt; a + b;

// 求一个数组各项之和
const sum = [1, 2, 3, 4, 5].reduce((x, y) =&amp;gt; x + y, 0); // 15

// 将数组中的元素按从小到大顺序排序
const array = [2, 4, 1, 5, 9, 7].sort((x, y) =&amp;gt; x - y); //  [1, 2, 4, 5, 7, 9]

// 过滤数组中为偶数的数字
const array = [0, 1, 2, 3, 4, 5, 6].filter(x =&amp;gt; x % 2 === 0); // [0, 2, 4, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 不绑定this&lt;/h4&gt;
&lt;p&gt;箭头函数体内的&lt;code&gt;this&lt;/code&gt;永远指向的是定义时所在的对象，而不是调用时所在的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Fn() {
  this.s1 = 0;
  this.s2 = 0;

  // 箭头函数
  setInterval(() =&amp;gt; this.s1++, 1000);

  // 普通函数
  setInterval(function() {
    this.s2++;
  }, 1000);
}

var fn = new Fn();
setTimeout(() =&amp;gt; console.log(&apos;s1= &apos;, fn.s1), 3100); // 3.1秒后输出s1= 3
setTimeout(() =&amp;gt; console.log(&apos;s2= &apos;, fn.s2), 3100); // 3.1秒后输出s2= 0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，Fn函数内部设置了两个定时器，分别使用了箭头函数和普通函数。&lt;/p&gt;
&lt;p&gt;箭头函数中的&lt;code&gt;this&lt;/code&gt;指向定义时所在的作用域（即Fn函数），this.s1++是处在箭头函数中，这里的this就是&lt;code&gt;fn&lt;/code&gt;，所以fn.s1的值为3。&lt;/p&gt;
&lt;p&gt;而普通函数中的&lt;code&gt;this&lt;/code&gt;指向运行时所在的作用域（即全局对象window），this.s2++实际等于window.s2++，fn.s2一次都没有更新，因而得到的是0。&lt;/p&gt;
&lt;p&gt;所以，从严格意义上讲，箭头函数中不会创建自己的&lt;code&gt;this&lt;/code&gt;，而是会从自己作用域链的上一层继承。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Person = {
  name: &apos;Kimmy&apos;,
  age: 20,
  doSomething: function() {
    setTimeout(() =&amp;gt; console.log(`name: ${this.name}, age: ${this.age}`), 1000);
  }
};

Person.doSomething(); // name: Kimmy, age: 20

const Person2 = {
  name: &apos;Kimmy&apos;,
  age: 20,
  doSomething: () =&amp;gt; {
    setTimeout(() =&amp;gt; console.log(`name: ${this.name}, age: ${this.age}`), 1000);
  }
};

Person2.doSomething(); // name: , age: undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两段代码的唯一区别在于doSomething()函数的写法，Person中使用了普通函数定义，Person2中使用了箭头函数定义。&lt;/p&gt;
&lt;p&gt;在第一段代码中，Person.doSomeThing()中的&lt;code&gt;this&lt;/code&gt;指向函数的调用体，即&lt;code&gt;Person&lt;/code&gt;本身，在调用&lt;code&gt;setTimeout()&lt;/code&gt;函数时，由于其函数体部分是通过箭头函数定义的，内部的&lt;code&gt;this&lt;/code&gt;会继承父作用域的&lt;code&gt;this&lt;/code&gt;（即&lt;code&gt;Person&lt;/code&gt;），从而输出&lt;code&gt;name: Kimmy, age: 20&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在第二段代码中，Person2.doSomething()中的&lt;code&gt;this&lt;/code&gt;指向外层作用域，而Person2的父作用域是全局作用域window，在调用&lt;code&gt;setTimeout()&lt;/code&gt;函数时，由于其函数体部分是通过箭头函数定义的，内部的&lt;code&gt;this&lt;/code&gt;会继承&lt;code&gt;doSomething()&lt;/code&gt;函数所在的作用域&lt;code&gt;this&lt;/code&gt;（即window），而window上name属性是&lt;code&gt;&apos;&apos;&lt;/code&gt;，age属性不存在，所以&lt;code&gt;Person2.doSomething()&lt;/code&gt;输出&lt;code&gt;name: , age: undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;综上所述，箭头函数根本没有自己的&lt;code&gt;this&lt;/code&gt;，导致内部的&lt;code&gt;this&lt;/code&gt;就是外层代码块的&lt;code&gt;this&lt;/code&gt;。正因为它没有&lt;code&gt;this&lt;/code&gt;，所以也就&lt;code&gt;不能用作构造函数&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;3. 不支持 call()、 apply()、bind() 函数的特性&lt;/h4&gt;
&lt;p&gt;通过调用 &lt;code&gt;call()&lt;/code&gt; 、&lt;code&gt;apply()&lt;/code&gt;、&lt;code&gt;bind()&lt;/code&gt; 函数可以改变一个函数的执行主体，即改变被调用函数中 &lt;code&gt;this&lt;/code&gt; 的指向&lt;/p&gt;
&lt;p&gt;但箭头函数中不支持 &lt;code&gt;call()&lt;/code&gt; 、&lt;code&gt;apply()&lt;/code&gt;、&lt;code&gt;bind()&lt;/code&gt;等，因为箭头函数中没有自己的 &lt;code&gt;this&lt;/code&gt; ，而是继承父作用域中的 &lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fn() {
  return () =&amp;gt; {
    console.log(`id= ${this.id}`);
  }
}

let f = fn.call({id: 1}); // id= 1
let f2 = f.call({id: 2})()(); // id = 1
let f3 = f().call({id: 3})(); // id= 1
let f4 = f()().call({id: 4}); // id= 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中只有一个&lt;code&gt;this&lt;/code&gt;，即函数&lt;code&gt;fn&lt;/code&gt;的&lt;code&gt;this&lt;/code&gt;，所以&lt;code&gt;f2&lt;/code&gt;、&lt;code&gt;f3&lt;/code&gt;、&lt;code&gt;f4&lt;/code&gt;都输出同样的结果。因为内层函数时箭头函数，没有自己的&lt;code&gt;this&lt;/code&gt;，它们的&lt;code&gt;this&lt;/code&gt;都是最外层&lt;code&gt;fn&lt;/code&gt;函数是&lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;4. 不绑定 arguments&lt;/h4&gt;
&lt;p&gt;在普通函数&lt;code&gt;function&lt;/code&gt;中，我们可以通过&lt;code&gt;arguments&lt;/code&gt;对象来获取到实际传入的参数值，但在箭头函数中是不存在的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fn = () =&amp;gt; {
  console.log(arguments);
}
fn(1, 2); // Uncaught ReferenceError: arguments is not defined

function fn() {
  setTimeout(() =&amp;gt; {
    console.log(arguments);
  }, 0);
}

fn(1, 2); // [1, 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5.支持嵌套&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const pipeline = (...funcs) =&amp;gt; val =&amp;gt; funcs.reduce((a, b =&amp;gt; b(a)), val);
const plus1 = a =&amp;gt; a + 1;
const mult2 = a =&amp;gt; a * 2;
const addThenMult = pipeline(plus1, mult2);
addThenMult(5); // 12
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二.箭头函数不适用场景&lt;/h3&gt;
&lt;h4&gt;（1）定义对象的方法且该方法内部包括&lt;code&gt;this&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const Person = {
  name: &apos;Kimmy&apos;,
  age: 20,
  doSomething: () =&amp;gt; {
    this.age++;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Person.doSomething()方法是一个箭头函数，调用Person.doSomething()时，如果是普通函数，该方法内部的&lt;code&gt;this&lt;/code&gt;指向Person，如果写成上面那样的箭头函数，使得&lt;code&gt;this&lt;/code&gt;指向全局对象。&lt;/p&gt;
&lt;h4&gt;（2）不能作为构造函数，不能使用 new 操作符&lt;/h4&gt;
&lt;p&gt;构造函数时通过&lt;code&gt;new&lt;/code&gt;操作符生成对象实例的，生成实例的过程也是通过构造函数给实例绑定&lt;code&gt;this&lt;/code&gt;的过程，而箭头函数没有自己的&lt;code&gt;this&lt;/code&gt;，因此不能使用箭头函数作为构造函数，也不能通过&lt;code&gt;new&lt;/code&gt;操作符来调用箭头函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 普通函数
function Person(name) {
  this.name = name;
}
let p = new Person(&apos;Kimmy&apos;); // 正常

// 箭头函数
let Person = (name) =&amp;gt; {
  this.name = name;
}
let p = new Person(&apos;Kimmy&apos;); // Uncaught TypeError: Person is not a constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）没有 prototype 属性&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let a = () =&amp;gt; 1;

function b() {
  return 2;
}

a.prototype // undefined
b.prototype // {constructor: ƒ}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）不适合将原型函数定义成箭头函数&lt;/h4&gt;
&lt;p&gt;在给构造函数添加原型函数时，如果使用箭头函数，其中的&lt;code&gt;this&lt;/code&gt;会指向全局作用域&lt;code&gt;window&lt;/code&gt;，而并不会指向构造函数，因此并不会访问到构造函数本身，也就无法访问到实例属性，这就失去了作为原型函数的意义。&lt;/p&gt;
</content:encoded></item><item><title>工作中常用的Git命令</title><link>https://nollieleo.github.io/posts/%E5%B7%A5%E4%BD%9C%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84git%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%B7%A5%E4%BD%9C%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84git%E5%91%BD%E4%BB%A4/</guid><description>常用的基本操作   git init  这个git init不用多说，大家都知道这个命令是初始化当前目录变成可以使用git管理的仓库，并且是空的。     git clone 远程地址[url]  通过git clone命令从远程地址下载出来，这个也不用过多描述。     git status  g...</description><pubDate>Thu, 04 Mar 2021 15:37:00 GMT</pubDate><content:encoded>&lt;h2&gt;常用的基本操作&lt;/h2&gt;
&lt;h3&gt;git init&lt;/h3&gt;
&lt;p&gt;这个&lt;code&gt;git init&lt;/code&gt;不用多说，大家都知道这个命令是初始化当前目录变成可以使用&lt;code&gt;git&lt;/code&gt;管理的仓库，并且是空的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.image&quot; alt=&quot;git init&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git clone 远程地址[url]&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;git clone&lt;/code&gt;命令从远程地址下载出来，这个也不用过多描述。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.image&quot; alt=&quot;git clone&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git status&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git status&lt;/code&gt;查看本次本地有多少个文件发生变更。可以看到&lt;code&gt;index.css&lt;/code&gt;d和&lt;code&gt;index.html&lt;/code&gt;发生变更&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.image&quot; alt=&quot;git status&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git log&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git log&lt;/code&gt;查看当前提交的日志。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.image&quot; alt=&quot;git log&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git diff&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git diff&lt;/code&gt;是查看当前改动的文件具体代码内容比对。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.image&quot; alt=&quot;git diff&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git checkout .&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git checkout .&lt;/code&gt;就是所有有改动的全部恢复到原来的样子, 当然也可以恢复指定的如：&lt;code&gt;git checkout index.css&lt;/code&gt;只恢复这个文件当前的修改。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.image&quot; alt=&quot;git checkout&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git add .&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git add .&lt;/code&gt;是将修改的内容新增到暂存区，也可以提交指定的的文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.image&quot; alt=&quot;git add&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git commit -m &quot;你的要提交的注释&quot;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git commit -m&lt;/code&gt;这里的内容从暂存区写入到对象库中, &lt;strong&gt;注意注释必须写&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.image&quot; alt=&quot;git commit&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git tag&lt;/h3&gt;
&lt;p&gt;查看当前tag标签&lt;/p&gt;
&lt;h3&gt;git tag tagName(你的tag名称)&lt;/h3&gt;
&lt;p&gt;新建一个Tag标签&lt;/p&gt;
&lt;h3&gt;git tag -a tagName -m &quot;tag备注&quot;&lt;/h3&gt;
&lt;p&gt;新建一个tag标签带有备注信息&lt;/p&gt;
&lt;h3&gt;git show tagName(你的tag标签名)&lt;/h3&gt;
&lt;p&gt;查看当前tag备注信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./9.image&quot; alt=&quot;git show tag&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git push origin tagName(你的tag名称)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git push origin v1.0&lt;/code&gt;推送到远程&lt;/p&gt;
&lt;h3&gt;git push origin branch(你的分支)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git push origin branch&lt;/code&gt;推送到远程仓库。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.image&quot; alt=&quot;git push origin branch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git pull origin branch(你的分支)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git pull origin branch&lt;/code&gt;从远程拉取到本地。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.image&quot; alt=&quot;git pull origin branch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git checkout branch(你的分支)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git checkout branch&lt;/code&gt;切换到别的分支上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./12.image&quot; alt=&quot;git checkout branch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git checkout -b branch(你的分支)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git checkout -b branch(分支名称)&lt;/code&gt;新建一个分支并切换到该分支上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./13.image&quot; alt=&quot;git checkout -b&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git branch -v&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git branch -v&lt;/code&gt;查看当前的分支并且后面带有最后一次提交的信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./14.image&quot; alt=&quot;git branch -v&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git branch -a&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git branch -a&lt;/code&gt;查看当前所有的分支包括远程分支&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./15.image&quot; alt=&quot;git branch -a&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git branch branch(你的分支)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git branch barnch(你的分支名称)&lt;/code&gt;新建一个本地分支。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./16.image&quot; alt=&quot;git branch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git branch -D name(分支名)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git branch -D name(分支名)&lt;/code&gt; 删除本地分支，但是不能在当前的分支上删除当前分支，必须切换到别的分支上，删除其它分支。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./17.image&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git remote -v&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git remote -v&lt;/code&gt;查看源地址&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./18.image&quot; alt=&quot;git remote -v&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git remove remote name(源地址名字)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git remove remote name&lt;/code&gt;删除源地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./19.image&quot; alt=&quot;git remove remote name&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git remote add name(源地址名字) 远程地址[url]&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git remote add name url&lt;/code&gt;添加一个源地址为要提交仓库的地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./20.image&quot; alt=&quot;git remote add name&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git fetch origin name(远程分支名称)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git fetch origin name&lt;/code&gt;如果我们本地没有该分支，远程有该分支，我们先拉下来远程分支，并且新建本地分支和远程分支关联上就可以了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./21.image&quot; alt=&quot;git fetch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;git merge name(要合并的分支名称)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git merge name(要合并的分支名称)&lt;/code&gt;将要合并的分支合并到其它分支上。将&lt;code&gt;test&lt;/code&gt;分支上的代码合并到&lt;code&gt;develop&lt;/code&gt;上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./22.image&quot; alt=&quot;git merge&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;特殊问题场景怎么解决&lt;/h2&gt;
&lt;h3&gt;只想把一个提交合并到其它分支上&lt;/h3&gt;
&lt;p&gt;比如一个场景&lt;code&gt;develop&lt;/code&gt;分支上有一些特殊的代码，所以不能把这个分支上的代码合并到&lt;code&gt;test&lt;/code&gt;分支上，我们只想合并当前修改的代码，该怎么办呢&lt;code&gt;git cherry-pick&lt;/code&gt;就是用来解决这问题的，来看下面例子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./23.image&quot; alt=&quot;git cherry-pick&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面example中，&lt;code&gt;git cherry-pick&lt;/code&gt;后面跟着一个&lt;code&gt;id&lt;/code&gt;这个&lt;code&gt;id&lt;/code&gt;就是别的分支提交记录的&lt;code&gt;id&lt;/code&gt;，查看这个&lt;code&gt;id&lt;/code&gt;的话上面说过了使用&lt;code&gt;git log&lt;/code&gt;查看日志。我这个案例代码是没有发生冲突情况的，那么有的小伙伴发生冲突的话，先解决冲突然后&lt;code&gt;git add .&lt;/code&gt;在&lt;code&gt;git cherry-pick --continue&lt;/code&gt;这个参数是继续执行当前的&lt;code&gt;git cherry-pick&lt;/code&gt;过程。下面来查看几个参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--continue&lt;/code&gt; 用户解决代码冲突后，第一步将修改的文件重新加入暂存区（git add .），第二步使用下面的命令，让 Cherry pick 过程继续执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--abort&lt;/code&gt; 发生代码冲突后，放弃合并，回到操作前的样子.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--quit&lt;/code&gt;  发生代码冲突后，退出 Cherry pick，但是不回到操作前的样子&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;如果commit时注释写错了怎么办？&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git commit --amend -m &quot;重新提交注释&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./24.image&quot; alt=&quot;git commit --amend&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;远程强制覆盖到本地&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ git fetch --all(下载远程库的所有内容)
$ git reset --hard origin/master(远程的分支名称)
$ git pull
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;commit提交完怎么撤回&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git reset HEAD~1&lt;/code&gt;撤回刚才的注释，如果提交了2次&lt;code&gt;commit&lt;/code&gt;那么就撤回2次&lt;code&gt;git reset HEAD~2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./25.image&quot; alt=&quot;git reset HEAD~1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Git开发错分支了&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;没提交代码时&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git stash (把暂存区的代码放入到git暂存栈)
git checkout name(切换到正确的分支)
git stash pop(把git暂存栈的代码放出来)
复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提交代码后&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git reset HEAD~1  （最近一次提交放回暂存区, 并取消此次提交）
git stash (暂存区的代码放入到git暂存栈)
git checkout (应该提交代码的分支)
git stash pop (把git暂存栈的代码放出来)
git checkout  (切换到刚才提交错的分支上)
git push origin 错误的分支 -f  (把文件回退掉)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>读深入浅出webpack做的笔记</title><link>https://nollieleo.github.io/posts/%E8%AF%BB%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAwebpack%E5%81%9A%E7%9A%84%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E8%AF%BB%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAwebpack%E5%81%9A%E7%9A%84%E7%AC%94%E8%AE%B0/</guid><description>第一章   1.2 常见构建工具对比...</description><pubDate>Sun, 21 Feb 2021 16:06:59 GMT</pubDate><content:encoded>&lt;h2&gt;第一章&lt;/h2&gt;
&lt;h3&gt;1.2 常见构建工具对比&lt;/h3&gt;
</content:encoded></item><item><title>处理文件流</title><link>https://nollieleo.github.io/posts/%E5%A4%84%E7%90%86%E6%96%87%E4%BB%B6%E6%B5%81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%A4%84%E7%90%86%E6%96%87%E4%BB%B6%E6%B5%81/</guid><description>如果在项目中第一次遇到下载、导出文件的时候，我们都会直接去请求API，期望会下载一个文件到本地，然后我们可以打开它。但是看到的结果却出乎意料。       并没有出现期望的情形，而是返回了一堆“乱码”。   AJAX无法下载文件的原因  下载其实是浏览器的内置事件，浏览器的 GET请求（frame、...</description><pubDate>Fri, 05 Feb 2021 09:41:32 GMT</pubDate><content:encoded>&lt;p&gt;如果在项目中第一次遇到下载、导出文件的时候，我们都会直接去请求API，期望会下载一个文件到本地，然后我们可以打开它。但是看到的结果却出乎意料。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/80/v2-4c649c463be510d72e883cf8ea260374_720w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并没有出现期望的情形，而是返回了一堆“乱码”。&lt;/p&gt;
&lt;h2&gt;AJAX无法下载文件的原因&lt;/h2&gt;
&lt;p&gt;下载其实是浏览器的内置事件，浏览器的 GET请求（frame、a）、 POST请求（form）具有如下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;response会交由浏览器处理&lt;/li&gt;
&lt;li&gt;response内容可以为二进制文件、字符串等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是AJAX请求不一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;response会交由 Javascript 处理&lt;/li&gt;
&lt;li&gt;response内容只能接收字符串才能继续处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，AJAX本身无法触发浏览器的下载功能。&lt;/p&gt;
&lt;h2&gt;Axios如何实现下载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;发送请求&lt;/li&gt;
&lt;li&gt;获得response&lt;/li&gt;
&lt;li&gt;通过response判断返回是否为流文件&lt;/li&gt;
&lt;li&gt;如果是文件则在页面中插入frame/a标签&lt;/li&gt;
&lt;li&gt;利用frame/a标签实现浏览器的get下载&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;首先封装一个download方法，用于发送请求&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// request.js

import Axios form &apos;axios&apos;;

/*
 * @params {string} url 请求地址
 * @params {object} resOpts 请求配置参数
 */
const download = (url, resOpts = {}) =&amp;gt; {
  const { type = &apos;get&apos;, data = &apos;&apos; } = resOpts
  const queryArgs = {
    url,
    method: type,
    data,
    headers: {
      Accept: &apos;application/json&apos;,
      &apos;Content-Type&apos;: &apos;application/json; charset=utf-8&apos;,
      withCredentials: true,
    },
  }
  // tips: 这里直接返回的是response整体!
  return Axios.request(queryArgs).catch(err =&amp;gt; console.log(err))
}

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拿到response之后我们需要将流文件通过浏览器下载&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// utils.js

export function convertRes2Blob(response) {
  // 提取文件名
  const fileName = response.headers[&apos;content-disposition&apos;].match(
    /filename=(.*)/
  )[1]
  // 将二进制流转为blob
  const blob = new Blob([response.data], { type: &apos;application/octet-stream&apos; })
  if (typeof window.navigator.msSaveBlob !== &apos;undefined&apos;) {
    // 兼容IE，window.navigator.msSaveBlob：以本地方式保存文件
    window.navigator.msSaveBlob(blob, decodeURI(filename))
  } else {
    // 创建新的URL并指向File对象或者Blob对象的地址
    const blobURL = window.URL.createObjectURL(blob)
    // 创建a标签，用于跳转至下载链接
    const tempLink = document.createElement(&apos;a&apos;)
    tempLink.style.display = &apos;none&apos;
    tempLink.href = blobURL
    tempLink.setAttribute(&apos;download&apos;, decodeURI(filename))
    // 兼容：某些浏览器不支持HTML5的download属性
    if (typeof tempLink.download === &apos;undefined&apos;) {
      tempLink.setAttribute(&apos;target&apos;, &apos;_blank&apos;)
    }
    // 挂载a标签
    document.body.appendChild(tempLink)
    tempLink.click()
    document.body.removeChild(tempLink)
    // 释放blob URL地址
    window.URL.revokeObjectURL(blobURL)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;download请求方法与convertRes2Blob处理文件下载的方法，需要分开调用&lt;/li&gt;
&lt;li&gt;download使用独立的实例，不能公用一个axios，基础配置需要单独维护&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>requestAnimationFrame和一般计时器处理动画区别</title><link>https://nollieleo.github.io/posts/requestanimationframe%E5%92%8C%E4%B8%80%E8%88%AC%E8%AE%A1%E6%97%B6%E5%99%A8%E5%A4%84%E7%90%86%E5%8A%A8%E7%94%BB%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/requestanimationframe%E5%92%8C%E4%B8%80%E8%88%AC%E8%AE%A1%E6%97%B6%E5%99%A8%E5%A4%84%E7%90%86%E5%8A%A8%E7%94%BB%E5%8C%BA%E5%88%AB/</guid><description>在Web应用中，实现动画效果的方法比较多，Javascript 中可以通过定时器 setTimeout 来实现，css3 可以使用 transition 和 animation 来实现，html5 中的 canvas 也可以实现。除此之外，html5 还提供一个专门用于请求动画的API，那就是 re...</description><pubDate>Mon, 28 Dec 2020 09:39:12 GMT</pubDate><content:encoded>&lt;p&gt;在Web应用中，实现动画效果的方法比较多，Javascript 中可以通过定时器 &lt;code&gt;setTimeout &lt;/code&gt;来实现，css3 可以使用 &lt;code&gt;transition &lt;/code&gt;和 &lt;code&gt;animation &lt;/code&gt;来实现，html5 中的 canvas 也可以实现。除此之外，html5 还提供一个专门用于请求动画的API，那就是 &lt;code&gt;requestAnimationFrame&lt;/code&gt;，顾名思义就是&lt;strong&gt;请求动画帧。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;编写动画循环的关键是要知道延迟时间多长合适。一方面，循环间隔必须足够短，这样才能让不同的动画效果显得平滑流畅；另一方面，循环间隔还要足够长，这样才能确保浏览器有能力渲染产生的变化。&lt;/p&gt;
&lt;p&gt;大多数电脑显示器的刷新频率是&lt;code&gt;60Hz&lt;/code&gt;，大概相当于每秒钟重绘&lt;code&gt;60&lt;/code&gt;次。大多数浏览器都会对重绘操作加以限制，不超过显示器的重绘频率，因为即使超过那个频率用户体验也不会有提升。因此，最平滑动画的最佳循环间隔是&lt;code&gt;1000ms/60&lt;/code&gt;，约等于&lt;code&gt;16.6ms&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;setTimeout&lt;/code&gt;和&lt;code&gt;setInterval&lt;/code&gt;的问题是，它们都不精确。它们的内在&lt;a href=&quot;https://www.cnblogs.com/xiaohuochai/p/5773183.html#anchor3&quot;&gt;运行机制&lt;/a&gt;决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务，那动画代码就要等前面的任务完成后再执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;采用系统时间间隔，保持最佳绘制效率，不会因为间隔时间过短，造成过度绘制，增加开销；也不会因为间隔时间太长，使用动画卡顿不流畅，让各种网页动画效果能够有一个统一的刷新机制，从而节省系统资源，提高系统性能，改善视觉效果&lt;/p&gt;
&lt;h2&gt;相关概念&lt;/h2&gt;
&lt;h3&gt;1. 页面激活（可见）&lt;/h3&gt;
&lt;p&gt;当页面被最小化或者被切换成后台标签页时，页面为不可见，浏览器会触发一个 &lt;code&gt;visibilitychange&lt;/code&gt;事件,并设置&lt;code&gt;document.hidden&lt;/code&gt;属性为&lt;code&gt;true&lt;/code&gt;；切换到显示状态时，页面为可见，也同样触发一个 &lt;code&gt;visibilitychange&lt;/code&gt;事件，设置&lt;code&gt;document.hidden&lt;/code&gt;属性为&lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;2. 动画帧请求回调函数列表&lt;/h3&gt;
&lt;p&gt;每个Document都有一个动画帧请求回调函数列表，该列表可以看成是由``元组组成的集合。其中&lt;code&gt;handlerId&lt;/code&gt;是一个整数，唯一地标识了元组在列表中的位置；&lt;code&gt;callback是&lt;/code&gt;回调函数。&lt;/p&gt;
&lt;h3&gt;3.  屏幕刷新频率&lt;/h3&gt;
&lt;p&gt;即图像在屏幕上更新的速度，也即屏幕上的图像每秒钟出现的次数，它的单位是赫兹(Hz)。 对于一般笔记本电脑，这个频率大概是60Hz， 这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响。&lt;/p&gt;
&lt;h3&gt;4. 动画原理&lt;/h3&gt;
&lt;p&gt;根据上面的原理我们知道，你眼前所看到图像正在以每秒60次的频率刷新，由于刷新频率很高，因此你感觉不到它在刷新。而&lt;strong&gt;动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果，这个变化要以连贯的、平滑的方式进行过渡。&lt;/strong&gt; 那怎么样才能做到这种效果呢？&lt;/p&gt;
&lt;p&gt;刷新频率为60Hz的屏幕每16.7ms刷新一次，我们在屏幕每次刷新前，将图像的位置向左移动一个像素，即1px。这样一来，屏幕每次刷出来的图像位置都比前一个要差1px，因此你会看到图像在移动；由于我们人眼的视觉停留效应，当前位置的图像停留在大脑的印象还没消失，紧接着图像又被移到了下一个位置，因此你才会看到图像在流畅的移动，这就是视觉效果上形成的动画。&lt;/p&gt;
&lt;h2&gt;特点&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;会把每一帧中的所有DOM操作集中起来，在一次重绘或回流中就完成，并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率&lt;/p&gt;
&lt;p&gt;在隐藏或不可见的元素中，&lt;code&gt;requestAnimationFrame&lt;/code&gt;将不会进行重绘或回流，这当然就意味着更少的CPU、GPU和内存使用量&lt;/p&gt;
&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;是由浏览器专门为动画提供的API，在运行时浏览器会自动优化方法的调用，并且如果页面不是激活状态下的话，动画会自动暂停，有效节省了CPU开销&lt;/p&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;异步，传入的函数在重绘之前调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;的用法与&lt;code&gt;setTimeout&lt;/code&gt;很相似，只是不需要设置时间间隔而已。&lt;/p&gt;
&lt;h3&gt;1. 写法：handlerId = requestAnimationFrame(callback)&lt;/h3&gt;
&lt;p&gt;(1) 传入一个&lt;code&gt;callback&lt;/code&gt;函数，即动画函数;&lt;/p&gt;
&lt;p&gt;(2) 返回值&lt;code&gt;handlerId&lt;/code&gt;为浏览器定义的、大于0的整数，唯一标识了该回调函数在列表中位置。&lt;/p&gt;
&lt;h3&gt;2. 浏览器执行过程:&lt;/h3&gt;
&lt;p&gt;(1) 首先要判断&lt;code&gt;document.hidden&lt;/code&gt;属性是否为&lt;code&gt;true&lt;/code&gt;,即页面处于可见状态下才会执行；&lt;/p&gt;
&lt;p&gt;(2) 浏览器清空上一轮的动画函数；&lt;/p&gt;
&lt;p&gt;(3) 这个方法返回的&lt;code&gt;handlerId&lt;/code&gt; 值会和动画函数&lt;code&gt;callback&lt;/code&gt;，以``  进入到动画帧请求回调函数列；&lt;/p&gt;
&lt;p&gt;(4) 浏览器会遍历动画帧请求回调函数列表，根据&lt;code&gt;handlerId&lt;/code&gt; 的值大小，依次去执行相应的动画函数。&lt;/p&gt;
&lt;h3&gt;3. 取消动画函数的方法：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cancelAnimationFrame(handlerId)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;setTimeout和requestAnimationFrame区别&lt;/h2&gt;
&lt;h3&gt;1. setTimeout&lt;/h3&gt;
&lt;p&gt;理解了上面的概念以后，我们不难发现，&lt;code&gt;setTimeout &lt;/code&gt;其实就是通过设置一个间隔时间来不断的改变图像的位置，从而达到动画效果的。但利用&lt;code&gt;seTimeout&lt;/code&gt;实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setTimeout&lt;/code&gt;的执行时间并不是确定的。在Javascript中， &lt;code&gt;setTimeout &lt;/code&gt;任务被放进了异步队列中，只有当主线程上的任务执行完以后，才会去检查该队列里的任务是否需要开始执行，因此 &lt;strong&gt;setTimeout 的实际执行时间一般要比其设定的时间晚一些。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;刷新频率受屏幕分辨率和屏幕尺寸的影响，因此不同设备的屏幕刷新频率可能会不同，而 &lt;code&gt;setTimeout&lt;/code&gt;只能设置一个固定的时间间隔，这个时间不一定和屏幕的刷新时间相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两种情况都会导致&lt;code&gt;setTimeout&lt;/code&gt;的执行步调和屏幕的刷新步调不一致，从而引起&lt;strong&gt;丢帧&lt;/strong&gt;现象。 那为什么步调不一致就会引起丢帧呢？&lt;/p&gt;
&lt;p&gt;首先要明白，&lt;strong&gt;&lt;code&gt;setTimeout&lt;/code&gt;的执行只是在内存中对图像属性进行改变，这个变化必须要等到屏幕下次刷新时才会被更新到屏幕上&lt;/strong&gt;。如果两者的步调不一致，就可能会导致中间某一帧的操作被跨越过去，而直接更新下一帧的图像。假设屏幕每隔16.7ms刷新一次，而&lt;code&gt;setTimeout&lt;/code&gt;每隔10ms设置图像向左移动1px， 就会出现如下绘制过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第0ms: 屏幕未刷新，等待中，&lt;code&gt;setTimeout&lt;/code&gt;也未执行，等待中；&lt;/li&gt;
&lt;li&gt;第10ms: 屏幕未刷新，等待中，&lt;code&gt;setTimeout&lt;/code&gt;开始执行并设置图像属性left=1px；&lt;/li&gt;
&lt;li&gt;第16.7ms: 屏幕开始刷新，屏幕上的图像向左移动了&lt;strong&gt;1px&lt;/strong&gt;， &lt;code&gt;setTimeout &lt;/code&gt;未执行，继续等待中；&lt;/li&gt;
&lt;li&gt;第20ms: 屏幕未刷新，等待中，&lt;code&gt;setTimeout&lt;/code&gt;开始执行并设置left=2px;&lt;/li&gt;
&lt;li&gt;第30ms: 屏幕未刷新，等待中，&lt;code&gt;setTimeout&lt;/code&gt;开始执行并设置left=3px;&lt;/li&gt;
&lt;li&gt;第33.4ms: 屏幕开始刷新，屏幕上的图像向左移动了&lt;strong&gt;3px&lt;/strong&gt;， &lt;code&gt;setTimeout&lt;/code&gt;未执行，继续等待中；&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从上面的绘制过程中可以看出，屏幕没有更新left=2px的那一帧画面，图像直接从1px的位置跳到了3px的的位置，这就是丢帧现象，这种现象就会引起动画卡顿。&lt;/p&gt;
&lt;h3&gt;2. requestAnimationFrame&lt;/h3&gt;
&lt;p&gt;与&lt;code&gt;setTimeout&lt;/code&gt;相比，&lt;code&gt;requestAnimationFrame&lt;/code&gt;最大的优势是**由系统来决定回调函数的执行时机。**具体一点讲，如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次，如果刷新率是75Hz，那么这个时间间隔就变成了1000/75=13.3ms，换句话说就是，&lt;code&gt;requestAnimationFrame&lt;/code&gt;的步伐跟着系统的刷新步伐走。&lt;strong&gt;它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次&lt;/strong&gt;，这样就不会引起丢帧现象，也不会导致动画出现卡顿的问题。&lt;/p&gt;
&lt;p&gt;这个API的调用很简单，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var progress = 0;
//回调函数
function render() {  
  progress += 1; //修改图像的位置  
  if (progress &amp;lt; 100) {  //在动画没有结束前，递归渲染    
    window.requestAnimationFrame(render); 
  }
}
//第一帧渲染
window.requestAnimationFrame(render);复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，&lt;code&gt;requestAnimationFrame&lt;/code&gt;还有以下两个优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CPU节能&lt;/strong&gt;：使用&lt;code&gt;setTimeout&lt;/code&gt;实现的动画，当页面被隐藏或最小化时，&lt;code&gt;setTimeout &lt;/code&gt;仍然在后台执行动画任务，由于此时页面处于不可见或不可用状态，刷新动画是没有意义的，完全是浪费CPU资源。而&lt;code&gt;requestAnimationFrame&lt;/code&gt;则完全不同，当页面处理未激活的状态下，该页面的屏幕刷新任务也会被系统暂停，因此跟着系统步伐走的&lt;code&gt;requestAnimationFrame&lt;/code&gt;也会停止渲染，当页面被激活时，动画就从上次停留的地方继续执行，有效节省了CPU开销。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;函数节流&lt;/strong&gt;：在高频率事件(&lt;code&gt;resize&lt;/code&gt;,&lt;code&gt;scroll&lt;/code&gt;等)中，为了防止在一个刷新间隔内发生多次函数执行，使用&lt;code&gt;requestAnimationFrame&lt;/code&gt;可保证每个刷新间隔内，函数只被执行一次，这样既能保证流畅性，也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的，因为显示器每16.7ms刷新一次，多次绘制并不会在屏幕上体现出来。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>时间分片</title><link>https://nollieleo.github.io/posts/%E6%97%B6%E9%97%B4%E5%88%86%E7%89%87/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%97%B6%E9%97%B4%E5%88%86%E7%89%87/</guid><description>海量数据优化-时间分片  时间分片的概念，就是一次性渲染大量数据，初始化的时候会出现卡顿等现象。我们必须要明白的一个道理，js执行永远要比dom渲染快的多。 ，所以对于大量的数据，一次性渲染，容易造成卡顿，卡死的情况。我们先来看一下例子  ./Content.tsx  jsx import { Bu...</description><pubDate>Mon, 28 Dec 2020 09:04:38 GMT</pubDate><content:encoded>&lt;h2&gt;海量数据优化-时间分片&lt;/h2&gt;
&lt;p&gt;时间分片的概念，就是一次性渲染大量数据，初始化的时候会出现卡顿等现象。我们必须要明白的一个道理，&lt;strong&gt;js执行永远要比dom渲染快的多。&lt;/strong&gt; ，所以对于大量的数据，一次性渲染，容易造成卡顿，卡死的情况。我们先来看一下例子&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;./Content.tsx&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Button } from &apos;choerodon-ui&apos;;
import Icon from &apos;choerodon-ui/lib/icon/Icon&apos;;
import { map } from &apos;lodash&apos;;
import { observer } from &apos;mobx-react-lite&apos;;
import React, { useEffect, useRef, useState } from &apos;react&apos;;

import &apos;./index.less&apos;;
import { useDemos } from &apos;./stores&apos;;

const Demos = () =&amp;gt; {
  const {
    mainStore,
  } = useDemos();

  const {
    lists,
    setLists,
  } = mainStore;

  useEffect(() =&amp;gt; {
  }, []);

  function add() {
    setLists(new Array(40000).fill(0));
  }
  function reset() {
    setLists([]);
  }

  return (
    &amp;lt;div className=&quot;demos&quot;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;Button funcType=&quot;raised&quot; type=&quot;primary&quot; onClick={add}&amp;gt;add +&amp;lt;/Button&amp;gt;
        &amp;lt;Button funcType=&quot;raised&quot; type=&quot;primary&quot; onClick={reset}&amp;gt;reset&amp;lt;/Button&amp;gt;

        {
        map(lists, (item:number, i:number) =&amp;gt; (
          &amp;lt;div className=&quot;demos-item&quot; key={i}&amp;gt;
            {`item-${i}`}
            &amp;lt;Icon type=&quot;close&quot; /&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default observer(Demos);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;./stores/index.tsx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* eslint-disable max-len */
import React, {
  createContext, useCallback, useContext, useMemo,
} from &apos;react&apos;;
import { DataSet } from &apos;choerodon-ui/pro&apos;;
import { injectIntl } from &apos;react-intl&apos;;
import { inject } from &apos;mobx-react&apos;;
import useStore from &apos;./useStore&apos;;

interface ContextProps {

}

const Store = createContext({} as ContextProps);

export function useDemos() {
  return useContext(Store);
}

export const StoreProvider = injectIntl(inject(&apos;AppState&apos;)((props: any) =&amp;gt; {
  const {
    children,
    intl: { formatMessage },
    AppState: { currentMenuType: { projectId } },
  } = props;

  const mainStore = useStore();

  const value = {
    ...props,
    formatMessage,
    projectId,
    mainStore,
  };
  return (
    &amp;lt;Store.Provider value={value}&amp;gt;
      {children}
    &amp;lt;/Store.Provider&amp;gt;
  );
}));

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;./stores/useStore.tsx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useLocalStore } from &apos;mobx-react-lite&apos;;

export default function useStore() {
  return useLocalStore(() =&amp;gt; ({
    lists: [],
    setLists(value:any) {
      this.lists = value;
    },
  }));
}

export type StoreProps = ReturnType&amp;lt;typeof useStore&amp;gt;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;./index.tsx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;
import { StoreProvider } from &apos;./stores&apos;;
import Content from &apos;./Content&apos;;

const Index = (props: any) =&amp;gt; (
  &amp;lt;StoreProvider {...props}&amp;gt;
    &amp;lt;Content /&amp;gt;
  &amp;lt;/StoreProvider&amp;gt;
);

export default Index;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;./index.less&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.demos {
  &amp;amp;-item {
    border: 1px solid #0fc2e2;
    background-color: #4adcf0;
    color: #fff;
    border-radius: 4px;
    height: 50px;
    width: 400px;
    text-align: center;
    font-size: 20px;
    line-height: 50px;
    &amp;amp; + &amp;amp; {
      margin-top: 10px;
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最主要的代码就是Content.tsx&lt;/p&gt;
&lt;p&gt;演示如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.gif&quot; alt=&quot;gif1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到 40000 个 简单列表渲染了，将近5秒的时间。为了解决一次性加载大量数据的问题。我们引出了时间分片的概念，就是用&lt;code&gt;setTimeout&lt;/code&gt;把任务分割，分成若干次来渲染。一共40000个数据，我们可以每次渲染100个， 分次400渲染。(这里用的是&lt;code&gt;window.requestAnimationFrame()&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Button } from &apos;choerodon-ui&apos;;
import Icon from &apos;choerodon-ui/lib/icon/Icon&apos;;
import { map } from &apos;lodash&apos;;
import { observer } from &apos;mobx-react-lite&apos;;
import React, { useEffect, useRef, useState } from &apos;react&apos;;

import &apos;./index.less&apos;;
import { useDemos } from &apos;./stores&apos;;

const Demos = () =&amp;gt; {
  const {
    mainStore,
  } = useDemos();

  const {
    lists,
    setLists,
  } = mainStore;

  useEffect(() =&amp;gt; {
  }, []);

  function handleSlice(list:number[], times:number) {
    if (times === 400) return;
    window.requestAnimationFrame(() =&amp;gt; {
      const newLists = list.slice(times, (times + 1) * 10);
      setLists(lists.concat(newLists));
      handleSlice(list, times + 1);
    });
  }

  function add() {
    handleSlice(new Array(4000).fill(0), 0);
  }
  function reset() {
    setLists([]);
  }

  return (
    &amp;lt;div className=&quot;demos&quot;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;Button funcType=&quot;raised&quot; type=&quot;primary&quot; onClick={add}&amp;gt;add +&amp;lt;/Button&amp;gt;
        &amp;lt;Button funcType=&quot;raised&quot; type=&quot;primary&quot; onClick={reset}&amp;gt;reset&amp;lt;/Button&amp;gt;

        {
        map(lists, (item:number, i:number) =&amp;gt; (
          &amp;lt;div className=&quot;demos-item&quot; key={i}&amp;gt;
            {`item-${i}`}
            &amp;lt;Icon type=&quot;close&quot; /&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default observer(Demos);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.gif&quot; alt=&quot;gif2&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>前端生成pdf</title><link>https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E7%94%9F%E6%88%90pdf/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%89%8D%E7%AB%AF%E7%94%9F%E6%88%90pdf/</guid><description>项目上要求，能够根据页面上所展示的测试报告，生成一份pdf。  根据模板：      最终生成的效果：         原理：通过插件js-pdf以及一个html2canvas插件结合，首先通过 html2canvas 可以在浏览器端直接对整个或部分页面进行截屏。脚本通过读取DOM并将不同的样式应用...</description><pubDate>Sat, 26 Dec 2020 15:52:39 GMT</pubDate><content:encoded>&lt;p&gt;项目上要求，能够根据页面上所展示的测试报告，生成一份pdf。&lt;/p&gt;
&lt;p&gt;根据模板：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.jpg&quot; alt=&quot;image-20201226160741507&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.jpg&quot; alt=&quot;image-20201226160741507&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终生成的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.jpg&quot; alt=&quot;image-20201226160741507&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.jpg&quot; alt=&quot;image-20201226160741507&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原理：通过插件js-pdf以及一个html2canvas插件结合，首先通过 html2canvas 可以在浏览器端直接对整个或部分页面进行截屏。脚本通过读取DOM并将不同的样式应用到这些元素上，从而将当页面渲染成一个Canvas图片。 之后利用js-pdf插件添加图片，将生成的canvas图片插入pdf中。 保存导出pdf&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 获取像素比&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* 根据window.devicePixelRatio获取像素比 */
  function DPR() {
    if (window.devicePixelRatio &amp;amp;&amp;amp; window.devicePixelRatio &amp;gt; 1) {
      return window.devicePixelRatio;
    }
    return 1;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;devicePixelRatio属性是干嘛的&lt;/strong&gt;
window的该属性能够获取到当前显示设备物理分辨率与css的像素分辨率之间的比率。简单说就是告诉浏览器应该使用多少个物理像素来会在单个css像素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 绘制canvas&lt;/h2&gt;
&lt;p&gt;首先要看绘制canvas需要的一些参数需要配置&lt;/p&gt;
&lt;p&gt;&lt;code&gt;html2canvas(element, options);&lt;/code&gt;返回一个promise&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;element 页面上的dom元素（这里要注意一些&lt;a href=&quot;https://html2canvas.hertzen.com/features/&quot;&gt;css&lt;/a&gt;样式是不支持的）&lt;/li&gt;
&lt;li&gt;options&lt;a href=&quot;https://html2canvas.hertzen.com/configuration&quot;&gt;配置详情看这里&lt;/a&gt;下面只是用到项目上需要的需求&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1）获取element&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  const pdfRef = React.useRef();
  return (
    &amp;lt;div className={`${prefixCls}`} ref={pdfRef}&amp;gt;
      ......
    &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) 配置options&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const getOpts = ()=&amp;gt; {
    const target:any = pdfRef.current;
	const width = target.offsetWidth; // 获取dom 宽度
    const height = target.offsetHeight; // 获取dom 高度
    const scale = DPR();
    const tempCanvas = document.createElement(&apos;canvas&apos;);
    tempCanvas.width = width * scale; // 定义canvas 宽度 * 缩放
    tempCanvas.height = height * scale; // 定义canvas高度 *缩放

    const opts = {
      useCORS: true,
      allowTaint: true,
      canvas: tempCanvas, // 现有的画布元素用作绘图的基础
      scale, // 提升画面质量，但是会增加文件大小
      scrollX: 0,
      scrollY: 0,
    };
    return opts;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于在配置项中设置跨域&lt;/strong&gt;
useCORS和allowTaint两种都可以设置跨域；
为什么要设置跨域：我们图片一般都是放到静态资源服务器上的，资源服务器地址一般和项目地址是不一样的；虽然图片可以在页面上显示，但是用canvas绘图时却绘制不出来；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. 根据生成的canvas生成pdf&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;html2canvas(target, opts).then(async (canvas) =&amp;gt; {
     (....处理逻辑)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1）首先转换图片&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HTMLCanvasElement.toDataURL()&lt;/code&gt;&lt;/strong&gt; 方法返回一个包含图片展示的 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs&quot;&gt;data URI&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const contentWidth = canvas.width;
const contentHeight = canvas.height;
// 将canvas转为base64图片
const pageData = canvas.toDataURL(&apos;image/jpeg&apos;, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2)  设置pdf的大小以及生成的图片大小&lt;/h3&gt;
&lt;p&gt;因为pdf的像素单位是不一样的，所以需要进行转换;&lt;/p&gt;
&lt;p&gt;已知 1pt/1px = 0.75, pt = (px/scale)*0.75&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;scale&lt;/code&gt;就是上面的像素比&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 设置pdf的尺寸，pdf要使用pt单位 已知 1pt/1px = 0.75   pt = (px/scale)* 0.75
// 2为上面的scale 缩放了2倍
const pdfX = (contentWidth + 10) / scale * 0.75;
const pdfY = (contentHeight) / scale * 0.75; // 500为底部留白

// 设置内容图片的尺寸，img是pt单位
const imgX = pdfX;
const imgY = (contentHeight / scale * 0.75); // 内容图片这里不需要留白的距离
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3）插入图片并生成pdf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const pdf = new JsPDF(&apos;&apos;, &apos;pt&apos;, [pdfX, pdfY]); // 第一个参数方向：默认&apos;&apos;时为纵向

// 将内容图片添加到pdf中，因为内容宽高和pdf宽高一样，就只需要一页，位置就是 0,0
await pdf.addImage(pageData, &apos;jpeg&apos;, 0, 0, imgX, imgY);
pdf.save(`API测试报告（#${viewId}）.pdf`);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 出现的问题以及解决方法&lt;/h2&gt;
&lt;h3&gt;1）导出的pdf中图片横向位置被截断&lt;/h3&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.jpg&quot; alt=&quot;image-20201226181331163&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是因为需要生成的dom的宽度大于其高度，生成图片插入pdf的时候由于pdf默认设置的是纵向排列，图片会被撑大以适应pdf的高度；&lt;/p&gt;
&lt;p&gt;按照下方处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始化jspdf 第一个参数方向：默认&apos;&apos;时为纵向，第二个参数设置pdf内容图片使用的长度单位为pt，第三个参数为PDF的大小，单位是pt
  let direct;
  if (contentHeight &amp;lt; contentWidth) {
    direct = &apos;l&apos;;
  } else {
    direct = &apos;p&apos;;
  }
  const pdf = new JsPDF(direct, &apos;pt&apos;, [pdfX, pdfY]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当高度小于宽度的时候将pdf进行横纵向的调整，以适应插入图片的宽高&lt;/p&gt;
</content:encoded></item><item><title>undefine和void()有什么区别？</title><link>https://nollieleo.github.io/posts/undefine%E5%92%8Cvoid-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/undefine%E5%92%8Cvoid-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/</guid><description>在 JavaScript 中，判断是否是 undefined，一般都这样写：  text function isUndefined(input) {     return input === void 0; }   为什么要使用 void 0 呢？  void 是 JS 中的一个运算符，语法是：  ...</description><pubDate>Fri, 25 Dec 2020 15:23:53 GMT</pubDate><content:encoded>&lt;p&gt;在 JavaScript 中，判断是否是 undefined，一般都这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function isUndefined(input) {
    return input === void 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要使用 void 0 呢？&lt;/p&gt;
&lt;p&gt;void 是 JS 中的一个运算符，语法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void expression
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它返回 &lt;code&gt;undefined&lt;/code&gt; 的原始值，同时语句中的 expression 会被运算，也即产生了副作用。可以这样理解：它运算了表达式，但是不返回值（或者说返回 undefined）。&lt;/p&gt;
&lt;p&gt;所以，通常使用 &lt;code&gt;void 0&lt;/code&gt; 来得到 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是，为什么不直接使用 undefined 呢？&lt;/p&gt;
&lt;p&gt;要弄清楚这个问题，先看看 undefined 这个词在不同语境下的含义。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt; 是&lt;strong&gt;术语（glossary）&lt;/strong&gt;。被用于表示一个概念时，它是一个术语，这个术语表示这样一个概念：未定义的值（即 undefined）。在 JS 中，只声明而未被赋值的变量、函数里未传实参的形参，都对应此概念。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt; 是&lt;strong&gt;类型（type）&lt;/strong&gt;。为了在语言层面实现上述的 &lt;code&gt;undefined&lt;/code&gt; 概念，JS 为这个概念提供了一种原始类型（primitive type），即 undefined 类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt; 是&lt;strong&gt;值（value）&lt;/strong&gt;。上述的 &lt;code&gt;undefined&lt;/code&gt; 类型非常特殊，不像其他诸如 &lt;code&gt;string&lt;/code&gt;、&lt;code&gt;number&lt;/code&gt; 类型可以定义出无限多的不同变量，undefined 类型的值只可能有一个，可以称之为 undefined 原始值（primitive value）。这个值在 JS 中没有字面量，准确的说是 JS 没有为程序员提供一个表示 undefined 原始值的字面量（可能有人会说代码里 undefined 不就是字面量吗？请看下一条）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt; 是&lt;strong&gt;属性（undefined）&lt;/strong&gt;。代码中出现的  &lt;code&gt;undefined&lt;/code&gt; 是全局变量的属性，所以说 JS 代码里出现的 undefined 并不是字面量。可以这样理解：JS 一开始就在内部将 undefined 原始值赋给了 undefined 属性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在再来回答为什么需要使用 &lt;code&gt;void 0&lt;/code&gt; 而不直接用 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;先说为什么使用 void 0。因为 void 0 返回的值是 undefined 原始值，这与我们写代码的意图完全一致。&lt;/p&gt;
&lt;p&gt;再说为什么不使用 undefined。 因为在两种情况下它有可能与我们的意图不一致。&lt;/p&gt;
&lt;p&gt;第一种情况：既然 undefined 是一个属性，那它就有可能被重新赋值。但是这个担心是是多余的，因为从 ES5 开始 undefined 属性就是是一个只读属性了，不可能被重新赋值。可以通过实验验证，打开 node CLI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; Object.getOwnPropertyDescriptor(global, &apos;undefined&apos;)
{ value: undefined,
 writable: false,
 enumerable: false,
 configurable: false }
&amp;gt; undefined = &apos;a string&apos;
&apos;a string&apos;
&amp;gt; typeof undefined
&apos;undefined&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是即便如此，为兼容性考虑还是要避免直接拿 undefined 来做比较。&lt;/p&gt;
&lt;p&gt;第二种情况：局部变量。因为 undefined 在 JS 中并不是保留字，所以在局部作用域中完全可以定义一个变量名为 undefined 的局部变量。看如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function(){ 
 let undefined = &apos;a string&apos;;
 console.log(undefined)
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码运行的结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：使用 void 0 是有足够理由的。&lt;/p&gt;
</content:encoded></item><item><title>react何时render呢？</title><link>https://nollieleo.github.io/posts/react%E4%BD%95%E6%97%B6render%E5%91%A2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E4%BD%95%E6%97%B6render%E5%91%A2/</guid><description>对于如下Demo，点击Parent组件的div，触发更新，Son组件会打印child render!么？  tsx function Son() {       console.log(&apos;child render!&apos;);       return &lt;divSon&lt;/div; } function P...</description><pubDate>Thu, 24 Dec 2020 09:50:41 GMT</pubDate><content:encoded>&lt;p&gt;对于如下Demo，点击&lt;code&gt;Parent&lt;/code&gt;组件的&lt;code&gt;div&lt;/code&gt;，触发更新，&lt;code&gt;Son&lt;/code&gt;组件会打印&lt;code&gt;child render!&lt;/code&gt;么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Son() {  
    console.log(&apos;child render!&apos;);  
    return &amp;lt;div&amp;gt;Son&amp;lt;/div&amp;gt;;
}
function Parent(props) {  
    const [count, setCount] = React.useState(0);  
    return (    
        &amp;lt;div onClick={() =&amp;gt; {setCount(count + 1)}}&amp;gt;      
            count:{count}      
            {props.children}    
        &amp;lt;/div&amp;gt;  
    );
}
function App() {  
    return (    
        &amp;lt;Parent&amp;gt;      
            &amp;lt;Son/&amp;gt;    
        &amp;lt;/Parent&amp;gt;  
    );
}
const rootEl = document.querySelector(&quot;#root&quot;);
ReactDOM.render(&amp;lt;App/&amp;gt;, rootEl);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;                                                不会    
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;render需要满足的条件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;React&lt;/code&gt;创建&lt;code&gt;Fiber树&lt;/code&gt;时，每个组件对应的&lt;code&gt;fiber&lt;/code&gt;都是通过如下两个逻辑之一创建的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;render。即调用&lt;code&gt;render&lt;/code&gt;函数，根据返回的&lt;code&gt;JSX&lt;/code&gt;创建新的&lt;code&gt;fiber&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;bailout。即满足一定条件时，&lt;code&gt;React&lt;/code&gt;判断该组件在更新前后没有发生变化，则复用该组件在上一次更新的&lt;code&gt;fiber&lt;/code&gt;作为本次更新的&lt;code&gt;fiber&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，当命中&lt;code&gt;bailout&lt;/code&gt;逻辑时，是不会调用&lt;code&gt;render&lt;/code&gt;函数的。&lt;/p&gt;
&lt;p&gt;所以，&lt;code&gt;Son&lt;/code&gt;组件不会打印&lt;code&gt;child render!&lt;/code&gt;是因为命中了&lt;code&gt;bailout&lt;/code&gt;逻辑。&lt;/p&gt;
&lt;h2&gt;bailout需要满足的条件&lt;/h2&gt;
&lt;p&gt;什么情况下会进入&lt;code&gt;bailout&lt;/code&gt;逻辑？当同时满足如下4个条件时：&lt;/p&gt;
&lt;h3&gt;1. oldProps === newProps ？&lt;/h3&gt;
&lt;p&gt;即本次更新的&lt;code&gt;props&lt;/code&gt;（newProps）不等于上次更新的&lt;code&gt;props&lt;/code&gt;（oldProps）。&lt;/p&gt;
&lt;p&gt;注意这里是&lt;strong&gt;全等比较&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们知道组件&lt;code&gt;render&lt;/code&gt;会返回&lt;code&gt;JSX&lt;/code&gt;，&lt;code&gt;JSX&lt;/code&gt;是&lt;code&gt;React.createElement&lt;/code&gt;的语法糖。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;render&lt;/code&gt;的返回结果实际上是&lt;code&gt;React.createElement&lt;/code&gt;的执行结果，即一个包含&lt;code&gt;props&lt;/code&gt;属性的对象。&lt;/p&gt;
&lt;p&gt;即使本次更新与上次更新&lt;code&gt;props&lt;/code&gt;中每一项参数都没有变化，但是本次更新是&lt;code&gt;React.createElement&lt;/code&gt;的执行结果，是一个全新的&lt;code&gt;props&lt;/code&gt;引用，所以&lt;code&gt;oldProps !== newProps&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果我们使用了&lt;code&gt;PureComponent&lt;/code&gt;或&lt;code&gt;Memo&lt;/code&gt;，那么在判断是进入&lt;code&gt;render&lt;/code&gt;还是&lt;code&gt;bailout&lt;/code&gt;时，不会判断&lt;code&gt;oldProps&lt;/code&gt;与&lt;code&gt;newProps&lt;/code&gt;是否全等，而是会对&lt;code&gt;props&lt;/code&gt;内每个属性进行浅比较。&lt;/p&gt;
&lt;h3&gt;2. context没有变化&lt;/h3&gt;
&lt;p&gt;即&lt;code&gt;context&lt;/code&gt;的&lt;code&gt;value&lt;/code&gt;没有变化。&lt;/p&gt;
&lt;h3&gt;3. workInProgress.type === current.type ？&lt;/h3&gt;
&lt;p&gt;更新前后&lt;code&gt;fiber.type&lt;/code&gt;是否变化，比如&lt;code&gt;div&lt;/code&gt;是否变为&lt;code&gt;p&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;4. !includesSomeLane(renderLanes, updateLanes) ？&lt;/h3&gt;
&lt;p&gt;当前&lt;code&gt;fiber&lt;/code&gt;上是否存在&lt;code&gt;更新&lt;/code&gt;，如果存在那么&lt;code&gt;更新&lt;/code&gt;的&lt;code&gt;优先级&lt;/code&gt;是否和本次整棵&lt;code&gt;fiber树&lt;/code&gt;调度的&lt;code&gt;优先级&lt;/code&gt;一致？&lt;/p&gt;
&lt;p&gt;如果一致则进入&lt;code&gt;render&lt;/code&gt;逻辑。&lt;/p&gt;
&lt;p&gt;就我们的Demo来说，&lt;code&gt;Parent&lt;/code&gt;是整棵树中唯一能触发&lt;code&gt;更新&lt;/code&gt;的组件（通过调用&lt;code&gt;setCount&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;Parent&lt;/code&gt;对应的&lt;code&gt;fiber&lt;/code&gt;是唯一满足条件4的&lt;code&gt;fiber&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Demo的详细执行逻辑&lt;/h2&gt;
&lt;p&gt;所以，Demo中&lt;code&gt;Son&lt;/code&gt;进入&lt;code&gt;bailout&lt;/code&gt;逻辑，一定是同时满足以上4个条件。我们一个个来看。&lt;/p&gt;
&lt;p&gt;条件2，Demo中没有用到&lt;code&gt;context&lt;/code&gt;，满足。&lt;/p&gt;
&lt;p&gt;条件3，更新前后&lt;code&gt;type&lt;/code&gt;都为&lt;code&gt;Son&lt;/code&gt;对应的函数组件，满足。&lt;/p&gt;
&lt;p&gt;条件4，&lt;code&gt;Son&lt;/code&gt;本身无法触发更新，满足。&lt;/p&gt;
&lt;p&gt;所以，重点是条件1。让我们详细来看下。&lt;/p&gt;
&lt;p&gt;本次更新开始时，&lt;code&gt;Fiber树&lt;/code&gt;存在如下2个&lt;code&gt;fiber&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FiberRootNode      
	|  
RootFiber      
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;FiberRootNode&lt;/code&gt;是整个应用的根节点，&lt;code&gt;RootFiber&lt;/code&gt;是调用&lt;code&gt;ReactDOM.render&lt;/code&gt;创建的&lt;code&gt;fiber&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先，&lt;code&gt;RootFiber&lt;/code&gt;会进入&lt;code&gt;bailout&lt;/code&gt;的逻辑，所以返回的&lt;code&gt;App fiber&lt;/code&gt;和更新前是一致的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FiberRootNode      
	|  
RootFiber           
	| 
App fiber
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;App fiber&lt;/code&gt;是&lt;code&gt;RootFiber&lt;/code&gt;走&lt;code&gt;bailout&lt;/code&gt;逻辑返回的，所以对于&lt;code&gt;App fiber&lt;/code&gt;，&lt;code&gt;oldProps === newProps&lt;/code&gt;。并且&lt;code&gt;bailout&lt;/code&gt;剩下3个条件也满足。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;App fiber&lt;/code&gt;也会走&lt;code&gt;bailout&lt;/code&gt;逻辑，返回&lt;code&gt;Parent fiber&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FiberRootNode      
	|  
RootFiber           
	|   
App fiber      
	| 
Parent fiber
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于更新是&lt;code&gt;Parent fiber&lt;/code&gt;触发的，所以他不满足条件4，会走&lt;code&gt;render&lt;/code&gt;的逻辑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来是关键&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;render&lt;/code&gt;返回的&lt;code&gt;Son&lt;/code&gt;是如下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Son/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会编译为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React.createElement(Son, null)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后返回&lt;code&gt;JSX&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;props&lt;/code&gt;的引用改变，&lt;code&gt;oldProps !== newProps&lt;/code&gt;。会走&lt;code&gt;render&lt;/code&gt;逻辑。&lt;/p&gt;
&lt;p&gt;但是在Demo中&lt;code&gt;Son&lt;/code&gt;是如下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{props.children}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;props.children&lt;/code&gt;是&lt;code&gt;Son&lt;/code&gt;对应的&lt;code&gt;JSX&lt;/code&gt;，而这里的&lt;code&gt;props&lt;/code&gt;是&lt;code&gt;App fiber&lt;/code&gt;走&lt;code&gt;bailout&lt;/code&gt;逻辑后返回的。&lt;/p&gt;
&lt;p&gt;所以&lt;code&gt;Son&lt;/code&gt;对应的&lt;code&gt;JSX&lt;/code&gt;与上次更新时一致，&lt;code&gt;JSX&lt;/code&gt;中保存的&lt;code&gt;props&lt;/code&gt;也就一致，满足条件1。&lt;/p&gt;
&lt;p&gt;可以看到，&lt;code&gt;Son&lt;/code&gt;满足&lt;code&gt;bailout&lt;/code&gt;的所有条件，所以不会&lt;code&gt;render&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>优雅的处理asnyc await</title><link>https://nollieleo.github.io/posts/%E4%BC%98%E9%9B%85%E7%9A%84%E5%A4%84%E7%90%86asnyc-await/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E4%BC%98%E9%9B%85%E7%9A%84%E5%A4%84%E7%90%86asnyc-await/</guid><description>上代码  js  const errorCaptured = async (asyncFunc) = {   try {     const res = await asyncFunc();     return [null, res];   } catch (error) {     return...</description><pubDate>Sun, 08 Nov 2020 14:39:36 GMT</pubDate><content:encoded>&lt;p&gt;上代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
const errorCaptured = async (asyncFunc) =&amp;gt; {
  try {
    const res = await asyncFunc();
    return [null, res];
  } catch (error) {
    return [error, null];
  }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>useRef拿子组件方法</title><link>https://nollieleo.github.io/posts/useref%E6%8B%BF%E5%AD%90%E7%BB%84%E4%BB%B6%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/useref%E6%8B%BF%E5%AD%90%E7%BB%84%E4%BB%B6%E6%96%B9%E6%B3%95/</guid><description>- useRef是一个方法，且useRef返回一个可变的ref对象（对象！！！）  - initialValue被赋值给其返回值的.current对象  - 可以保存任何类型的值:dom、对象等任何可辨值  - ref对象与自建一个{current：‘’}对象的区别是：useRef会在每次渲染时返回...</description><pubDate>Sun, 08 Nov 2020 12:58:36 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://user-gold-cdn.xitu.io/2020/5/29/1725f508e4f8019d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;useRef是一个方法，且useRef返回一个可变的ref对象（对象！！！）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;initialValue被赋值给其返回值的.current对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以保存任何类型的值:dom、对象等任何可辨值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ref对象与自建一个{current：‘’}对象的区别是：useRef会在每次渲染时返回同一个ref对象，即返回的ref对象在组件的整个生命周期内保持不变。自建对象每次渲染时都建立一个新的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ref对象的值发生改变之后，不会触发组件重新渲染。有一个窍门，把它的改边动作放到useState()之前。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本质上，useRef就是一个其.current属性保存着一个可变值“盒子”。目前我用到的是pageRef和sortRef分别用来保存分页信息和排序信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 父组件
import React, {
  useRef,
  useEffect,
  useImperativeHandle,
  forwardRef,
} from &quot;react&quot;;

import Child from &apos;./TestItem&apos;;

const RefDemo = () =&amp;gt; {
  const domRef = useRef(1);
  const childRef = useRef(null);

  useEffect(() =&amp;gt; {
    console.log(&quot;ref:deom-init&quot;, domRef, domRef.current);
    console.log(&quot;ref:child-init&quot;, childRef, childRef.current);
  },[]);

  const showChild = () =&amp;gt; {
    console.log(&quot;ref:child&quot;, childRef, childRef.current);
    childRef.current.say();
  };

  return (
    &amp;lt;div style={{ margin: &quot;100px&quot;, border: &quot;2px dashed&quot;, padding: &quot;20px&quot; }}&amp;gt;
      &amp;lt;h2&amp;gt;这是外层组件&amp;lt;/h2&amp;gt;
      &amp;lt;div
        onClick={() =&amp;gt; {
          console.log(&quot;ref:deom&quot;, domRef, domRef.current);
          domRef.current.focus();
          if(!domRef.current.value){
            domRef.current.value = &apos;hh&apos;;
          }
        }}
      &amp;gt;
       &amp;lt;label&amp;gt;这是一个dom节点&amp;lt;/label&amp;gt;&amp;lt;input ref={domRef} /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;p onClick={showChild} style={{ marginTop: &quot;20px&quot; }}&amp;gt;
        这是子组件
      &amp;lt;/p&amp;gt;
      &amp;lt;div style={{ border: &quot;1px solid&quot;, padding: &quot;10px&quot; }}&amp;gt;
        &amp;lt;Child ref={childRef} /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
export default RefDemo;

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;useImperativeHandle(ref,createHandle,[deps])可以自定义暴露给父组件的实例值。如果不使用，父组件的ref(chidlRef)访问不到任何值（childRef.current==null）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;useImperativeHandle应该与forwradRef搭配使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;React.forwardRef会创建一个React组件，这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;React.forward接受渲染函数作为参数，React将使用prop和ref作为参数来调用此函数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 子组件
import React, {
  useRef,
  useEffect,
  useImperativeHandle,
  forwardRef,
} from &quot;react&quot;;

const ChildComponent = (props, ref) =&amp;gt; {
  useImperativeHandle(ref, () =&amp;gt; ({
    say: sayHello,
  }));
  const sayHello = () =&amp;gt; {
    alert(&quot;hello,我是子组件&quot;);
  };
  return &amp;lt;h3&amp;gt;子组件&amp;lt;/h3&amp;gt;;
};
const Child = forwardRef(ChildComponent);

export default Child;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>网格布局具体实现</title><link>https://nollieleo.github.io/posts/%E7%BD%91%E6%A0%BC%E5%B8%83%E5%B1%80%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%BD%91%E6%A0%BC%E5%B8%83%E5%B1%80%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0/</guid><description>需求  类似下图的效果：     需求分析     这种就是网格布局   父容器的宽自适应，子容器的宽计算公式为 (100% - (n - 1) \ 24) / n 。如果页面宽度大于1280px，n等于5，小于1280px时，n等于4，页面最小宽度是960px;   解决方案   Grid布局  ...</description><pubDate>Wed, 14 Oct 2020 14:43:23 GMT</pubDate><content:encoded>&lt;h2&gt;需求&lt;/h2&gt;
&lt;p&gt;类似下图的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.gif&quot; alt=&quot;1.gif&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;需求分析&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;2.gif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种就是网格布局&lt;/p&gt;
&lt;p&gt;父容器的宽自适应，子容器的宽计算公式为 &lt;strong&gt;(100% - (n - 1) * 24) / n&lt;/strong&gt; 。如果页面宽度大于1280px，n等于5，小于1280px时，n等于4，页面最小宽度是960px;&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;h3&gt;Grid布局&lt;/h3&gt;
&lt;p&gt;教程看这里：&lt;a href=&quot;http://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html&quot;&gt;阮一峰grid布局教程&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;style&amp;gt;
    .parent {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-row-gap: 24px;
      grid-column-gap: 24px;
    }

    .children {
      background-color: antiquewhite;
      height: 20px;
    }

    @media screen and (max-width: 1280px) {
      .parent {
        grid-template-columns: repeat(4, 1fr);
      }
    }

  &amp;lt;/style&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;parent&quot;&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;children&quot;&amp;gt;
        &amp;lt;div class=&quot;children-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>策略模式</title><link>https://nollieleo.github.io/posts/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</guid><description>策略模式   在程序设计中，要实现某一个功能有多种方案可以选择。   定义：定义一系列地算法，把他们一个个封装起来，并且使他们可以互相替换...</description><pubDate>Sun, 30 Aug 2020 09:57:30 GMT</pubDate><content:encoded>&lt;h1&gt;策略模式&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在程序设计中，要实现某一个功能有多种方案可以选择。&lt;/p&gt;
&lt;p&gt;定义：定义一系列地算法，把他们一个个封装起来，并且使他们可以互相替换&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>函数柯里化</title><link>https://nollieleo.github.io/posts/%E5%87%BD%E6%95%B0%E6%9F%AF%E9%87%8C%E5%8C%96/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%87%BD%E6%95%B0%E6%9F%AF%E9%87%8C%E5%8C%96/</guid><description>柯里化    在计算机科学中，柯里化（Currying）是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数，并且返回接受余下的参数且返回结果的新函数的技术。   例如需要实现以下得示例  javascript add(1, 2, 3) // 6 add(1) // 1 add...</description><pubDate>Mon, 24 Aug 2020 22:19:56 GMT</pubDate><content:encoded>&lt;h1&gt;柯里化&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在计算机科学中，柯里化（Currying）是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数，并且返回接受余下的参数且返回结果的新函数的技术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如需要实现以下得示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本来有这么一个求和函数&lt;code&gt;dynamicAdd()&lt;/code&gt;，接受任意个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function dynamicAdd() {
  return [...arguments].reduce((prev, curr) =&amp;gt; {
    return prev + curr
  }, 0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在需要通过柯里化把它变成一个新的函数，这个新的函数预置了第一个参数，并且可以在调用时继续传入剩余参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function curry(fn,firstArg){
	// 返回一个新函数
	return function(){
		// 将arguments转化为真正数组
		var restArgs = Array.from(arguments);
		return fn.apply(this,[firstArg,...restArgs])
	}
}

// 柯里化，预置参数10
var add10 = curry(dynamicAdd, 10)
add10(5); // 15
// 柯里化，预置参数20
var add20 = curry(dynamicAdd, 20);
add20(5); // 25
// 也可以对一个已经柯里化的函数add10继续柯里化，此时预置参数10即可
var anotherAdd20 = curry(add10, 10);
anotherAdd20(5); // 25

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;柯里化是在一个函数的基础上进行变换，得到一个新的预置了参数的函数。最后在调用新函数时，实际上还是会调用柯里化前的原函数。  并且柯里化得到的新函数可以继续被柯里化&lt;/p&gt;
&lt;p&gt;实际使用时也会出现柯里化的变体，&lt;strong&gt;不局限于只预置一个参数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function curry(fn) {
  // 保存预置参数
  var presetArgs = [].slice.call(arguments, 1)
  // 返回一个新函数
  return function() {
    // 新函数调用时会继续传参
    var restArgs = [].slice.call(arguments)
    // 参数合并，通过apply调用原函数
    return fn.apply(this, [...presetArgs, ...restArgs])
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 参数定长的柯里化&lt;/h2&gt;
&lt;p&gt;假设存在一个原函数&lt;code&gt;fn&lt;/code&gt;，&lt;code&gt;fn&lt;/code&gt;接受三个参数&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;，那么函数&lt;code&gt;fn&lt;/code&gt;最多被柯里化三次（&lt;strong&gt;有效地绑定参数算一次&lt;/strong&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fn(a, b, c) {
  return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); // 6
// 再次柯里化也没有意义，原函数只需要三个参数
var c4 = curry(c3, 4);
c4();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，我们可以&lt;strong&gt;通过柯里化缓存的参数数量，来判断是否到达了执行时机&lt;/strong&gt;。那么我们就得到了一个柯里化的通用模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function curry(fn) {
  // 获取原函数的参数长度
  const argLen = fn.length;
  // 保存预置参数
  const presetArgs = [].slice.call(arguments, 1)
  // 返回一个新函数
  return function() {
    // 新函数调用时会继续传参
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    if (allArgs.length &amp;gt;= argLen) {
      // 如果参数够了，就执行原函数
      return fn.apply(this, allArgs)
    } else {
      // 否则继续柯里化
      return curry.call(null, fn, ...allArgs)
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;function fn(a,b,c){
	return a+b+c;
}
var curried = curry(fn);
curried(1,2,3);
curried(1,2)(3);
curried(1)(2,3);
curried(1)(2)(3);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 参数不定长地柯里化&lt;/h2&gt;
&lt;p&gt;在&lt;strong&gt;参数不定长&lt;/strong&gt;的情况下，要同时支持&lt;code&gt;1~N&lt;/code&gt;次调用还是挺难&lt;/p&gt;
&lt;p&gt;如果要支持参数不定长的场景，&lt;strong&gt;已经柯里化的函数在执行完毕时不能返回一个值，只能返回一个函数；同时要让JS引擎在解析得到的这个结果时，能求出我们预期的值&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;经&lt;code&gt;curry&lt;/code&gt;处理，得到一个新函数，这一点不变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;// curry是一个函数
var curried = curry(add);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;新函数执行后仍然返回一个结果函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// curried10也是一个函数
var curried10 = curried(10);
var curried30 = curried10(20);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;结果函数可以被Javascript引擎解析，得到一个预期的值。&lt;pre&gt;&lt;code&gt;curried10; // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键点在于3，如何让Javascript引擎按我们的预期进行解析，这就回到Javascript基础了。在解析一个函数的原始值时，会用到&lt;code&gt;toString&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function curry(fn) {
  // 保存预置参数
  const presetArgs = [].slice.call(arguments, 1)
  // 返回一个新函数
  function curried () {
    // 新函数调用时会继续传参
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    return curry.call(null, fn, ...allArgs)
  }
  // 重写toString
  curried.toString = function() {
    return fn.apply(null, presetArgs)
  }
  return curried;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;柯里化是一种&lt;strong&gt;函数式编程&lt;/strong&gt;思想，实际上在项目中可能用得少，或者说用得不深入，但是如果你掌握了这种思想，也许在未来的某个时间点，你会用得上！&lt;/p&gt;
&lt;p&gt;大概来说，柯里化有如下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简洁代码&lt;/strong&gt;：柯里化应用在较复杂的场景中，有简洁代码，可读性高的优点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数复用&lt;/strong&gt;：公共的参数已经通过柯里化预置了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟执行&lt;/strong&gt;：柯里化时只是返回一个预置参数的新函数，并没有立刻执行，实际上在满足条件后才会执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管道式流水线编程&lt;/strong&gt;：利于使用函数组装管道式的流水线工序，不污染原函数。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>单例模式</title><link>https://nollieleo.github.io/posts/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/</guid><description>单例模式   保证一个类仅有一个实例，并提供给一个访问它的全局访问点。   1. 实现简单的单例模式  javascript var Singleton = function(name){ 	this.name = name; } Singleton.instance = null; Singlet...</description><pubDate>Sun, 23 Aug 2020 13:28:14 GMT</pubDate><content:encoded>&lt;h1&gt;单例模式&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;保证&lt;strong&gt;一个类仅有一个实例&lt;/strong&gt;，并提供给一个访问它的全局访问点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 实现简单的单例模式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;var Singleton = function(name){
	this.name = name;
}
Singleton.instance = null;
Singleton.prototype.getName = function(){
	alert(this.name);
}
Singleton.getInstance = function(name){
   	if(!this.instance){
        this.instance = new Singleton(name);
    }
    return this.instance
}

var a1 = Singleton.getInstance(&apos;a1&apos;);
var a2 = Singleton.getInstance(&apos;a2&apos;);

a1 === a2 // true;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接用闭包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var Singleton = function(name){
	this.name = name;
}
Singleton.prototype.getName = function(){
	altert(this.name);
}
Singleton.getInstance = (function(){
	var instance = null;
	return function(name){
		if(!instance){
			instance = new Singleton(name);
		}
		return this.instance;
	}
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式相对简单，但是增加了这个类的“不透明性”，这个类的使用者必须知道这个是一个单例，与之前的new一个类不同，使用者要用getinstance来获取对象&lt;/p&gt;
&lt;h2&gt;2.透明的单例模式&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;目标：实现一个“透明的单例类”，用户从这个类中创建对象的时候，可以像使用其他任何普通类一样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面例子用CreateDiv单例类，负责在页面中创建唯一的div节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var CreateDiv = (function{
	var instance;
	var CreateDiv = function(html){
		if(instance){
			return instance;
		}
		this.html = html;
		this.init();
		return instance = this;
	}
	CreateDiv.prototype.init = function(){
		var div = document.createElement(&apos;div&apos;);
		div.innerHtml = this.html;
		document.body.appendChild(div);
	}
	return CreateDiv;
})();
var a1 = new CreateDiv(&apos;a1&apos;);
var a2 = new CreateDiv(&apos;a2&apos;);
a1 === a2  // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然透明了，但有缺点，为了把instance封起来，我们使用了自执行的匿名函数和闭包，并且让这个匿名函数返回真正的Singleton构造方法，增加了复杂度。&lt;/p&gt;
&lt;p&gt;假如某天我们需要利用这个类，再页面中创建许多div，则就要让这个单例类变成一个普通的可产生多个实例的类，那我们必须得改写CreateDiv构造函数，把控制创建唯一对象的那段去掉。&lt;/p&gt;
&lt;h2&gt;3.用代理实现&lt;/h2&gt;
&lt;p&gt;由上可以知道，要想使得这个单例类变成一个普通的可以产生多个实例的类，那我们必须得改。&lt;/p&gt;
&lt;p&gt;这时候我们把负责管理单例的代码移除出去，使他成为一个普通的创建div的类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var CreateDiv = function(html){
	this.html = html;
	this.init();
}
CreateDiv.prototype.init = function(){
	var div = document.createElement(&apos;div&apos;);
	div.innerHTML = this.html;
	document.body.appendChild(div);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来引入代理类ProxySingletonCreateDiv&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ProxySingletonCreateDiv = (function(){
	var instance;
	return function(html){
		if(!instance){
			instance = new CreateDiv(html);
		}
		return instance;
	}
})();
var a1 = new ProxySingletonCreateDiv(&apos;a1&apos;);
var a2 = new ProxySingletonCreateDiv(&apos;a2&apos;);
a1 === a2; // true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个连起来就就可以就可以实现单例模式的实现，通过代理类加上一个CreateDiv普通类，组合&lt;/p&gt;
&lt;h2&gt;4. js中的单例模式&lt;/h2&gt;
&lt;p&gt;js是一门无类语言。传统的单例模式实现再js中并不适用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;单例模式的核心是确保只有一个实例，并提供全局访问&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全局变量不是单例模式，单再js开发中，我们经常会把全局变量当成单例来使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a ={};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样变量容易被覆盖。&lt;/p&gt;
&lt;p&gt;因此需要降低全局变量命名的污染。&lt;/p&gt;
&lt;h3&gt;4.1 使用命名空间&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var namespace = {
	a:function(){
		...
	},
	b:function(){
		...
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 使用闭包封装私有变量&lt;/h3&gt;
&lt;p&gt;这种方法把一些变量封装再闭包内部，只暴露接口与外界通信&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var user = (function(){
	var _name = &apos;seven&apos;,_age = 29;
	return {
		getUserInfo:function(){
			return _name + &apos;-&apos; + _age;
		}
	}
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用下划线约定_name 和 _age。&lt;/p&gt;
&lt;h2&gt;5.惰性单例&lt;/h2&gt;
&lt;p&gt;惰性单例指的是需要的时候才去创建的对象实例。&lt;/p&gt;
&lt;p&gt;例如：我们需要点击一个按钮显示一个弹窗，这个浮窗再这个页面里头是唯一的，不可能同时出现两个&lt;/p&gt;
&lt;p&gt;**方法一：**在页面加载之后就去创建，然后点击btn把他显示出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;button id=&quot;loginBtn&quot;&amp;gt;
            登录
        &amp;lt;/button&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;script&amp;gt;
    	var loginLayer = (function(){
            var div = document.createElement(&apos;div&apos;);
            div.innerHTML = &apos;我是登录浮窗&apos;;
            div.style.display = &apos;none&apos;;
            document.body.appendChild(div);
            return div;
        })();
        document.getElementById(&apos;loginBtn&apos;).onclick = function(){
            loginLayer.style.display = &apos;block&apos;
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;，但是如果有时候不需要就不需要浪费空间去创建节点，需要改进&lt;/p&gt;
&lt;p&gt;现在是点击的时候才会去创建节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;button id=&quot;loginBtn&quot;&amp;gt;
            登录
        &amp;lt;/button&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;script&amp;gt;
    	var loginLayer = function(){
            var div = document.createElement(&apos;div&apos;);
            div.innerHTML = &apos;我是登录浮窗&apos;;
            div.style.display = &apos;none&apos;;
            document.body.appendChild(div);
            return div;
        };
        document.getElementById(&apos;loginBtn&apos;).onclick = function(){
            var loginLayer = createLoginLayer();
            loginLayer.style.display = &apos;block&apos;
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是虽然实现了惰性，这样就失去了单例的效果，频繁的增删不太合适。&lt;/p&gt;
&lt;p&gt;此时可以用一个变量判断是否已经创建过浮窗了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var createLoginLayer = (function(){
	var div;
	return function(){
		if(!div){
			var div = document.createElement(&apos;div&apos;);
            div.innerHTML = &apos;我是登录浮窗&apos;;
            div.style.display = &apos;none&apos;;
            document.body.appendChild(div);
		}
		return div;
	}
})()
document.getElementById(&apos;loginBtn&apos;).onclick = function(){
    var loginLayer = createLoginLayer();
    loginLayer.style.display = &apos;block&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这个却违反了单一职责原则，这里创建和管理单例的逻辑全部都放在了一个func里头，但下次如果要创建iframe或者其他的东西，那就只能照抄代码了&lt;/p&gt;
&lt;p&gt;因此把不变的部分分出来，把管理单例的逻辑抽出来，之后传创建的逻辑就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var getSingle = function(fn){
	var result ;
	return function(){
		return result || result = fn.apply(this,arguments);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就可以用于创建弹窗的方法用于参数fn的形式传入getSingle，创建啥都行，用result保存fn的己算结果。result身在闭包中，永远不会销毁，之后如果result有值，直接返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var createLoginLayer = function(){
	var div = document.createElement(&apos;div&apos;);
    div.innerHTML = &apos;我是登录浮窗&apos;;
    div.style.display = &apos;none&apos;;
    document.body.appendChild(div);
    return div;
}

var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById(&apos;loginBtn&apos;).onclick = function(){
    var loginLayer = createSingleLoginLayer();
    loginLayer.style.display = &apos;block&apos;
}
// 创建ifame啥的也是一样
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>typeScript总结</title><link>https://nollieleo.github.io/posts/typescript%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/typescript%E6%80%BB%E7%BB%93/</guid><description>ts中需要理解的   - any vs unknown  any 表示任意类型，这个类型会逃离 Typescript 的类型检查，和在 Javascript 中一样，any 类型的变量可以执行任意操作，编译时不会报错。 unknown 也可以表示任意类型，但它同时也告诉 Typescript 开发者...</description><pubDate>Sun, 02 Aug 2020 22:44:13 GMT</pubDate><content:encoded>&lt;h2&gt;ts中需要理解的&lt;/h2&gt;
&lt;h3&gt;- any vs unknown&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;any&lt;/code&gt; 表示任意类型，这个类型会逃离 &lt;code&gt;Typescript&lt;/code&gt; 的类型检查，和在 &lt;code&gt;Javascript&lt;/code&gt; 中一样，&lt;code&gt;any&lt;/code&gt; 类型的变量可以执行任意操作，编译时不会报错。 &lt;code&gt;unknown&lt;/code&gt; 也可以表示任意类型，但它同时也告诉 &lt;code&gt;Typescript&lt;/code&gt; 开发者对其也是一无所知，做任何操作时需要慎重。这个类型仅可以执行有限的操作（&lt;code&gt;==、=== 、||、&amp;amp;&amp;amp;、?、!、typeof、instanceof&lt;/code&gt; 等等），其他操作需要向 &lt;code&gt;Typescript&lt;/code&gt; 证明这个值是什么类型，否则会提示异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let foo: any
let bar: unknown

foo.functionNotExist()
bar.functionNotExist() // 对象的类型为 &quot;unknown&quot;。

if (!!bar) { // ==、=== 、||、&amp;amp;&amp;amp;、?、!、typeof、instanceof
  console.log(bar)
}

bar.toFixed(1) // Error

if (typeof bar=== &apos;number&apos;) {
  bar.toFixed(1) // OK
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;any&lt;/code&gt; 会增加了运行时出错的风险，不到万不得已不要使用。表示【不知道什么类型】的场景下使用 &lt;code&gt;unknown&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;- {} vs object vs Object&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;object&lt;/code&gt; 表示的是常规的 &lt;code&gt;Javascript&lt;/code&gt; 对象类型，非基础数据类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function create(o: object): void;

create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // Error
create(&quot;string&quot;); // Error
create(false); // Error
create({
  toString() {
    return 3;
  },
}); // OK

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;{}&lt;/code&gt; 表示的非 null，非 undefined 的任意类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function create(o: {}): void;

create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // OK
create(&quot;string&quot;); // OK
create(false); // OK
create({
  toString() {
    return 3;
  },
}); // OK

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 和 &lt;code&gt;{}&lt;/code&gt; 几乎一致，区别是 &lt;code&gt;Object&lt;/code&gt; 类型会对 &lt;code&gt;Object&lt;/code&gt; 原型内置的方法（&lt;code&gt;toString/hasOwnPreperty&lt;/code&gt;）进行校验。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function create(o: Object): void;

create({ prop: 0 }); // OK
create(null); // Error
create(undefined); // Error
create(42); // OK
create(&quot;string&quot;); // OK
create(false); // OK
create({
  toString() {
    return 3;
  },
}); // Error

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- Never 类型&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;never&lt;/code&gt; 类型表示的是那些永不存在的值的类型。 例如，&lt;code&gt;never&lt;/code&gt; 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 TypeScript 中，可以利用 never 类型的特性来实现全面性检查，具体示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === &quot;string&quot;) {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === &quot;number&quot;) {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意在 else 分支里面，我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确，那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo = string | number | boolean;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而他忘记同时修改 &lt;code&gt;controlFlowAnalysisWithNever&lt;/code&gt; 方法中的控制流程，这时候 else 分支的 foo 类型会被收窄为 &lt;code&gt;boolean&lt;/code&gt; 类型，导致无法赋值给 never 类型，这时就会产生一个编译错误。通过这个方式，我们可以确保&lt;code&gt;controlFlowAnalysisWithNever&lt;/code&gt; 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例，我们可以得出一个结论：&lt;strong&gt;使用 never 避免出现新增了联合类型没有对应的实现，目的就是写出类型绝对安全的代码。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;- type vs interface&lt;/h3&gt;
&lt;p&gt;两者都可以用来定义类型。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interface&lt;/code&gt;（接口） 只能声明对象类型，支持声明合并（可扩展）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface User {
  id: string
}
 
interface User {
  name: string
}
 
const user = {} as User
 
console.log(user.id);
console.log(user.name);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt;（类型别名）不支持声明合并、行为有点像&lt;code&gt;const&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt; 有块级作用域。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  id: string,
}

if (true) {
  type User = {
    name: string,
  }

  const user = {} as User;
  console.log(user.name);
  console.log(user.id) // 类型“User”上不存在属性“id”。
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 更为通用，右侧可以是任意类型，包括表达式运算，以及映射类型等等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = number
type B = A | string
type ValueOf&amp;lt;T&amp;gt; = T[keyof T];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你是在开发一个包，模块，允许别人进行扩展就用 &lt;code&gt;interface&lt;/code&gt;，如果需要定义基础数据类型或者需要类型运算，使用 &lt;code&gt;type&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;- enum vs const enum&lt;/h3&gt;
&lt;p&gt;默认情况下 &lt;code&gt;enum&lt;/code&gt; 会被编译成 &lt;code&gt;Javascript&lt;/code&gt; 对象，并且可以通过 &lt;code&gt;value&lt;/code&gt; 反向查找。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ActiveType {
  active = 1,
  inactive = 2,
}

function isActive(type: ActiveType) {}
isActive(ActiveType.active);

// ============================== compile result:
// var ActiveType;
// (function (ActiveType) {
//     ActiveType[ActiveType[&quot;active&quot;] = 1] = &quot;active&quot;;
//     ActiveType[ActiveType[&quot;inactive&quot;] = 2] = &quot;inactive&quot;;
// })(ActiveType || (ActiveType = {}));
// function isActive(type) { }
// isActive(ActiveType.active);

ActiveType[1]; // OK
ActiveType[10]; // OK！！！

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cosnt enum&lt;/code&gt; 默认情况下不会生成 &lt;code&gt;Javascript&lt;/code&gt; 对象而是把使用到的代码直接输出 &lt;code&gt;value&lt;/code&gt;，不支持 &lt;code&gt;value&lt;/code&gt; 反向查找。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const enum ActiveType {
  active = 1,
  inactive = 2,
}

function isActive(type: ActiveType) {}
isActive(ActiveType.active);

// ============================== compile result:
// function isActive(type) { }
// isActive(1 /* active */);

ActiveType[1]; // Error
ActiveType[10]; // Error

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型运算&lt;/h2&gt;
&lt;h3&gt;集合运算&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; 在 JS 中表示位与运算符，在 Typescript 中用来计算两个类型的交集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Type1 = &quot;a&quot; | &quot;b&quot;;
type Type2 = &quot;b&quot; | &quot;c&quot;;
type Type3 = Type1 &amp;amp; Type2; // &apos;b&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;|&lt;/code&gt; 在 JS 中表示位或运算符，在 Typescript 中用来计算两个类型的并集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Type1 = &quot;a&quot; | &quot;b&quot;;
type Type2 = &quot;b&quot; | &quot;c&quot;;
type Type3 = Type1 | Type2; // &apos;a&apos; &apos;b&apos; &apos;c&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;索引签名&lt;/h3&gt;
&lt;p&gt;索引签名可以用来定义对象内的属性、值的类型，例如定义一个 &lt;code&gt;React&lt;/code&gt; 组件，允许 &lt;code&gt;Props&lt;/code&gt; 可以传任意 &lt;code&gt;key&lt;/code&gt; 为 &lt;code&gt;string&lt;/code&gt;，&lt;code&gt;value&lt;/code&gt; 为 &lt;code&gt;number&lt;/code&gt; 的 &lt;code&gt;props&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Props {
  [key: string]: number
}

&amp;lt;Component count={1} /&amp;gt; // OK
&amp;lt;Component count={true} /&amp;gt; // Error
&amp;lt;Component count={&apos;1&apos;} /&amp;gt; // Error

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类型键入&lt;/h3&gt;
&lt;p&gt;类型键入允许 &lt;code&gt;Typescript&lt;/code&gt; 像对象取属性值一样使用类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  userId: string
  friendList: {
    fristName: string
    lastName: string
  }[]
}

type UserIdType = User[&apos;userId&apos;] // string
type FriendList = User[&apos;friendList&apos;] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，我们利用类型键入的功能从 &lt;code&gt;User&lt;/code&gt; 类型中计算出了其他的几种类型。&lt;code&gt;FriendList[number]&lt;/code&gt; 这里的 &lt;code&gt;number&lt;/code&gt; 是关键字，用来取数组子项的类型。在元组中也可以使用字面量数字得到数组元素的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Tuple = [number, string]
type First = Tuple[0] // number
type Second = Tuple[1] // string
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;typeof value&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt; 关键字在 JS 中用来获取变量的类型，运算结果是一个字符串（值）。而在 TS 中表示的是推算一个变量的类型（类型）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let str1 = &apos;fooooo&apos;
type Type1 = typeof str1 // type string

const str2 = &apos;fooooo&apos;
type Type2 = typeof str2 // type &quot;fooooo&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt; 在计算变量和常量时有所不同，由于常量时不会变的，所以 &lt;code&gt;Typescript&lt;/code&gt; 会使用严格的类型，例如下面 &lt;code&gt;Type2&lt;/code&gt; 的例子，&lt;code&gt;str2&lt;/code&gt; 的是个 &apos;fooooo&apos; 类型的字符串。而变量会是宽松的字符串类型。&lt;/p&gt;
&lt;h4&gt;keyof Type&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;keyof&lt;/code&gt; 关键字可以用来获取一个对象类型的所有 &lt;code&gt;key&lt;/code&gt; 类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  id: string;
  name: string;
};

type UserKeys = keyof User; //&quot;id&quot; | &quot;name&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;enum&lt;/code&gt; 在 Typescript 中有一定的特殊性（有时表示类型，又是表示值），如果要获取 &lt;code&gt;enum&lt;/code&gt; 的 key 类型，需要先把它当成值，用 &lt;code&gt;typeof&lt;/code&gt; 再用 &lt;code&gt;keyof&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ActiveType {
  Active,
  Inactive
}

type KeyOfType = keyof typeof ActiveType // &quot;Active&quot; | &quot;Inactive&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;extends&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;extends&lt;/code&gt; 关键字同样存在多种用途，在 &lt;code&gt;interface&lt;/code&gt; 中表示类型扩展，在条件类型语句中表示布尔运算，在泛型中起到限制的作用，在 &lt;code&gt;class&lt;/code&gt; 中表示继承。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 表示类型扩展
interface A {
  a: string
}

interface B extends A { // { a: string, b: string }
  b: string
}

// 条件类型中起到布尔运算的功能
type Bar&amp;lt;T&amp;gt; = T extends string ? &apos;string&apos; : never
type C = Bar&amp;lt;number&amp;gt; // never
type D = Bar&amp;lt;string&amp;gt; // string
type E = Bar&amp;lt;&apos;fooo&apos;&amp;gt; // string

// 起到类型限制的作用
type Foo&amp;lt;T extends object&amp;gt; = T
type F = Foo&amp;lt;number&amp;gt; // 类型“number”不满足约束“object”。
type G = Foo&amp;lt;string&amp;gt; // 类型“string”不满足约束“object”。
type H = Foo&amp;lt;{}&amp;gt; // OK

// 类继承
class I {}
class J extends I {}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型&lt;/h2&gt;
&lt;p&gt;假设 &lt;code&gt;filter&lt;/code&gt; 方法传入一个数字类型的数组，及一个返回布尔值的方法，最终过滤出想要的结果返回，声明大致如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function filter(
  array: number[],
  fn: (item: unknown) =&amp;gt; boolean
): number[];

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过了一段时间，需要使用 &lt;code&gt;filter&lt;/code&gt; 方法来过滤一些字符串，可以使用 &lt;code&gt;Typescript&lt;/code&gt; 的函数重载的功能，filter 内部代码不变，只需要添加类型定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function filter(
  array: string[],
  fn: (item: unknown) =&amp;gt; boolean
): string[];

declare function filter(
  array: number[],
  fn: (item: unknown) =&amp;gt; boolean
): number[];

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又过了一段时间，需要用 &lt;code&gt;filter&lt;/code&gt; 来过滤 &lt;code&gt;boolean[]&lt;/code&gt;, 过滤 &lt;code&gt;object[]&lt;/code&gt;, 过滤其他具体类型，如果仍然使用重载的方法将会出现非常多重复的代码。这时候就可以考虑使用泛型了，&lt;code&gt;Dont repeat yourself&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;泛型就像 &lt;code&gt;Typescript&lt;/code&gt; “语言” 中的“方法”，可以通过“传参”来得到新的类型。日常开发中经常用到的泛型有 &lt;code&gt;Promise、Array、React.Component&lt;/code&gt; 等等。&lt;/p&gt;
&lt;p&gt;使用泛型来改造 &lt;code&gt;filter&lt;/code&gt; 方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare function filter&amp;lt;T&amp;gt;(
  array: T[],
  fn: (item: unknown) =&amp;gt; boolean
): T[];

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要在方法名后面加上尖括号&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;，表示方法支持一个泛型参数，(这里的 T 可以改为任意你喜欢的变量名，大部分人的偏好是从 T、U、V...开始命名)，&lt;code&gt;array: T[]&lt;/code&gt; 表示传入的第一个参数是泛型模板类型的数组，&lt;code&gt;:T[]&lt;/code&gt; 表示方法会返回模板类型的数组。&lt;code&gt;Typescript&lt;/code&gt; 将会自动根据传参类型辨别出 &lt;code&gt;T&lt;/code&gt; 实际代表的类型，这样就可以保留类型的同时，避免重复代码了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;filter([1, 2, 3], () =&amp;gt; true) // function filter&amp;lt;number&amp;gt;(array: number[], fn: (item: unknown) =&amp;gt; boolean): number[]
filter([&apos;1&apos;, &apos;2&apos;, &apos;3&apos;], () =&amp;gt; true) // function filter&amp;lt;string&amp;gt;(array: string[], fn: (item: unknown) =&amp;gt; boolean): string[]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把泛型比喻成“方法”之后，很多行为都很好理解。“方法”可以传参，可以有多个参数，可以有默认值，泛型也可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Foo&amp;lt;T, U = string&amp;gt; = { // 多参数、默认值
  foo: Array&amp;lt;T&amp;gt; // 可以传递
  bar: U
}

type A = Foo&amp;lt;number&amp;gt; // type A = { foo: number[]; bar: string; }
type B = Foo&amp;lt;number, number&amp;gt; // type B = { foo: number[]; bar: number; }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;别忘了，泛型参数还可以有限制，例如下面的例子 &lt;code&gt;extends&lt;/code&gt; 的作用是限制 &lt;code&gt;T&lt;/code&gt; 至少是个 &lt;code&gt;HTMLElement&lt;/code&gt; 类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyEvent&amp;lt;T extends HTMLElement = HTMLElement&amp;gt; = {
   target: T,
   type: string
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;映射类型&lt;/h3&gt;
&lt;h4&gt;关键字 in&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;in&lt;/code&gt; 关键字在类型中表示类型映射，和索引签名的写法有些相似。下面的例子中声明一个 &lt;code&gt;Props&lt;/code&gt; 的类型，&lt;code&gt;key&lt;/code&gt; 类型为 &apos;count&apos; | &apos;id&apos; 类型，&lt;code&gt;value&lt;/code&gt; 为 &lt;code&gt;number&lt;/code&gt; 类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Props = {
  [key in &apos;count&apos; | &apos;id&apos;]: number
}

const props1: Props = { // OK
  count: 1,
  id: 1
}

const props2: Props = {
  count: &apos;1&apos;, // ERROR
  id: 1
}

const props3: Props = {
  count: 1,
  id: 1,
  name: 1 // ERROR
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Record&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Record&lt;/code&gt; 定义键类型为 &lt;code&gt;Keys&lt;/code&gt;、值类型为 &lt;code&gt;Values&lt;/code&gt; 的对象类型。&lt;/p&gt;
&lt;p&gt;示例 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ErrorCodes {
  Timeout = 10001,
  ServerBusy = 10002,
  
}

const ErrorMessageMap: Record&amp;lt;ErrorCodes, string&amp;gt; = {
  [ErrorCodes.Timeout]: &apos;Timeout, please try again&apos;,
  [ErrorCodes.ServerBusy]: &apos;Server is busy now&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型映射还可以用来做全面性检查，例如上面的例子中如果漏了某个 ErrorCodes，Typescript 同样会抛出异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ErrorCodes {
  Timeout = 10001,
  ServerBusy = 10002,
  AuthFailed = 10003
}

// 类型 &quot;{ 10001: string; 10002: string; }&quot; 中缺少属性 &quot;10003&quot;，但类型 &quot;Record&amp;lt;ErrorCodes, string&amp;gt;&quot; 中需要该属性
const ErrorMessageMap: Record&amp;lt;ErrorCodes, string&amp;gt; = { 
  [ErrorCodes.Timeout]: &apos;Timeout, please try again&apos;,
  [ErrorCodes.ServerBusy]: &apos;Server is busy now&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Record&amp;lt;K extends keyof any, T&amp;gt; = {
  [P in K]: T;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Partial&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Partial&lt;/code&gt; 可以将类型定义的属性变成可选。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  id?: string,
  gender: &apos;male&apos; | &apos;female&apos;
}

type PartialUser =  Partial&amp;lt;User&amp;gt;  // { id?: string, gender?: &apos;male&apos; | &apos;female&apos;}

function createUser (user: PartialUser = { gender: &apos;male&apos; }) {}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;User&lt;/code&gt; 类型对于 &lt;code&gt;gender&lt;/code&gt; 属性是要求必须有的(: 用户必须有性别才行。而在设计 &lt;code&gt;createUser&lt;/code&gt; 方法时，为了方便程序会给 &lt;code&gt;gender&lt;/code&gt; 赋予默认值。这时候可以将参数修改成 &lt;code&gt;Partial&lt;/code&gt;，使用者就可以不用必须传 &lt;code&gt;gender&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Partial&amp;lt;T&amp;gt; = {
  [U in keyof T]?: T[U];
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Required&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Required&lt;/code&gt; 和 &lt;code&gt;Partial&lt;/code&gt; 的作用相反，是将对象类型的属性都变成必须。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  id?: string,
  gender: &apos;male&apos; | &apos;female&apos;
}

type RequiredUser = Required&amp;lt;User&amp;gt; // { readonly id: string, readonly gender: &apos;male&apos; | &apos;female&apos;}

function showUserProfile (user: RequiredUser) {
  console.log(user.id) // 不需要加 ！
  console.log(user.gender)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任然使用 &lt;code&gt;User&lt;/code&gt; 类型，&lt;code&gt;id&lt;/code&gt; 属性定义的时候是可选的（要创建了才有 &lt;code&gt;id&lt;/code&gt;），而展示的时候 &lt;code&gt;User id&lt;/code&gt; 肯定已经存在了，这时候可以使用 &lt;code&gt;Required&lt;/code&gt;，那么调用 &lt;code&gt;showUserProfile&lt;/code&gt; 时 &lt;code&gt;User&lt;/code&gt; 所有属性都必须非 &lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Required&amp;lt;T&amp;gt; = {
  [U in keyof T]-?: T[U];
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-?&lt;/code&gt; 符号在这里表示的意思是去掉可选符号 &lt;code&gt;?&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;Readonly&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Readonly&lt;/code&gt; 是将对象类型的属性都变成只读。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ReadonlyUser = Readonly&amp;lt;User&amp;gt; // { readonly id?: string, readonly gender: &apos;male&apos; | &apos;female&apos;}

const user: ReadonlyUser = {
  id: &apos;1&apos;,
  gender: &apos;male&apos;
}

user.gender = &apos;femail&apos; // 无法分配到 &quot;gender&quot; ，因为它是只读属性。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Readonly&amp;lt;T&amp;gt; = {
  readonly [U in keyof T]: T[U];
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pick&lt;/h4&gt;
&lt;p&gt;Pick 是挑选类型中的部分属性。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Location = {
  latitude: number
  longitude: number
  city: string
  address: string
  province: string
  district: string
}

type LatLong = Pick&amp;lt;Location, &apos;latitude&apos; | &apos;longitude&apos;&amp;gt; //  { latitude: number; longitude: number; }

const region: LatLong = {
  latitude: 22.545001,
  longitude: 114.011712
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Pick&amp;lt;T, K extends keyof T&amp;gt; = {
  [P in K]: T[P];
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Omit&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Omit&lt;/code&gt; 结合了 &lt;code&gt;Pick&lt;/code&gt; 和 &lt;code&gt;Exclude&lt;/code&gt;，将忽略对象类型中的部分 keys。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit&amp;lt;Todo, &quot;description&quot;&amp;gt;; // { title: string; completed: boolean; }

const todo: TodoPreview = {
  title: &quot;Clean room&quot;,
  completed: false,
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Omit&amp;lt;T, K extends keyof any&amp;gt; = Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;条件类型&lt;/h3&gt;
&lt;h4&gt;三目运算符&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Typescript&lt;/code&gt; 类型运算也支持“三目运算符”，称之为条件类型，一般通过 &lt;code&gt;extends&lt;/code&gt; 关键字判断条件成不成立，成立的话得到一个类型，不成立的话返回另一个类型。条件类型通常是与泛型同时出现的（：因为如果是已知固定类型就没必要再判断了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type IsString&amp;lt;T&amp;gt; = T extends string ? true : false

type A = IsString&amp;lt;number&amp;gt; // false
type B = IsString&amp;lt;string&amp;gt; // true

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在处理并集时，条件类型还具有条件分配的逻辑，&lt;code&gt;number | string&lt;/code&gt; 做条件运算等价于 &lt;code&gt;number 条件运算 | string&lt;/code&gt; 条件运算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ToArray&amp;lt;T&amp;gt; = T[]
type A = ToArray&amp;lt;number | string&amp;gt; // (string | number)[]

type ToArray2&amp;lt;T&amp;gt; = T extends unknown ? T[] : T[];
type B = ToArray2&amp;lt;number | string&amp;gt;; // string[] | number[]

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;infer&lt;/h4&gt;
&lt;p&gt;除了显示声明泛型参数，&lt;code&gt;Typescript&lt;/code&gt; 还支持动态推导泛型，用到的是 &lt;code&gt;infer&lt;/code&gt; 关键字。什么场景下还需要动态推导？通常是需要通过传入的泛型参数去获取新的类型，这和直接定义一个新的泛型参数不一样。&lt;/p&gt;
&lt;p&gt;例如现在定义了 &lt;code&gt;ApiResponse&lt;/code&gt; 的两个具体类型 &lt;code&gt;UserResponse&lt;/code&gt; 和 &lt;code&gt;EventResponse&lt;/code&gt;，如果想得到 &lt;code&gt;User&lt;/code&gt; 实体类型和 &lt;code&gt;Event&lt;/code&gt; 实体类型需要怎么做？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ApiResponse&amp;lt;T&amp;gt; = {
  code: number
  data: T
};

type UserResponse = ApiResponse&amp;lt;{
  id: string,
  name: string
}&amp;gt;

type EventResponse = ApiResponse&amp;lt;{
  id: string,
  title: string
}&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然可以拎出来单独定义新的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = {
  id: string,
  name: string
}

type UserResponse = ApiResponse&amp;lt;User&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果类型是由其他人提供的就不好处理了。这时可以尝试下使用 &lt;code&gt;infer&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ApiResponseEntity&amp;lt;T&amp;gt; = T extends ApiResponse&amp;lt;infer U&amp;gt; ? U : never;

type User = ApiResponseEntity&amp;lt;UserResponse&amp;gt;; // { id: string; name: string; }
type Event = ApiResponseEntity&amp;lt;EventResponse&amp;gt;; // { id: string; title: string; }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例中，判断传入的类型 &lt;code&gt;T&lt;/code&gt; 是不是 &lt;code&gt;T extends ApiResponse&lt;/code&gt; 的子集，这里的 &lt;code&gt;infer&lt;/code&gt; 既是让 &lt;code&gt;Typescript&lt;/code&gt; 尝试去理解 &lt;code&gt;T&lt;/code&gt; 具体是那种类型的 &lt;code&gt;ApiResponse&lt;/code&gt;，生成新的泛型参数 &lt;code&gt;U&lt;/code&gt;。如果满足 &lt;code&gt;extends&lt;/code&gt; 条件则将 &lt;code&gt;U&lt;/code&gt; 类型返回。&lt;/p&gt;
&lt;p&gt;充分理解了条件类型和 &lt;code&gt;infer&lt;/code&gt;关键字之后，&lt;code&gt;Typescript&lt;/code&gt; 自带的条件泛型工具也就很好理解了。&lt;/p&gt;
&lt;h4&gt;ReturnType&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Returntype&lt;/code&gt; 用来获取方法的返回值类型&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = (a: number) =&amp;gt; string
type B = ReturnType&amp;lt;A&amp;gt; // string

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ReturnType&amp;lt;T&amp;gt; = T extends (
  ...args: any[]
) =&amp;gt; infer R ? R : any;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Parameters&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Parameters&lt;/code&gt; 用来获取方法的参数类型&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type EventListenerParamsType = Parameters&amp;lt;typeof window.addEventListener&amp;gt;;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Parameters&amp;lt;T extends (...args: any) =&amp;gt; any&amp;gt; = T extends (...args: infer P) =&amp;gt; any
  ? P : never;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Exclude&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Exclude&lt;/code&gt; 用来计算在 T 中而不在 U 中的类型&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = number | string
type B = string
type C = Exclude&amp;lt;A, B&amp;gt; // number
	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Exclude&amp;lt;T, U&amp;gt; = T extends U ? never : T;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Extract&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Extract&lt;/code&gt; 用来计算 T 中可以赋值给 U 的类型&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = number | string
type B = string
type C = Extract&amp;lt;A, B&amp;gt; // string

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Extract&amp;lt;T, U&amp;gt; = T extends U ? T : never;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;NonNullable&lt;/h4&gt;
&lt;p&gt;从类型中排除 &lt;code&gt;null&lt;/code&gt; 和 &lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A = {
  a?: number | null
}
type B = NonNullable(A[&apos;a&apos;]) // number

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type NonNullable&amp;lt;T&amp;gt; = T extends null | undefined ? never : T;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Event事件对象类型&lt;/h2&gt;
&lt;p&gt;常用的Event事件对象类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ClipboardEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 剪贴板事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DragEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 拖拽事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ChangeEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt;  Change 事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;KeyboardEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 键盘事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MouseEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 鼠标事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TouchEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt;  触摸事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;WheelEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 滚轮事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AnimationEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 动画事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TransitionEvent&amp;lt;T =Element&amp;gt;&lt;/code&gt; 过渡事件对象&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import { MouseEvent } from &apos;react&apos;;

interface IProps {
  onClick (event: MouseEvent&amp;lt;HTMLDivElement&amp;gt;): void,
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组件开发&lt;/h2&gt;
&lt;h3&gt;有状态组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export class MyForm extends React.Component&amp;lt;FormProps, FormState&amp;gt; {
	...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的FormProps和FormState分别代表这状态组件的props和state的interface&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**注意：**在只有state而没有props的情况下，props的位置可以用{}或者object占位，这两个值都表示有效的空对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;无状态组件&lt;/h3&gt;
&lt;p&gt;无状态组件也被称为展示组件，如果一个展示组件没有内部的state可以被写为纯函数组件。 如果写的是函数组件，在&lt;code&gt;@types/react&lt;/code&gt;中定义了一个类型&lt;code&gt;type SFC = StatelessComponent;&lt;/code&gt;。我们写函数组件的时候，能指定我们的组件为&lt;code&gt;SFC&lt;/code&gt;或者&lt;code&gt;StatelessComponent&lt;/code&gt;。这个里面已经预定义了&lt;code&gt;children&lt;/code&gt;等，所以我们每次就不用指定类型children的类型了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { ReactNode, SFC } from &apos;react&apos;;
import style from &apos;./step-complete.less&apos;;

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC&amp;lt;IProps&amp;gt; = ({ title, description, children }) =&amp;gt; {
  return (
    &amp;lt;div className={style.complete}&amp;gt;
      &amp;lt;div className={style.completeTitle}&amp;gt;
        {title}
      &amp;lt;/div&amp;gt;
      &amp;lt;div className={style.completeSubTitle}&amp;gt;
        {description}
      &amp;lt;/div&amp;gt;
      &amp;lt;div&amp;gt;
        {children}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
export default StepComplete;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>30-seconds-of-js</title><link>https://nollieleo.github.io/posts/30-seconds-of-js/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/30-seconds-of-js/</guid><description>方法  - 对象转queryStrings  通过Object.entries以及reduce进行queryString的累加，只能将对象中值为string的转进去，可以改进  javascript const objectToQuerystring = (queryParams) = ...</description><pubDate>Fri, 24 Jul 2020 14:07:43 GMT</pubDate><content:encoded>&lt;h1&gt;方法&lt;/h1&gt;
&lt;h2&gt;- 对象转queryStrings&lt;/h2&gt;
&lt;p&gt;通过Object.entries以及reduce进行queryString的累加，只能将对象中值为string的转进去，可以改进&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const objectToQuerystring = (queryParams) =&amp;gt; {
    return Object.entries(queryParams).reduce((queryString, [key, value], index) =&amp;gt; {
        const symbol = queryString.length === 0 ? &apos;?&apos; : &apos;&amp;amp;&apos;;
        queryString += typeof value === &apos;string&apos; ? `${symbol}${key}=${value}` : &apos;&apos;;
        return queryString;
    }, &apos;&apos;);
}

// ?name=weng&amp;amp;address=11111
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;- 深度冻结一个对象&lt;/h2&gt;
&lt;p&gt;使用Object.keys（）获取所传递对象的所有属性，使用Array.prototype.forEach（）遍历它们。 在所有属性上递归调用Object.freeze（obj），检查是否使用Object.isFrozen（）冻结了每个属性，并根据需要应用deepFreeze（）。 最后，使用Object.freeze（）冻结给定的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const deepFreeze = obj =&amp;gt; {
  Object.keys(obj).forEach(prop =&amp;gt; {
    if (typeof(obj[prop]) === &apos;object&apos; &amp;amp;&amp;amp; !Object.isFrozen(obj[prop]))   deepFreeze(obj[prop]);
  });
  return Object.freeze(obj);
};


EXAMPLES
&apos;use strict&apos;;

const o = deepFreeze([1, [2, 3]]);

o[0] = 3; // not allowed
o[1][0] = 4; // not allowed as well
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;- 获取当前url（不带任何参数）&lt;/h2&gt;
&lt;p&gt;返回没有任何参数的当前URL。&lt;/p&gt;
&lt;p&gt;使用String.prototype.indexOf（）检查给定的url是否具有参数，使用String.prototype.slice（）删除它们（如有必要）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const getBaseURL = url =&amp;gt;
  url.indexOf(&apos;?&apos;) &amp;gt; 0 ? url.slice(0, url.indexOf(&apos;?&apos;)) : url;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;- 获取当前URL下的值&lt;/h2&gt;
&lt;p&gt;返回一个包含当前URL参数的对象。&lt;/p&gt;
&lt;p&gt;使用带有适当正则表达式的String.prototype.match（）来获取所有键值对，使用Array.prototype.reduce（）来映射它们并将它们组合成一个对象。 传递location.search作为参数以应用于当前网址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const getURLParameters = url =&amp;gt;
    (url.match(/([^?=&amp;amp;]+)(=([^&amp;amp;]*))/g) || []).reduce(
    (a, v) =&amp;gt; ((a[v.slice(0, v.indexOf(&apos;=&apos;))] = v.slice(v.indexOf(&apos;=&apos;) + 1)), a),
    {}
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;JavaScript api&lt;/h1&gt;
&lt;h2&gt;-for in , for of 和 forEach&lt;/h2&gt;
&lt;h3&gt;for in&lt;/h3&gt;
&lt;p&gt;for ... in用于迭代对象的所有可枚举属性，包括继承的可枚举属性。 该迭代语句可用于数组字符串或普通对象，但不能用于Map或Set对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (let prop in [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]) 
  console.log(prop);            // 0, 1, 2 (array indexes)

for (let prop in &apos;str&apos;) 
  console.log(prop);            // 0, 1, 2 (string indexes)

for (let prop in {a: 1, b: 2, c: 3}) 
  console.log(prop);            // a, b, c (object property names)

for (let prop in new Set([&apos;a&apos;, &apos;b&apos;, &apos;a&apos;, &apos;d&apos;])) 
  console.log(prop);            // undefined (no enumerable properties)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;for of&lt;/h3&gt;
&lt;p&gt;for ... of用于迭代可迭代对象，迭代其值而不是其属性。 该迭代语句可用于数组，字符串，Map或Set对象，但不能用于普通对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (let val of [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]) 
  console.log(val);            // a, b, c (array values)

for (let val of &apos;str&apos;) 
  console.log(val);            // s, t, r (string characters)

for (let val of {a: 1, b: 2, c: 3}) 
  console.log(prop);           // TypeError (not iterable)

for (let val of new Set([&apos;a&apos;, &apos;b&apos;, &apos;a&apos;, &apos;d&apos;])) 
  console.log(val);            // a, b, d (Set values)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;forEach&lt;/h3&gt;
&lt;p&gt;forEach（）是Array原型的一种方法，它允许您遍历数组的元素。 尽管forEach（）仅迭代数组，但它可以在迭代时访问每个元素的值和索引。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;].forEach(
  val =&amp;gt; console.log(val)     // a, b, c (array values)
);

[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;].forEach(
  (val, i) =&amp;gt; console.log(i)  // 0, 1, 2 (array indexes)
);	
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>react常用库</title><link>https://nollieleo.github.io/posts/react%E5%B8%B8%E7%94%A8%E5%BA%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E5%B8%B8%E7%94%A8%E5%BA%93/</guid><description>1.  2.  3. react-cropper 4. core-js 5. Material UI 6. superagent 7. restful-error-es6 8. browserify 9.  10.  11. semantic-ui 12. react-date-range日期选择 ...</description><pubDate>Sun, 19 Jul 2020 19:05:06 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GriddleGriddle/Griddle&quot;&gt;griddle-react&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/react-bootstrap/react-bootstrap&quot;&gt;react-bootstrap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;react-cropper&lt;/li&gt;
&lt;li&gt;core-js&lt;/li&gt;
&lt;li&gt;Material UI&lt;/li&gt;
&lt;li&gt;superagent&lt;/li&gt;
&lt;li&gt;restful-error-es6&lt;/li&gt;
&lt;li&gt;browserify&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-select-popover&quot;&gt;react-select-popover 标签选择&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CassetteRocks/react-infinite-scroller&quot;&gt;react-infinite-scroll 无限滚动&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;semantic-ui&lt;/li&gt;
&lt;li&gt;react-date-range日期选择&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fisshy/react-scroll&quot;&gt;react-scroll 快速定位滚动&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;react-timer-mixin suer timer&lt;/li&gt;
&lt;li&gt;react-autosuggest auto input&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/leecade/react-native-swiper&quot;&gt;react-native-swiper轮播&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;react-split-container分割线拖拽 ,源码已下架?&lt;/li&gt;
&lt;li&gt;reactjs-iscroll下拉上拉刷新&lt;/li&gt;
&lt;li&gt;react-hammerjs触屏事件库&lt;/li&gt;
&lt;li&gt;react-emoji-react emoji表情库&lt;/li&gt;
&lt;li&gt;react-ace在线编辑器&lt;/li&gt;
&lt;li&gt;react-highcharts highchart&lt;/li&gt;
&lt;li&gt;react-dropzone 上传&lt;/li&gt;
&lt;li&gt;react-fileupload-progress文件上传带processor&lt;/li&gt;
&lt;li&gt;react-fontawesome 字体icon库&lt;/li&gt;
&lt;li&gt;react-pdf pdf文档操作&lt;/li&gt;
&lt;li&gt;react-desktop桌面UI&lt;/li&gt;
&lt;li&gt;react-intl Internationalize React apps&lt;/li&gt;
&lt;li&gt;react-image-gallery图片轮播&lt;/li&gt;
&lt;li&gt;react-s-alert alert&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-event-calendar&quot;&gt;react-event-calendar事件日历&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Lazyshot/react-color-picker&quot;&gt;react-color-picker 颜色选择器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jasonslyvia/react-lazyload&quot;&gt;react-lazyload 延迟加载&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;react-tag-input tag input&lt;/li&gt;
&lt;li&gt;revalidator格式验证&lt;/li&gt;
&lt;li&gt;react-bootstrap-daterangepicker时间范围&lt;/li&gt;
&lt;li&gt;react-transitive-number增减数&lt;/li&gt;
&lt;li&gt;react-css-transition-replace动画&lt;/li&gt;
&lt;li&gt;react-images image list&lt;/li&gt;
&lt;li&gt;react-clockwall 时间画布&lt;/li&gt;
&lt;li&gt;react-autobind fun auto bind&lt;/li&gt;
&lt;li&gt;react-simple-markdown-editor markdown编辑&lt;/li&gt;
&lt;li&gt;react-remarkable markdown 显示&lt;/li&gt;
&lt;li&gt;random-gem 随机数&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/eiriklv/react-masonry-component&quot;&gt;react-masonry-component 瀑布流&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;react-alap 高德地图&lt;/li&gt;
&lt;li&gt;react-baidu-map 百度地图&lt;/li&gt;
&lt;li&gt;react-swipeable-views views滑动&lt;/li&gt;
&lt;li&gt;react-swipnable-tabs 可横向滚动的tab&lt;/li&gt;
&lt;li&gt;react-motion 动画&lt;/li&gt;
&lt;li&gt;react-image-fallback 图片lazy加载&lt;/li&gt;
&lt;li&gt;react-mobile-datepicker 滚动选择时间&lt;/li&gt;
&lt;li&gt;react-images 幻灯片灯箱&lt;/li&gt;
&lt;li&gt;react-image-magnify 图片细节放大&lt;/li&gt;
&lt;li&gt;urlencode node encode编码&lt;/li&gt;
&lt;li&gt;react-mobile-datepicker 滚动选择时间 年月日&lt;/li&gt;
&lt;li&gt;react-mobile-datetimepicker滚动选择时间 年月日时分&lt;/li&gt;
&lt;li&gt;react-fastclick消除touch click 300ms延迟&lt;/li&gt;
&lt;li&gt;react-sortable react-anything-sortable 拖动排序&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/moroshko/react-autosuggest&quot;&gt;react-autosuggest&lt;/a&gt; 自动提示&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/DominicTobias/react-image-crop&quot;&gt;react-image-crop&lt;/a&gt; 图片裁剪&lt;/li&gt;
&lt;li&gt;react-device-detect 检测浏览器版本的&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>react路由拦截自定义弹框</title><link>https://nollieleo.github.io/posts/react%E8%B7%AF%E7%94%B1%E6%8B%A6%E6%88%AA%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%B9%E6%A1%86/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/react%E8%B7%AF%E7%94%B1%E6%8B%A6%E6%88%AA%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%B9%E6%A1%86/</guid><description>https://medium.com/@michaelchan_13570/using-react-router-v4-prompt-with-custom-modal-component-ca839f5faf39...</description><pubDate>Sat, 18 Jul 2020 15:26:14 GMT</pubDate><content:encoded>&lt;p&gt;https://medium.com/@michaelchan_13570/using-react-router-v4-prompt-with-custom-modal-component-ca839f5faf39&lt;/p&gt;
</content:encoded></item><item><title>一些整合的配置库</title><link>https://nollieleo.github.io/posts/%E4%B8%80%E4%BA%9B%E6%95%B4%E5%90%88%E7%9A%84%E9%85%8D%E7%BD%AE%E5%BA%93/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E4%B8%80%E4%BA%9B%E6%95%B4%E5%90%88%E7%9A%84%E9%85%8D%E7%BD%AE%E5%BA%93/</guid><description>代码规范库   umi-fabric   一个包含 prettier，eslint，stylelint 的配置文件合集  github : https://github.com/umijs/fabric      lint-staged   Lint 就是对代码做静态分析，并试图找出潜在问题的工具，...</description><pubDate>Sat, 11 Jul 2020 11:05:01 GMT</pubDate><content:encoded>&lt;h1&gt;代码规范库&lt;/h1&gt;
&lt;h2&gt;umi-fabric&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一个包含 prettier，eslint，stylelint 的配置文件合集&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;github : https://github.com/umijs/fabric&lt;/p&gt;
&lt;h2&gt;lint-staged&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Lint 就是对代码做静态分析，并试图找出潜在问题的工具，实战中我们也用 Lint 来指使用工具的过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;npm:  https://www.npmjs.com/package/lint-staged&lt;/p&gt;
&lt;h2&gt;husky&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Husky can prevent bad &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt; and more 🐶 &lt;em&gt;woof!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;npm:  https://www.npmjs.com/package/husky&lt;/p&gt;
</content:encoded></item><item><title>写JS的时候一些小技巧或者常用操作</title><link>https://nollieleo.github.io/posts/%E5%86%99js%E7%9A%84%E6%97%B6%E5%80%99%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%8A%80%E5%B7%A7/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%86%99js%E7%9A%84%E6%97%B6%E5%80%99%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%8A%80%E5%B7%A7/</guid><description>记录一些平时开发过程或者编写JS过程小技巧或者注意事项   关于数组   - 变量赋值  javascript const array = new Array();  // 一般不这样搞一个数组  const array = [];  // 这样搞才棒      - 数组...</description><pubDate>Sat, 11 Jul 2020 00:27:18 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;记录一些平时开发过程或者编写JS过程小技巧或者注意事项&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;关于数组&lt;/h2&gt;
&lt;h3&gt;- 变量赋值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const array = new Array();  // 一般不这样搞一个数组

const array = [];  // 这样搞才棒
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组排序&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;sort是浏览器内置方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,4,5];
arr.sort((a,b)=&amp;gt;a-b);
arr.sort((a,b)=&amp;gt;b-a);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组浅拷贝&lt;/h3&gt;
&lt;p&gt;一般不用一个循环将数组复制一个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3];

//复制
const arrCopy = [ ...arr ] // 直接用拓展运算符
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 多个数组合并&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr1 = [1,2,3,4];
const arr2 = [5,6,7,8];
arr3 = [...arr1,...arr2];
// [1,2,3,4,5,6,7,8];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 类数组转成数组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const fakeArr = [0:&apos;hello&apos;,1:&apos;world&apos;,2:&apos;shit&apos;,length:3];

// bad
const arr = Array.prototype.slice.call(arrLike)

// good
const arr = Array.from(arrLike);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组解构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4]

// bad
const first = arr[0]
const second = arr[1]

// good
const [first, second] = arr; // 注意不像对象那样是{}符号，而是[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 替换数组中的特定值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,4,5];
arr.splice(0,2,&quot;hello&quot;,&quot;world&quot;); // 0~2 开始 2除外开始按顺序替换值
// [&apos;hello&apos;,&apos;world&apos;,3,4,5]

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- Array.from 替换map效果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [
	{
		name:&apos;weng&apos;,
		age:21,
	},
	{
		name:&apos;wang&apos;,
		age:11,
	},
	{
		name:&apos;sange&apos;,
		age:41
	},
];
Array.from(arr,({name})=&amp;gt;name); // [&apos;weng&apos;,&apos;wang&apos;,&apos;sange&apos;];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组去重&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,4,5,5,6,6,7,7,10,10];

const uniqueArr = Array.from(new Set(arr));
const uniqueArr = [ ...new Set(arr) ]; // good
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 二维数组转一维数组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [[1,2,3,4],[5,6,7]];
[].concat.apply(...arr); // [1,2,3,4,5,6,7] 确保在此数组是纯二维数组的情况下

const arr = [[1,2,3,4],[5,6],7]; // 类似这样的不能用以上的方法转一维数组，可以用es6的flat
arr.flat(); // [1,2,3,4,5,6,7]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 多维数组转一维数组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,[[4,5],7,6],[0,10,8]];
arr.flat(Infinity); // 这个玩意牛逼

// 当兼容性不好的时候
var arr = [1,[2,3],[4,[5,6,[7]]]]
while(arr.some(Array.isArray)){
 arr = [].concat(...arr)
} // 一招搞定
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 去除数组中的空对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [{name:1,age:2},{},{}];
const deleteObj=(arr)=&amp;gt;{
	if(Array.isArray(arr) &amp;amp;&amp;amp; arr.length&amp;gt;1){
		arr.filter(item =&amp;gt; {
			return Object.keys(item).length&amp;gt;0
		})
	}
	return [];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组中的所有值是否都满足条件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;如果提供的谓词函数对集合中的所有元素返回true，则返回true，否则返回false。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const all = (arr, fn = Boolean) =&amp;gt; arr.every(fn);
all([4,2,3],x=&amp;gt; x&amp;gt;1) // true
all([1,2,3],x=&amp;gt;x&amp;gt;1) // false;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组中是否有一项满足&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[1,2,3].some(item =&amp;gt; item &amp;gt;2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 判断数组中是否存在某个值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,4,5,6];
arr.indexOf(1)!==-1 // true  方法1

arr.includes(1) // true  方法2

arr.find(item =&amp;gt; item === 1); // 返回数组中满足条件的第一个元素的值，如果没有，返回undefined

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 返回两个数组不一样的值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const difference = (a,b)=&amp;gt; {
	const s = new Set(b);
	return a.filter(x=&amp;gt;!s.has(x));
}

difference([1,2,3],[3]) // [1,2];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数组累加累乘&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1,2,3,4,5,6];
// 累乘
arr.reduce((t,v)=&amp;gt;t*v,1); // t就是总乘积
eval(arr.join(&apos;*&apos;)); // eval会将传进去的string用作js代码执行

// 累加
arr.reduce((t,v)=&amp;gt;t+v,0);
eval(arr.join(&apos;+&apos;)); // eval会将传进去的string用作js代码执行

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 将数组转换为对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr = [&apos;weng&apos;,&apos;wang&apos;,&apos;zhang&apos;,&apos;li&apos;];
objArr = {...arr};
// {0:&apos;weng&apos;,1:&apos;wang&apos;,2:&apos;zhang&apos;,3:&apos;li&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;关于对象&lt;/h2&gt;
&lt;h3&gt;- 对象结构赋值&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;更推荐使用扩展运算符 ...，而不是 Object.assign。解构赋值获取对象指定的几个属性时，推荐用 rest 运算符，也是 ...。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
	name:&apos;hello&apos;,
	age:23,
};

// bad
const copy = Object.assign({}, obj, { c: 3 }) // copy =&amp;gt; { name: &apos;hello&apos;, age: 23, c: 3 }

// so good
const objCopy = { ...obj, address:&apos;beijing&apos; }; // objCopy =&amp;gt; {name: &apos;hello&apos;, age: 23,address:&apos;beijing&apos;}

// 解构拆分对象得时候
const {address,...restObj} = objCopy; // address = &apos;beijing&apos; resObj ={name:&apos;hello&apos;,age:23,}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 对象属性值的缩写&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const age = 23;
const name = &apos;wengkaimin&apos;;

// bad 
const Obj = {
	name:name,
	age:age,
}

//good
const objG = {
	name,
	age,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;属性的缩写要放在对象的开头才舒服点&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// bad 
const Obj = {
	address:&apos;beijing&apos;
	name,
	age,
}

//good
const objG = {
	name,
	age,
	address:&apos;beijing&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 使用动态对象属性创建对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function getName({name}){
	return `VIP${name}`
}

const obj = {
	age:23,
	realName:&apos;wengkaimin&apos;,
	[getName(&apos;xiaohua&apos;)]: true,
} 
clg(obj)  // obj=&amp;gt; {age:23,realName:&apos;wengkaimin&apos;,VIPxiaohua:true}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 对象里存在方法时候&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// bad
const obj = {
	name:&apos;hello world&apos;,
	getName:function(){
		return this.name
	},
}

// goooooood 
const obj = {
	name:&apos;hello world&apos;,
	getName(){
		return this.name
	},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 不要直接调用Object原型中的方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Object.prototype 中的hasOwnProperty，isPrototypeOf等等，不能写出object.hasOwnProperty&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const objTest = {name:&apos;1&apos;,hasOwnProperty:true};

//bad 
console.log(objTest.hasOwnProperty(key)) // error 这玩意hasOwnProperty在这个对象中是属性，从原型链最顶层找的话第一层就被找到了，就不会再去找objTest的原型下的hasOwnProperty函数了

// goooood
console.log(Object.prototype.hasOwnProperty.call(objTest,name)); // &apos;1&apos;

// best 
const has = Object.prototype.hasOwnProperty // 存起来，这个模块内就可以多次查找,就不需要每次写那么长
console.log(has.call(objTest,name));
/* or */
import has from &apos;has&apos;; // https://www.npmjs.com/package/has
console.log(has(object, name));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 浅拷贝&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const shallowClone = obj =&amp;gt; Object.assign({},obj);// 上面说了不推荐这样写法

const shallowClone = obj =&amp;gt; {...obj};

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 深拷贝&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const deepMapKeys = (obj, fn) =&amp;gt;
  Array.isArray(obj)
    ? obj.map(val =&amp;gt; deepMapKeys(val, fn))
    : typeof obj === &apos;object&apos;
    ? Object.keys(obj).reduce((acc, current) =&amp;gt; {
        const key = fn(current);
        const val = obj[current];
        acc[key] =
          val !== null &amp;amp;&amp;amp; typeof val === &apos;object&apos; ? deepMapKeys(val, fn) : val;
        return acc;
      }, {})
    : obj;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于函数&lt;/h2&gt;
&lt;h3&gt;- 函数参数使用默认值替代使用条件语句进行赋值。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// good
function newFun(name = &apos;Jack&apos;) {
   ...
}

// bad
function newFun(name) {
  const userNameName = name || &apos;Jack&apos;
   ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 函数参数使用结构语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;函数参数越少越好，如果参数超过两个，要使用 ES6 的解构语法，不用考虑参数的顺序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// good
function createMenu({ title, body, buttonText, cancellable }) {
   ...
}

createMenu({
  title: &apos;Foo&apos;,
  body: &apos;Bar&apos;,
  cancellable: true,
  buttonText: &apos;Baz&apos;,
})

// bad
function createMenu(title, body, buttonText, cancellable) {
  // ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 优先使用 rest 语法...，而不是arguments&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments) // arguments是伪数组，处理成数组，这里用上面说的Array.from(arguments)才好
  return args.join(&apos;&apos;)
}

// good
function concatenateAll(...args) {
  return args.join(&apos;&apos;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 函数返回值是多个的情况下&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 当我们调用函数并将值分配给 a,b,c,d 时，我们需要注意返回数据的顺序。这里的一个小错误可能会成为调试的噩梦,而且倘若只需要c,d值，那么就无法确切获取
const func =()=&amp;gt;{
 	const a = 1;
 	const b = 2;
	const c = 3;
    const d = 4;
 	return [a,b,c,d]; // very bad
}
const [a,b,c,d] = func();

// 使用对象结构
const func =()=&amp;gt;{
 	const a = 1;
 	const b = 2;
	const c = 3;
    const d = 4;
 	return {a,b,c,d}; // good
}
const {c,d} = func();

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于字符串&lt;/h2&gt;
&lt;h3&gt;- 字符串翻转&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function reverseStr(str = &quot;&quot;) {
  return str.split(&quot;&quot;).reduceRight((t, v) =&amp;gt; t + v);
}

const str = &quot;reduce123&quot;;
console.log(reverseStr(str)); // &quot;321recuder&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于数字&lt;/h2&gt;
&lt;h3&gt;- 判断奇偶数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const num=5;
!!(num &amp;amp; 1) // true
!!(num % 2) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 数字千分位&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 方法一
function thousandNum(num = 0) {
  const str = (+num).toString().split(&quot;.&quot;);
  const int = nums =&amp;gt; nums.split(&quot;&quot;).reverse().reduceRight((t, v, i) =&amp;gt; t + (i % 3 ? v : `${v},`), &quot;&quot;).replace(/^,|,$/g, &quot;&quot;);
  const dec = nums =&amp;gt; nums.split(&quot;&quot;).reduce((t, v, i) =&amp;gt; t + ((i + 1) % 3 ? v : `${v},`), &quot;&quot;).replace(/^,|,$/g, &quot;&quot;);
  return str.length &amp;gt; 1 ? `${int(str[0])}.${dec(str[1])}` : int(str[0]);
}

thousandNum(1234); // &quot;1,234&quot;
thousandNum(1234.00); // &quot;1,234&quot;
thousandNum(0.1234); // &quot;0.123,4&quot;
console.log(thousandNum(1234.5678)); // &quot;1,234.567,8&quot;

// 方法二
(121314).toLocaleString();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 字符串转数字&lt;/h3&gt;
&lt;h4&gt;方法一&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;实际就是用 *1来转化为数字，实际上是调用了&lt;code&gt;.valueOf&lt;/code&gt;的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&apos;32&apos; * 1 // 32
&apos;ds&apos; * 1 // NaN
null * 1 // 0
undefine * 1 // NaN
1 * { valueOf:()=&amp;gt;&apos;3&apos;};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法二&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+ &apos;123&apos; // 123
+ &apos;ds&apos; // NaN
+ &apos;&apos; // 0
+ null // 0
+ undefine // NaN
+ {valueOf: ()=&amp;gt;&apos;3&apos;} // 3 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 判断小数是否相等&lt;/h3&gt;
&lt;h4&gt;方法1：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Number.EPSILON=(function(){   //解决兼容性问题
    return Number.EPSILON?Number.EPSILON:Math.pow(2,-52);
})();
//上面是一个自调用函数，当JS文件刚加载到内存中，就会去判断并返回一个结果
function numbersequal(a,b){ 
    return Math.abs(a-b)&amp;lt;Number.EPSILON;
  }
//接下来再判断   
const a=0.1+0.2, b=0.3;
console.log(numbersequal(a,b)); //这里就为true了

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法2：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;(0.1*100+0.2*100)/100===0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;- 双位运算符&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;双位运算符比&lt;code&gt;Math.floor()&lt;/code&gt;和&lt;code&gt;Math.ceil()&lt;/code&gt;速度快&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;~~7.5                // 7
Math.ceil(7.5)       // 8
Math.floor(7.5)      // 7


~~-7.5        		// -7
Math.floor(-7.5)     // -8
Math.ceil(-7.5)      // -7

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以负数时，双位运算符和Math.ceil结果一致，正数时和Math.floor结果一致&lt;/p&gt;
&lt;h3&gt;- 取整和奇偶性判断&lt;/h3&gt;
&lt;p&gt;取整&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3.3 | 0         // 3
-3.9 | 0        // -3

parseInt(3.3)  // 3
parseInt(-3.3) // -3

// 四舍五入取整
Math.round(3.3) // 3
Math.round(-3.3) // -3

// 向上取整
Math.ceil(3.3) // 4
Math.ceil(-3.3) // -3

// 向下取整
Math.floor(3.3) // 3
Math.floor(-3.3) // -4

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断奇偶&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const num=5;
!!(num &amp;amp; 1) // true
!!(num % 2) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;布尔型&lt;/h2&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;h3&gt;非空判断&lt;/h3&gt;
&lt;p&gt;之前写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(value !== null &amp;amp;&amp;amp; value !== undefined &amp;amp;&amp;amp; value !== &apos;&apos;){
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(value??&apos;&apos; !== &apos;&apos;){
  //...
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>防抖动和节流</title><link>https://nollieleo.github.io/posts/%E9%98%B2%E6%8A%96%E5%8A%A8%E5%92%8C%E8%8A%82%E6%B5%81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%98%B2%E6%8A%96%E5%8A%A8%E5%92%8C%E8%8A%82%E6%B5%81/</guid><description>场景：  在页面中很多的事件是会频繁执行的、如：  1. window的resize，scroll事件 2. 拖拽过程中的 mousemove事件 3. 文字输入过程中的keyup等事件  这些事件一旦触发会频繁执行、但是实际上我们可能只需要在特定的时候去执行绑定了这些事件的函数  例如：我需要检测...</description><pubDate>Sun, 03 May 2020 14:27:27 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;场景：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在页面中很多的事件是会频繁执行的、如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;window的resize，scroll事件&lt;/li&gt;
&lt;li&gt;拖拽过程中的 mousemove事件&lt;/li&gt;
&lt;li&gt;文字输入过程中的keyup等事件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些事件一旦触发会频繁执行、但是实际上我们可能只需要在特定的时候去执行绑定了这些事件的函数&lt;/p&gt;
&lt;p&gt;例如：我需要检测一次拖拉浏览器，移动过程中都算是一次，知道最后鼠标抬起来了，才算是完成了一次拉伸窗口；有比如，我们输入搜索框内的文字的时候，需要发ajax 到后台去请求数据，实际上我们并不需要每一次的输入都发送一个请求，而是在用户已经输完了一整段话或者是几个文字之后再去发送一个ajax，这样会节省很多的资源开销 。等等的这些问题都需要函数防抖动或者函数节流来解决&lt;/p&gt;
&lt;h2&gt;防抖（debounce）&lt;/h2&gt;
&lt;p&gt;原理： 当我们调用一个动作的时候，会设置在n毫秒后才执行，而在这n毫秒内，如果这个动作再次被调用的话则将重新在计算n毫秒，采取执行这个东西。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一次事件发生后，事件处理器要等一定阈值的时间，如果这段时间过去后 再也没有 事件发生，就处理最后一次发生的事件。假设还差 &lt;code&gt;0.01&lt;/code&gt; 秒就到达指定时间，这时又来了一个事件，那么之前的等待作废，需要重新再等待指定时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    #container {
      width: 200px;
      height: 300px;
      background-color: aquamarine;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div id=&quot;container&quot;&amp;gt;

  &amp;lt;/div&amp;gt;
  &amp;lt;script&amp;gt;
    var count = 1;
    var containerDom = document.getElementById(&apos;container&apos;);
    function addNumber() {
      containerDom.innerHTML = count++;
    }
    containerDom.onmousemove = addNumber;

  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;鼠标只要移动那么就会频繁的去触发addNumber的函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./debounce.gif&quot; alt=&quot;debounce&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候我们对他进行防抖动&lt;/p&gt;
&lt;p&gt;先写我们的第一版函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function debounce(fun, wait) {
  var time;
  return function () {
    clearTimeout(time);
    time = setTimeout(fun, wait);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后包裹住这个addnumber的函数&lt;/p&gt;
&lt;p&gt;现在随你怎么移动，反正你移动完 1000ms 内不再触发，我才执行事件。看看使用效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./debounce-1.gif&quot; alt=&quot;debounce 第一版&quot; /&gt;&lt;/p&gt;
&lt;p&gt;顿时就从 165 次降低成了 1 次!&lt;/p&gt;
&lt;p&gt;之后我们对他优化&lt;/p&gt;
&lt;h3&gt;this指向优化&lt;/h3&gt;
&lt;p&gt;如果我们再addNumber函数中加入&lt;code&gt;console.log(this)&lt;/code&gt;，没有再addNumber外层包裹debounce的时候this指向是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;container&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是包裹了debounce之后this就会指向windows对象&lt;/p&gt;
&lt;p&gt;所以需要对它进行修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var count = 0;
var containerDom = document.getElementById(&apos;container&apos;);
function addNumber() {
  containerDom.innerHTML = `&amp;lt;div&amp;gt;
  值为${++count}
  &amp;lt;/div&amp;gt;`;
  console.log(this);
}
function debounce(fun, wait) {
  var time;
  return function () {
    var that = this;
    clearTimeout(time);
    time = setTimeout(fun.bind(that), wait);
  }
}
containerDom.onmousemove = debounce(addNumber, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这里又有一个问题，就是这样包裹debounce的时候，onmousemove自带的事件对象event就会丢失，没有将参数传进去，故我们需要修改一哈，这样再我们需要用到event对象的时候就能拿得到&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200503153245577.png&quot; alt=&quot;image-20200503153245577&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var count = 0;
var containerDom = document.getElementById(&apos;container&apos;);
function addNumber(e) {
  containerDom.innerHTML = `&amp;lt;div&amp;gt;
  值为${++count}
  &amp;lt;/div&amp;gt;`;
  console.log(this);
  console.log(e);
}
function debounce(fun, wait) {
  var time;
  return function () {
    var that = this, args = arguments;
    clearTimeout(time);
    time = setTimeout(fn.bind(that, ...args), wait);
  }
}
containerDom.onmousemove = debounce(addNumber, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候我们函数就非常的完善了。&lt;/p&gt;
&lt;h3&gt;第一次立刻执行&lt;/h3&gt;
&lt;p&gt;假设我需要立即触发这个函数，但是又想防抖，就是相当于先执行函数再N秒内如果再此触发这个函数，就给他重新计算等待值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function debounce(fun, wait, immediate) {
  var time;
  return function () {
    var that = this, args = arguments;
    if (time) clearTimeout(time);
    if (immediate) {
      // 如果已经执行过，不再执行
      var callNow = !time;
      time = setTimeout(function () {
        time = null
      }, wait);
      if (callNow) fun.apply(that, args);
    } else {
      time = setTimeout(function () {
        fun.apply(that, args);
      }, wait);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;[Image missing: debounce 第四版]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;完~&lt;/p&gt;
&lt;h2&gt;节流&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;可以理解为事件在一个管道中传输，加上这个节流阀以后，事件的流速就会减慢。实际上这个函数的作用就是如此，它可以将一个函数的调用频率限制在一定阈值内，例如 1s，那么 1s 内这个函数一定不会被调用两次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以下是一个简版的节流,这里的this和argument处理方式和上面的防抖是一样的处理方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function throttle(fn, wait) {
	let prev = new Date();
	return function() { 
	    const args = arguments;
		const now = new Date();
		if (now - prev &amp;gt; wait) {
			fn.apply(this, args);
			prev = new Date();
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照以上的实现，第一次进入的时候就立马执行事件，但是如果最后一次的时间和pre的时间插值小于我们的wait时间，这时候的函数是不执行的，但是我们想要让它执行，这时候加入定时器，让他在最后一次调用执行函数的时候，算出它距离wait的时间还剩多少，然后让他在剩余时间内再执行；&lt;/p&gt;
&lt;p&gt;最终的代码是这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function throttle(fn, wait) {
    let prev = new Date();
    let time;
    return function () {
      const args = arguments;
      const now = new Date();
      clearTimeout(time);
      if (now - prev &amp;gt;= wait) {
        fn.apply(this, args);
        prev = new Date();
      } else {
        time = setTimeout(fn.bind(this, args), wait - (prev - now));
      }
    };
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>动态创建文本域如何判断文本是否超出所在容器</title><link>https://nollieleo.github.io/posts/%E5%8A%A8%E6%80%81%E5%88%9B%E5%BB%BA%E6%96%87%E6%9C%AC%E5%9F%9F%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%96%87%E6%9C%AC%E6%98%AF%E5%90%A6%E8%B6%85%E5%87%BA%E6%89%80%E5%9C%A8%E5%AE%B9%E5%99%A8/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%8A%A8%E6%80%81%E5%88%9B%E5%BB%BA%E6%96%87%E6%9C%AC%E5%9F%9F%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%96%87%E6%9C%AC%E6%98%AF%E5%90%A6%E8%B6%85%E5%87%BA%E6%89%80%E5%9C%A8%E5%AE%B9%E5%99%A8/</guid><description>在项目上开发的时候碰到了一个这样的需求    每一个操作记录都是动态创建出来的，包括文本域，后端返回一个content表示文本的内容；每个文本都是定宽的（当然必须在这个容器内部），一旦content数据超出了文本域的定宽就会以省略号的形式表示还有更多文本，并且重点来了：超出的每个模块必须在头部展现出...</description><pubDate>Tue, 07 Apr 2020 21:27:19 GMT</pubDate><content:encoded>&lt;p&gt;在项目上开发的时候碰到了一个这样的需求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200407213338480.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每一个操作记录都是动态创建出来的，包括文本域，后端返回一个content表示文本的内容；每个文本都是定宽的（当然必须在这个容器内部），一旦content数据超出了文本域的定宽就会以省略号的形式表示还有更多文本，并且&lt;strong&gt;重点&lt;/strong&gt;来了：&lt;strong&gt;超出的每个模块必须在头部展现出一个button按钮，点击button按钮能够展示更多的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面说一下我刚开始的思路：通过判断content字符的长度去动态渲染这个button（不行）。&lt;/p&gt;
&lt;p&gt;因为不仅仅content当中包括了中文字符，英文字符，还包括标点符号数字等等，不能单纯的通过此方法来去判断。&lt;/p&gt;
&lt;p&gt;因此想了一下，可以设置一个监听器，通过监听对应的文本，判断它宽度的变化满足什么条件，从而进行页面的二次渲染，再第一次渲染的基础上加上button&lt;/p&gt;
&lt;p&gt;经过一个大佬的指导，发现可以用此方法实现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200407214857295.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过new ResizeObserver()的api去监听一个dom元素的文本域变化，详细用法可以看下面这个文章，&lt;a href=&quot;https://segmentfault.com/a/1190000020771182?utm_source=tag-newest&quot;&gt;ResizeObserver是什么？&lt;/a&gt; 还有这  &lt;a href=&quot;https://zhuanlan.zhihu.com/p/41418813&quot;&gt;ResizeObserver API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;至于为什么要用 scrollWidth 和width来判断，懂得都懂，不懂得去查概念去，&lt;a href=&quot;https://www.cnblogs.com/pengshengguang/p/8021743.html&quot;&gt;scrollWidth, clientWidth, offsetWidth的区别&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我用的是&lt;strong&gt;react hooks&lt;/strong&gt;，所以要实现监听，必须再useEffect里面模拟出window.onload&lt;/p&gt;
&lt;h3&gt;下面说一下我详细得解法：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;要控制button得显隐，既然用了react肯定是要面向数据层来改变页面，因此在刚开始获取到这个操作记录的数据的时候，给他每一项加上一个参数叫做display，之后再在所需要渲染的button上手动加上style（这里用的是猪齿鱼pro组件的DataSet）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200407220056426.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200407220213779.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;之后在useEffect当中去监听&lt;strong&gt;这些&lt;/strong&gt;动态渲染的文本域&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    const flow = document.getElementsByClassName(&apos;c7n-pOverflow&apos;);
    if (flow &amp;amp;&amp;amp; flow.length &amp;gt; 0) {
      for (let i = 0; i &amp;lt; flow.length; i += 1) {
        new ResizeObserver((entries) =&amp;gt; {
          entries.forEach((entry) =&amp;gt; {
            const pDom = entry.target;
            const scrollW = Math.ceil(pDom.scrollWidth);
            const width = Math.ceil(pDom.clientWidth);
            if (scrollW &amp;gt; width) {
              optsDs.records[i].set(&apos;display&apos;, &apos;block&apos;);
            }
          });
        }).observe(flow[i]);
      }
    }
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，useEffect的最后一个参数不能写。&lt;/p&gt;
&lt;p&gt;通过循环这个请求到list数据的文本数组，去监听每一个文本域的变化，&lt;/p&gt;
&lt;p&gt;通过判定这个文本域的scrollWidth和width去动态地再次改变对应数据中的display属性，就能实现页面的二次渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (scrollW &amp;gt; width) {
  optsDs.records[i].set(&apos;display&apos;, &apos;block&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是文本域的样式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
    font-size: .12rem;
    font-weight: 400;
    margin: .08rem 0 0 0;
    color: rgba(58, 52, 95, 0.65);
    line-height: .2rem;
    max-width: 4.09rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里一定是要设置宽度的，无论是width还是max-width只要能实现它超出溢出就行。&lt;/p&gt;
</content:encoded></item><item><title>JS中typeof和instanceof的区别</title><link>https://nollieleo.github.io/posts/js%E4%B8%ADtypeof%E5%92%8Cinstanceof%E7%9A%84%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/js%E4%B8%ADtypeof%E5%92%8Cinstanceof%E7%9A%84%E5%8C%BA%E5%88%AB/</guid><description>undefined, number, string, boolean属于简单的值类型，不是对象。剩下的几种情况——函数、数组、对象、null、new Number(10)都是对象。他们都是引用类型。      typeof  javascript console.log(typeof (x)); /...</description><pubDate>Tue, 31 Mar 2020 09:29:49 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;属于简单的&lt;strong&gt;值类型&lt;/strong&gt;，不是对象。剩下的几种情况——函数、数组、对象、&lt;code&gt;null&lt;/code&gt;、&lt;code&gt;new Number(10)&lt;/code&gt;都是对象。他们都是引用类型。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://pxc3lp.coding-pages.com/2020/03/16/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%BC%95%E7%94%A8%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/&quot;&gt;基本数据类型和引用类型&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;typeof&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;console.log(typeof (x)); // &apos;undefined&apos;
console.log(typeof (10)); // &apos;number&apos;
console.log(typeof (&apos;abc&apos;)); // &apos;string&apos;
console.log(typeof (true)); // &apos;boolean&apos;
console.log(typeof (function () { })); // &apos;function&apos;
console.log(typeof ([1, &apos;dsa&apos;, true])); // &apos;object&apos;
console.log(typeof ({ a: 123, b: true })); // &apos;object&apos;
console.log(typeof (null)); // &apos;object&apos;
console.log(typeof (new Number(0))); // &apos;object&apos;
console.log(typeof(new Date())); // &apos;object&apos;
console.log(typeof(/a/g)); // &apos;object&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们可以用typeof判断一个变量是否存在&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(typeof a !==&apos;undefined&apos;){
	alert(&apos;OK&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不是&lt;code&gt;if(a)&lt;/code&gt;这样的用法，因为如果这样a为定义就会报错。&lt;/p&gt;
&lt;p&gt;简单的&lt;strong&gt;值类型&lt;/strong&gt;直接用&lt;code&gt;typeof&lt;/code&gt;就能够判断出来&lt;/p&gt;
&lt;p&gt;但是引用类型使用 &lt;code&gt;typeof&lt;/code&gt; 判断就不太准确。如上代码所示，例如数组，正则表达式，日期，对象等&lt;code&gt;typeof&lt;/code&gt;返回值都是为 &lt;code&gt;object&lt;/code&gt; 函数的返回值则是&lt;code&gt;function&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 JavaScript 中，判断一个变量的类型尝尝会用 &lt;code&gt;typeof&lt;/code&gt; 运算符，在使用 &lt;code&gt;typeof&lt;/code&gt; 运算符时采用引用类型存储值会出现一个问题，无论引用的是什么类型的对象，它都返回 “object”。这就需要用到&lt;code&gt;instanceof&lt;/code&gt;来检测某个对象是不是另一个对象的实例。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;instanceof&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;语法：object instanceof constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;code&gt;object&lt;/code&gt;（要检测的对象.）&lt;code&gt;constructor&lt;/code&gt;（某个构造函数）&lt;/p&gt;
&lt;p&gt;描述：&lt;code&gt;instanceof&lt;/code&gt;运算符用来检测&lt;code&gt;constructor.prototype&lt;/code&gt;是否存在于参数object的原型链上&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对象与函数的关系&lt;/strong&gt;： 函数是一种对象，但函数不像数组正则日期这些对象。其他的对象（函数除外）都是对象的一个子集，但是函数却可以创造出对象来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function People(){
	this.name = &apos;hello world&apos;;
	this.birth - 1998;
}
var people = new People();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;people这个对象是由People的构造函数创建出来的，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;instanceof&lt;/code&gt;的使用规则：&lt;code&gt;A instanceof B&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A沿着&lt;strong&gt;proto&lt;/strong&gt;这条线来找，同时B沿着prototype这条线来找，如果两条线能找到同一个引用，即同一个对象，那么就返回true。如果找到终点还未重合，则返回false。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./20180203152226402.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;h3&gt;可以用于判断一个变量是否是某个对象的实例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(people instanceof People); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;console.log(people instanceof Object); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为people是由 People构造函数搞出来的，而People又是object的子类&lt;/p&gt;
&lt;h3&gt;可以在继承关系中用来判断一个实例是否属于它的父类型&lt;/h3&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function People() {
  this.name = &apos;hello world&apos;;
  this.birth = 1998;
}
function Male(){}
Male.prototype = new People();
const man = new Male();
console.log(man instanceof Male); // true 
console.log(man instanceof People); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义构造函数
function C(){} 
function D(){} 

var o = new C();

// true，因为 Object.getPrototypeOf(o) === C.prototype
o instanceof C; 

// false，因为 D.prototype不在o的原型链上
o instanceof D; 

o instanceof Object; // true,因为Object.prototype.isPrototypeOf(o)返回true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype指向了一个空对象,这个空对象不在o的原型链上.

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，如果表达式 &lt;code&gt;obj instanceof Foo&lt;/code&gt; 返回true，则并不意味着该表达式会永远返回true，因为&lt;code&gt;Foo.prototype&lt;/code&gt;属性的值有可能会改变，改变之后的值很有可能不存在于&lt;code&gt;obj&lt;/code&gt;的原型链上，这时原表达式的值就会成为&lt;code&gt;false&lt;/code&gt;。另外一种情况下，原表达式的值也会改变，就是改变对象&lt;code&gt;obj&lt;/code&gt;的原型链的情况，虽然在目前的ES规范中，我们只能读取对象的原型而不能改变它，但借助于非标准的&lt;code&gt;__proto__&lt;/code&gt;魔法属性，是可以实现的。比如执行&lt;code&gt;obj.__proto__ = {}&lt;/code&gt;之后，&lt;code&gt;obj instanceof Foo&lt;/code&gt;就会返回&lt;code&gt;false&lt;/code&gt;了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function People(){
  this.name = &apos;hello&apos;
}
const a = new People();
console.log(a instanceof People); // true
a.__proto__ = {};
console.log(a instanceof People); // false
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>实现边框loading动画效果</title><link>https://nollieleo.github.io/posts/%E5%AE%9E%E7%8E%B0%E8%BE%B9%E6%A1%86loading%E5%8A%A8%E7%94%BB%E6%95%88%E6%9E%9C/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%AE%9E%E7%8E%B0%E8%BE%B9%E6%A1%86loading%E5%8A%A8%E7%94%BB%E6%95%88%E6%9E%9C/</guid><description>效果图如下      代码如下：  html &lt;body   &lt;style     .box {       display: inline-block;       position: relative;       width: 220px;       height: 100px;      ...</description><pubDate>Mon, 30 Mar 2020 13:44:04 GMT</pubDate><content:encoded>&lt;p&gt;效果图如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200330134546508.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .box {
      display: inline-block;
      position: relative;
      width: 220px;
      height: 100px;
      box-sizing: border-box;
    }

    .box::before {
      content: &apos;&apos;;
      position: absolute;
      left: -4px;
      top: -4px;
      right: 0;
      width: 228px;
      height: 108px;
      bottom: 0;
      z-index: -1;
      box-sizing: border-box;
      border-radius: 5px;
      background: linear-gradient(to right, #0638a8, #0ab7f9, #06a892);
      box-sizing: border-box;
      animation: borderAround 1.5s infinite linear;
    }

    @keyframes borderAround {

      0%,
      100% {
        clip: rect(0 228px 4px 76px);
      }

      10% {
        clip: rect(0, 152px, 4px, 0);
      }

      20% {
        clip: rect(0, 76px, 54px, 0);
      }

      30% {
        clip: rect(0, 4px, 112px, 0);
      }

      40% {
        clip: rect(54px, 76px, 112px, 0);
      }

      50% {
        clip: rect(104px, 152px, 112px, 0);
      }

      60% {
        clip: rect(104px, 228px, 112px, 76px);
      }

      70% {
        clip: rect(54px, 228px, 112px, 152px);
      }

      80% {
        clip: rect(0, 228px, 112px, 224px);
      }

      90% {
        clip: rect(0, 228px, 54px, 152px);
      }
    }

    .child {
      width: 100%;
      height: 100%;
      background: #FFF;
      z-index: 999;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;box&quot;&amp;gt;
    &amp;lt;div class=&quot;child&quot;&amp;gt;
      dsadasd
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>垂直居中的几种方式</title><link>https://nollieleo.github.io/posts/%E5%9E%82%E7%9B%B4%E5%B1%85%E4%B8%AD%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9E%82%E7%9B%B4%E5%B1%85%E4%B8%AD%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F/</guid><description>在阅读此文章之前要搞明白行内元素和块级元素的区别，和其常用的标签     行内元素垂直居中   1. 设置外层块元素的height，line-height为相同的值  适用范围：作用于单行为文字，使文字垂直居中显示  原理：line-height与font-size的计算之差（在css中成为“行间距...</description><pubDate>Thu, 26 Mar 2020 10:09:04 GMT</pubDate><content:encoded>&lt;p&gt;在阅读此文章之前要搞明白行内元素和块级元素的区别，和其常用的标签&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://pxc3lp.coding-pages.com/2020/03/26/%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%EF%BC%8C%E8%A1%8C%E5%86%85%E5%85%83%E7%B4%A0%E5%92%8C%E8%A1%8C%E5%86%85%E5%9D%97%E5%85%83%E7%B4%A0/&quot;&gt;行内元素和块级元素&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;行内元素垂直居中&lt;/h2&gt;
&lt;h3&gt;1. 设置外层块元素的&lt;code&gt;height&lt;/code&gt;，&lt;code&gt;line-height&lt;/code&gt;为相同的值&lt;/h3&gt;
&lt;p&gt;适用范围：作用于单行为文字，使文字垂直居中显示&lt;/p&gt;
&lt;p&gt;原理：&lt;code&gt;line-height&lt;/code&gt;与&lt;code&gt;font-size&lt;/code&gt;的计算之差（在css中成为“行间距”）分为两半，分别加到一个文本行内容的顶部和底部。（可以包含这些内容的最小框就是行框）实现了单行文字居中，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      height: 50px;
      line-height: 50px;
      background:aqua;
    }

    .content {
      background-color: bisque;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;span class=&quot;content&quot;&amp;gt;垂直居中&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326102804196.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.  vertical-align&lt;/h3&gt;
&lt;p&gt;适用范围：外层块元素包含&lt;strong&gt;大于一个行内元素&lt;/strong&gt;需要垂直居中时 （如图片和文字需要垂直居中显示时），可以通过对图片的标签或文字的标签（行内元素）设置&lt;code&gt;vertical-align&lt;/code&gt;，可以达到效果；&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父级块元素不需要设置宽高&lt;/li&gt;
&lt;li&gt;只需将基线较高的行内元素设置&lt;code&gt;vertial-align&lt;/code&gt;就行&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;vertical-align属性的定义：该属性定义行内元素的基线相对于该元素所在行的基线的垂直对齐。允许指定负长度值和百分比值。这会使元素降低而不是升高。在表单元格中，这个属性会设置单元格框中的单元格内容的对齐方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      background: aqua;
    }

    .content {
      background-color: bisque;
    }

    img {
      vertical-align: middle;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;span class=&quot;content&quot;&amp;gt;垂直居中&amp;lt;/span&amp;gt;
    *[Image missing: bd_logo1.png]*
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326103805967.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.  外层父元素display: table-cell;vertical-align: middle&lt;/h3&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外层元素需要设置高度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用范围： 适用于在块元素内存在单个行内块元素，将它垂直居中；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      background: aqua;
      height: 300px;
      display: table-cell;
      vertical-align: middle;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    *[Image missing: bd_logo1.png]*
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20200326110227357.png]&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;4. &lt;code&gt;display:flex;&lt;/code&gt; &lt;code&gt;align-items:center&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flex布局&lt;/li&gt;
&lt;li&gt;父元素可以不设置高度，宽度由内部行内元素撑起（这里设置高度是为了看的更清楚）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      height: 300px;
      background: aqua;
      display: flex;
      align-items: center;
    }

    img {
      max-width: 300px;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;span&amp;gt;12111111111111111111111111111111111111111111111111eeeeeeeeeeee1111111dwaedwqqqqqqqqqqqqqqqqq&amp;lt;/span&amp;gt;
    *[Image missing: bd_logo1.png]*
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326113535458.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.  绝对定位+负外边距；&lt;/h3&gt;
&lt;p&gt;适用范围：包裹的行内元素必须是可以设置高度的，但是像文本元素是不建议使用这个的，因为文本的内容不确定，除非定高且设置超出溢出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      height: 300px;
      background: aqua;
      position: relative;
    }

    img {
      height: 200px;
      margin-top: -100px;
      top: 50%;
      position: absolute;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    *[Image missing: bd_logo1.png]*
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;块级元素垂直居中&lt;/h2&gt;
&lt;p&gt;类似，不解释&lt;/p&gt;
&lt;h2&gt;多行文本垂直居中&lt;/h2&gt;
&lt;p&gt;利用 &lt;code&gt;display：table；&lt;/code&gt;和&lt;code&gt;display:table-cell;&lt;/code&gt; 来实现多行文本的垂直居中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .container {
      height: 300px;
      background: aqua;
      display: table;
    }

    p {
      display: table-cell;
      vertical-align: middle;
      background-color: bisque;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;p&amp;gt;
      dsadwqdssacadseqdsfcdsfsasddsa
      dwqdssacadseqdsfcdsfsasddsadwqd
      ssacadseqdsfcdsfsasddsadwqdssaca
      dseqdsfcdsfsasddsadwqdssacadseqds
      fcdsfsasddsadwqdssacadseqdsfcdsfsasd
    &amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;未知元素的宽高情况&lt;/h2&gt;
&lt;p&gt;在前面的实现行内元素的垂直居中中有提到几种方法&lt;/p&gt;
&lt;h3&gt;1. 组合使用&lt;code&gt;display：table-cell&lt;/code&gt;和&lt;code&gt;vertical-align&lt;/code&gt;、&lt;code&gt;text-align&lt;/code&gt;，&lt;/h3&gt;
&lt;p&gt;使父元素内的所有行内元素水平垂直居中（内部div设置display：inline-block即可）这在子元素不确定宽度和高度时&lt;/p&gt;
&lt;p&gt;与其他一些display属性类似，table-cell同样会被其他一些css属性破坏，例如float，position：absolute，所以在使用display：table-cell时，尽量不要使用float或者position:absolute（可以考虑为之增加一个父div定义float等属性。）；设置了table-cell的元素对宽度和高度敏感（在父元素上设置table-row等属性，也会使其不感知height。），对margin值无反应，响应padding属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .father {
      width: 400px;
      height: 200px;
      border: 1px solid #000;
      display: table-cell;
      text-align: center;
      vertical-align: middle;
    }

    .son {
      width: 200px;
      height: 100px;
      background: red;
      display: inline-block;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;div class=&quot;son&quot;&amp;gt;
      display:table-cell;&amp;lt;/br&amp;gt;text-align:center;&amp;lt;/br&amp;gt; vertical-align:middle
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326142740669.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对table-cell元素设置百分比（如100%）的宽高值是无效的，但是可以将元素设置display:table，再将父元素设置百分比跨高，子元素table-cell会自动撑满父元素。这就可以做相对于整个页面的水平垂直居中。嗯，看下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    html,
    body {
      height: 100%;
      margin: 0;
      padding: 0;
    }

    #box {
      display: table;
      width: 100%;
      height: 100%;
    }

    .father {
      width: 400px;
      height: 200px;
      border: 1px solid #000;
      display: table-cell;
      text-align: center;
      vertical-align: middle;
    }

    .son {
      width: 200px;
      height: 100px;
      background: red;
      display: inline-block;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div id=&quot;box&quot;&amp;gt;
    &amp;lt;div class=&quot;father&quot;&amp;gt;
      &amp;lt;div class=&quot;son&quot;&amp;gt;
        display:table-cell;&amp;lt;/br&amp;gt;text-align:center;&amp;lt;/br&amp;gt; vertical-align:middle
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326143016605.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. display:flex的方法&lt;/h3&gt;
&lt;p&gt;参考上面所说的行内元素的方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置container的display的类型为flex，激活为flexbox模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;justify-content定义水平方向的元素位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;align-items定义垂直方向的元素位置&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.  display:inline-block +伪元素生成content内容&lt;/h3&gt;
&lt;p&gt;实现原理：原理：利用&lt;code&gt;inline-block&lt;/code&gt;的&lt;code&gt;vertical-align: middle&lt;/code&gt;去对齐before伪元素，before伪元素的高度与父对象一样，就实现了高度方向的对齐。居中块的尺寸可以做包裹性、自适应内容，兼容性也相当好。缺点是水平居中需要考虑inline-block间隔中的留白（代码换行符遗留问题。）。（宽度是已知的，高度可以是未知的）&lt;/p&gt;
&lt;p&gt;(这里子元素加了宽高是为了方便看，去掉也无所谓)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .father {
      width: 400px;
      height: 200px;
      border: 1px solid #000;
      text-align: center;
    }

    .father:before {
      content: &quot; &quot;;
      display: inline-block;
      vertical-align: middle;
      height: 100%;
    }

    .son {
      width: 200px;
      height: 100px;
      background: red;
      display: inline-block;
      vertical-align: middle;
    }
  &amp;lt;/style&amp;gt;


  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;div class=&quot;son&quot;&amp;gt;
      display:inline-block;&amp;lt;/br&amp;gt;伪元素生成content内容&amp;lt;/br&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 绝对定位+transform反向偏移。&lt;/h3&gt;
&lt;p&gt;position:absolute; transform:translate(-50%,-50%);&lt;/p&gt;
&lt;p&gt;原理很简单：由于top、left偏移了父对象的50%宽度高度，所以需要利用transform反向偏移居中块的50%宽高&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;transform的计算基准是元素本身，所以这里可以用50%来做反向偏移&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(这里子元素加了宽高是为了方便看，去掉也无所谓)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .father {
      width: 400px;
      height: 200px;
      border: 1px solid #000;
      position: relative;
    }

    .son {
      width: 200px;
      height: 100px;
      background: red;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;div class=&quot;son&quot;&amp;gt;
      position:absolute;&amp;lt;/br&amp;gt;left:50%;top:50%;&amp;lt;/br&amp;gt;transform
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326145604077.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;知道元素宽高的情况下&lt;/h2&gt;
&lt;h3&gt;1.绝对定位相对定位（ 绝对定位+margin：auto；position:absolute; left:0; top:0; right:0; bottom:0; margin:auto）&lt;/h3&gt;
&lt;p&gt;一个条件都不能少&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .father {
      width: 400px;
      height: 200px;
      position: relative;
      border: 1px solid #000;
    }

    .son {
      width: 200px;
      height: 100px;
      background: red;
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      right: 0;
      margin: auto;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;div class=&quot;son&quot;&amp;gt;
      position:absolute;&amp;lt;/br&amp;gt; left:0; top:0;&amp;lt;/br&amp;gt; right:0; bottom:0; &amp;lt;/br&amp;gt;margin:auto
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326145824002.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;当一个绝对定位元素，其对立定位方向属性同时有具体定位数值的时候，流体特性就发生了。&lt;/p&gt;
&lt;p&gt;具有流体特性绝对定位元素的margin:auto的填充规则和普通流体元素一模一样：&lt;/p&gt;
&lt;p&gt;如果一侧定值，一侧auto，auto为剩余空间大小；
如果两侧均是auto, 则平分剩余空间；
例如，下面的CSS代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.father {
    width: 300px; height:150px;
    position: relative;
}
.son { 
    position: absolute; 
    top: 0; right: 0; bottom: 0; left: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如图所示流体元素会布满整个父元素，此时&lt;code&gt;.son&lt;/code&gt;这个元素的尺寸表现为“格式化宽度和格式化高度”，和的“正常流宽度”一样，同属于外部尺寸，也就是尺寸自动填充父级元素的可用尺寸的&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: image-20200326150745436.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;然后，此时我们给.son设置尺寸，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.son { 
    position: absolute; 
    top: 0; right: 0; bottom: 0; left: 0;
    width: 200px; height: 100px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200326151049822.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时宽高被限制，原本应该填充的空间就被多余了出来，这多余的空间就是&lt;code&gt;margin:auto&lt;/code&gt;计算的空间，因此，如果这时候，我们再设置一个&lt;code&gt;margin:auto&lt;/code&gt;，那么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.son { 
    position: absolute; 
    top: 0; right: 0; bottom: 0; left: 0;
    width: 200px; height: 100px;
    margin: auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们这个&lt;code&gt;.son&lt;/code&gt;元素就水平和垂直方向同时居中了&lt;/p&gt;
&lt;h3&gt;2. 绝对定位+margin反向偏移&lt;/h3&gt;
&lt;p&gt;在上面的行内块元素种有提及到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; position：absolute; 
 top:50%; 
 left:50%; 
 margin-left:-(width+padding)/2+&apos;px&apos;; 
 margin-top:-(height+padding)/2+&apos;px&apos;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;margin值的设置不能使用百分比，因为margin是基于父元素的宽度来计算百分比的&lt;/p&gt;
&lt;p&gt;这个原理和上面的方案4很相似，由于top、left偏移了父对象的50%宽度高度，所以需要利用margin反向偏移居中块的50%宽高&lt;/p&gt;
</content:encoded></item><item><title>块级元素，行内元素和行内块元素</title><link>https://nollieleo.github.io/posts/%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%E8%A1%8C%E5%86%85%E5%85%83%E7%B4%A0%E5%92%8C%E8%A1%8C%E5%86%85%E5%9D%97%E5%85%83%E7%B4%A0/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%E8%A1%8C%E5%86%85%E5%85%83%E7%B4%A0%E5%92%8C%E8%A1%8C%E5%86%85%E5%9D%97%E5%85%83%E7%B4%A0/</guid><description>块级元素  每个块级元素都是独占一行或者多行，可以对其单独设置高度，宽度以及对齐等属性    块级元素有div、p、table，nav、aside、header、footer、section、article、ul，li、 &lt;h1&lt;h6 ，address等。  块级元素的特点  - 块级元素会独占一行...</description><pubDate>Thu, 26 Mar 2020 09:00:44 GMT</pubDate><content:encoded>&lt;h2&gt;块级元素&lt;/h2&gt;
&lt;p&gt;每个块级元素都是独占一行或者多行，可以对其单独设置高度，宽度以及对齐等属性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;块级元素有&lt;strong&gt;div&lt;/strong&gt;、&lt;strong&gt;p&lt;/strong&gt;、&lt;strong&gt;table&lt;/strong&gt;，nav、aside、&lt;strong&gt;header&lt;/strong&gt;、&lt;strong&gt;footer&lt;/strong&gt;、&lt;strong&gt;section&lt;/strong&gt;、article、&lt;strong&gt;ul&lt;/strong&gt;，&lt;strong&gt;li&lt;/strong&gt;、 &lt;strong&gt;&amp;lt;h1&amp;gt;~&amp;lt;h6&amp;gt;&lt;/strong&gt; ，address等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;块级元素的特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;块级元素会独占一行B&lt;/li&gt;
&lt;li&gt;高度，行高，外边距和内边距都可以单独设置&lt;/li&gt;
&lt;li&gt;宽度默认是容器的100%&lt;/li&gt;
&lt;li&gt;可以容纳内联元素和其他块级元素&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;行内元素&lt;/h2&gt;
&lt;p&gt;行内元素（内联元素）不占有独立的区域，仅仅依靠自己的字体大小或者是图像大小来支撑结构，一般不可以设置宽度，高度以及对齐等属性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;常见的行内元素有：&amp;lt;a&amp;gt;,&amp;lt;strong&amp;gt;,&amp;lt;b&amp;gt;,&amp;lt;em&amp;gt;,&amp;lt;del&amp;gt;,&amp;lt;span&amp;gt;等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;行内元素的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和相邻的行内元素在同一行上&lt;/li&gt;
&lt;li&gt;高度宽度上无效，但是水平方向上的padding和margin可以设置，垂直方向上无效&lt;/li&gt;
&lt;li&gt;默认的宽度就是它本身的宽度&lt;/li&gt;
&lt;li&gt;行内元素只能容纳纯文本或者是其他的行类元素，除了（a标签）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;notes：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;只有文字才能组成段落，因此类似&amp;lt;p&amp;gt;,&amp;lt;h1&amp;gt;~&amp;lt;h6&amp;gt;,&amp;lt;dt&amp;gt;等都是文字块级标签，所以里面不能放块级元素&lt;/li&gt;
&lt;li&gt;链接里面不能再存放链接&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;行内块元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;常见的有：&amp;lt;img/&amp;gt;, &amp;lt;input/&amp;gt; ,&amp;lt;td/&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以设置他们的宽高度和对齐的属性&lt;/p&gt;
&lt;p&gt;行内块元素的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和相邻的行内元素（行内块）再一行上，但是中间会有空白的间隙&lt;/li&gt;
&lt;li&gt;默认的宽度就是本身内容的宽度&lt;/li&gt;
&lt;li&gt;高度，行高，内边距和外边距都可以设置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;显示模式的转换&lt;/h2&gt;
&lt;p&gt;块转行内：&lt;code&gt;display:inline&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;行内转块：&lt;code&gt;display:block&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;块，行内元素转换为行内块：&lt;code&gt;display:inline-block&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>浏览器的渲染机制</title><link>https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E6%B8%B2%E6%9F%93%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E6%B8%B2%E6%9F%93%E6%9C%BA%E5%88%B6/</guid><description>浏览器的渲染过程  面试题：在浏览器地址栏里输入一个URL,到这个页面呈现出来，中间会发生什么？  目前市面上常见的浏览器内核可以分为这四种：Trident（IE）、Gecko（火狐）、Blink（Chrome、Opera）、Webkit（Safari）。这里面大家最耳熟能详的可能就是 Webkit...</description><pubDate>Wed, 25 Mar 2020 12:40:54 GMT</pubDate><content:encoded>&lt;h1&gt;浏览器的渲染过程&lt;/h1&gt;
&lt;p&gt;*面试题：在浏览器地址栏里输入一个URL,到这个页面呈现出来，中间会发生什么？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目前市面上常见的浏览器内核可以分为这四种：Trident（IE）、Gecko（火狐）、Blink（Chrome、Opera）、Webkit（Safari）。这里面大家最耳熟能详的可能就是 Webkit 内核了，Webkit 内核是当下浏览器世界真正的霸主。
本文我们就以 Webkit 为例，对现代浏览器的渲染过程进行一个深度的剖析。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 页面加载的过程&lt;/h2&gt;
&lt;p&gt;首先需要找到这个URL域名的服务器IP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寻找缓存， 查看缓存中是否有记录（浏览器缓存 -&amp;gt; 系统缓存 -&amp;gt; 路由器缓存 ），缓存中没则去查找系统的hosts文件记录，如果再没有就去查询DNS服务器。&lt;/li&gt;
&lt;li&gt;浏览器会根据DNS服务器得到域名的IP地址&lt;/li&gt;
&lt;li&gt;根据这个IP 以及相应的端口号  构建一个HTTP请求，并将这个HTTP请求封装在一个TCP包中，发送给服务器（依次经过传输层，网络层，数据链路层，物理层，服务器）&lt;/li&gt;
&lt;li&gt;服务器收到这个包并且解析这个http请求返回相应的HTML给浏览器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实就是一堆 HMTL 格式的字符串，因为只有 HTML 格式浏览器才能正确解析，这是 W3C 标准的要求。接下来就是浏览器的&lt;strong&gt;渲染过程。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2. 浏览器的渲染过程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./1585097884.jpg&quot; alt=&quot;webkit渲染过程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1） 浏览器会解析三个东西&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML/SVG/XHML，HTML字符串描述了一个页面的结构，浏览器会把HTML结构字符串解析转换DOM树形结构（DOM的构建）（在dom树的构建过程中如果遇到JS脚本和外部JS连接，则会停止构建DOM树来执行和下载相应的代码，造成阻塞，后面会说到）。&lt;/li&gt;
&lt;li&gt;CSS，解析CSS产生CSS rule tree，和他DOM树的结构比较像&lt;/li&gt;
&lt;li&gt;JS脚本，等到JS脚本文件加载完成之后，通过DOM API和CSSOM API来操作DOM tree和CSS rule tree&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2）解析完成，浏览器引擎会通过DOM tree和css Rule Tree来构造Rendering Tree。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rendering Tree 渲染树并不等同于DOM树，渲染树只会包括需要显示的节点和这些节点的样式信息 （这里主要做的是排除非视觉节点，比如script，meta标签和排除display为none的节点）&lt;/li&gt;
&lt;li&gt;CSS的rule tree主要是为了完成匹配并且把CSS rule附加上Rendering tree上的每一个Element（也就是每一个Frame）&lt;/li&gt;
&lt;li&gt;然后计算每个Frame的位置，得到节点的几何信息（位置，大小），这个又叫做&lt;strong&gt;回流（layout）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;根据渲染树以及回流得到的几何信息，得到节点的绝对像素，这个过程叫做&lt;strong&gt;重绘（painting）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;display:将像素发送给GPU，展示在页面上。（这一步其实还有很多内容，比如会在GPU将多个合成层合并为同一个层，并展示在页面中。而css3硬件加速的原理则是新建合成层&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3）最后通过调用操作系统Native GUI的api绘制&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;接下来叙述一些详细步骤&lt;/p&gt;
&lt;h2&gt;构建DOM树&lt;/h2&gt;
&lt;p&gt;浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上，可以分为几个步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200325123754388.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器从磁盘或网络读取HTML的原始字节，并且根据文件的指定编码（例：UTF-8）来将他们转换成字符串。（网络中传输的内容其实都是0 1这些字节数据。当浏览器收到这些字节数据以后，会将他们转成字符串，也就是我们写的代码）&lt;/li&gt;
&lt;li&gt;将字符串转换成TOKEN，例如：&lt;code&gt;&amp;lt; html &amp;gt;&lt;/code&gt;,&lt;code&gt;&amp;lt; body &amp;gt;&lt;/code&gt;等。Token中会自动标识出当前Token是“开始标签”还是“结束标签”或者是“文本”等信息。&lt;/li&gt;
&lt;li&gt;生成节点对象并且构建DOM。（ 构建DOM的过程中，不是等所有Token都转换完成后再去生成节点对象，而是一边生成Token一边消耗Token来生成节点对象。换句话说，每个Token被生成后，会立刻消耗这个Token创建出节点对象。&lt;strong&gt;注意：带有结束标签标识的Token不会创建节点对象。&lt;/strong&gt; ）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;构建CSSOM&lt;/h2&gt;
&lt;p&gt;DOM会捕获页面的内容，但浏览器还需要知道页面如何展示，所以需要构建CSSOM。&lt;/p&gt;
&lt;p&gt;构建CSSOM的过程与构建DOM的过程非常相似，当浏览器接收到一段CSS，浏览器首先要做的是识别出Token，然后构建节点并生成CSSOM。
&lt;img src=&quot;./11123.png&quot; alt=&quot;&quot; /&gt;
在这一过程中，浏览器会确定下每一个节点的样式到底是什么，并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点，也可以通过继承获得。在这一过程中，浏览器得递归 CSSOM 树，然后确定具体的元素到底是什么样式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以，DOM树要小，CSS尽量用id和class，千万不要过渡层叠下去&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;构建渲染树&lt;/h2&gt;
&lt;p&gt;当我们生成 DOM 树和 CSSOM 树以后，就需要将这两棵树组合为渲染树。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1585097943.jpg&quot; alt=&quot;生成渲染树&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这一过程中，不是简单的将两者合并就行了。为了构建渲染树，浏览器主要完成了以下工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从DOM树的根节点开始遍历每个可见节点。&lt;/li&gt;
&lt;li&gt;对于每个可见的节点，找到CSSOM树中对应的规则，并应用它们。&lt;/li&gt;
&lt;li&gt;根据每个可见节点以及其对应的样式，组合生成渲染树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一步中，既然说到了要遍历可见的节点，那么我们得先知道，什么节点是不可见的。不可见的节点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一些不会渲染输出的节点，比如script、meta、link等。&lt;/li&gt;
&lt;li&gt;一些通过css进行隐藏的节点。比如display:none。注意，利用visibility和opacity隐藏的节点，还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：渲染树只包含可见的节点&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;回流和重绘&lt;/h2&gt;
&lt;p&gt;在另一篇文章（包含了渲染性能的优化策略等）&lt;/p&gt;
&lt;h2&gt;一些疑问&lt;/h2&gt;
&lt;h3&gt;渲染过程中碰到JS文件应该如何处理？&lt;/h3&gt;
&lt;p&gt;渲染过程中如果遇到了&amp;lt;script&amp;gt;标签就停止渲染，执行JS代码。因为浏览器有GUI渲染线程和JS引擎线程，为了防止渲染出现不可预期的结果，这两个线程是互斥的关系，JS的加载，解析和执行会阻塞DOM的构建，也就是说，在构建DOM的时候,HTML解析器遇到了JS，那么会暂停构建DOM，将控制权移交给JS引擎，等JS引擎运行完毕，浏览器再从中断的地方恢复DOM构建。&lt;/p&gt;
&lt;p&gt;也就是说，如果你想首屏渲染的越快，就越不应该在首屏就加载 JS 文件，这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下，并不是说 script 标签必须放在底部，因为你可以给 script 标签添加 defer 或者 async 属性（下文会介绍这两者的区别）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JS文件不只是阻塞DOM的构建，它会导致CSSOM也阻塞DOM的构建&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原本DOM和CSSOM的构建是互不影响，井水不犯河水，但是一旦引入了JavaScript，CSSOM也开始阻塞DOM的构建，只有CSSOM构建完毕后，DOM再恢复DOM构建。&lt;/p&gt;
&lt;p&gt;这是什么情况？&lt;/p&gt;
&lt;p&gt;这是因为JavaScript不只是可以改DOM，它还可以更改样式，也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的，如果JavaScript想访问CSSOM并更改它，那么在执行JavaScript时，必须要能拿到完整的CSSOM。所以就导致了一个现象，如果浏览器尚未完成CSSOM的下载和构建，而我们却想在此时运行脚本，那么浏览器将延迟脚本执行和DOM构建，直至其完成CSSOM的下载和构建。也就是说，&lt;strong&gt;在这种情况下，浏览器会先下载和构建CSSOM，然后再执行JavaScript，最后在继续构建DOM&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>画一条0.5px的边</title><link>https://nollieleo.github.io/posts/%E7%94%BB%E4%B8%80%E6%9D%A10-5px%E7%9A%84%E8%BE%B9/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E7%94%BB%E4%B8%80%E6%9D%A10-5px%E7%9A%84%E8%BE%B9/</guid><description>什么是像素？  像素是屏幕显示最小的单位，在一个1080p的屏幕上，它的像素数量是1920 1080，即横边有1920个像素，而竖边为1080个。一个像素就是一个单位色块，是由rgba四个通道混合而成。对于一个1200万像素的相机镜头来说，它有1200万个感光单元，它能输出的最大图片分辨率大约为30...</description><pubDate>Wed, 25 Mar 2020 10:20:15 GMT</pubDate><content:encoded>&lt;p&gt;什么是像素？&lt;/p&gt;
&lt;p&gt;像素是屏幕显示最小的单位，在一个1080p的屏幕上，它的像素数量是1920 &lt;em&gt;1080，即横边有1920个像素，而竖边为1080个。一个像素就是一个单位色块，是由rgba四个通道混合而成。对于一个1200万像素的相机镜头来说，它有1200万个感光单元，它能输出的最大图片分辨率大约为3000&lt;/em&gt; 4000。&lt;/p&gt;
&lt;p&gt;那么像素本身有大小吗，一个像素有多大？&lt;/p&gt;
&lt;p&gt;有的，如果一个像素越小，那么在同样大小的屏幕上，需要的像素点就越多，像素就越密集，如果一英寸有435个像素，那么它的dpi/ppi就达到了435。Macbook Pro 15寸的分辨率为2880 x 1800，15寸是指屏幕的对角线为15寸（具体为15.4），根据长宽比换算一下得到横边为13寸，所以ppi为2880 / 13 = 220 ppi. 像素越密集即ppi(pixel per inch)越高，那么屏幕看起来就越细腻越高清。&lt;/p&gt;
&lt;p&gt;怎么在高清屏上画一条0.5px的边呢？0.5px相当于高清屏物理像素的1px。这样的目的是在高清屏上看起来会更细一点，效果会更好一点，例如更细的分隔线。&lt;/p&gt;
&lt;p&gt;理论上px的最小单位是1，但是会有几个特例，高清屏的显示就是一个特例。高清屏确实可以画0.5px，对比效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./381845-30d6bfdb16105a41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;猜想方法如下：&lt;/p&gt;
&lt;h2&gt;1. 直接设置&lt;/h2&gt;
&lt;p&gt;如果我们直接设置0.5px，在不同的浏览器会有不同的表现，使用如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    div{
      width: 300px;
      background: #000;
    }
    .half-px {
      height: .5px;
    }
    .one-px {
      height: 1px;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;p&amp;gt;0.5px&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;half-px&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;p&amp;gt;1px&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;one-px&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在PC上的不同浏览器上测试测试结果如下所示：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: 0.5px381845-8bc8f0fa7f9fb678.png]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;其中Chrome把0.5px四舍五入变成了1px，而firefox/safari能够画出半个像素的边，并且Chrome会把小于0.5px的当成0，而Firefox会把不小于0.55px当成1px，Safari是把不小于0.75px当成1px，进一步在手机上观察iOS的Chrome会画出0.5px的边，而安卓(5.0)原生浏览器是不行的。所以直接设置0.5px不同浏览器的差异比较大，并且我们看到不同系统的不同浏览器对小数点的px有不同的处理。所以如果我们把单位设置成小数的px包括宽高等，其实不太可靠，因为不同浏览器表现不一样。&lt;/p&gt;
&lt;h2&gt;2. 缩放scale 0.5&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    div{
      width: 300px;
      background: #000;
    }
    .half-px {
      height: 1px;
      transform: scaleY(.5);
    }
    .one-px {
      height: 1px;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;p&amp;gt;1px + scaleY(.5)&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;half-px&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;p&amp;gt;1px&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;one-px&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./381845-3b74e4afcb12de3e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们发现Chrome/Safari都变虚了，只有Firefox比较完美看起来是实的而且还很细，效果和直接设置0.5px一样。所以通过transform: scale会导致Chrome变虚了，而粗细几乎没有变化，所以这个效果不好。&lt;/p&gt;
&lt;p&gt;我们还想到做移动端的时候还使用了rem做缩放，但实际上rem的缩放最后还是会转化成px，所以和直接使用0.5px的方案是一样的&lt;/p&gt;
&lt;h2&gt;3. 线性渐变linear-gradient&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .hr {
      width: 300px;
      height: 1px;
      background-color: #000;
    }

    .hr.gradient {
      height: 1px;
      background: linear-gradient(0deg, #fff, #000);
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;p&amp;gt;linear-gradient(0deg, #fff, #000)&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;hr gradient&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;p&amp;gt;1px&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;hr&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;linear-gradient(0deg, #fff, #000)的意思是：渐变的角度从下往上，从白色#fff渐变到黑色#000，而且是线性的，在高清屏上，1px的逻辑像素代表的物理（设备）像素有2px，由于是线性渐变，所以第1个px只能是#fff，而剩下的那个像素只能是#000，这样就达到了画一半的目的。逻辑分析很完美，实际的效果又怎么样呢，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200325110612191.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们发现这种方法在各个流览器上面都不完美，效果都是虚的，和完美的0.5px还是有差距。这个效果和scale 0.5的差不多，都是通过虚化线，让人觉得变细了。&lt;/p&gt;
&lt;h2&gt;4. box-shadow&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;style&amp;gt;
    .hr {
      width: 300px;
      height: 1px;
      background-color: #000;
    }

    .hr.boxShadow {
      height: 1px;
      background: none;
      box-shadow: 0 0.5px 0 #000;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;p&amp;gt;boxShadow&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;hr boxShadow&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;p&amp;gt;1px&amp;lt;/p&amp;gt;
  &amp;lt;div class=&quot;hr&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置box-shadow的第二个参数为0.5px，表示阴影垂直方向的偏移为0.5px，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200325111341469.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个方法在Chrome和Firefox都非常完美，但是Safari不支持小于1px的boxshadow，所以完全没显示出来了。不过至少找到了一种方法能够让PC的Chrome显示0.5px。&lt;/p&gt;
&lt;h2&gt;5. 设置viewport的scale&lt;/h2&gt;
&lt;p&gt;在移端开发里面一般会把viewport的scale设置成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-sacle=1&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中width=device-width表示将viewport视窗的宽度调整为设备的宽度，这个宽度通常是指物理上宽度。默认的缩放比例为1，如iphone 6竖屏的宽度为750px，它的dpr=2，用2px表示1px，这样设置之后viewport的宽度就变成375px。这时候0.5px的边就使用我们上面讨论的方法。&lt;/p&gt;
&lt;p&gt;但是你可以把scale改成0.5：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-sacle=0.5&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，viewport的宽度就是原本的750px，所以1个px还是1px，正常画就行，但这样也意味着UI需要按2倍图的出，整体面面的单位都会放大一倍。&lt;/p&gt;
&lt;p&gt;在iPhone X和一些安卓手机等dpr = 3的设备上，需要设置&lt;code&gt;scale&lt;/code&gt;为0.333333，这个时候就是3倍地画了。&lt;/p&gt;
&lt;p&gt;综上讨论了像素和viewport的一些概念，并介绍和比较了在高清屏上画0.5px的几种方法——可以通过直接设置宽高&lt;code&gt;border&lt;/code&gt;为0.5px、设置&lt;code&gt;box-shadow&lt;/code&gt;的垂直方向的偏移量为0.5px、借助线性渐变&lt;code&gt;linear-gradient&lt;/code&gt;、使用&lt;code&gt;transform: scaleY(0.5)&lt;/code&gt;的方法&lt;/p&gt;
</content:encoded></item><item><title>CSS盒子模型</title><link>https://nollieleo.github.io/posts/css%E7%9B%92%E5%AD%90%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/css%E7%9B%92%E5%AD%90%E6%A8%A1%E5%9E%8B/</guid><description>css盒子模型  先看例子：  html   &lt;style     div {       width: 300px;       height: 400px;       border: 5px solid ccc;       background-color: aqua;       padd...</description><pubDate>Wed, 25 Mar 2020 09:22:07 GMT</pubDate><content:encoded>&lt;h1&gt;css盒子模型&lt;/h1&gt;
&lt;p&gt;先看例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    div {
      width: 300px;
      height: 400px;
      border: 5px solid #ccc;
      background-color: aqua;
      padding: 20px;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200325100338772.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图所示：设置一个宽300px,高400px的div，border为5px，padding值设为20px.&lt;/p&gt;
&lt;p&gt;这里我们会发现明明我们设置了&lt;code&gt;300*400&lt;/code&gt;长宽比，为什么呈现出来的是一个&lt;code&gt;350*450&lt;/code&gt;的盒子呢？&lt;/p&gt;
&lt;p&gt;我们可以看一下这个图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200325100748520.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这张图中，我们发现我们设置的&lt;code&gt;300*400&lt;/code&gt;出现在了最里面的那个蓝框中，与此同时我们可以发现在这个盒模型中除了我们设置的内容（&lt;code&gt;content&lt;/code&gt;），还有&lt;code&gt;margin&lt;/code&gt;（外边距）、&lt;code&gt;border&lt;/code&gt;（边框）、&lt;code&gt;padding&lt;/code&gt;（内边框）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;margin(外边距)&lt;/code&gt; - 清除边框外的区域，外边距是透明的。
&lt;code&gt;border(边框)&lt;/code&gt; - 围绕在内边距和内容外的边框。
&lt;code&gt;padding(内边距)&lt;/code&gt; - 清除内容周围的区域，内边距是透明的。
&lt;code&gt;content(内容)&lt;/code&gt; - 盒子的内容，显示文本和图像。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了正确设置元素在所有浏览器中的宽度和高度，你需要知道盒模型是如何工作的。&lt;/p&gt;
&lt;p&gt;而我们在测试效果图看到的350*450盒子，&lt;/p&gt;
&lt;p&gt;350（width） = 300（content） + 20（padding）* 2 + 5（border）* 2
450（height）= 400 （content）+ 20（padding）* 2 + 5（border）* 2&lt;/p&gt;
&lt;h2&gt;css的两种盒子模型&lt;/h2&gt;
&lt;p&gt;css的两种盒子模型不同&lt;/p&gt;
&lt;h3&gt;W3C的标准盒模型&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./20180324150509906.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;在标准的盒子模型中，width指content部分的宽度&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;IE的盒模型&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./20180324150533356.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;在IE盒子模型中，width表示content+padding+border这三个部分的宽度&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以看出我们上面的使用的默认正是&lt;code&gt;W3C标准盒模型&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;盒子模型类型的切换&lt;/h2&gt;
&lt;p&gt;如果想要切换盒模型也很简单，这里需要借助css3的&lt;code&gt;box-sizing&lt;/code&gt;属性&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;box-sizing: content-box&lt;/code&gt; 是W3C盒子模型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;box-sizing: border-box&lt;/code&gt; 是IE盒子模型&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;box-sizing的默认属性是content-box&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>回流和重绘(重排)</title><link>https://nollieleo.github.io/posts/%E5%9B%9E%E6%B5%81%E5%92%8C%E9%87%8D%E7%BB%98-%E9%87%8D%E6%8E%92/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9B%9E%E6%B5%81%E5%92%8C%E9%87%8D%E7%BB%98-%E9%87%8D%E6%8E%92/</guid><description>...</description><pubDate>Tue, 24 Mar 2020 10:12:58 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000017329980&quot;&gt;回流和重绘&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ljianshu/Blog/issues/51&quot;&gt;浏览器渲染机制&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>如何画一个三角形</title><link>https://nollieleo.github.io/posts/%E5%A6%82%E4%BD%95%E7%94%BB%E4%B8%80%E4%B8%AA%E4%B8%89%E8%A7%92%E5%BD%A2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%A6%82%E4%BD%95%E7%94%BB%E4%B8%80%E4%B8%AA%E4%B8%89%E8%A7%92%E5%BD%A2/</guid><description>用css如何画一个三角形？  html &lt;style     div {       width: 0;       height: 0;       border-top: 10px solid green;       border-bottom: 10px solid transparent;...</description><pubDate>Mon, 23 Mar 2020 11:04:07 GMT</pubDate><content:encoded>&lt;h2&gt;用css如何画一个三角形？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
    div {
      width: 0;
      height: 0;
      border-top: 10px solid green;
      border-bottom: 10px solid transparent;
      border-left: 10px solid transparent;
      border-right: 10px solid transparent;
    }
&amp;lt;/style&amp;gt;
&amp;lt;div/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200323111030478.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;三角形原理：边框的均分原理&lt;/p&gt;
</content:encoded></item><item><title>HTTP常用请求头</title><link>https://nollieleo.github.io/posts/http%E5%B8%B8%E7%94%A8%E8%AF%B7%E6%B1%82%E5%A4%B4/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/http%E5%B8%B8%E7%94%A8%E8%AF%B7%E6%B1%82%E5%A4%B4/</guid><description>常用请求头  | 协议头              | 说明                                                         | | ------------------- | -------------------------------------...</description><pubDate>Mon, 23 Mar 2020 10:50:27 GMT</pubDate><content:encoded>&lt;h2&gt;常用请求头&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;协议头&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accept&lt;/td&gt;
&lt;td&gt;可接受的响应内容类型（Content-Types）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Charset&lt;/td&gt;
&lt;td&gt;可接受的字符集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Encoding&lt;/td&gt;
&lt;td&gt;可接受的响应内容的编码方式。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Language&lt;/td&gt;
&lt;td&gt;可接受的响应内容语言列表。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Datetime&lt;/td&gt;
&lt;td&gt;可接受的按照时间来表示的响应内容版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;用于表示HTTP协议中需要认证资源的认证信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache-Control&lt;/td&gt;
&lt;td&gt;用来指定当前的请求/回复中的，是否使用缓存机制。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection&lt;/td&gt;
&lt;td&gt;客户端（浏览器）想要优先使用的连接类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;由之前服务器通过Set-Cookie（见下文）设置的一个HTTP协议Cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Length&lt;/td&gt;
&lt;td&gt;以8进制表示的请求体的长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-MD5&lt;/td&gt;
&lt;td&gt;请求体的内容的二进制 MD5 散列值（数字签名），以 Base64 编码的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;请求体的MIME类型 （用于POST和PUT请求中）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;发送该消息的日期和时间（以&lt;a href=&quot;https://www.nowcoder.com/tutorial/96/24304825a0c04ea9a53cdb09cb664834#section-7.1.1.1&quot;&gt;RFC 7231&lt;/a&gt;中定义的&quot;HTTP日期&quot;格式来发送）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expect&lt;/td&gt;
&lt;td&gt;表示客户端要求服务器做出特定的行为&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;From&lt;/td&gt;
&lt;td&gt;发起此请求的用户的邮件地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口（80），则端口号可以省略。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Match&lt;/td&gt;
&lt;td&gt;仅当客户端提供的实体与服务器上对应的实体相匹配时，才进行对应的操作。主要用于像 PUT 这样的方法中，仅当从用户上次更新某个资源后，该资源未被修改的情况下，才更新该资源。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Modified-Since&lt;/td&gt;
&lt;td&gt;允许在对应的资源未被修改的情况下返回304未修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-None-Match&lt;/td&gt;
&lt;td&gt;允许在对应的内容未被修改的情况下返回304未修改（ 304 Not Modified ），参考 超文本传输协议 的实体标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Range&lt;/td&gt;
&lt;td&gt;如果该实体未被修改过，则向返回所缺少的那一个或多个部分。否则，返回整个新的实体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Unmodified-Since&lt;/td&gt;
&lt;td&gt;仅当该实体自某个特定时间以来未被修改的情况下，才发送回应。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max-Forwards&lt;/td&gt;
&lt;td&gt;限制该消息可被代理及网关转发的次数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin&lt;/td&gt;
&lt;td&gt;发起一个针对&lt;a href=&quot;http://itbilu.com/javascript/js/VkiXuUcC.html&quot;&gt;跨域资源共享&lt;/a&gt;的请求（该请求要求服务器在响应中加入一个Access-Control-Allow-Origin的消息头，表示访问控制所允许的来源）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pragma&lt;/td&gt;
&lt;td&gt;与具体的实现相关，这些字段可能在请求/回应链中的任何时候产生。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy-Authorization&lt;/td&gt;
&lt;td&gt;用于向代理进行认证的认证信息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range&lt;/td&gt;
&lt;td&gt;表示请求某个实体的一部分，字节偏移以0开始。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referer&lt;/td&gt;
&lt;td&gt;表示浏览器所访问的前一个页面，可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer其实是Referrer这个单词，但RFC制作标准时给拼错了，后来也就将错就错使用Referer了。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TE&lt;/td&gt;
&lt;td&gt;浏览器预期接受的传输时的编码方式：可使用回应协议头Transfer-Encoding中的值（还可以使用&quot;trailers&quot;表示数据传输时的分块方式）用来表示浏览器希望在最后一个大小为0的块之后还接收到一些额外的字段。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-Agent&lt;/td&gt;
&lt;td&gt;浏览器的身份标识字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade&lt;/td&gt;
&lt;td&gt;要求服务器升级到一个高版本协议。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Via&lt;/td&gt;
&lt;td&gt;告诉服务器，这个请求是由哪些代理发出的。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;一个一般性的警告，表示在实体内容体中可能存在错误。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>HTTP返回状态码</title><link>https://nollieleo.github.io/posts/http%E8%BF%94%E5%9B%9E%E7%8A%B6%E6%80%81%E7%A0%81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/http%E8%BF%94%E5%9B%9E%E7%8A%B6%E6%80%81%E7%A0%81/</guid><description>状态码  100  Continue  继续。客户端应继续其请求  101  Switching Protocols  切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议，例如，切换到HTTP的新版本协议  200  OK  请求成功。一般用于GET与POST请求  201  Crea...</description><pubDate>Mon, 23 Mar 2020 10:40:35 GMT</pubDate><content:encoded>&lt;h2&gt;状态码&lt;/h2&gt;
&lt;p&gt;100  Continue  继续。客户端应继续其请求&lt;/p&gt;
&lt;p&gt;101  Switching Protocols  切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议，例如，切换到HTTP的新版本协议&lt;/p&gt;
&lt;p&gt;200  OK  请求成功。一般用于GET与POST请求&lt;/p&gt;
&lt;p&gt;201  Created  已创建。成功请求并创建了新的资源&lt;/p&gt;
&lt;p&gt;202  Accepted  已接受。已经接受请求，但未处理完成&lt;/p&gt;
&lt;p&gt;203  Non-Authoritative Information  非授权信息。请求成功。但返回的meta信息不在原始的服务器，而是一个副本&lt;/p&gt;
&lt;p&gt;204  No Content  无内容。服务器成功处理，但未返回内容。在未更新网页的情况下，可确保浏览器继续显示当前文档&lt;/p&gt;
&lt;p&gt;205  Reset Content  重置内容。服务器处理成功，用户终端（例如：浏览器）应重置文档视图。可通过此返回码清除浏览器的表单域&lt;/p&gt;
&lt;p&gt;206  Partial Content  部分内容。服务器成功处理了部分GET请求&lt;/p&gt;
&lt;p&gt;300  Multiple Choices  多种选择。请求的资源可包括多个位置，相应可返回一个资源特征与地址的列表用于用户终端（例如：浏览器）选择&lt;/p&gt;
&lt;p&gt;301  Moved Permanently  永久移动。请求的资源已被永久的移动到新URI，返回信息会包括新的URI，浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替&lt;/p&gt;
&lt;p&gt;302  Found  临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI&lt;/p&gt;
&lt;p&gt;303  See Other  查看其它地址。与301类似。使用GET和POST请求查看&lt;/p&gt;
&lt;p&gt;304  Not Modified  未修改。所请求的资源未修改，服务器返回此状态码时，不会返回任何资源。客户端通常会缓存访问过的资源，通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源&lt;/p&gt;
&lt;p&gt;305  Use Proxy  使用代理。所请求的资源必须通过代理访问&lt;/p&gt;
&lt;p&gt;306  Unused  已经被废弃的HTTP状态码&lt;/p&gt;
&lt;p&gt;307  Temporary Redirect  临时重定向。与302类似。使用GET请求重定向&lt;/p&gt;
&lt;p&gt;400  Bad Request  客户端请求的语法错误，服务器无法理解&lt;/p&gt;
&lt;p&gt;401  Unauthorized  请求要求用户的身份认证&lt;/p&gt;
&lt;p&gt;402  Payment Required  保留，将来使用&lt;/p&gt;
&lt;p&gt;403  Forbidden  服务器理解请求客户端的请求，但是拒绝执行此请求&lt;/p&gt;
&lt;p&gt;404  Not Found  服务器无法根据客户端的请求找到资源（网页）。通过此代码，网站设计人员可设置&quot;您所请求的资源无法找到&quot;的个性页面&lt;/p&gt;
&lt;p&gt;405  Method Not Allowed  客户端请求中的方法被禁止&lt;/p&gt;
&lt;p&gt;406  Not Acceptable  服务器无法根据客户端请求的内容特性完成请求&lt;/p&gt;
&lt;p&gt;407  Proxy Authentication Required  请求要求代理的身份认证，与401类似，但请求者应当使用代理进行授权&lt;/p&gt;
&lt;p&gt;408  Request Time-out  服务器等待客户端发送的请求时间过长，超时&lt;/p&gt;
&lt;p&gt;409  Conflict  服务器完成客户端的PUT请求是可能返回此代码，服务器处理请求时发生了冲突&lt;/p&gt;
&lt;p&gt;410  Gone  客户端请求的资源已经不存在。410不同于404，如果资源以前有现在被永久删除了可使用410代码，网站设计人员可通过301代码指定资源的新位置&lt;/p&gt;
&lt;p&gt;411  Length Required  服务器无法处理客户端发送的不带Content-Length的请求信息&lt;/p&gt;
&lt;p&gt;412  Precondition Failed  客户端请求信息的先决条件错误&lt;/p&gt;
&lt;p&gt;413  Request Entity Too Large  由于请求的实体过大，服务器无法处理，因此拒绝请求。为防止客户端的连续请求，服务器可能会关闭连接。如果只是服务器暂时无法处理，则会包含一个Retry-After的响应信息&lt;/p&gt;
&lt;p&gt;414  Request-URI Too Large  请求的URI过长（URI通常为网址），服务器无法处理&lt;/p&gt;
&lt;p&gt;415  Unsupported Media Type  服务器无法处理请求附带的媒体格式&lt;/p&gt;
&lt;p&gt;416  Requested range not satisfiable  客户端请求的范围无效&lt;/p&gt;
&lt;p&gt;417  Expectation Failed  服务器无法满足Expect的请求头信息&lt;/p&gt;
&lt;p&gt;500  Internal Server Error  服务器内部错误，无法完成请求&lt;/p&gt;
&lt;p&gt;501  Not Implemented  服务器不支持请求的功能，无法完成请求&lt;/p&gt;
&lt;p&gt;502  Bad Gateway  作为网关或者代理工作的服务器尝试执行请求时，从远程服务器接收到了一个无效的响应&lt;/p&gt;
&lt;p&gt;503  Service Unavailable  由于超载或系统维护，服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中&lt;/p&gt;
&lt;p&gt;504  Gateway Time-out  充当网关或代理的服务器，未及时从远端服务器获取请求&lt;/p&gt;
&lt;p&gt;505  HTTP Version not supported  服务器不支持请求的HTTP协议的版本，无法完成处理&lt;/p&gt;
</content:encoded></item><item><title>面试题</title><link>https://nollieleo.github.io/posts/%E9%9D%A2%E8%AF%95%E9%A2%98/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%9D%A2%E8%AF%95%E9%A2%98/</guid><description>说一下http和https  (1) http和https的概念：  http：超文本传输协议，是互联网上使用广泛的一种网络协议，是一种客户端和服务器端请求和应答的标准（TCP的标准），是用于从www服务器传输超文本到本地浏览器的传输协议，它可以使浏览器更加高效，使得网络传输减少  https：是h...</description><pubDate>Mon, 23 Mar 2020 09:10:29 GMT</pubDate><content:encoded>&lt;h2&gt;*说一下http和https&lt;/h2&gt;
&lt;p&gt;(1) http和https的概念：&lt;/p&gt;
&lt;p&gt;http：超文本传输协议，是互联网上使用广泛的一种网络协议，是一种客户端和服务器端请求和应答的标准（TCP的标准），是用于从www服务器传输超文本到本地浏览器的传输协议，它可以使浏览器更加高效，使得网络传输减少&lt;/p&gt;
&lt;p&gt;https：是http的安全版，http下加入了SSL层，HTTPS的安全基础是SSL，因此加密详细内容就需要SSL，https的作用就是建立一条安全的通道，来确保传输数据的安全性，&lt;strong&gt;简单来说https协议就是由http和ssl协议构建的可进行加密传输和身份确认的网络协议。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(2) http和https区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https需要ca证书，费用高&lt;/li&gt;
&lt;li&gt;http是超文本传输协议，信息是明文传输，https则是具有安全性的ssl加密传输协议。&lt;/li&gt;
&lt;li&gt;使用不同链接方式：端口不同，http是80，https是443&lt;/li&gt;
&lt;li&gt;http连接简单，无状态的；https是由SSL+http协议构建的数据加密传输身份认证的网络协议。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(3) https协议的优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保数据的传输安全性和正确发送到对应的客户机和服务器。&lt;/li&gt;
&lt;li&gt;加密传输和身份认证使得其在数据传输的过程中不容易被窃取改变，保证数据的完整性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(4) https协议的缺点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;握手阶段比较费时&lt;/li&gt;
&lt;li&gt;缓存效率不高，增加数据开销&lt;/li&gt;
&lt;li&gt;SLL协议要钱&lt;/li&gt;
&lt;li&gt;SLL需要绑定IP，一个IP不能绑定多个域名&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;*TCP三次握手，一句话概括&lt;/h2&gt;
&lt;p&gt;客户端和服务端都需要直到各自可收发，因此需要三次握手。&lt;/p&gt;
&lt;p&gt;简化三次握手：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*[Image missing: 42496289-1c6d668a-8458-11e8-98b3-65db50f64d48.png]*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从图片可以得到三次握手可以简化为：C发起请求连接S确认，也发起连接C确认我们再看看每次握手的作用：第一次握手：S只可以确认 自己可以接受C发送的报文段，第二次握手：C可以确认 S收到了自己发送的报文段，并且可以确认 自己可以接受S发送的报文段第三次握手：S可以确认 C收到了自己发送的报文段。&lt;/p&gt;
&lt;h2&gt;TCP和UDP的区别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;TCP是面向连接的，UDP是无连接的，就是发送连接之前不需要先建立连接&lt;/li&gt;
&lt;li&gt;TCP提供可靠的服务。通过TCP传输的数据，无差错，不丢失，不重复，按序到达。UDP尽最大努力交付，不保证可靠的交付。因为TCP可靠且面向连接，不会丢失数据因此适合大数据量的交换。&lt;/li&gt;
&lt;li&gt;TCP是面向字节流，UDP面向报文，如果网络故障出现拥塞不会使得发送速率降低，会出现丢包。&lt;/li&gt;
&lt;li&gt;TCP只能1对1，UDP能够1对1，1对多&lt;/li&gt;
&lt;li&gt;TCP首部较大的字节为20字节，UDP只有8字节&lt;/li&gt;
&lt;li&gt;TCP是面向连接的可靠性传输，而UDP是不可靠的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一个图片url访问后直接下载怎样实现？&lt;/h2&gt;
&lt;h3&gt;参考回答：&lt;/h3&gt;
&lt;p&gt;请求的返回头里面，用于浏览器解析的重要参数就是OSS的API文档里面的返回http头，决定用户下载行为的参数。&lt;/p&gt;
&lt;p&gt;下载的情况下：&lt;/p&gt;
&lt;p&gt;\1. x-oss-object-type:&lt;/p&gt;
&lt;p&gt;Normal&lt;/p&gt;
&lt;p&gt;\2. x-oss-request-id:&lt;/p&gt;
&lt;p&gt;598D5ED34F29D01FE2925F41&lt;/p&gt;
&lt;p&gt;\3. x-oss-storage-class:&lt;/p&gt;
&lt;p&gt;Standard&lt;/p&gt;
&lt;h2&gt;http返回的状态码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://pxc3lp.coding-pages.com/2020/03/23/HTTP%E8%BF%94%E5%9B%9E%E7%8A%B6%E6%80%81%E7%A0%81/&quot;&gt;HTTP返回的状态码&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;http常用请求头&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://pxc3lp.coding-pages.com/2020/03/23/HTTP%E5%B8%B8%E7%94%A8%E8%AF%B7%E6%B1%82%E5%A4%B4/&quot;&gt;HTTP常用请求头&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;几个很实用的BOM属性对象方法?&lt;/h2&gt;
&lt;h2&gt;说一下http2.0&lt;/h2&gt;
&lt;p&gt;http2.0是基于http1.0之后的首次更新&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**提升了访问的速率：**相比较于HTTP1.0请求资源所需要的时间更少，访问的速度更快&lt;/li&gt;
&lt;li&gt;**允许多路复用：**多路复用允许同时通过单一的HTTP/2连接发送多重请求-响应信息&lt;/li&gt;
&lt;li&gt;**二进制分帧：**在http2.0会将所有的传输信息分割为更小的信息或者帧，并且对他们进行二进制编码的首部压缩&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;*在地址栏里输入一个URL,到这个页面呈现出来，中间会发生什么？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;首先需要找到这个URL域名的服务器IP，浏览器首先会寻找缓存&lt;/strong&gt;，查看缓存中是否有记录（浏览器缓存 -&amp;gt; 系统缓存 -&amp;gt; 路由器缓存 ），缓存中没则去查找系统的hosts文件记录，如果再没有就去查询DNS服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;浏览器根据得到的IP以及相应的端口号，构建一个HTTP请求，并将这个HTTP请求封装在一个TCP包中，发送给服务器&lt;/strong&gt;（依次经过传输层，网络层，数据链路层，物理层，服务器）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;服务器解析请求，返回相应的HTML给浏览器，浏览器根据HTML构建DOM树&lt;/strong&gt;（在dom树的构建过程中如果遇到JS脚本和外部JS连接，则会停止构建DOM树来执行和下载相应的代码，造成阻塞）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;之后根据外部样式，内部样式，内联样式去构建一个CSS对象模型树叫CSSOM树，构建完成之后和DOM树合并为渲染树&lt;/strong&gt;（ 这里主要做的是排除非视觉节点，比如script，meta标签和排除display为none的节点 ）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进行布局，确定各个元素位置和尺寸&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;渲染页面。&lt;/strong&gt;（ 因为html文件中会含有图片，视频，音频等资源，在解析DOM的过程中，遇到这些都会进行并行下载，浏览器对每个域的并行下载数量有一定的限制，一般是4-6个，当然在这些所有的请求中我们还需要关注的就是缓存，缓存一般通过Cache-Control、Last-Modify、Expires等首部字段控制。 Cache-Control和Expires的区别在于Cache-Control使用相对时间，Expires使用的是基于服务器 端的绝对时间，因为存在时差问题，一般采用Cache-Control，在请求这些有设置了缓存的数据时，会先 查看是否过期，如果没有过期则直接使用本地缓存，过期则请求并在服务器校验文件是否修改，如果上一次 响应设置了ETag值会在这次请求的时候作为If-None-Match的值交给服务器校验，如果一致，继续校验 Last-Modified，没有设置ETag则直接验证Last-Modified，再决定是否返回304 ）&lt;/p&gt;
&lt;p&gt;浏览器缓存可以查看这篇文章：&lt;a href=&quot;https://www.jianshu.com/p/54cc04190252&quot;&gt;深入理解浏览器的缓存机制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;渲染机制可以看：&lt;a href=&quot;https://github.com/ljianshu/Blog/issues/51&quot;&gt;深入浅出浏览器渲染原理&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;GET和POST的区别&lt;/h2&gt;
&lt;h3&gt;参考回答：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get参数通过url传递，post放在request body中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get请求在url中传递的参数是有长度限制的，而post没有。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get比post更不安全，因为参数直接暴露在url中，所以不能用来传递敏感信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get请求只能进行url编码，而post支持多种编码方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get请求会浏览器主动cache，而post支持多种编码方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get请求参数会被完整保留在浏览历史记录里，而post中的参数不会被保留。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GET和POST本质上就是TCP链接，并无差别。但是由于HTTP的规定和浏览器/服务器的限制，导致他们在应用过程中体现出一些不同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GET产生一个TCP数据包；POST产生两个TCP数据包。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;*Cookie、sessionStorage、localStorage的区别&lt;/h2&gt;
&lt;p&gt;共同点：都是保存在浏览器端，并且是同源的&lt;/p&gt;
&lt;p&gt;不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存储环境和大小:&lt;/strong&gt; cookie数据始终在同源的HTTP请求中携带（即便是不需要），即&lt;strong&gt;cookie在浏览器和服务器之间来回传递&lt;/strong&gt;，cookie的数据还有路径的概念，可以限制cookie只属于某个路劲下，存储大小只有4k左右；sessionStorage 和localStorage不会自动把数据发送给服务器，仅在本地缓存，数据量都比cookie来的要大的多&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据持久性不同:&lt;/strong&gt; sessionStorage仅在当前浏览器窗口关闭之前有效果，自然不可能持续保存；localstorage始终有效，窗口或者浏览器关闭也一直保存，因此用作持久数据；cookie只在设置的cookie过期时间之前一直有效，即窗口或浏览器关闭。 （key：本身就是一个回话过程，关闭浏览器后消失，session为一个回话，当页面不同即使是同一页面打开两次，也被视为同一次回话）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用域不同（共享范围）&lt;/strong&gt;：sessionStorage 不在不同的浏览器窗口中共享，即使是同一个页面 ；localStorage在所有的同源窗口中都是共享的；cookie也是在所有的同源窗口中都是共享的。（ 同源窗口都会共享，并且不会失效，不管窗口或者浏览器关闭与否都会始终生效 ）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;cookie的作用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;保存用户的登录状态： 例如将用户id存储于一个cookie内，这样当用户下次访问该页面时就不需要重新登录了，现在很多论坛和社区都提供这样的功能。 cookie还可以设置过期时间，当超过时间期限后，cookie就会自动消失。因此，系统往往可以提示用户保持登录状态的时间：常见选项有一个月、三个 月、一年等。&lt;/li&gt;
&lt;li&gt;跟踪用户的行为： 例如一个天气预报网站，能够根据用户选择的地区显示当地的天气情况。如果每次都需要选择所在地是烦琐的，当利用了cookie后就会显得很人性化了，系统能够记住上一次访问的地区，当下次再打开该页面时，它就会自动显示上次用户所在地区的天气情况。因为一切都是在后 台完成，所以这样的页面就像为某个用户所定制的一样，使用起来非常方便定制页面。如果网站提供了换肤或更换布局的功能，那么可以使用cookie来记录用户的选项，例如：背景色、分辨率等。当用户下次访问时，仍然可以保存上一次访问的界面风格。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>.gitingnore匹配规则以及语法</title><link>https://nollieleo.github.io/posts/gitingnore%E5%8C%B9%E9%85%8D%E8%A7%84%E5%88%99%E4%BB%A5%E5%8F%8A%E8%AF%AD%E6%B3%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/gitingnore%E5%8C%B9%E9%85%8D%E8%A7%84%E5%88%99%E4%BB%A5%E5%8F%8A%E8%AF%AD%E6%B3%95/</guid><description>Git配置 .gitignore文件  git      一. 忽略规则匹配语法  | 通配符 | 说明                               | | ------ | ---------------------------------- | | 空格   | 仅作为分隔符  ...</description><pubDate>Thu, 19 Mar 2020 09:17:13 GMT</pubDate><content:encoded>&lt;h1&gt;Git配置 .gitignore文件&lt;/h1&gt;
&lt;p&gt;git &lt;a href=&quot;https://www.liaoxuefeng.com/wiki/896043488029600&quot;&gt;廖雪峰git教程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git-scm.com/docs/gitignore&quot;&gt;git官网&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;一. 忽略规则匹配语法&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;通配符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;空格&lt;/td&gt;
&lt;td&gt;仅作为分隔符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#&lt;/td&gt;
&lt;td&gt;注释&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!&lt;/td&gt;
&lt;td&gt;不忽略某个文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/ 开头&lt;/td&gt;
&lt;td&gt;仅仅匹配根目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结束 /&lt;/td&gt;
&lt;td&gt;匹配该文件夹及该文件夹下的所有内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;匹配多个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**&lt;/td&gt;
&lt;td&gt;匹配多级目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;？&lt;/td&gt;
&lt;td&gt;匹配单个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[]&lt;/td&gt;
&lt;td&gt;匹配包含单个字符的列表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Tips:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前目录定义的规则优先级高于父级目录定义的规则&lt;/li&gt;
&lt;li&gt;每行指定一条忽略规则&lt;/li&gt;
&lt;li&gt;如果一行规则不包含“/”，则它匹配当前.gitignore文件所在路径的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二. 匹配示例&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;abc/&lt;/td&gt;
&lt;td&gt;# 忽略当前路径下的 abc 文件夹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/abc&lt;/td&gt;
&lt;td&gt;# 忽略根目录下的 abc 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/*.txt&lt;/td&gt;
&lt;td&gt;# 忽略根目录下的 abc.txt，不忽略 app/abc.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abc/*.txt&lt;/td&gt;
&lt;td&gt;# 忽略 abc/abc.txt，不忽略 abc/def/abc.txt 和 app/abc/abc.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**/abc&lt;/td&gt;
&lt;td&gt;# 忽略 /abc、a/abc、a/b/abc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a/**/b&lt;/td&gt;
&lt;td&gt;# 忽略 a/b、a/x/y/b&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!/app/abc.txt&lt;/td&gt;
&lt;td&gt;# 不忽略 app 目录下的 abc.txt 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*.log&lt;/td&gt;
&lt;td&gt;# 忽略所有 .log 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abc.txt&lt;/td&gt;
&lt;td&gt;#忽略当前路径下的 abc.txt 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*abc/&lt;/td&gt;
&lt;td&gt;#忽略名词中末尾为abc的文件夹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;* abc */&lt;/td&gt;
&lt;td&gt;#忽略名称中间包含abc的文件夹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;三.  .gitignore文件不生效&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt;只能忽略那些原来没有被track的文件，如果某些文件已经被纳入了版本管理中，则修改&lt;code&gt;.gitignore&lt;/code&gt;是无效的。所以一定要养成在项目开始就创建&lt;code&gt;.gitignore&lt;/code&gt;文件的习惯。
解决方法就是先把本地缓存删除(改变成未track状态)，然后再提交：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果远程仓库原来就已经存在现在.gitignore规则中忽略的内容，那么现在那些规则对那些内容是无效的，必须先删除本地缓存&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$ git rm -r --cached&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不过，这样做虽然可以删除远程仓库里被 .gitignore 规则忽略的内容，但是这些内容的 git 压缩版本依然被保存在可回滚记录中以备以后回滚，这会导致 git 仓库过大。虽然有一些办法可以给 git 仓库进行瘦身，但是操作比较繁琐，非常的不推荐。因此，个人觉得还是应该有良好的 git 使用习惯，对于不用提交到仓库的内容，要第一时间用 .gitignore 进行过滤。对于占用空间小的内容，可以不予理会，但如果有较多、较大文件，不光是占了太多的远程仓库空间和本地空间，并且在 clone、push 的时候，也会因为仓库过大，而影响 clone 速度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四. 模板&lt;/h2&gt;
&lt;h3&gt;java 开发通用版本模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#java
*.class

#package file
*.war
*.ear
*.zip
*.tar.gz
*.rar
#maven ignore
target/
build/

#eclipse ignore
.settings/
.project
.classpatch

#Intellij idea
.idea/
/idea/
*.ipr
*.iml
*.iws

# temp file
*.log
*.cache
*.diff
*.patch
*.tmp

# system ignore
.DS_Store
Thumbs.db
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;前端项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Numerous always-ignore extensions
*.bak
*.patch
*.diff
*.err
 
# temp file for git conflict merging
*.orig
*.log
*.rej
*.swo
*.swp
*.zip
*.vi
*~
*.sass-cache
*.tmp.html
*.dump
 
# OS or Editor folders
.DS_Store
._*
.cache
.project
.settings
.tmproj
*.esproj
*.sublime-project
*.sublime-workspace
nbproject
thumbs.db
*.iml
 
# Folders to ignore
.hg
.svn
.CVS
.idea
node_modules/
jscoverage_lib/
bower_components/
dist/
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>项目初始化-HTML模板</title><link>https://nollieleo.github.io/posts/%E9%A1%B9%E7%9B%AE%E5%88%9D%E5%A7%8B%E5%8C%96-html%E6%A8%A1%E6%9D%BF/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%A1%B9%E7%9B%AE%E5%88%9D%E5%A7%8B%E5%8C%96-html%E6%A8%A1%E6%9D%BF/</guid><description>我们在项目初始化的时候，都会有一个html的模板，里面会做各种兼容和声明等工作。PC端如果需要适配IE8需要加入很多垫片，而且还要做好双核浏览器的优先选择配置等；M端更是需要做到不同分辨率屏幕的适配，另外还有300ms延迟问题。这里送上笔者在项目当中整理的两个模板，已经经过了一定项目的考验，供大家参...</description><pubDate>Wed, 18 Mar 2020 22:56:11 GMT</pubDate><content:encoded>&lt;p&gt;我们在项目初始化的时候，都会有一个html的模板，里面会做各种兼容和声明等工作。PC端如果需要适配IE8需要加入很多垫片，而且还要做好双核浏览器的优先选择配置等；M端更是需要做到不同分辨率屏幕的适配，另外还有300ms延迟问题。这里送上笔者在项目当中整理的两个模板，已经经过了一定项目的考验，供大家参考。&lt;/p&gt;
&lt;p&gt;另外，本文中涉及的M端适配并非rem，而是修改viewport的显示比例，这个方案相较rem来说起码有2个优点：一是省字符，二是不用计算，设计稿是多少就是多少。不过在实践当中，UC浏览器貌似不认user-scalabe=0，还是会被双击放大，不过笔者认为还是可以接受的。如果还遇到了其他bug，请反馈，谢谢。&lt;/p&gt;
&lt;h2&gt;PC模板&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge,chrome=1&quot; /&amp;gt;
    &amp;lt;meta name=&quot;renderer&quot; content=&quot;webkit&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
&amp;lt;!--[if lt IE 9]&amp;gt;
    &amp;lt;script src=&quot;//cdn.staticfile.org/html5shiv/r29/html5.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;//cdn.staticfile.org/respond.js/1.4.2/respond.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;//cdn.staticfile.org/es5-shim/4.5.9/es5-shim.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;//cdn.staticfile.org/es5-shim/4.5.9/es5-sham.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;![endif]--&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;//cdn.staticfile.org/normalize/6.0.0/normalize.min.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;M模板&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;format-detection&quot; content=&quot;telephone=no&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
    &amp;lt;script&amp;gt;
    (function() {
        var w = 750; // 设计稿尺寸
        document.write(&apos;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=&apos; + w + &apos;, initial-scale=&apos; + 		  window.screen.width / w + &apos;, user-scalable=0&quot; /&amp;gt;&apos;);
    })()
    &amp;lt;/script&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;//cdn.staticfile.org/normalize/6.0.0/normalize.min.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;script src=&quot;//cdn.staticfile.org/fastclick/1.0.6/fastclick.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
    document.addEventListener(&apos;DOMContentLoaded&apos;, function() {
        Origami.fastclick(document.body);
    }, false);
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;公共部分说明&lt;/h2&gt;
&lt;p&gt;1、&amp;lt;!DOCTYPE html&amp;gt;
在 HTML 4.01 中，&amp;lt;!DOCTYPE&amp;gt; 声明引用 DTD，因为 HTML 4.01 基于 SGML。DTD 规定了标记语言的规则，这样浏览器才能正确地呈现内容。HTML5 不基于 SGML，所以不需要引用 DTD。
请始终向 HTML 文档添加 &amp;lt;!DOCTYPE&amp;gt; 声明，这样浏览器才能获知文档类型。它不是 HTML 标签，是指示 web 浏览器关于页面使用哪个 HTML 版本进行编写的指令；必须是 HTML 文档的第一行，位于 &amp;lt;html&amp;gt; 标签之前，没有结束标签，对大小写不敏感；
参考文献：http://www.w3school.com.cn/tags/tag_doctype.asp&lt;/p&gt;
&lt;p&gt;2、&amp;lt;html lang=&quot;zh&quot;&amp;gt;
HTML 的 lang 属性可用于网页或部分网页的语言。这对搜索引擎和浏览器是有帮助的。根据 W3C 推荐标准，您应该通过 &amp;lt;html&amp;gt; 标签中的 lang 属性对每张页面中的主要语言进行声明。
参考文献：http://www.w3school.com.cn/tags/html_ref_language_codes.asp&lt;/p&gt;
&lt;p&gt;3、&amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
当你的 html 文件是以 UTF-8 编码保存的，而且里面有中文，你试试加与不加在 Chrome 的效果你就知道有没有区别了&lt;/p&gt;
&lt;p&gt;4、&amp;lt;meta name=&quot;format-detection&quot; content=&quot;telephone=no,email=no,adress=no&quot; /&amp;gt;
告诉浏览器是否识别特定格式的文本。根据项目需求修改值。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;PC部分说明&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;1、&amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge,chrome=1&quot; /&amp;gt;
控制IE渲染内核的选择。chrome=1针对装了chrome frame插件的IE浏览器起作用，以防万一写上。
参考文献：https://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx&lt;/p&gt;
&lt;p&gt;2、&amp;lt;meta name=&quot;renderer&quot; content=&quot;webkit|ie-comp|ie-stand&quot; /&amp;gt;
控制双核浏览器渲染引擎，content的取值为webkit、ie-comp、ie-stand之一，区分大小写，分别代表用webkit内核，IE兼容内核，IE标准内核。
参考文献：http://se.360.cn/v6/help/meta.html&lt;/p&gt;
&lt;p&gt;3、&amp;lt;!--[if lt IE 9]&amp;gt; //code here
兼容IE9以下（不含）的写法，只有IE认。（例子中js为IE9-的垫片）
参考文献：http://www.weste.net/2013/8-9/93104.html&lt;/p&gt;
&lt;h2&gt;M部分说明&lt;/h2&gt;
&lt;p&gt;1、js输出viewport缩放屏幕，适配不同大小设备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function() {
    var w = 750; //设计稿设备宽度
    document.write(&apos;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=&apos; + w + &apos;, initial-scale=&apos; + 	  window.screen.width / w + &apos;, user-scalable=0&quot; /&amp;gt;&apos;);
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入以上代码，需修改变量值为设计稿的设备宽度，样式全部按照设计稿的数值和单位写就可以。详见：移动端常用布局方法
参考文献：http://www.cnblogs.com/2050/p/3877280.html&lt;/p&gt;
&lt;p&gt;2、fastclick解决iphone等手机的300ms延迟问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;//st01.chrstatic.com/themes/chr-cdn/fastclick/v1.0.6/fastclick.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    document.addEventListener(&apos;DOMContentLoaded&apos;, function() {
        Origami.fastclick(document.body);
    }, false);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先加载fastclick.min.js，之后将需要去除300ms的dom挂上，压缩版写Origami.fastclick(document.body);，非压缩版写FastClick.attach(document.body);即可
参考文献：https://github.com/ftlabs/fastclick/&lt;/p&gt;
</content:encoded></item><item><title>配置前端脚手架的时候碰到的问题</title><link>https://nollieleo.github.io/posts/%E9%85%8D%E7%BD%AE%E5%89%8D%E7%AB%AF%E8%84%9A%E6%89%8B%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%E7%A2%B0%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%85%8D%E7%BD%AE%E5%89%8D%E7%AB%AF%E8%84%9A%E6%89%8B%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%E7%A2%B0%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/</guid><description>这段时间在自己搭建前端的脚手架，配置参照        配置过程中出现了一些问题，  1. Cannot find module &apos;@babel/core&apos; babel-loader@8 requires Babel 7.x (the package &apos;@babel/c     解决帮助文章：  2...</description><pubDate>Wed, 18 Mar 2020 22:42:30 GMT</pubDate><content:encoded>&lt;p&gt;这段时间在自己搭建前端的脚手架，配置参照&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/zhaolandelong/article/details/79620735&quot;&gt; 从零搭建前端开发环境（零）——基础篇：1.npm、git及项目初始化&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/zhaolandelong/article/details/79658026&quot;&gt;从零搭建前端开发环境（零）——基础篇：2.webpack生产与开发环境配置&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/zaifeng0108/p/7268260.html&quot;&gt;webpack引入jquery的几种方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;配置过程中出现了一些问题，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Cannot find module &apos;@babel/core&apos; babel-loader@8 requires Babel 7.x (the package &apos;@babel/c&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;解决帮助文章：&lt;a href=&quot;https://blog.csdn.net/zr15829039341/article/details/86553652&quot;&gt;babel安装问题，Cannot find module &apos;@babel/core&apos; babel-loader@8 requires Babel 7.x (the package &apos;@babel/c&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Error: Cannot find module &apos;webpack/bin/config-yargs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;解决帮助文章：&lt;a href=&quot;https://www.cnblogs.com/zixuan00/p/10974970.html&quot;&gt;Error: Cannot find module &apos;webpack/bin/config-yargs&apos; 报错原因, webpack@4.X踩的坑~&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Error: Loading PostCSS Plugin failed: Cannot find module &apos;autoprefixer&apos;&lt;/code&gt;，解决帮助文章：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_41877243/article/details/101295001&quot;&gt;webpack4 postcss-loader autoprefixer无效问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/wang0112233/article/details/90484800&quot;&gt;Error: Loading PostCSS Plugin failed: Cannot find module &apos;autoprefixer&apos;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;问题正在发生...&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>BFC块级格式化上下文</title><link>https://nollieleo.github.io/posts/bfc%E5%9D%97%E7%BA%A7%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%B8%8A%E4%B8%8B%E6%96%87/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/bfc%E5%9D%97%E7%BA%A7%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%B8%8A%E4%B8%8B%E6%96%87/</guid><description>什么是BFC？  BFC的全称为：块格式化上下文(Block Formatting Context) ，它是布局过程中生成块级盒子的区域，也是浮动元素与其他元素的交互限定区域。简单来说，BFC是一个独立的渲染区域   官方解释：   一个块格式化上下文（block formatting contex...</description><pubDate>Mon, 16 Mar 2020 22:56:11 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/0d713b32cd0d&quot;&gt;参考文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3cplus.com/css/understanding-block-formatting-contexts-in-css.html&quot;&gt;参考文章2&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;什么是BFC？&lt;/h2&gt;
&lt;p&gt;BFC的全称为：&lt;strong&gt;块格式化上下文(Block Formatting Context)&lt;/strong&gt; ，它是布局过程中生成块级盒子的区域，也是浮动元素与其他元素的交互限定区域。简单来说，BFC是一个独立的渲染区域&lt;/p&gt;
&lt;p&gt;官方解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个块格式化上下文（block formatting context） 是Web页面的可视化CSS渲染出的一部分。它是块级盒布局出现的区域，也是浮动层元素进行交互的区域。
一个块格式化上下文由以下之一创建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根元素或其它包含它的元素&lt;/li&gt;
&lt;li&gt;浮动元素 (元素的 &lt;code&gt;float&lt;/code&gt; 不是 &lt;code&gt;none&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;绝对定位元素 (元素具有 &lt;code&gt;position&lt;/code&gt; 为 &lt;code&gt;absolute&lt;/code&gt; 或 &lt;code&gt;fixed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;内联块 (元素具有 &lt;code&gt;display: inline-block&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;表格单元格 (元素具有 &lt;code&gt;display: table-cell&lt;/code&gt;，HTML表格单元格默认属性)&lt;/li&gt;
&lt;li&gt;表格标题 (元素具有 &lt;code&gt;display: table-caption&lt;/code&gt;, HTML表格标题默认属性)&lt;/li&gt;
&lt;li&gt;具有&lt;code&gt;overflow&lt;/code&gt; 且值不是 &lt;code&gt;visible&lt;/code&gt; 的块元素，&lt;/li&gt;
&lt;li&gt;&lt;code&gt;display: flow-root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;column-span: all&lt;/code&gt; 应当总是会创建一个新的格式化上下文，即便具有 &lt;code&gt;column-span: all&lt;/code&gt; 的元素并不被包裹在一个多列容器中。&lt;/li&gt;
&lt;li&gt;一个块格式化上下文包括创建它的元素内部所有内容，除了被包含于创建新的块级格式化上下文的后代元素内的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;块格式化上下文对于定位 (参见 float) 与清除浮动 (参见 clear) 很重要。定位和清除浮动的样式规则只适用于处于同一块格式化上下文内的元素。浮动不会影响其它块格式化上下文中元素的布局，并且清除浮动只能清除同一块格式化上下文中在它前面的元素的浮动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;按照综上所述的话，BFC是一个html盒子然后呢必须至少满足下列其中的任意一个条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;float&lt;/code&gt;的值不为&lt;code&gt;none&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt;不为&lt;code&gt;static&lt;/code&gt;或者&lt;code&gt;relative&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;display&lt;/code&gt;的值为 &lt;code&gt;table-cell&lt;/code&gt;, &lt;code&gt;table-caption&lt;/code&gt;, &lt;code&gt;inline-block&lt;/code&gt;, &lt;code&gt;flex&lt;/code&gt;, 或者 &lt;code&gt;inline-flex&lt;/code&gt;中的其中一个&lt;/li&gt;
&lt;li&gt;overflow的值不为visible&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;遵循规则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;BFC在Web页面上是一个独立的容器，容器内外互不影响&lt;/li&gt;
&lt;li&gt;和标准文档流一样，BFC内的元素垂直方向的边距会发生重叠&lt;/li&gt;
&lt;li&gt;BFC不会与浮动元素的盒子重叠&lt;/li&gt;
&lt;li&gt;计算BFC高度时即使子元素浮动也参与计算&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实现一个BFC&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.container {
	overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
  test BFC
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BFC常见作用（特性，功能）&lt;/h2&gt;
&lt;h3&gt;一. BFC中的盒子对齐&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在BFC中，每个盒子的&lt;strong&gt;左外边框&lt;/strong&gt;紧挨着包含块的&lt;strong&gt;左边框&lt;/strong&gt;（从右到左的格式，则为紧挨右边框）。即使存在浮动也是这样的（尽管一个盒子的边框会由于浮动而收缩），除非这个盒子的内部创建了一个新的BFC浮动，盒子本身将会变得更窄）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Image missing: bfc-1.jpg]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;简单来说，在上图中我们可以看到，所有属于同一个BFC的盒子都左对齐（左至右的格式），他们的左外边框紧贴着包含块的左边框。在最后一个盒子里我们可以看到尽管那里有一个浮动元素（棕色）在它的左边，另一个元素（绿色）仍然紧贴着包含块的左边框。关于为什么会发生这种情况的原理将会在下面的文字环绕部分进行讨论&lt;/p&gt;
&lt;h3&gt;二. BFC导致的外边距折叠&lt;/h3&gt;
&lt;p&gt;在常规文档流中，盒子都是从包含块的顶部开始一个接着一个垂直堆放。两个兄弟盒子之间的垂直距离是由他们个体的外边距所决定的，但不是他们的两个外边距之和。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: bfc-2.jpg]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在上图中我们看到在红色盒子（一个&lt;code&gt;div&lt;/code&gt;）中包含两个绿色的兄弟元素（&lt;code&gt;p&lt;/code&gt;元素），一个BFC已经被创建。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
    overflow: hidden;
    background-color: yellow;
}

.container p {
    margin: 10px 0;
    background-color: green;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;p&amp;gt;hsdsdsddd&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;hsdsdsddd&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理论上两个兄弟元素之间的边距应该是来两个元素的边距之和（&lt;code&gt;20px&lt;/code&gt;），但它实际上为&lt;code&gt;10px&lt;/code&gt;。这就是被称为外边距折叠。当兄弟元素的外边距不一样时，将以最大的那个外边距为准。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200318134622277.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另一种情况就是两个元素的margin值不一样，那么他们两个元素直接的距离就是他们最大的margin距离&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  .container p:first-of-type {
    margin: 10px 0;
    background-color: green;
  }
  .container p:nth-of-type(2){
    margin: 20px 0;
    background-color: green;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200318135655864.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;三. 使用BFC防止外边距折叠&lt;/h3&gt;
&lt;p&gt;毗邻块盒子的垂直外边距折叠只有他们是在同一BFC时才会发生。如果他们属于不同的BFC，BFC是独立的容器， 容器内外互不影响 ，所以他们之间的外边距将不会折叠。所以通过创建一个新的BFC我们可以防止外边距折叠。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;p&amp;gt;hsdsdsddd&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;hsdsdsddd&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;newBfc&quot;&amp;gt;
      &amp;lt;p&amp;gt;sdadsadsa&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  .container {
    overflow: hidden;
    width: 200px;
    background-color: yellow;
  }

  .container p {
    margin: 10px 0;
    background-color: green;
  }

  .newBfc {
    overflow: hidden;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318135206974.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;四. BFC和浮动问题&lt;/h3&gt;
&lt;p&gt;一个BFC可以包含住浮动的元素。一个容器中有浮动元素，我们大多时候是通过清除浮动的方式&lt;code&gt;clearfix&lt;/code&gt;来解决，但也可以通过定义一个BFC来达到目的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.w3cplus.com/sites/default/files/blogs/2015/1508/bfc-4.jpg&quot; alt=&quot;BFC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .container {
      border: 10px solid yellow;
      min-height: 20px;
    }

    .in {
      height: 50px;
      width: 200px;
      float: left;
      background-color: forestgreen;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;in&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318141614117.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;此时如果加上&lt;code&gt;float:left&lt;/code&gt;的话，就会脱离普通的文档流&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318141742296.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;通过使得外层元素产生一个BFC从而包裹住内层浮动的元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .container {
      border: 10px solid yellow;
      min-height: 20px;
      overflow: hidden; 
      /* 产生BFC其中一个条件 */
    }

    .in {
      height: 50px;
      width: 200px;
      float: left;
      background-color: forestgreen;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;in&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318142031107.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;5. 解决浮动后发生元素重叠的问题&lt;/h3&gt;
&lt;p&gt;普通文档流中，当一个元素使用&lt;code&gt;float:left&lt;/code&gt;脱离正常的文档流之后，会使得其他的元素与其发生重叠，BFC可以解决这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .container {
     background: grey;
    }

    .left {
      height: 100px;
      width: 100px;
      background-color: red;
      float: left;
    }
    .right {
      height: 150px;
      width: 150px;
      background: green;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;left&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;right&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318144231982.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;为了使其不发生重叠，可以使得right元素产生BFC（BFC容器内外互不影响）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	.right {
      height: 150px;
      width: 150px;
      background: green;
      overflow: hidden;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318144457476.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;六. 和浮动的元素能够产生边界&lt;/h3&gt;
&lt;p&gt;​	第五点说到了如何用BFC解决浮动产生的两个元素重叠的问题，一般来说要解决这种浮动的问题，都会让第五点中right元素的&lt;code&gt;margin-left&lt;/code&gt;设置为left元素宽度，如果说要和浮动的元素产生边距的话，就需要 &lt;code&gt;距离+left元素宽度&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;​	此时让right元素产生BFC之后，得让left的margin-right设置为你想要的边距。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .container {
     background: grey;
    }

    .left {
      height: 100px;
      width: 100px;
      background-color: red;
      float: left;
      margin-right: 10px;
    }
    .right {
      height: 150px;
      width: 150px;
      background: green;
      overflow: hidden;
      margin-left: 10px;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;left&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;right&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;./image-20200318150941105.png&quot; alt=&quot;&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;七. 使用BFC防止文字环绕&lt;/h3&gt;
&lt;p&gt;有时候一个浮动&lt;code&gt;div&lt;/code&gt;周围的文字环绕着它（如下图中的左图所示）但是在某些案例中这并不是可取的，我们想要的是外观跟下图中的右图一样的。为了解决这个问题，我们可能使用外边距，但是我们也可以使用一个BFC来解决。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Image missing: bfc-5.jpg]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;首先让我们理解文字为什么会环绕。为此我们需要知道当一个元素浮动时盒子模型是如何工作的。&lt;/p&gt;
&lt;p&gt;这个&lt;code&gt;p&lt;/code&gt;元素并没有移动，但是它却出现在浮动元素的下方。&lt;code&gt;p&lt;/code&gt;元素的&lt;code&gt;line boxes&lt;/code&gt;（指的是文本行）进行了移位。此处&lt;code&gt;line boxes&lt;/code&gt;的水平收缩为浮动元素提供了空间。&lt;/p&gt;
&lt;p&gt;随着文字的增加，因为&lt;code&gt;line boxes&lt;/code&gt;不再需要移位,最终将会环绕在浮动元素的下方，因此出现了那样的情况。这就解释了为什么即使在浮动元素存在时，段落也将紧贴在包含块的左边框上，还有为什么&lt;code&gt;line boxes&lt;/code&gt;会缩小以容纳浮动元素。&lt;/p&gt;
&lt;p&gt;如果我们能够移动整个&lt;code&gt;p&lt;/code&gt;元素，那么这个环绕的问题就可以解决了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在BFC中，每个盒子的左外边框紧挨着左边框的包含块（从右到左的格式化时，则为右边框紧挨）。即使在浮动里也是这样的（尽管一个盒子的边框会因为浮动而萎缩），除非这个盒子的内部创建了一个新的BFC（这种情况下,由于浮动，盒子本身将会变得更窄）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt; 
    &amp;lt;div class=&quot;floated&quot;&amp;gt;Floated div&amp;lt;/div&amp;gt; 
    &amp;lt;p&amp;gt;
        Quae hic ut ab perferendis sit quod architecto,dolor 			debitis quam rem provident aspernatur tempora expedita.
    &amp;lt;/p&amp;gt; 
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200318151645115.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此为了使其不产生这样文字环绕的效果可以使得p产生一个BFC，这样就不会紧紧挨了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .container {
     background: grey;
    }

    .floated {
      height: 20px;
      width: 100px;
      background-color: red;
      float: left;
    }
    p{
      display: flow-root;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt; 
    &amp;lt;div class=&quot;floated&quot;&amp;gt;Floated div&amp;lt;/div&amp;gt; 
    &amp;lt;p&amp;gt;
        Quae hic ut ab perferendis sit quod architecto,dolor 			debitis quam rem provident aspernatur tempora expedita.
    &amp;lt;/p&amp;gt; 
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200318152917968.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;八. 在多列布局中使用BFC&lt;/h3&gt;
&lt;p&gt;如果我们正在创建的一个多列布局占满了整个容器的宽度，在某些浏览器中最后一列有时候将会被挤到下一行。会发生这样可能是因为浏览器舍入（取整）了列的宽度使得总和的宽度超过了容器的宽度。然而，如果我们在一个列的布局中建立了一个新的BFC，它将会在前一列填充完之后的后面占据所剩余的空间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;style&amp;gt;
    .column {
      width: 31.33%;
      height: 100px;
      background-color: green;
      float: left;
      margin: 0 1%;
    }

    .column:last-child {
      float: none;
      overflow: hidden;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;column&quot;&amp;gt;column 1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;column&quot;&amp;gt;column 2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;column&quot;&amp;gt;column 3&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20200318153802285.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>原型模式和基于原型继承的Javascript对象系统</title><link>https://nollieleo.github.io/posts/%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%E5%92%8C%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B%E7%BB%A7%E6%89%BF%E7%9A%84javascript%E5%AF%B9%E8%B1%A1%E7%B3%BB%E7%BB%9F/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%E5%92%8C%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B%E7%BB%A7%E6%89%BF%E7%9A%84javascript%E5%AF%B9%E8%B1%A1%E7%B3%BB%E7%BB%9F/</guid><description>在以类为中心的面向对象编程语言中，类和对象的关系可以想象成铸模和铸件的关系，对象 总是&lt;u从类中创建&lt;/u而来。而在原型编程的思想中，类并不是必需的，对象未必需要从类中创建而来， 一个对象是通过克隆另外一个对象所得到的。   1. 使用克隆的原型模式  从设计模式的角度讲，原型模式是用于创建对象的一...</description><pubDate>Mon, 16 Mar 2020 22:28:09 GMT</pubDate><content:encoded>&lt;p&gt;在&lt;strong&gt;以类为中心的面向对象编程&lt;/strong&gt;语言中，类和对象的关系可以想象成铸模和铸件的关系，对象
总是&amp;lt;u&amp;gt;从类中创建&amp;lt;/u&amp;gt;而来。而在&lt;strong&gt;原型编程&lt;/strong&gt;的思想中，类并不是必需的，对象未必需要从类中创建而来，
一个对象是通过克隆另外一个对象所得到的。&lt;/p&gt;
&lt;h2&gt;1. 使用克隆的原型模式&lt;/h2&gt;
&lt;p&gt;从设计模式的角度讲，原型模式是用于创建对象的一种模式，如果我们想要创建一个对象，
一种方法是先指定它的类型，然后通过类来创建这个对象。原型模式选择了另外一种方式，我们
不再关心对象的具体类型，而是找到一个对象，然后通过克隆来创建一个一模一样的对象&lt;/p&gt;
&lt;p&gt;既然原型模式是通过克隆来创建对象的，那么很自然地会想到，如果需要一个跟某个对象一
模一样的对象，就可以使用原型模式&lt;/p&gt;
&lt;p&gt;原型模式的实现关键，是语言本身是否提供了 clone 方法。ECMAScript 5提供了 Object.create
方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var Plane = function () {
    this.blood = 100;
    this.attackLevel = 1;
    this.defenceLevel = 1;
}
var myPlane = new Plane();
myPlane.blood = 500;
myPlane.attackLevel = 10;
myPlane.defenceLevel = 7;
// 在不支持 Object.create 方法的浏览器中，则可以使用以下代码
Object.create = Object.create || function (obj) {
    var F = function () { };
    F.prototype = obj;
    return new F();
}
var clonePlane = Object.create(myPlane);
console.log(clonePlane);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1579487242257.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. 原型编程范型的一些规则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;所有的数据都是对象&lt;/li&gt;
&lt;li&gt;要得到一个对象，不是通过实例化类，而是找到一个对象作为原型并克隆它。&lt;/li&gt;
&lt;li&gt;对象会记住它的原型。&lt;/li&gt;
&lt;li&gt;如果对象无法响应某个请求，它会把这个请求委托给它自己的原型。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. JavaScript中的原型继承(构建对象系统)&lt;/h2&gt;
&lt;p&gt;同理，js也同样遵循以上的编程规则&lt;/p&gt;
&lt;h3&gt;1. 所有的数据都是对象&lt;/h3&gt;
&lt;p&gt;JavaScript在设计的时候，模仿 Java 引入了两套类型机制：基本类型和对象类型。基本类型包括 undefined 、 number 、 boolean 、 string 、 function 、 object&lt;/p&gt;
&lt;p&gt;除了 undefined 之外，一切都应是对象。为了实现这一目标，number 、 boolean 、 string 这几种基本类型数据也可以通过“包装类”的方式变成对象类型数据来处理&lt;/p&gt;
&lt;p&gt;不能说在 JavaScript中所有的数据都是对象，但可以说绝大部分数据都是对象。那么相信在 JavaScript中也一定会有一个根对象存在，这些对象追根溯源都来源于这个根对象。&lt;/p&gt;
&lt;p&gt;JavaScript 中的根对象是 &lt;code&gt;Object.prototype&lt;/code&gt; 对象。 &lt;code&gt;Object.prototype&lt;/code&gt; 对象是一个空的对象。我们在JavaScript 遇到的每个对象，实际上都是从 &lt;code&gt;Object.prototype&lt;/code&gt; 对象克隆而来的，&lt;code&gt;Object.prototype&lt;/code&gt; 对象就是它们的原型。&lt;/p&gt;
&lt;p&gt;可以利用 ECMAScript 5提供的 Object.getPrototypeOf 来查看这两个对象的原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj1 = new Object();
var obj2 = {};
console.log( Object.getPrototypeOf( obj1 ) === Object.prototype ); // 输出：true
console.log( Object.getPrototypeOf( obj2 ) === Object.prototype ); // 输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 要得到一个对象，不是通过实例化类，而是找到一个对象作为原型并克隆它&lt;/h3&gt;
&lt;p&gt;在 JavaScript语言里，我们并不需要关心克隆的细节，因为这是引擎内部负责实现的。我
们所需要做的只是显式地调用 &lt;code&gt;var obj1 = new Object()&lt;/code&gt; 或者 &lt;code&gt;var obj2 = {}&lt;/code&gt; 。此时，引擎内部会从&lt;code&gt;Object.prototype&lt;/code&gt; 上面克隆一个对象出来，我们最终得到的就是这个对象.&lt;/p&gt;
&lt;p&gt;用 new 运算符从构造器中得到一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person( name ){
	this.name = name;
};
Person.prototype.getName = function(){
	return this.name;
};
var a = new Person( &apos;sven&apos; )
console.log( a.name ); // 输出：sven
console.log( a.getName() ); // 输出：sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 Person 并不是类，而是函数构造器，JavaScript 的函数既可以作为普通函数被调用，也可以作为构造器被调用,当使用 new 运算符来调用函数时，此时的函数就是一个构造器。 用new 运算符来创建对象的过程，实际上也只是先克隆 Object.prototype 对象，再进行一些其他额外操作的过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以模拟new的过程：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 模拟new的实现方式
// 先来一个Person构造器

function Person(name) {
    this.name = name;
}

Person.prototype.getName = function () {
    return this.name;
}

var objectFactory = function () {
    var obj = new Object(), //从Object.prototype上克隆一个空的对象{}
        Constructor = [].shift.call(arguments); // 取得外部传入的构造器，此例是 Person
    obj.__proto__ = Constructor.prototype; // 指向正确的原型
    var ret = Constructor.apply(obj, arguments); // 借用外部传入的构造器给 obj 设置属性
    return typeof ret === &apos;object&apos; ? ret : obj; // 确保构造器总是会返回一个对象
}

var a = objectFactory(Person, &apos;weng&apos;);
console.log(a.getName());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 对象会记住它的原型&lt;/h3&gt;
&lt;p&gt;对象的原型”，就 JavaScript 的真正实现来说，其实并不能说对象有原型，而只能说&lt;strong&gt;对象的构造器有原型&lt;/strong&gt;。对于“对象把请求委托给它自己的原型”这句话，更好的说法是&lt;strong&gt;对象把请求委托给它的构造器的原型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JavaScript 给对象提供了一个名为 &lt;code&gt;__proto__&lt;/code&gt; 的隐藏属性，某个对象的 &lt;code&gt;__proto__&lt;/code&gt; 属性默认会指向它的构造器的原型对象，即 &lt;code&gt;{Constructor}.prototype &lt;/code&gt;。在一些浏览器中， &lt;code&gt;__proto__&lt;/code&gt; 被公开出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = new Object();
console.log ( a.__proto__=== Object.prototype ); // 输出：true 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__proto__&lt;/code&gt; 就是对象跟“对象构造器的原型”联系起来的纽带。正因为对象要通过&lt;code&gt;_proto__&lt;/code&gt; 属性来记住它的构造器的原型，所以我们的 objectFactory 函数来模拟用 new创建对象时， 需要手动给 obj 对象设置正确的 &lt;code&gt;__proto__&lt;/code&gt; 指向&lt;/p&gt;
&lt;p&gt;&lt;code&gt;obj.__proto__ = Constructor.prototype;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;让 obj.&lt;strong&gt;proto&lt;/strong&gt; 指向 Person.prototype ，而不是原来的 Object.prototype&lt;/p&gt;
&lt;h3&gt;4. 如果对象无法响应某个请求，它会把这个请求委托给它的构造器的原型&lt;/h3&gt;
&lt;p&gt;在 JavaScript 中，每个对象都是从 Object.prototype 对象克隆而来的，如果是这样的话，
我们只能得到单一的继承关系，即每个对象都继承自 Object.prototype 对象，这样的对象系统显
然是非常受限的。&lt;/p&gt;
&lt;p&gt;实际上，虽然 JavaScript 的对象最初都是由 Object.prototype 对象克隆而来的，但对象构造
器的原型并不仅限于 Object.prototype 上，而是可以动态指向其他对象。这样一来，当对象 a 需
要借用对象 b 的能力时，可以有选择性地把对象 a 的构造器的原型指向对象 b ，从而达到继承的
效果&lt;/p&gt;
&lt;h4&gt;常用原型继承代码解析&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var obj = { name: &apos;sven&apos; };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log( a.name ); // 输出：sven
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;首先，尝试遍历对象 a 中的所有属性，但没有找到 name 这个属性&lt;/li&gt;
&lt;li&gt;查找 name 属性的这个请求被委托给对象 a 的构造器的原型，它被 &lt;code&gt;a. __proto__&lt;/code&gt; 记录着并且
指向 &lt;code&gt;.prototype&lt;/code&gt; ，而 &lt;code&gt;A.prototype&lt;/code&gt; 被设置为对象 obj&lt;/li&gt;
&lt;li&gt;在对象 obj 中找到了 name 属性，并返回它的值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们期望得到一个“类”继承自另外一个“类”的效果时，往往会用下面的代码来模拟实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var A = function(){};
A.prototype = { name: &apos;sven&apos; };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log( b.name ); // 输出：sven
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;首先，尝试遍历对象 b 中的所有属性，但没有找到 name 这个属性&lt;/li&gt;
&lt;li&gt;查找 name 属性的请求被委托给对象 b 的构造器的原型，它被 &lt;code&gt;b. __proto__&lt;/code&gt; 记录着并且指向
&lt;code&gt;B.prototype&lt;/code&gt; ，而 &lt;code&gt;B.prototype &lt;/code&gt;被设置为一个通过 &lt;code&gt;new A() &lt;/code&gt;创建出来的对象&lt;/li&gt;
&lt;li&gt;在该对象中依然没有找到 name 属性，于是请求被继续委托给这个对象构造器的原型
&lt;code&gt;A.prototype&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 A.prototype 中找到了 name 属性，并返回它的值&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>基本数据类型和引用数据类型</title><link>https://nollieleo.github.io/posts/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%BC%95%E7%94%A8%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%BC%95%E7%94%A8%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</guid><description>ECMA script中的数据类型     1. 基本数据类型   （undefined，boolean，number，string，null）  基本数据类型主要是：undefined，boolean，number，string，null。   1.1 基本数据类型存放在栈中...</description><pubDate>Mon, 16 Mar 2020 22:28:09 GMT</pubDate><content:encoded>&lt;h2&gt;ECMA script中的数据类型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 基本数据类型&lt;/h3&gt;
&lt;h3&gt;（&lt;code&gt;undefined，boolean，number，string，null&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;基本数据类型主要是：&lt;code&gt;undefined，boolean，number，string，null&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;1.1 基本数据类型存放在栈中&lt;/h4&gt;
&lt;p&gt;存放在栈内存中的简单数据段，数据大小确定，内存空间大小可以分配，是直接按值存放的，所以可以直接访问。&lt;/p&gt;
&lt;h4&gt;1.2 基本数据类型值不可变&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;javascript中的原始值（undefined、null、布尔值、数字和字符串）与对象（包括数组和函数）有着根本区别。原始值是不可更改的：任何方法都无法更改（或“突变”）一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通，而对字符串来说就不那么明显了，因为字符串看起来像由字符组成的数组，我们期望可以通过指定索引来假改字符串中的字符。实际上，javascript 是禁止这样做的。&lt;strong&gt;字符串中所有的方法看上去返回了一个修改后的字符串，实际上返回的是一个新的字符串值&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;基本数据类型的值是不可变的，动态修改了基本数据类型的值，它的原始值也是不会改变的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var str = &quot;abc&quot;;

console.log(str[1]=&quot;f&quot;);    // f

console.log(str);           // abc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一点其实开始我是比较迷惑的，总是感觉 js 是一个灵活的语言，任何值应该都是可变的，真是图样图森破，我们通常情况下都是对一个变量重新赋值，而不是改变基本数据类型的值。就如上述引用所说的那样，在 js 中没有方法是可以改变布尔值和数字的。倒是有很多操作字符串的方法，但是这些方法都是返回一个新的字符串，并没有改变其原有的数据。&lt;/p&gt;
&lt;p&gt;所以，记住这一点：&lt;strong&gt;基本数据类型值不可变&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;1.3 基本类型的比较是值的比较&lt;/h4&gt;
&lt;p&gt;基本类型的比较是值的比较，只要它们的值相等就认为他们是相等的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = 1;
var b = 1;
console.log(a === b);//true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较的时候最好使用严格等，因为 &lt;code&gt;==&lt;/code&gt; 是会进行类型转换的，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = 1;
var b = true;
console.log(a == b);//true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 引用类型&lt;/h3&gt;
&lt;h4&gt;2.1 引用类型存放在堆中&lt;/h4&gt;
&lt;p&gt;引用类型（&lt;code&gt;object&lt;/code&gt;）是存放在堆内存中的，变量实际上是一个存放在栈内存的指针，这个指针指向堆内存中的地址。每个空间大小不一样，要根据情况开进行特定的分配，例如。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var person1 = {name:&apos;jozo&apos;};
var person2 = {name:&apos;xiaom&apos;};
var person3 = {name:&apos;xiaoq&apos;};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;堆内存&quot; /&gt;堆内存&lt;/p&gt;
&lt;h4&gt;2.2  引用类型值可变&lt;/h4&gt;
&lt;p&gt;引用类型是可以直接改变其值的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.3 引用类型的比较是引用的比较&lt;/h4&gt;
&lt;p&gt;所以每次我们对 js 中的引用类型进行操作的时候，都是操作其对象的引用（保存在栈内存中的指针），所以比较两个引用类型，是看其的引用是否指向同一个对象。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false复制代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然变量 a 和变量 b 都是表示一个内容为 1，2，3 的数组，但是其在内存中的位置不一样，也就是说变量 a 和变量 b 指向的不是同一个对象，所以他们是不相等的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;引用类型在内存中的存储&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 传值与传址&lt;/h3&gt;
&lt;p&gt;了解了基本数据类型与引用类型的区别之后，我们就应该能明白传值与传址的区别了。
在我们进行赋值操作的时候，基本数据类型的赋值（=）是在内存中新开辟一段栈内存，然后再把再将值赋值到新的栈中。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;基本数据类型的赋值&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以说，基本类型的赋值的两个变量是两个独立相互不影响的变量。&lt;/p&gt;
&lt;p&gt;但是引用类型的赋值是传址。只是改变指针的指向，例如，也就是说引用类型的赋值是对象保存在栈中的地址的赋值，这样的话两个变量就指向同一个对象，因此两者之间操作互相有影响。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = {}; // a保存了一个空对象的实例
var b = a;  // a和b都指向了这个空对象

a.name = &apos;jozo&apos;;
console.log(a.name); // &apos;jozo&apos;
console.log(b.name); // &apos;jozo&apos;

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;引用类型的赋值&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>鸭子类型</title><link>https://nollieleo.github.io/posts/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B/</guid><description>我们可以通过一个小故事来更深刻地了解鸭子类型。 从前在 JavaScript王国里，有一个国王，他觉得世界上最美妙的声音就是鸭子的叫 声，于是国王召集大臣，要组建一个 1000 只鸭子组成的合唱团。大臣们找遍了全国， 终于找到 999只鸭子，但是始终还差一只，最后大臣发现有一只非常特别的鸡，它的叫 ...</description><pubDate>Mon, 16 Mar 2020 22:22:27 GMT</pubDate><content:encoded>&lt;p&gt;我们可以通过一个小故事来更深刻地了解鸭子类型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从前在 JavaScript王国里，有一个国王，他觉得世界上最美妙的声音就是鸭子的叫
声，于是国王召集大臣，要组建一个 1000 只鸭子组成的合唱团。大臣们找遍了全国，
终于找到 999只鸭子，但是始终还差一只，最后大臣发现有一只非常特别的鸡，它的叫
声跟鸭子一模一样，于是这只鸡就成为了合唱团的最后一员。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./1579395626197.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个故事告诉我们，国王要听的只是鸭子的叫声，这个声音的主人到底是鸡还是鸭并不重要。
鸭子类型指导我们只关注对象的行为，而不关注对象本身，也就是关注 HAS-A, 而不是 IS-A。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var duck = {
    type: &apos;duck&apos;,
    duckSinging: function () {
        console.log(&apos;gagagag&apos;);
    }
}
var chicken = {
    type: &apos;chicken&apos;,
    duckSinging: function () {
        console.log(&apos;gagagag&apos;);
    }
}

var choir = [];

var joinChoir = function (animal) {
    if (animal &amp;amp;&amp;amp; typeof animal.duckSinging === &apos;function&apos;) {
        choir.push(animal);
        console.log(`恭喜${animal.type}加入合唱团`);
        console.log(`合唱团已有人数为${choir.length}`);
    }
}
joinChoir(duck);
joinChoir(chicken);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到，对于加入合唱团的动物，大臣们根本无需检查它们的类型，而是只需要保证它们
拥有 duckSinging 方法。如果下次期望加入合唱团的是一只小狗，而这只小狗刚好也会鸭子叫，
我相信这只小狗也能顺利加入。&lt;/p&gt;
&lt;p&gt;在动态类型语言的面向对象设计中，鸭子类型的概念至关重要。利用鸭子类型的思想，我们
不必借助超类型的帮助，就能轻松地在&amp;lt;u&amp;gt;动态类型语言中实现一个原则&amp;lt;/u&amp;gt;：“&lt;strong&gt;面向接口编程，而不是&lt;/strong&gt;
&lt;strong&gt;面向实现编程&lt;/strong&gt;*”。例如，一个对象若有 push 和 pop 方法，并且这些方法提供了正确的实现，它就
可以被当作栈来使用。一个对象如果有 length 属性，也可以依照下标来存取属性（最好还要拥
有 slice 和 splice 等方法），这个对象就可以被当作数组来使用。&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;静态类型语言&lt;/strong&gt;中，要实现“面向接口编程”并不是一件容易的事情，往往要通过抽象类或
者接口等将对象进行向上转型。当对象的真正类型被隐藏在它的超类型身后，这些对象才能在类
型检查系统的“监视”之下互相被替换使用。只有当对象能够被互相替换使用，才能体现出对象
&lt;strong&gt;多态性&lt;/strong&gt;的价值。&lt;/p&gt;
&lt;p&gt;“面向接口编程”是设计模式中最重要的思想，但在 JavaScript语言中，“面向接口编程”的
过程跟主流的静态类型语言不一样，因此，在 JavaScript中实现设计模式的过程与在一些我们熟
悉的语言中实现的过程会大相径庭。&lt;/p&gt;
</content:encoded></item><item><title>深拷贝与浅拷贝</title><link>https://nollieleo.github.io/posts/%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D2/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B7%B1%E6%8B%B7%E8%B4%9D2/</guid><description>深拷贝与浅拷贝  一下部分参照知乎中的提问：    浅拷贝   赋值（=）和浅拷贝的区别  那么赋值和浅拷贝有什么区别呢，我们看下面这个例子：  js var obj1 = {     &apos;name&apos; : &apos;zhangsan&apos;,     &apos;age&apos; :  &apos;18&apos;,     &apos;language&apos; : ...</description><pubDate>Mon, 16 Mar 2020 22:02:38 GMT</pubDate><content:encoded>&lt;h1&gt;深拷贝与浅拷贝&lt;/h1&gt;
&lt;p&gt;一下部分参照知乎中的提问： &lt;a href=&quot;https://www.zhihu.com/question/23031215&quot;&gt;javascript中的深拷贝和浅拷贝&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;浅拷贝&lt;/h2&gt;
&lt;h3&gt;赋值（=）和浅拷贝的区别&lt;/h3&gt;
&lt;p&gt;那么赋值和浅拷贝有什么区别呢，我们看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj1 = {
    &apos;name&apos; : &apos;zhangsan&apos;,
    &apos;age&apos; :  &apos;18&apos;,
    &apos;language&apos; : [1,[2,3],[4,5]],
};

var obj2 = obj1;


var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}

obj2.name = &quot;lisi&quot;;
obj3.age = &quot;20&quot;;

obj2.language[1] = [&quot;二&quot;,&quot;三&quot;];
obj3.language[2] = [&quot;四&quot;,&quot;五&quot;];

console.log(obj1);  
//obj1 = {
//    &apos;name&apos; : &apos;lisi&apos;,
//    &apos;age&apos; :  &apos;18&apos;,
//    &apos;language&apos; : [1,[&quot;二&quot;,&quot;三&quot;],[&quot;四&quot;,&quot;五&quot;]],
//};

console.log(obj2);
//obj2 = {
//    &apos;name&apos; : &apos;lisi&apos;,
//    &apos;age&apos; :  &apos;18&apos;,
//    &apos;language&apos; : [1,[&quot;二&quot;,&quot;三&quot;],[&quot;四&quot;,&quot;五&quot;]],
//};

console.log(obj3);
//obj3 = {
//    &apos;name&apos; : &apos;zhangsan&apos;,
//    &apos;age&apos; :  &apos;20&apos;,
//    &apos;language&apos; : [1,[&quot;二&quot;,&quot;三&quot;],[&quot;四&quot;,&quot;五&quot;]],
//};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先定义个一个原始的对象 &lt;code&gt;obj1&lt;/code&gt;，然后使用赋值得到第二个对象 &lt;code&gt;obj2&lt;/code&gt;，然后通过浅拷贝，将 &lt;code&gt;obj1&lt;/code&gt; 里面的属性都赋值到 &lt;code&gt;obj3&lt;/code&gt; 中。也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;obj1&lt;/code&gt;：原始数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;obj2&lt;/code&gt;：赋值操作得到&lt;/li&gt;
&lt;li&gt;&lt;code&gt;obj3&lt;/code&gt;：浅拷贝得到&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后我们改变 &lt;code&gt;obj2&lt;/code&gt; 的 &lt;code&gt;name&lt;/code&gt; 属性和 &lt;code&gt;obj3&lt;/code&gt; 的 &lt;code&gt;name&lt;/code&gt; 属性，可以看到，改变赋值得到的对象 &lt;code&gt;obj2&lt;/code&gt; 同时也会改变原始值 &lt;code&gt;obj1&lt;/code&gt;，而改变浅拷贝得到的的 &lt;code&gt;obj3&lt;/code&gt; 则不会改变原始对象 &lt;code&gt;obj1&lt;/code&gt;。这就可以说明赋值得到的对象 &lt;code&gt;obj2&lt;/code&gt; 只是将指针改变，其引用的仍然是同一个对象，而浅拷贝得到的的 &lt;code&gt;obj3&lt;/code&gt; 则是重新创建了新对象。&lt;/p&gt;
&lt;p&gt;然而，我们接下来来看一下改变引用类型会是什么情况呢，我又改变了赋值得到的对象 &lt;code&gt;obj2&lt;/code&gt; 和浅拷贝得到的 &lt;code&gt;obj3&lt;/code&gt; 中的 &lt;code&gt;language&lt;/code&gt; 属性的第二个值和第三个值（&lt;code&gt;language&lt;/code&gt; 是一个数组，也就是引用类型）。结果见输出，可以看出来，无论是修改赋值得到的对象 &lt;code&gt;obj2&lt;/code&gt; 和浅拷贝得到的 &lt;code&gt;obj3&lt;/code&gt; 都会改变原始数据。&lt;/p&gt;
&lt;p&gt;这是因为浅拷贝只复制一层对象的属性，并不包括对象里面的为引用类型的数据。所以就会出现改变浅拷贝得到的 &lt;code&gt;obj3&lt;/code&gt; 中的引用类型时，会使原始数据得到改变。&lt;/p&gt;
&lt;p&gt;深拷贝：将 B 对象拷贝到 A 对象中，包括 B 里面的子对象，&lt;/p&gt;
&lt;p&gt;浅拷贝：将 B 对象拷贝到 A 对象中，但不包括 B 里面的子对象&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;--&lt;/th&gt;
&lt;th&gt;和原数据是否指向同一对象&lt;/th&gt;
&lt;th&gt;第一层数据为基本数据类型&lt;/th&gt;
&lt;th&gt;原数据中包含子对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;赋值&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;改变会使原数据一同改变&lt;/td&gt;
&lt;td&gt;改变会使原数据一同改变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浅拷贝&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;改变&lt;strong&gt;不&lt;/strong&gt;会使原数据一同改变&lt;/td&gt;
&lt;td&gt;改变会使原数据一同改变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;深拷贝&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;改变&lt;strong&gt;不&lt;/strong&gt;会使原数据一同改变&lt;/td&gt;
&lt;td&gt;改变&lt;strong&gt;不&lt;/strong&gt;会使原数据一同改变&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;深拷贝&lt;/h2&gt;
&lt;p&gt;看了这么半天，你也应该清楚什么是深拷贝了吧，如果还不清楚，我就剖腹自尽(ಥ_ಥ)&lt;/p&gt;
&lt;p&gt;深拷贝是对对象以及对象的所有子对象进行拷贝。&lt;/p&gt;
&lt;p&gt;那么问题来了，怎么进行深拷贝呢？&lt;/p&gt;
&lt;p&gt;思路就是递归调用刚刚的浅拷贝，把所有属于对象的属性类型都遍历赋给另一个对象即可。我们直接来看一下 Zepto 中深拷贝的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 内部方法：用户合并一个或多个对象到第一个对象
// 参数：
// target 目标对象  对象都合并到target里
// source 合并对象
// deep 是否执行深度合并
function extend(target, source, deep) {
    for (key in source)
        if (deep &amp;amp;&amp;amp; (isPlainObject(source[key]) || isArray(source[key]))) {
            // source[key] 是对象，而 target[key] 不是对象， 则 target[key] = {} 初始化一下，否则递归会出错的
            if (isPlainObject(source[key]) &amp;amp;&amp;amp; !isPlainObject(target[key]))
                target[key] = {}

            // source[key] 是数组，而 target[key] 不是数组，则 target[key] = [] 初始化一下，否则递归会出错的
            if (isArray(source[key]) &amp;amp;&amp;amp; !isArray(target[key]))
                target[key] = []
            // 执行递归
            extend(target[key], source[key], deep)
        }
        // 不满足以上条件，说明 source[key] 是一般的值类型，直接赋值给 target 就是了
        else if (source[key] !== undefined) target[key] = source[key]
}

// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target){
    var deep, args = slice.call(arguments, 1);

    //第一个参数为boolean值时，表示是否深度合并
    if (typeof target == &apos;boolean&apos;) {
        deep = target;
        //target取第二个参数
        target = args.shift()
    }
    // 遍历后面的参数，都合并到target上
    args.forEach(function(arg){ extend(target, arg, deep) })
    return target
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;利用weakMap实现递归clone&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function clone(target, weakMap = new weakMap) {
    if ((typeof target === &apos;object&apos;)) {
        let cloneTarget = Array.isArray.call(target) ? [] : {};
        if (weakMap.get(target)) {
            return weakMap.get(target);
        }
        weakMap.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], weakMap);
        }
        return cloneTarget;
    } else {
        return target;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;扩展运算符（...） 是什么类型的拷贝？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;浅拷贝&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>静态类型语言与动态类型语言区别</title><link>https://nollieleo.github.io/posts/%E9%9D%99%E6%80%81%E7%B1%BB%E5%9E%8B%E8%AF%AD%E8%A8%80%E4%B8%8E%E5%8A%A8%E6%80%81%E7%B1%BB%E5%9E%8B%E8%AF%AD%E8%A8%80%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%9D%99%E6%80%81%E7%B1%BB%E5%9E%8B%E8%AF%AD%E8%A8%80%E4%B8%8E%E5%8A%A8%E6%80%81%E7%B1%BB%E5%9E%8B%E8%AF%AD%E8%A8%80%E5%8C%BA%E5%88%AB/</guid><description>编程语言按照数据类型大体可以分为两类，一类是静态类型语言，另一类是动态类型语言   区别  静态类型语言在编译时便&lt;u已确定变量的类型&lt;/u，而动态类型语言的变量类型要到程序运行的时 候，待变量被赋予某个值之后，才会具有某种类型。  静态类型语言的优点首先是在编译时就能发现类型不匹配的错误，编辑器可...</description><pubDate>Mon, 16 Mar 2020 17:46:53 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;编程语言按照数据类型大体可以分为两类，一类是静态类型语言，另一类是动态类型语言&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;区别&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;静态类型语言&lt;/strong&gt;在编译时便&amp;lt;u&amp;gt;已确定变量的类型&amp;lt;/u&amp;gt;，而&lt;strong&gt;动态类型语言&lt;/strong&gt;的变量类型要到程序运行的时
候，待变量被赋予某个值之后，才会具有某种类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态类型语言&lt;/strong&gt;的&lt;em&gt;优点&lt;/em&gt;首先是在编译时就能发现类型不匹配的错误，编辑器可以帮助我们提前
避免程序在运行期间有可能发生的一些错误。其次，如果在程序中明确地规定了数据类型，编译
器还可以针对这些信息对程序进行一些优化工作，提高程序执行速度&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态类型语言&lt;/strong&gt;的&lt;em&gt;缺点&lt;/em&gt;首先是迫使程序员依照强契约来编写程序，为每个变量规定数据类型，
归根结底只是辅助我们编写可靠性高程序的一种手段，而不是编写程序的目的，毕竟大部分人编
写程序的目的是为了完成需求交付生产。其次，类型的声明也会增加更多的代码，在程序编写过
程中，这些细节会让程序员的精力从思考业务逻辑上分散开来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态类型语言&lt;/strong&gt;的&lt;em&gt;优点&lt;/em&gt;是编写的代码数量更少，看起来也更加简洁，程序员可以把精力更多地
放在业务逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解，但整体而言，代码量
越少，越专注于逻辑表达，对阅读程序是越有帮助的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态类型语言&lt;/strong&gt;的&lt;em&gt;缺点&lt;/em&gt;是无法保证变量的类型，从而在程序的运行期有可能发生跟类型相关的
错误。&lt;/p&gt;
&lt;p&gt;javascript是一门动态类型语言，动态类型语言对变量类型的宽容给实际编码带来了很大的灵活性。由于无需进行类型检测，我们可以尝试调用任何对象的任意方法，而无需去考虑它原本是否被设计为拥有该方法。&lt;/p&gt;
</content:encoded></item><item><title>高阶组件</title><link>https://nollieleo.github.io/posts/%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E9%AB%98%E9%98%B6%E7%BB%84%E4%BB%B6/</guid><description>高阶组件  1.高阶组件定义 高阶组件就是一个函数，且该函数接受一个组件作为参数，并返回一个新的组件   2.函数模拟高阶函数 javascript function banana() {     let name = &apos;wengkaimin&apos;;     console.log(${name} li...</description><pubDate>Mon, 16 Mar 2020 17:43:47 GMT</pubDate><content:encoded>&lt;h1&gt;高阶组件&lt;/h1&gt;
&lt;h2&gt;1.高阶组件定义&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;高阶组件就是一个函数，且该函数接受一个组件作为参数，并返回一个新的组件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2.函数模拟高阶函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function banana() {
    let name = &apos;wengkaimin&apos;;
    console.log(`${name} likes banana`);
}
function apple(myName) {
    let name = &apos;wengkaimin&apos;;
    console.log(`${name} likes apple`);
}
banana();
apple();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上有冗余的代码，在平时开发过程中可能会有一大堆
&amp;lt;span style=&quot;color:#eb1414&quot;&amp;gt;写一个中间函数来处理这个相同的代码&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function banana(myName) {
    console.log(`${myName} likes banana`);
}
function apple(myName) {
    console.log(`${myName} likes apple`)
}
function getFruit(fun) {
    let myFruit = (() =&amp;gt; {
        let myName = &apos;wengkaimin&apos;;
        fun(myName);
    })
    return myFruit
}
banana = getFruit(banana);
apple = getFruit(apple);
banana();
apple();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getFruit&lt;/code&gt;函数就叫做一个高阶函数，他帮我们处理了前面两个函数的相同模块，帮忙把myName自动的加入了所有继承&lt;code&gt;getFruit&lt;/code&gt;的函数，此场景在react中的&lt;strong&gt;高阶组件&lt;/strong&gt;有广泛的应用。&lt;/p&gt;
&lt;h2&gt;3.react中的高阶组件&lt;/h2&gt;
&lt;p&gt;改成react高阶组件形式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import React, { Component } from &apos;react&apos;

class Banana extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: &apos;&apos;
        }
    }

    componentWillMount() {
        let username = &apos;wengkaimin&apos;
        this.setState({
            username: username
        })
    }

    render() {
        return (
            &amp;lt;div&amp;gt; {this.state.username} likes banana&amp;lt;/div&amp;gt;
        )
    }
}

export default Banana;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
import React, { Component } from &apos;react&apos;

class Apple extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: &apos;&apos;
        }
    }

    componentWillMount() {
        let username = &apos;wengkaimin&apos;
        this.setState({
            username: username
        })
    }

    render() {
        return (
            &amp;lt;div&amp;gt; {this.state.username} likes apple&amp;lt;/div&amp;gt;
        )
    }
}

export default Apple;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从两段代码来看有很多重复的组件代码。按照高阶函数的思想我们中间封装一个&lt;strong&gt;高阶组件&lt;/strong&gt;来处理这些东西。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, {Component} from &apos;react&apos;

export default (fruitComponent) =&amp;gt; {
    class Myfruit extends Component {
        constructor() {
            super();
            this.state = {
                username: &apos;&apos;
            }
        }

        componentWillMount() {
            let username = &apos;wengkaimin&apos;
            this.setState({
                username: username
            })
        }

        render() {
            return &amp;lt;fruitComponent username={this.state.username}/&amp;gt;
        }
    }

    return Myfruit
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简化apple和banana组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, {Component} from &apos;react&apos;;
import Myfruit from &apos;Myfruit&apos;;

class Banana extends Component {

    render() {
        return (
            &amp;lt;div&amp;gt; {this.props.username} likes banana&amp;lt;/div&amp;gt;
        )
    }
}

Banana = Myfruit(Banana);

export default Banana;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理apple高阶组件把&lt;code&gt;username&lt;/code&gt;通过props把他传给目标组件。&lt;/p&gt;
</content:encoded></item><item><title>多态</title><link>https://nollieleo.github.io/posts/%E5%A4%9A%E6%80%81/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%A4%9A%E6%80%81/</guid><description>多态的实际含义是：同一操作作用于不同的对象上面，可以产生不同的解释和不同的执行结 果。换句话说，给不同的对象发送同一个消息的时候，这些对象会根据这个消息分别给出不同的 反馈。  javascript var makeSound = function (animal) {     if(a...</description><pubDate>Mon, 16 Mar 2020 17:16:30 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;多态的实际含义是：同一操作作用于不同的对象上面，可以产生不同的解释和不同的执行结
果。换句话说，给不同的对象发送同一个消息的时候，这些对象会根据这个消息分别给出不同的
反馈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var makeSound = function (animal) {
    if(animal instanceof Duck){
        console.log(&apos;嘎嘎嘎&apos;);
    }else if(animal instanceof Dog){
        console.log(&apos;汪汪&apos;);
    }
}
var Duck = function(){};
var Dog = function(){};

makeSound(new Duck());
makeSound(new Dog());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	这段代码确实体现了“多态性”，当我们分别向鸭和鸡发出“叫唤”的消息时，它们根据此
消息作出了各自不同的反应。但这样的“多态性”是无法令人满意的，如果后来又增加了一只动
物，比如狗，显然狗的叫声是“汪汪汪”，此时我们必须得改动 makeSound 函数，才能让狗也发出
叫声。修改代码总是危险的，修改的地方越多，程序出错的可能性就越大，而且当动物的种类越
来越多时， makeSound 有可能变成一个巨大的函数。&lt;/p&gt;
&lt;p&gt;​	多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来，也就是将“不变的事
物”与 “可能改变的事物”分离开来。在这个故事中，动物都会叫，这是不变的，但是不同类
型的动物具体怎么叫是可变的。把不变的部分隔离出来，把可变的部分封装起来，这给予了我们
扩展程序的能力，程序看起来是可生长的，也是符合&lt;strong&gt;开放 — 封闭原则&lt;/strong&gt;的，相对于修改代码来说，
仅仅增加代码就能完成同样的功能，这显然优雅和安全得多&lt;/p&gt;
&lt;h3&gt;1. 对象的多态性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var makeSound = function (animal) {
    if (typeof animal === &apos;object&apos; &amp;amp;&amp;amp; animal.sound) {
        animal.sound();
    } else {
        console.log(&apos;我不是动物我不能叫&apos;)
    }
}
var Duck = function () { };
var Dog = function () { };

Duck.prototype.sound = function () {
    console.log(&apos;嘎嘎嘎嘎&apos;);
}

makeSound(new Duck());
makeSound(new Dog());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. JavaScript的多态&lt;/h3&gt;
&lt;p&gt;多态的思想实际上是把“做什么”和“谁去做”分离开来，要实现这一点，归根结底先要消除类型之间的耦合关系。如果类型之间的耦合关系没有被消除，那么我们在 makeSound 方法中指定了发出叫声的对象是某个类型，它就不可能再被替换为另外一个类型。&lt;/p&gt;
&lt;p&gt;在 Java中，可以通过向上转型来实现多态。&lt;/p&gt;
&lt;p&gt;而 JavaScript的变量类型在运行期是可变的。一个 JavaScript对象，既可以表示 Duck 类型的
对象，又可以表示 Chicken 类型的对象，这意味着 JavaScript对象的多态性是与生俱来的&lt;/p&gt;
&lt;p&gt;这种与生俱来的多态性并不难解释。JavaScript作为一门动态类型语言，它在编译时没有类型
检查的过程，既没有检查创建的对象类型，又没有检查传递的参数类型。&lt;/p&gt;
&lt;p&gt;由此可见，某一种动物能否发出叫声，只取决于它有没有 makeSound 方法，而不取决于它是
否是某种类型的对象，这里不存在任何程度上的“类型耦合”。在 JavaScript中，并不需要诸如向上转型之类的技术来取得多态的效果。&lt;/p&gt;
&lt;h3&gt;3. 多态在面向对象程序设计中的作用&lt;/h3&gt;
&lt;p&gt;有许多人认为，多态是面向对象编程语言中最重要的技术。但我们目前还很难看出这一点，
毕竟大部分人都不关心鸡是怎么叫的，也不想知道鸭是怎么叫的。让鸡和鸭在同一个消息之下发
出不同的叫声，这跟程序员有什么关系呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Martin Fowler在《重构：改善既有代码的设计》里写到：
多态的最根本好处在于，你不必再向对象询问“你是什么类型”而后根据得到的答
案调用对象的某个行为——你只管调用该行为就是了，其他的一切多态机制都会为你安
排妥当。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;换句话说，&lt;strong&gt;多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性，从而&lt;/strong&gt;
&lt;strong&gt;消除这些条件分支语句。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Martin Fowler的话可以用下面这个例子很好地诠释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在电影的拍摄现场，当导演喊出“action”时，主角开始背台词，照明师负责打灯
光，后面的群众演员假装中枪倒地，道具师往镜头里撒上雪花。在得到同一个消息时，
每个对象都知道自己应该做什么。如果不利用对象的多态性，而是用面向过程的方式来
编写这一段代码，那么相当于在电影开始拍摄之后，导演每次都要走到每个人的面前，
确认它们的职业分工（类型），然后告诉他们要做什么。如果映射到程序中，那么程序
中将充斥着条件分支语句。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;利用对象的多态性，导演在发布消息时，就不必考虑各个对象接到消息后应该做什么。对象
应该做什么并不是临时决定的，而是已经事先约定和排练完毕的。每个对象应该做什么，已经成
为了该对象的一个方法，被安装在对象的内部，每个对象负责它们自己的行为。所以这些对象可
以根据同一个消息，有条不紊地分别进行各自的工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将行为分布在各个对象中，并让这些对象各自负责自己的行为，这正是面向对象设计的优点。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>封装</title><link>https://nollieleo.github.io/posts/%E5%B0%81%E8%A3%85/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E5%B0%81%E8%A3%85/</guid><description>封装  封装的目的是将信息隐藏。一般而言，我们讨论的封装是封装数据和封装实现。这一节将讨 论更广义的封装，不仅包括封装数据和封装实现，还包括封装类型和封装变化。   1. 封装数据  在许多语言的对象系统中，封装数据是由语法解析来实现的，这些语言也许提供了 private 、 pub...</description><pubDate>Mon, 16 Mar 2020 17:16:30 GMT</pubDate><content:encoded>&lt;h2&gt;封装&lt;/h2&gt;
&lt;p&gt;封装的目的是将信息隐藏。一般而言，我们讨论的封装是封装数据和封装实现。这一节将讨
论更广义的封装，不仅包括封装数据和封装实现，还包括封装类型和封装变化。&lt;/p&gt;
&lt;h3&gt;1. 封装数据&lt;/h3&gt;
&lt;p&gt;在许多语言的对象系统中，封装数据是由语法解析来实现的，这些语言也许提供了 private 、
public 、 protected 等关键字来提供不同的访问权限。&lt;/p&gt;
&lt;p&gt;但 JavaScript并没有提供对这些关键字的支持，我们只能依赖变量的作用域来实现封装特性，
而且只能模拟出 public 和 private 这两种封装性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var myObject = (function () {
    var _name = &apos;sven&apos;;
    return {
        getName: function () {
            return _name;
        }
    }
})()
console.log(myObject.getName());		
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 封装实现&lt;/h3&gt;
&lt;p&gt;封装的目的是将信息隐藏，封装应该被视为“任何形式的封装”，也就是说，封装不仅仅是
隐藏数据，还包括隐藏实现细节、设计细节以及隐藏对象的类型等&lt;/p&gt;
&lt;p&gt;从封装实现细节来讲，封装使得对象内部的变化对其他对象而言是透明的，也就是不可见的。
对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合松散，对象之间只通过暴露的 API接口来通信。当我们修改一个对象时，可以随意地修改它的
内部实现，只要对外的接口没有变化，就不会影响到程序的其他功能。&lt;/p&gt;
</content:encoded></item><item><title>this,call和apply</title><link>https://nollieleo.github.io/posts/this-call%E5%92%8Capply/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/this-call%E5%92%8Capply/</guid><description>this, call 和apply   1. this  JavaScript的 this 总是指向一个对象，而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的，而非函数被声明时的环境   1.1 this指向  - 作为对象方法调用 - 作为普通函数调用 - 构造器调用 - Function...</description><pubDate>Mon, 16 Mar 2020 17:16:30 GMT</pubDate><content:encoded>&lt;h1&gt;this, call 和apply&lt;/h1&gt;
&lt;h2&gt;1. this&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;JavaScript的 this 总是指向一个对象，而具体指向哪个对象是&lt;strong&gt;在运行时基于函数的执行环境动态绑定&lt;/strong&gt;的，而非函数被声明时的环境&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.1 this指向&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;作为对象方法调用&lt;/li&gt;
&lt;li&gt;作为普通函数调用&lt;/li&gt;
&lt;li&gt;构造器调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function.prototype.apply&lt;/code&gt;或``Function.prototype.call`&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.1.1 作为对象方法调用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var obj = {
    a:1,
    getA:function(){
        alert(this === obj);
        alert(this.a);
    }
}
obj.getA();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.1.2 作为普通函数调用&lt;/h4&gt;
&lt;p&gt;当函数&amp;lt;u&amp;gt;不作为对象的属性&amp;lt;/u&amp;gt;被调用时，也就是我们常说的普通函数方式，此时的 &lt;strong&gt;this 总是指向全局&lt;/strong&gt;对象。在浏览器的 JavaScript里，这个全局对象是 window 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.name = &apos;globalName&apos;;
var myObject = {
    name: &apos;sven&apos;,
    getName: function () {
        return this.name;
    }
};
var getName = myObject.getName; //作为普通函数，这时候this指向全局，因为它不是以对象的属性调用的，可以看1.2
console.log(getName()); // globalName
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.1.3 构造器调用&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;JavaScript 中没有类，但是可以从构造器中创建对象，同时也提供了 new 运算符，使得构造器看起来更像一个类&lt;/p&gt;
&lt;p&gt;除了宿主提供的一些内置函数，大部分 JavaScript函数都可以当作构造器使用。构造器的外表跟普通函数一模一样，它们的区别在于被调用的方式。当用 new 运算符调用函数时，该函数总会返回一个对象，通常情况下，构造器里的 this 就指向返回的这个对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var MyClass = function(){
this.name = &apos;sven&apos;;
};
var obj = new MyClass();
alert ( obj.name ); // 输出：sven
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但用 new 调用构造器时，还要注意一个问题，如果构造器显式地返回了一个 object 类型的对象，那么此次运算结果最终会返回这个对象，而不是我们之前期待的 this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var MyClass = function(){
this.name = &apos;sven&apos;;
return { // 显式地返回一个对象
name: &apos;anne&apos;
}
};
var obj = new MyClass();
alert ( obj.name ); // 输出：anne
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果构造器不显式地返回任何数据，或者是返回一个非对象类型的数据，就不会造成上述
问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var MyClass = function(){
this.name = &apos;sven&apos;
return &apos;anne&apos;; // 返回 string 类型
};
var obj = new MyClass();
alert ( obj.name ); // 输出：sven
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.1.4  Function.prototype.call 或 Function.prototype.apply 调用&lt;/h4&gt;
&lt;p&gt;跟普通的函数调用相比，用 Function.prototype.call 或 Function.prototype.apply 可以动态地
改变传入函数的 this ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj1 = {
	name: &apos;sven&apos;,
	getName: function(){
		return this.name;
	}
};
var obj2 = {
	name: &apos;anne&apos;
};
console.log( obj1.getName() ); // 输出: sven
console.log( obj1.getName.call( obj2 ) ); // 输出：anne
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 丢失的this&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var obj = {
myName: &apos;sven&apos;,
	getName: function(){
	return this.myName;
	}
};
console.log( obj.getName() ); // 输出：&apos;sven&apos;
var getName2 = obj.getName;
console.log( getName2() ); // 输出：undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用 obj.getName 时， getName 方法是作为 obj 对象的属性被调用的，当用&lt;strong&gt;另外一个变量&lt;/strong&gt; getName2 来引用 obj.getName ，并且调用getName2 时，此时是以普通函数的形式来调用的， this 是指向全局 window 的&lt;/p&gt;
&lt;p&gt;再来个栗子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById = (function( func ){
	return function(){
		return func.apply( document, arguments );
}
})( document.getElementById );
var getId = document.getElementById;
var div = getId( &apos;div1&apos; );
alert (div.id); // 输出： div1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用 apply 把 document 当作 this 传入 getId 函数，帮助“修正” this&lt;/p&gt;
&lt;p&gt;否则如果想要简化document.getElementById单纯的是不可取的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; var getId = document.getElementById;
        console.log(getId(&apos;hello&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. call 和apply&lt;/h2&gt;
&lt;h3&gt;2.1 区别&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Function.prototype.call &lt;/code&gt;和 &lt;code&gt;Function.prototype.apply &lt;/code&gt;都是非常常用的方法。它们的作用一模一样，区别仅在于传入参数形式的不同&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;apply&lt;/strong&gt; 接受两个参数，&lt;strong&gt;第一个参数&lt;/strong&gt;指定了函数体内 this 对象的指向，&lt;strong&gt;第二个参数&lt;/strong&gt;为一个带下标的集合，这个集合可以为&amp;lt;u&amp;gt;数组&amp;lt;/u&amp;gt;，也可以为&amp;lt;u&amp;gt;类数组&amp;lt;/u&amp;gt;,apply 方法把这个集合中的元素作为参数传递给被调用的函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;call&lt;/strong&gt; 传入的参数数量不固定，跟 apply 相同的是，第一个参数也是代表函数体内的 this 指向，从&lt;strong&gt;第二个参数&lt;/strong&gt;开始往后，每个参数被依次传入函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var func = function (a, b, c) {
    alert([a, b, c]); // 输出 [ 1, 2, 3 ]
};
func.call(null, 1, 2, 3);

func.apply(null, [1, 2, 3]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用一个函数时，JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别，JavaScript的参数在内部就是用一个数组来表示的。从这个意义上说， apply 比 call 的使用率更高，我们不必关心具体有多少参数被传入函数，只要用 apply 一股脑地推过去就可以了&lt;/p&gt;
&lt;p&gt;&amp;lt;u&amp;gt;当使用 call 或者 apply 的时候，如果我们传入的第一个参数为 null ，函数体内的 this 会指向默认的宿主对象，在浏览器中则是 window&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;p&gt;有时候我们使用 call 或者 apply 的目的不在于指定 this 指向，而是另有用途，比如借用其
他对象的方法。那么我们可以传入 null 来代替某个具体的对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出：5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 call 和 apply 的用途&lt;/h3&gt;
&lt;h4&gt;2.2.1 改变 this 指向&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;window.name = &apos;motherfucker&apos;;
obj1 = {
    name: &apos;helloobj1&apos;,
}
obj2 = {
    name: &apos;hellowobj2&apos;,
}
var getName = function () {
    console.log(this.name);
}
getName();
getName.apply(obj1);
getName.apply(obj2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1579747558506.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实际开发过程中会遇到this指向被不经意改变&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById( &apos;div1&apos; ).onclick = function(){
	alert( this.id ); // 输出：div1
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如该事件函数中有一个内部函数 func ，在事件内部调用 func 函数时， func 函数体内的 this就指向了 window ，而不是我们预期的 div&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById( &apos;div1&apos; ).onclick = function(){
	alert( this.id ); // 输出：div1
	var func = function(){
		alert ( this.id ); // 输出：undefined
	}
	func();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法有两种，一种是之前提到的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById(&apos;hello&apos;).onclick = function () {
    var that = this;
    console.log(that.id); // 输出：hello
    var func = function () {
        console.log(that.id); // 输出：undefined
    }
    func();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种就是call和apply&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById( &apos;div1&apos; ).onclick = function(){
	var func = function(){
		alert ( this.id ); // 输出：div1
	}
	func.call( this );
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>布局详解</title><link>https://nollieleo.github.io/posts/flex%E5%B8%83%E5%B1%80%E8%AF%A6%E8%A7%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/flex%E5%B8%83%E5%B1%80%E8%AF%A6%E8%A7%A3/</guid><description>flex布局深入了解   1. flex布局   flex容器的属性  | 属性名称          | 属性含义                                     | 属性可能的值                                               ...</description><pubDate>Mon, 16 Mar 2020 17:05:18 GMT</pubDate><content:encoded>&lt;h2&gt;flex布局深入了解&lt;/h2&gt;
&lt;h3&gt;1. flex布局&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;flex容器的属性&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名称&lt;/th&gt;
&lt;th&gt;属性含义&lt;/th&gt;
&lt;th&gt;属性可能的值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-direction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;决定主轴的方向&lt;/td&gt;
&lt;td&gt;&lt;code&gt;row&lt;/code&gt;(默认) 水平，起点在左端&amp;lt;br&amp;gt;&lt;code&gt;row-reverse&lt;/code&gt;水平，起点在右端&amp;lt;br&amp;gt;&lt;code&gt;column&lt;/code&gt;:垂直，起点在上沿&lt;code&gt;column-reverse&lt;/code&gt;:垂直，起点在下沿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-wrap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;决定一条轴线放不下，如何换行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Nowrap&lt;/code&gt;(默认) 不换行&amp;lt;br&amp;gt;&lt;code&gt;Wrap&lt;/code&gt;:换行，第一行在上面&amp;lt;br&amp;gt;&lt;code&gt;Wrap-reverse&lt;/code&gt;:换行，第一行在下面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-flow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是上面两个属性的简写&lt;/td&gt;
&lt;td&gt;默认值是 &lt;code&gt;row nowrap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;justify-content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义项目在主轴上的对齐方式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Flex-start&lt;/code&gt;(默认值）左对齐&amp;lt;br&amp;gt;&lt;code&gt;Flex-end&lt;/code&gt; 右对齐&amp;lt;br&amp;gt;&lt;code&gt;Center&lt;/code&gt;居中&amp;lt;br&amp;gt;&lt;code&gt;Space-between&lt;/code&gt;:两端对齐，项目之间的间隔都相等&amp;lt;br&amp;gt;&lt;code&gt;Space-around&lt;/code&gt;:每个项目之间的间隔相等，所以项目之间的间隔比项目与边框之间的间隔大一倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;align-items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义项目在交叉轴上如何对齐&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Flex-start&lt;/code&gt;交叉轴的起点对齐&amp;lt;br&amp;gt;&lt;code&gt;Flex-end&lt;/code&gt;交叉轴的终点对齐&amp;lt;br&amp;gt;&lt;code&gt;Center&lt;/code&gt;:交叉轴的中点对齐&amp;lt;br&amp;gt;&lt;code&gt;Baseline&lt;/code&gt;：项目的第一行文字的基线对齐&amp;lt;br&amp;gt;&lt;code&gt;Stretch&lt;/code&gt; （默认值）如果项目未设置高度或者设为auto，将占满整个容器的高度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;align-content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义多跟轴线对齐方式，一条轴线该属性不起作用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Flex-start&lt;/code&gt;: 与交叉轴的起点对齐&amp;lt;br&amp;gt;&lt;code&gt;Flex-end&lt;/code&gt; 与交叉轴的终点对齐&amp;lt;br&amp;gt;&lt;code&gt;Center&lt;/code&gt;:与交叉轴的中点对齐&amp;lt;br&amp;gt;&lt;code&gt;Space-between&lt;/code&gt;:与交叉轴的两端对齐，轴线之间的间隔平均分布&amp;lt;br&amp;gt;&lt;code&gt;Space-around&lt;/code&gt;：每根轴线之间的间隔都相等，所以轴线之间的间隔比轴线与边框之间的间隔大一倍&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;**flex容器下面项目的属性 **&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名称&lt;/th&gt;
&lt;th&gt;属性含义&lt;/th&gt;
&lt;th&gt;属性可能的值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义项目的排列顺序，数值越小，排列越靠前&lt;/td&gt;
&lt;td&gt;默认0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-grow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义项目的放大比例，如果存在剩余空间，不放大&lt;/td&gt;
&lt;td&gt;默认0（如果所有项目的&lt;code&gt;flex-grow&lt;/code&gt;属性为1，则等分剩余空间）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-shrink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义项目的缩小比例&lt;/td&gt;
&lt;td&gt;默认1 负值对该属性无效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex-basis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义在分配多余空间之前，项目占据的主轴空间，浏览器根据这个属性来计算主轴是否有多余空间&lt;/td&gt;
&lt;td&gt;默认&lt;code&gt;auto&lt;/code&gt;,即项目本来大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是上面三个的简写&lt;/td&gt;
&lt;td&gt;默认值 0 1 auto 后两个值可选&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;align-self&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许单个项目与其他项目不一样的对齐方式，可覆盖&lt;code&gt;align-items&lt;/code&gt;属性&lt;/td&gt;
&lt;td&gt;默认值auto 表示继承父元素的&lt;code&gt;align-items&lt;/code&gt;属性，如果没有父元素，则等同于&lt;code&gt;stretch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.1 常用的垂直居中&lt;/h3&gt;
&lt;h4&gt;css&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;.container {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 500px;
            background: #666;
        }

.box {
            width: 200px;
            height: 200px;
            background: #ff0;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Html&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
       &amp;lt;div class=&quot;box&quot;&amp;gt;hello 翁恺敏&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图：&lt;br /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_ecda5fce38594ff7b893af8dd5cb68c4_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.2 常用的ul,li布局横向等宽排列&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_cfc8e57740604e10bd9378889fde1e68_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Html&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul class=&quot;container&quot;&amp;gt;
        &amp;lt;li&amp;gt;翁&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;恺&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;敏&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;你&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;好&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;css&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;.container {
            width: 500px;
            display: flex;
            list-style: none;
            padding: 0;
        }
        .container &amp;gt;li{
            flex: 1;
            height: 50px;
        }
        .container &amp;gt;li:nth-of-type(2n){
            background: red
        }
        .container &amp;gt;li:nth-of-type(2n-1){
            background: green
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 骰子布局详解&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_f6079317a2e3445796478176bc306d7c_blob.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_7a65571cbb25432a9de4a6557c40b141_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2.3.1 单项目&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;首先，只有左上角1个点的情况。Flex布局默认就是首行左对齐，所以一行代码就够了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_45c389ba290f42c3af741f50942f5a88_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;设置项目的对齐方式，就能实现居中对齐和右对齐。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_05b27b46c345437797abed62d73b959a_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_a845891f97f6458fa000d0e1ad7d79ee_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;设置交叉轴对齐方式，可以垂直移动主轴。垂直居中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_8b036161a4df4824b1063f829207a698_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_5434f7b72e9b4aa5a28859941622ff2d_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: center;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_be4232dfd726459b92a1ae3e80ce4342_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: center;
  align-items: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_37247d42221d40babf93bc802e10f617_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: flex-end;
  align-items: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;2.3.2 双项目&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_4843452462524a108303291045b5cdf4_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_e8b334c7d27d437e95c9c60e560c333f_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_7007b1fd8de2451184e637542da7043c_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_5a55fd7f8e564cc987637c07375ec35b_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_dd0c4cb6c6294864b5ee55eaf70999b7_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
}

.item:nth-child(2) {
  align-self: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_6c83dbcb80c247909bf08fec24431e69_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  justify-content: space-between;
}

.item:nth-child(2) {
  align-self: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;2.3.3 三项目&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_c807a61bd2ea4fa08d2ddb89f490da88_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
}

.item:nth-child(2) {
  align-self: center;
}

.item:nth-child(3) {
  align-self: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;2.3.3 四项目&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_679f1c5cec374c8da5a46484ff8b4e8e_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_d9295ac403c24c11926e9a69875e00fb_blob.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
HTML&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;box&quot;&amp;gt;
  &amp;lt;div class=&quot;column&quot;&amp;gt;
    &amp;lt;span class=&quot;item&quot;&amp;gt;&amp;lt;/span&amp;gt;
    &amp;lt;span class=&quot;item&quot;&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;column&quot;&amp;gt;
    &amp;lt;span class=&quot;item&quot;&amp;gt;&amp;lt;/span&amp;gt;
    &amp;lt;span class=&quot;item&quot;&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;css&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-wrap: wrap;
  align-content: space-between;
}

.column {
  flex-basis: 100%;
  display: flex;
  justify-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.3.4  六项目&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_1ad2ee2075364173bc1464044f04395d_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
.box {
  display: flex;
  flex-wrap: wrap;
  align-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_c5505f05517e4619bcd2f768936da6e9_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 圣杯布局（常见）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_6843bc851bbd4a6aa3477ca0286cce47_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;html&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;header&amp;gt;Header&amp;lt;/header&amp;gt;
        &amp;lt;div class=&quot;content&quot;&amp;gt;
            &amp;lt;main&amp;gt;Main&amp;lt;/main&amp;gt;
            &amp;lt;nav&amp;gt;Nav&amp;lt;/nav&amp;gt;
            &amp;lt;aside&amp;gt;Aside&amp;lt;/aside&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;footer&amp;gt;Footer&amp;lt;/footer&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;css&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;* {
            margin: 0;
            padding: 0;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        .container {
            display: flex;
            /* 垂直*/
            flex-direction: column;
            width: 100%;
            /*视口被均分为100单位的vh 占据整个窗口*/
            min-height: 100vh;
        }

        header,
        footer {
            background: #999;
            /*放大缩小比例为0 占据垂直方向80px*/
            flex: 0 0 80px;
        }

        .content {
            display: flex;
            /*1 1 auto 后两个值省略*/
            flex: 1;
        }

        nav {
            /*默认 0 数值越小 排列越靠前*/
            order: -1;
            flex: 0 0 80px;
            background: royalblue;
        }

        aside {
            flex: 0 0 80px;
            background: aqua;
        }

        main {
            flex: 1;
            background: green;
        }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>==与===区别</title><link>https://nollieleo.github.io/posts/%E4%B8%8E%E5%8C%BA%E5%88%AB/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/%E4%B8%8E%E5%8C%BA%E5%88%AB/</guid><description>- - -   1. 相等操作符（==和===）   1.1 使用==时，不同类型的值也可以被看作相等     如果x和y是相同类型，JavaScript会比较它们的值或对象值。其他没有列在这个表格中的情况都会返回false   toNumber和toPrimitive方法是内部的  toNumbe...</description><pubDate>Mon, 16 Mar 2020 17:02:08 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;1. 相等操作符（==和===）&lt;/h2&gt;
&lt;h3&gt;1.1 使用==时，不同类型的值也可以被看作相等&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_0b0bfadb90a7452b8eb2fb44f8e88f8a_blob.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
如果x和y是相同类型，JavaScript会比较它们的值或对象值。其他没有列在这个表格中的情况都会返回false&lt;br /&gt;
&lt;code&gt;toNumber&lt;/code&gt;和&lt;code&gt;toPrimitive&lt;/code&gt;方法是内部的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;toNumber&lt;/code&gt;方法对不同类型返回的结果如下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_c4601e1b344d4034955ca19797b6ad87_blob.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;&lt;code&gt;toPrimitive&lt;/code&gt;方法对不同类型返回的结果如下：&lt;/strong&gt;&lt;br /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_632feccc1abe4987b2486be2d569c538_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;验证：&lt;/strong&gt;&lt;br /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_6a5f8349c2af4d09bde1228ad09e52db_blob.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_c11654ac8cef4a929335162c9581cd7c_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;显而易见结果并不相同，为啥？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，布尔值会被&lt;code&gt;toNumber&lt;/code&gt;方法转成数字，因此得到&lt;code&gt;wengkaimin == 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;其次，用&lt;code&gt;toNumber&lt;/code&gt;转换字符串值。因为字符串包含有字母，所以会被转成&lt;code&gt;NaN&lt;/code&gt;，表达式 就变成了&lt;code&gt;NaN == 1&lt;/code&gt;，结果就是false。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这行代码也是同样原理
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_48da7c9e7cd04724af9e357e9d13756c_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，布尔值会被&lt;code&gt;toNumber&lt;/code&gt;方法转成数字，因此得到&lt;code&gt;wengkaimin == 0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;其次，用&lt;code&gt;toNumber&lt;/code&gt;转换字符串值。因为字符串包含有字母，所以会被转成NaN，表达式 就变成了&lt;code&gt;NaN == 0&lt;/code&gt;，结果就是false&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一样的，只是将字符串转数字了，‘1’转1（number）类型
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_02c4ada486aa40b398f33c755291a954_blob.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_57971f84bd334956a89df7b8695326a2_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.2   &lt;code&gt;===&lt;/code&gt; 操作符&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;如果比较的两个值类型不同，比较的结果就是false。如果 比较的两个值类型相同，结果会根据下表判断&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://minio.choerodon.com.cn/knowledgebase-service/file_bc093fa43e6a418296e132a1612a19ae_blob.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果x和y类型不同，结果就是&lt;code&gt;false&lt;/code&gt;。
**例子： **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;packt&apos; === true); //false  
console.log(&apos;packt&apos; === &apos;packt&apos;); //true  
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;var person1 = {name:&apos;John&apos;}; 
var person2 = {name:&apos;John&apos;}; 
console.log(person1 === person2); //false，不同的对象
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>nodejs学习记录</title><link>https://nollieleo.github.io/posts/nodejs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/nodejs%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</guid><description>1.node自带模块fs文件管理     1.1 readdir和readdirSync的区别   两者都用与读取文件或者文件夹里的文件有啥  ---  fs.readdir(path[, options], callback)异步函数，需要传入一个回调  - path &lt;string &lt;buffe...</description><pubDate>Mon, 16 Mar 2020 15:17:47 GMT</pubDate><content:encoded>&lt;h3&gt;1.node自带模块fs文件管理&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/fs.html&quot;&gt;nodejs官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1.1 readdir和readdirSync的区别&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;两者都用与读取文件或者文件夹里的文件有啥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;fs.readdir(path[, options], callback)&lt;/code&gt;异步函数，需要传入一个回调&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt; &amp;lt;string&amp;gt; &amp;lt;buffer&amp;gt;&amp;lt;url&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;options&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;encoding&lt;/code&gt; &lt;strong&gt;默认值:&lt;/strong&gt; &lt;code&gt;&apos;utf8&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;withFileTypes&lt;/code&gt;  &lt;strong&gt;默认值:&lt;/strong&gt; &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callback&lt;/code&gt;(两个参数，错误回调优先)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;err&lt;/code&gt;  默认值为null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;files&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;错误处理代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let dirs = fs.readdir(&apos;./hello.txt&apos;, (err, files) =&amp;gt; {
    if(err){
        console.log(err);
    }else{
        console.log(files);
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;fs.readdirSync(path[, options])&lt;/code&gt; 同步函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;options&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;encoding&lt;/code&gt;&lt;strong&gt;默认值:&lt;/strong&gt; &lt;code&gt;&apos;utf8&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;withFileTypes&lt;/code&gt;&lt;strong&gt;默认值:&lt;/strong&gt; &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;返回:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;错误处理代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
    let dirs = fs.readdirSync(&apos;./node.js&apos;); //异步
}
catch (err) {
    console.log(&apos;粗ucol&apos;);
    console.log(err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为同步函数没有异步那样的处理错误机制，因此需要配合&lt;code&gt;try catch&lt;/code&gt;才能捕获错误的同时又不暂停下面的代码运行。&lt;/p&gt;
&lt;h4&gt;1.2 mkdir和mkdirSync 创建文件&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/fs.html#fs_fs_mkdir_path_options_callback&quot;&gt;mkdir文档&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1.3 rename 重命名 和 rmdir删除文件&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/fs.html#fs_fs_rename_oldpath_newpath_callback&quot;&gt;rename&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/fs.html#fs_fs_rmdir_path_options_callback&quot;&gt;rmdir&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1.4 writeFile 覆盖写入 和appendFile添加写入文件&lt;/h4&gt;
&lt;h4&gt;1.5 readFile 读取文件内容(二进制数据流)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;fs.readFile(&apos;name.txt&apos;,(err,data)=&amp;gt;{
    console.log(data.toString(&apos;UTF-8&apos;))
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用toString(&apos;UTF-8&apos;)的方法将文字提取出来；&lt;/p&gt;
&lt;p&gt;或者直接在配置项中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fs.readFile(&apos;name.txt&apos;, &apos;UTF-8&apos;, (err, data) =&amp;gt; {
    console.log(data)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.6 fs.stat(path[, options], callback)&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/fs.html#fs_fs_stat_path_options_callback&quot;&gt;fs.stat&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2. url内置模块&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./1570860020599.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2.1 url.parse()和url.format()&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost&quot;&gt;url.parse()&lt;/a&gt;将其解析成一个url对象，从而可以从url中获取href中的各种值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1570865560878.png&quot; alt=&quot;1570865560878&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/url.html#url_url_format_url_options&quot;&gt;url.format()&lt;/a&gt;将对象再拼起来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let url = require(&quot;url&quot;);
let urlStirng = &apos;https://code.choerodon.com.cn/choerodon-framework?page=2&apos;
let urlObj = url.parse(urlStirng);
console.log(urlObj);
let string = url.format(urlObj);
console.log(string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1571021112249.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3. querystring内置模块&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/querystring.html#querystring_query_string&quot;&gt;querystring&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;3.1 stringfy()对象转成字符串，parse()根据符号解析querystring&lt;/h4&gt;
&lt;h4&gt;3.2 escape()和unescape()编码与解码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let qs = require(&apos;querystring&apos;)
// let string = &apos;name=weng&amp;amp;pass=12121&amp;amp;sex=0&amp;amp;hello=111&apos;
// let obj = qs.parse(string);
// console.log(obj)
let string = &apos;w=你好&amp;amp;boo=21&apos;;
let code = qs.escape(string);
console.log(code);
let parseCode = qs.unescape(code);
console.log(parseCode);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1571021180480.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4. nodemailer 第三方模块&lt;/h3&gt;
&lt;p&gt;基本的发送邮箱代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;use strict&apos;;
const nodemailer = require(&apos;nodemailer&apos;);

// 创建发送邮件的请求对象
let transporter = nodemailer.createTransport({
    host: &apos;smtp.qq.com&apos;, //发送方用的邮箱
    port: 465, //端口号
    secure: true, // true for 465, false for other ports
    auth: {
        user: &apos;736653759@qq.com&apos;, // 发生方邮箱地址
        pass: &apos;hebnhkaruvzlbfdg&apos; // mtp验证码
    }
});

// send mail with defined transport object
let mailObj = {
    from: &apos;&quot;Fred Foo 👻&quot; &amp;lt;736653759@qq.com&amp;gt;&apos;,
    to: &apos;736653759@qq.com&apos;,
    subject: &apos;Hello test nodemailer ✔&apos;, // Subject line
    text: &apos;Hello world of node.js?&apos;, // plain text body
    html: &apos;&amp;lt;b&amp;gt;Hello world?&amp;lt;/b&amp;gt;&apos; // html body
};


// main().catch(console.error);
setTimeout(() =&amp;gt; {
    transporter.sendMail(mailObj, (err, data) =&amp;gt; {
        console.log(data)
    })
}, 2000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. error对象&lt;/h3&gt;
&lt;p&gt;错误对象本身没有终止代码执行，所以你需要throw抛出异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let err = new Error(&apos;发生错误&apos;);
throw err;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 爬虫案例 （方法2待补充）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取目标网站&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分析网站内容  &lt;a href=&quot;https://cheerio.js.org/&quot;&gt;cheerio&lt;/a&gt; 可以用&lt;code&gt;jquery&lt;/code&gt;中的选择器进行网页内容分析&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/629a81b4e013&quot;&gt;cheerio中文文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cheerio = require(&apos;cheerio&apos;);
let $ = cheerio.load(&apos;&amp;lt;div&amp;gt;&amp;lt;p&amp;gt;hello&amp;lt;/p&amp;gt;&amp;lt;img src = &quot;http://www.baidu.com&quot;/&amp;gt;&apos;);
const img = $(&apos;div img&apos;).attr(&apos;src&apos;);
console.log(img)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将一组html转化为类dom,可以通过cheerio实现jq中的$选择器，从而实现一些dom获取的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取有效信息，下载或者其他操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/http.html#http_http_get_url_options_callback&quot;&gt;http&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nodejs.cn/api/https.html&quot;&gt;https&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;根据不同的请求选择不同的协议。&lt;/p&gt;
&lt;p&gt;&amp;lt;span style=&apos;color:red&apos;&amp;gt;*爬图片的代码&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;h5&gt;方法1：&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;downLoad.js文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const request = require(&apos;request&apos;);
var dirPath = path.join(__dirname, &quot;MarvelImages&quot;); //__dirname当前路径，加上要创建的文件名
function downloadfile(downloadUrl, index) {
    // 创建文件夹
    if (!fs.existsSync(dirPath)) { // 同步查询文件夹是否存在
        fs.mkdirSync(dirPath); // 同步创建文件夹
        console.log(dirPath + &apos;文件创建成功&apos;);
    }
    let imgUrlArray = downloadUrl.split(&apos;/&apos;);  //分割Url
    let imgUrl = imgUrlArray[imgUrlArray.length - 1]; //获取文件名
    let filename = imgUrl;
    let stream = fs.createWriteStream(path.join(dirPath, filename));
    if (downloadUrl.indexOf(&apos;http&apos;) !== -1) {
        request(downloadUrl).pipe(stream).on(&apos;error&apos;, (err) =&amp;gt; {
            console.log(err);
        }).on(&apos;close&apos;, () =&amp;gt; {
            console.log(`文件【${filename}】下载完`);
            // callback(null, dest);
        })
    } else {
        console.log(`${downloadUrl}文件找不到`);
    }
}
module.exports = downloadfile;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;splider.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const http = require(&apos;http&apos;);
const cheerio = require(&apos;cheerio&apos;);
let downLoad = require(&apos;./downLoad&apos;);
let mavelUrl = &apos;http://marvel.mtime.com/&apos;
http.get(mavelUrl, (res) =&amp;gt; {
    const { statusCode } = res; //状态码
    const contentType = res.headers[&apos;content-type&apos;]; //请求到的文件类型
    console.log(`请求到的文件类型` + contentType);
    // 做安全判断
    let error;
    if (statusCode !== 200) {
        error = new Error(&apos;请求失败.&apos; + `Status Code: ${statusCode}错误`);
    } else if (!/^text\/html/.test(contentType)) {
        error = new Error(&apos;Invalid content-type.\n&apos; + `Expected text/html but received ${contentType}`)
    }
    if (error) { //如果出错的话直接来到这一行
        console.error(error.message);
        res.resume(); //重置缓存
        return false;
    }
    // 数据是分段的,每一次接受到一段数据都会触发data事件，chunk就是数据片段
    // 所以必须用一个string来当总chunk，每一次请求一个拼接一个
    let rawData = &apos;&apos;;
    res.on(&apos;data&apos;, (chunk) =&amp;gt; {
        rawData += chunk.toString(&apos;UTF-8&apos;);
    });
    res.on(&apos;end&apos;, () =&amp;gt; {
        let $ = cheerio.load(rawData);
        $(&apos;img&apos;).each((index, ele) =&amp;gt; {
            let imgSrc = $(ele).attr(&apos;src&apos;);
            downLoad(&apos;http://marvel.mtime.com/&apos; + imgSrc, index);
        })
    })
}).on(&apos;error&apos;, (err) =&amp;gt; {
    console.log(&apos;请求错误&apos;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;方法2：&lt;/h5&gt;
&lt;h3&gt;7. express框架（写api）&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://www.expressjs.com.cn/4x/api.html&quot;&gt;express中文文档&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;7.1.简要用法&lt;/h4&gt;
&lt;p&gt;先明确一点，接口的构成要素有啥&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ip&lt;/li&gt;
&lt;li&gt;port&lt;/li&gt;
&lt;li&gt;pathname&lt;/li&gt;
&lt;li&gt;method (get post put delete)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;写一个get接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const app = express() //express实例化
// 监听3000端口
app.listen(3000, () =&amp;gt; {
    console.log(&apos;server start at localhost://3000&apos;);
})

// 写一个get接口
app.get(&apos;/user/login&apos;, (req, res) =&amp;gt; {
    console.log(req.query); //接受到的值
    console.log(&apos;你好&apos;);
    res.send({ status: &apos;success&apos; });
})

// 协议 http https
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;地址栏中输入http://localhost:3000/user/login?username=wengkaimin&amp;amp;ps=123456&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1571145900643.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;控制台中就能接受到传过来的数据，通过req.query接受get方法传过来的参数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写一个post接口&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;express&lt;/code&gt;不提供解析消息体的功能，所以req.body是拿不到主体的，这边提供了一个第三方插件&lt;/p&gt;
&lt;p&gt;&lt;code&gt;body-parser&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/body-parser&quot;&gt;body-parser npm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1571152056613.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const app = express() //express实例化
const bp = require(&apos;body-parser&apos;);
// express中app.use表示使用一个中间件
// parse application/x-www-form-urlencoded解析表单格式数据
app.use(bp.urlencoded({ extended: false }))
// parse application/json
app.use(bp.json());
// 监听3000端口
app.listen(3000, () =&amp;gt; {
    console.log(&apos;server start at localhost://3000&apos;);
})

// 写一个get接口
app.get(&apos;/user/login&apos;, (request, res) =&amp;gt; {
    console.log(request.query);
    console.log(&apos;你好&apos;);
    let { username, ps } = request.query;
    if (username === &apos;weng&apos; &amp;amp;&amp;amp; ps === &apos;1234&apos;) {
        res.send({ status: &apos;success&apos; });
    } else {
        res.send(&apos;登陆失败&apos;)
    }
});

// 写一个post接口
app.post(&apos;/user/reg&apos;, (request, result) =&amp;gt; {
    // request.body获取数据，消息体，请求体
    // let { us, ps } = request.body;
    console.log(request.body);
    // express不能直接解析消息体
    // 需要第三方插件 body-parser
    result.send({ status: 1 })
})
// 协议 http https
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1571152747028.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1571152776993.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;7.2 中间件 middlewear&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;内置中间件 static&lt;/li&gt;
&lt;li&gt;自定义中间件（全局）（局部）&lt;/li&gt;
&lt;li&gt;第三方中间件（body-parse) (拦截器)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!--一定要在合适的地方next()--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);

const app = express();

// 中间件
// 全局中间件,所有请求发送之前都要走这一步
app.use(&apos;/&apos;, (req, res, next) =&amp;gt; {
    let { token } = req.query;
    if (token) {
        res.send(&quot;OK&quot;);
        next(); //是否继续往下执行
    } else {
        res.send(&apos;缺少token&apos;);
    }
    
})

app.get(&apos;/test1&apos;, (requst, result) =&amp;gt; {
    console.log(&apos;test1&apos;)
    console.log(requst.query)
})

app.get(&apos;/test2&apos;, (requst, result) =&amp;gt; {
    console.log(&apos;test2&apos;)
})

app.listen(3000, () =&amp;gt; {
    console.log(&apos;server start at localhost://3000&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.3 静态资源目录 static&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;指定一个目录，可以被访问 像apache的www一样&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;部署静态资源文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const path = require(&apos;path&apos;);
const app = express();
const dirPath = path.join(__dirname, &apos;/static&apos;);
console.log(dirPath);

app.use(express.static(dirPath)); //可以从这个路径直接获取到资源

app.listen(3000, () =&amp;gt; {
    console.log(&apos;server start at localhost://3000&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7.4 路由&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./1571385182120.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;server.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const app = express();

let userRouter = require(&apos;./router/userRouter&apos;);
let foodRouter = require(&apos;./router/foodRouter&apos;);

app.use(&apos;/user&apos;, userRouter);
app.use(&apos;/food&apos;, foodRouter)


app.listen(3000, () =&amp;gt; {
    console.log(&quot;server start localhost:3000&quot;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;userRouter.js&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const router = express.Router();

router.get(&apos;/add&apos;, (req, res) =&amp;gt; {
    res.send(&apos;user add&apos;)
})

router.get(&apos;/del&apos;, () =&amp;gt; {
    console.log(&apos;user delete&apos;)
})

module.exports = router
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. MongoDB &lt;strong&gt;非关系型数据库&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;8.1 指令基本&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;mongodb 数据库名&lt;/li&gt;
&lt;li&gt;mongod 命令行启动数据库命令&lt;/li&gt;
&lt;li&gt;mongo 命令行操作数据库指令&lt;/li&gt;
&lt;li&gt;mongoose node 操作数据库插件    &lt;a href=&quot;http://www.mongoosejs.net/docs/cnhome.html&quot;&gt;中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;show dbs&lt;/li&gt;
&lt;li&gt;show collections&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>flex3个属性详解</title><link>https://nollieleo.github.io/posts/flex3%E4%B8%AA%E5%B1%9E%E6%80%A7%E8%AF%A6%E8%A7%A3/</link><guid isPermaLink="true">https://nollieleo.github.io/posts/flex3%E4%B8%AA%E5%B1%9E%E6%80%A7%E8%AF%A6%E8%A7%A3/</guid><description>Flex 3个属性详解   flex属性 flex:(flex-grow)(flex-shrink)(flex-basis)  - flex-grow 一个数字，规定项目将相对于其他灵活的项目进行扩展的量（放大属性） - flex-shrink 一个数字，规定项目将相对于其他灵活的项目进行收缩的量（...</description><pubDate>Mon, 16 Mar 2020 13:18:33 GMT</pubDate><content:encoded>&lt;h1&gt;Flex 3个属性详解&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;flex属性 flex:(flex-grow)(flex-shrink)(flex-basis)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;flex-grow&lt;/strong&gt; 一个数字，规定项目将相对于其他灵活的项目进行扩展的量（放大属性）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;flex-shrink&lt;/strong&gt; 一个数字，规定项目将相对于其他灵活的项目进行收缩的量（缩小属性）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;flex-basis&lt;/strong&gt; 项目的长度。合法值：&quot;auto&quot;、&quot;inherit&quot; 或一个后跟 &quot;%&quot;、&quot;px&quot;、&quot;em&quot; 或任何其他长度单位的数字。（基本宽度）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. flex: none&lt;/h3&gt;
&lt;p&gt;等同于 （&lt;code&gt;flex: 0 0 auto&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;这时候容器完全不再灵活。即不能放大也不能缩小。&lt;/p&gt;
&lt;p&gt;除了它不允许 flex-shrink 属性（不能缩小），即使元素可能会溢出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1574993498581.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. flex:initial(flex: 0 1 auto )&lt;/h3&gt;
&lt;p&gt;等同于（&lt;code&gt;0 auto&lt;/code&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-grow&lt;/code&gt;为0，则存在剩余空间也不放大&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-shrink&lt;/code&gt;为1，则空间不足该项目缩小 z&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-basis&lt;/code&gt;为auto，则该项目本来的大小 ,长度等于灵活项目的长度。如果该项目未指定长度，则长度将根据内容决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;.parent {
    display: flex;
    width: 600px;
    border: 1px solid #000;
}
.parent&amp;gt;div {
    height: 100px;
}
.item1 {
    width: 100px;
    background-color: #666;
}

.item2 {
    width: 100px;
    background: blue;
}

.item3 {
    width: 100px;
    background: lightblue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;parent&quot;&amp;gt;
        &amp;lt;div class=&quot;item1&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;item2&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;item3&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./1574992712098.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时，&lt;strong&gt;当容器剩余一些空闲空间时&lt;/strong&gt;，该属性使灵活的项目变得不灵活，因为其不能自由拉伸放大。&lt;/p&gt;
&lt;p&gt;但是&lt;strong&gt;当容器没有足够的空间时&lt;/strong&gt;，该属性允许其缩小到项目的最小值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置前面两个元素为&lt;code&gt;flex: none, width:100px&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./1574995110792.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;全是initial，width分别设置为：100px ，100px ，700px，此时已经超出了容器的宽度，这时候里头的3个元素自动收缩&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1574995539373.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;baseWidth&lt;/strong&gt;: flex-basis ? flex-basis : width (flex-basis的优先级比width高);&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;allWidth&lt;/strong&gt;: flex元素的flex-basis和，或者width和其的和，看优先级;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;overflowWidth&lt;/strong&gt;: allwidth - 父容器width&lt;/p&gt;
&lt;p&gt;每个flex元素缩小值计算：
$$(baseWidth/allWidth) * overflowWidth$$
所以这次情况是：div1宽度为100px - 100/900 * 300 = 66.66....&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. flex: auto&lt;/h3&gt;
&lt;p&gt;相当于 flex: 1 1 auto&lt;/p&gt;
&lt;p&gt;这个时候，该属性使3个item完全灵活，它们能够吸收主轴上额外的空间。既可以自由放大也可以自由缩小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1574998175148.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4. flex:&amp;lt; positive-number &amp;gt; &amp;lt;意为 正数&amp;gt;&lt;/h3&gt;
&lt;p&gt;等同于 &lt;strong&gt;flex: &amp;lt; positive-number &amp;gt;  1  0px&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当 flex 取值为一个非负数字，则该数字为 &lt;code&gt;flex-grow&lt;/code&gt; 的值，&lt;code&gt;flex-shrink&lt;/code&gt; 取 1，&lt;code&gt;flex-basis&lt;/code&gt; 取 0%。&lt;/p&gt;
&lt;p&gt;这个时候，该属性使弹性项目变得灵活，flex元素在主轴方向上的初始大小flex-basis为零。&lt;/p&gt;
&lt;p&gt;项目将根据容器的大小按照自身的比例自由伸缩。
&lt;img src=&quot;./1575008755738.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据他们&lt;code&gt;flex-grow&lt;/code&gt;来计算他们分配到的宽度 :&lt;br /&gt;
$$flexGrow * flexWidth$$&lt;/p&gt;
&lt;p&gt;$$flexWidth = (600-allFlexBasisWidth)/(1+1+2) = (600 - 0)/4 =150px$$
由于flex:1 ,其中的flex-basis是为0px的，所以说,要根据自己div的宽度来决定元素的基础宽度，如果div没设置宽度那就根据内部元素的宽度撑开的宽度，例如：&lt;/p&gt;
&lt;p&gt;如果这时候一个元素的&lt;strong&gt;flex-basis&lt;/strong&gt;设置为&lt;strong&gt;auto&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1575010449681.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这时候就需要根据这个设置了auto元素的宽度来进行计算，因为item2没有设置width所以得根据他的内容宽度来计算分配到的宽度。&lt;/p&gt;
&lt;p&gt;当他没有设置flex属性的时候，内容宽度为99.08px&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1575010656266.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以算他在成为flex元素时候分配到的宽度为：&lt;/p&gt;
&lt;p&gt;(600-99.08)/4=125.23&lt;/p&gt;
&lt;h3&gt;5. flex:&amp;lt; positive-number &amp;gt;  xxpx ||%xx&lt;/h3&gt;
&lt;p&gt;该属性和上边的CSS案例一致，即 当 flex 取值为一个非负数字和一个长度（px）或百分比（%），&lt;/p&gt;
&lt;p&gt;则分别视为 flex-grow 和 flex-basis 的值，flex-shrink 默认取 1&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;例题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style type=&quot;text/css&quot;&amp;gt;
    .parent {
        display: flex;
        width: 600px;
        border: 1px solid #000;
    }

    .parent&amp;gt;div {
        height: 100px;
    }

    .item1 {
        background-color: #666;
        flex: 2 1 30%;
    }

    .item2 {
        width:100px;
        background: blue;
        flex: 2 1 auto;
    }

    .item3 {
        flex: 1 1 200px;
        background: lightblue;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;parent&quot;&amp;gt;
        &amp;lt;div class=&quot;item1&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;item2&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;item3&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算出3个item的宽度&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先判断他们几个的basisWidth和是否超出了容器的宽度: 600*30% +200 + 0 = 380px &amp;lt;600px，故，剩余的宽度用于flex元素的宽度扩展&lt;/li&gt;
&lt;li&gt;计算伸展因子：(600 - 380)/ (2+2+1) = 44 px&lt;/li&gt;
&lt;li&gt;因此他们所分配到的宽度各为：2 * 44+180 ，2 * 44，1 * 44+200&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6. 归纳&lt;/h3&gt;
&lt;p&gt;每次去计算一个flexbox的元素宽度&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;flex-grow 不为0 的时候并且flex-basis宽度加起来不超出容器宽，以flex-basis为基准算伸展因子，当flex-grow为0时，flex-basis最大，width第二。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flex-grow 不为0的时候并且flex-basis为auto的时候，虽然有width值，但计算收缩因子时候这一块的flex-basis也只能算是0.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item></channel></rss>