<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nexo]]></title><description><![CDATA[a website]]></description><link>https://9999886.xyz</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 06:18:59 GMT</lastBuildDate><atom:link href="https://9999886.xyz/feed" rel="self" type="application/rss+xml"/><pubDate>Fri, 10 Apr 2026 06:18:59 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[屎山 Electron 应用体积裁剪记录]]></title><description><![CDATA[:::info

本文章由 cladue opus 4.5 润色。

:::

最近 Vibe coding 了一个 AI 桌面编程助手，基于 Electron + Vite。最近发现安装包膨胀到 180MB 了，下载体验很差，于是花了点时间做了一轮系统性优化。

最终成果：DMG 从 180MB 降到 80MB，app.asar 从 416MB 降到 56MB。没改任何业务代码。

这篇文章记录整个过程。

***

## 最有效

### 1. 把 renderer-only 依赖移到 devDependencies（asar -267MB）

这是整个优化里收益最大的一步，效果超过其他所有步骤加起来。

原理很简单：应用用 Vite 打包 renderer 进程，产出的是一个自包含的 bundle（20MB），运行时根本不需要 node\_modules 里的东西。但 electron-builder 打包时会把 `dependencies` 里所有包都塞进 asar——不管你 bundle 用没用到。

所以像 `@lobehub/ui`、`react`、`zustand`、`lucide-react` 这些只在 renderer 里用的包，全部应该放到 `devDependencies`。

```sh
npm install --save-dev @lobehub/ui react react-dom zustand lucide-react
```

一行命令，asar 从 323MB 直接掉到 56MB。

electron-builder 打包会把 `dependencies` 里的包连带整个依赖树一起打进去。这两个过程是独立的。

### 2. electron-builder 开启最大压缩（DMG -23MB）

一行配置的事：

```json
{  
  "build": {  
    "compression": "maximum"  
  }  
}
```

默认是 `normal`，改成 `maximum` 后 DMG 立减 14%。代价是构建时间稍微长一点，但对发布构建来说完全可以接受。

### 3. afterPack 钩子清理垃圾文件（unpacked -19MB）

Electron 用 native 模块（比如 better-sqlite3）时，需要把 `.node` 文件从 asar 里解压出来。问题是解压的时候会把整个模块目录都带出来，包括一堆运行时根本不需要的东西：

* `sqlite3.c` 源码（9MB）

* README、LICENSE、CHANGELOG

* `.gyp`、`Makefile` 等构建文件

另外 Electron 框架自带 70 多种语言的 locale 文件，每个几百 KB，应用只需要保留一种即可（参考 innei 播客）。

用 afterPack 钩子在打包后、签名前把这些都清掉：

```js:scripts/afterPack.js
exports.default = async function(context) {  
  // 清理 native 模块里的源码和文档  
  const junkPatterns = ['**/*.c', '**/*.h', '**/README.md', '**/*.gyp']  
  // ... 删除匹配的文件  
​  
  // 只保留英文 locale  
  const keepLocales = ['en.lproj']  
  // ... 删除其他 locale 目录  
}
```

unpacked 目录从 21MB 降到 2MB，DMG 也跟着小了 12MB。

***

## 中等收益的优化

这几个不如上面三个效果明显，但也值得做：

**移除未使用的依赖（asar -87MB）**

全局搜一下 import 语句，发现 antd、react-markdown 这些包装了但根本没用到。Web 项目里这不是大问题（Vite 会 tree-shake 掉），但 Electron 会把整个 node\_modules 打进去，所以该删还是得删。

**Vite 构建配置**

```json
esbuild: {  
  drop: ['console', 'debugger']  // 生产环境移除调试代码  
}
```

renderer bundle 能小个几百 KB。

***

## 试了但没啥用的

记录一下，省得踩同样的坑：

**精确配置 asarUnpack 的 glob 模式**

我以为把 glob 写精确点，就能只解压 `.node` 文件而不带上源码：

"asarUnpack": \["\*\*/better-sqlite3/build/Release/\*.node"]

然而并没有用。electron-builder 的逻辑是：glob 匹配到哪个模块，就把整个模块目录都解压出来。想精确控制解压内容，只能用 afterPack 钩子事后清理。

**静态资源压缩**

把 logo.png 从 228KB 压到 26KB，icon.icns 从 1.2MB 压到 656KB。但经过 asar 打包和 DMG 压缩后，在 MB 级别的测量精度下基本看不出差异。属于代码卫生层面的事，对最终体积影响不大。

**files 配置排除 .map 文件**

"files": \["!out/\*\*/\*.map"]

配了，但当前构建本来就没生成这些文件，所以没有实际收益。算是防御性配置。

***

## 各步骤收益汇总

| 优化项                  | DMG        | asar       | unpacked  |
| :------------------- | :--------- | :--------- | :-------- |
| renderer 依赖移到 devDep | -49MB      | -267MB     | —         |
| 最大压缩配置               | -23MB      | -12MB      | —         |
| afterPack 清理         | -12MB      | —          | -19MB     |
| 移除未使用依赖              | -16MB      | -87MB      | —         |
| Vite 构建优化            | —          | -12MB      | —         |
| **累计**               | **-100MB** | **-360MB** | **-19MB** |

***

## 一些经验

**每步都要打包验证**。不要一口气做完所有优化再测，而是改一步、打包一次、记录数据。这样才能知道哪步真正有效、哪步是白忙活。我们就是靠这个方法发现 asarUnpack 优化其实没用的。

**构建有波动**。相同代码多次打包，asar 体积可能有 ±6-12MB 的波动，别被误导了。

**Electron 的依赖管理比 Web 敏感得多**。Web 应用只打包 import 的代码，Electron 会把整个 node\_modules 带上。定期审计依赖是必要的。

***

剩下的优化空间主要在 Electron 框架本身（比如 16MB 的 Vulkan 渲染器我们其实用不到）和 renderer bundle 的懒加载（mermaid 只在渲染图表时需要）。这些后面再搞。
]]></description><link>https://9999886.xyz/posts/jS9v4Zek3AAC1KVMUeytI</link><guid isPermaLink="true">https://9999886.xyz/posts/jS9v4Zek3AAC1KVMUeytI</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Mon, 09 Feb 2026 14:44:33 GMT</pubDate></item><item><title><![CDATA[Vue Computed Watch]]></title><description><![CDATA[### computed

> 接受一个 getter 函数，返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

```ts
// 只读
function computed<T>(
  getter: () => T,
  // 参见下面的"Computed 调试"链接
  debuggerOptions?: DebuggerOptions,
): Readonly<Ref<Readonly<T>>>

// 可写
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions,
): Ref<T>
```

computed 有两个签名，可读和可写。

#### 支持get

```ts
import type { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { type Ref, trackRefValue, triggerRefValue } from './ref'

declare const ComputedRefSymbol: unique symbol

export interface ComputedRef<T = any> extends Ref {
  readonly value: T
  [ComputedRefSymbol]: true
}

type ComputedGetter<T> = (...args: any[]) => T

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public _dirty = true

  constructor(getter: ComputedGetter<T>) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }

  set value(_newValue: T) {}
}

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T> {
  return new ComputedRefImpl(getter) as any
}
```

当组件中读取计算属性时，会触发 get value()，调用trackRefValue收集依赖，`_dirty` 属性用来标记属性是否是脏值(脏值不需要重新计算)，第一次访问时`_dirty`肯定是 false，于是就会触发`this.effect.run()`，而在 constructor中，`this.effect`是 ReactiveEffect 传入getter（computed 传入的函数）以及：

```ts
// 我是scheduler
() => {
  if (!this._dirty) {
	this._dirty = true
	triggerRefValue(this)
  }
}
```

```ts:ReactiveEffect.ts
export class ReactiveEffect<T = any> {

  // fn 指 updateComponent()
  constructor(public fn: () => T, public scheduler?: EffectScheduler | null) {}

  run() {
    let parent: ReactiveEffect | undefined = activeEffect
    activeEffect = this
    const res = this.fn()
    activeEffect = parent
    return res
  }
}
```

调用`this.effect.run`时，activeEffect 为 this.effect，那么执行getter时，其中如果有响应式数据，就会将 activeEffect 收集到 Dep 中，每当其中的响应式数据更新时，会触发set，set 会遍历 dep（其中就包括这个的 computed 的 effect）并执行他的scheduler（ReactiveEffect的第二个函数）。scheduler 中如果`_dirty`是false（已经有值且标记为不需要更新），那么就会设置`_dirty`为 true，并执行triggerRefValue。对应的组件 effect 会被触发，就会重新访问get value()，这时就会触发重新计算。

#### 支持 set

```ts
import { isFunction } from '../shared'
import type { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { type Ref, trackRefValue, triggerRefValue } from './ref'

declare const ComputedRefSymbol: unique symbol

export interface ComputedRef<T = any> extends Ref {
  readonly value: T
  [ComputedRefSymbol]: true
}

export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public _dirty = true

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

export function computed<T>(getterOrOptions: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(getterOrOptions: WritableComputedOptions<T>): Ref<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)

  if (onlyGetter) {
    getter = getterOrOptions
    setter = () => {}
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(getter, setter) as any
}
```

### watch

> 侦听一个或多个响应式数据源，并在数据源变化时调用所给的回调函数。

```ts
// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): WatchHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): WatchHandle

type WatchCallback<T> = (
  value: T,
  oldValue: T,
  onCleanup: (cleanupFn: () => void) => void
) => void

type WatchSource<T> =
  | Ref<T> // ref
  | (() => T) // getter
  | (T extends object ? T : never) // 响应式对象

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // 默认：false
  deep?: boolean | number // 默认：false
  flush?: 'pre' | 'post' | 'sync' // 默认：'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  once?: boolean // 默认：false (3.4+)
}

interface WatchHandle {
  (): void // 可调用，与 `stop` 相同
  pause: () => void
  resume: () => void
  stop: () => void
}
```

使用 ReactiveEffect 就可以快速实现

```ts
import { ReactiveEffect } from "../reactivity"

export type WatchEffect = (onCleanup: OnCleanup) => void

export type WatchSource<T = any> = () => T

type OnCleanup = (cleanupFn: () => void) => void

export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  let oldValue: T
  const effect = new ReactiveEffect(() => {
    const newValue = source()
    cb(newValue, oldValue)
    oldValue = newValue
  }, () => {
    const newValue = source()
    cb(newValue, oldValue)
    oldValue = newValue
  })
  effect.run()
}
```

优化:

```ts
export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  const getter = () => source()
  let oldValue = getter()
  const job = () => {
    const newValue = getter()
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }

  const effect = new ReactiveEffect(getter, job)
  effect.run()
}
```

然后在此基础上继续拓展，支持 Ref:

```ts:supportRef
export type WatchSource<T = any> = () => T | Ref<T>

type OnCleanup = (cleanupFn: () => void) => void

export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  const getter = () => {
    const value = source()
    return isRef(value) ? value.value : value
  }
  let oldValue = getter()
  const job = () => {
    const newValue = getter()
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }

  const effect = new ReactiveEffect(getter, job)
  effect.run()
}
```

支持数组类型的 source

```ts:supportArraySource
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
) {
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

  let oldValue = getter()
  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  effect.run()
}
```

支持 immediate

```ts:supportImmediate
export type WatchSource<T = any> = Ref<T> | (() => T)

type OnCleanup = (cleanupFn: () => void) => void

interface WatchOptions {
  immediate?: boolean
  deep?: boolean | number
}

export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions,
) {
  const { immediate = false } = options || {}
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  let oldValue: any
  if (immediate) {
    // job中会调用一次effect.run拿值，所以不用再次调用了
    job()
  } else {
    oldValue = effect.run()
  }
}
```

支持 deep

```ts:supportDeep
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions,
) {
  const { immediate = false, deep = false } = options || {}
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

  if (deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  let oldValue: any
  if (immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
}

export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value)) return value

  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (Array.isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key], seen)
    }
  }
  return value
}
```

### watchEffect

> 立即运行一个函数，同时响应式地追踪其依赖，并在依赖更改时重新执行。

```ts:supportWatchEffect
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions
) {
  doWatch(source, cb, options);
}

export function watchEffect(source: WatchEffect) {
  doWatch(source, null)
}

function doWatch<T>(
  source: WatchSource<T> | WatchSource<T>[] | WatchEffect,
  cb: null | ((newValue: T | T[], oldValue: T | T[]) => void),
  options?: WatchOptions
) {
  const { immediate = false, deep = false } = options || {};
  let getter: () => any;
  let isMultiSource = false;
  if (isFunction(source)) {
    getter = source as () => any;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (Array.isArray(source)) {
    isMultiSource = true;
    getter = () => {
      return source.map((s) => {
        if (isRef(s)) {
          return s.value;
        }
        return s();
      });
    };
  } else {
    getter = () => source;
  }

  if (deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }

  const job = () => {
    const newValue = effect.run();
    if (
      isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as T[])?.[i])
          )
        : hasChanged(newValue, oldValue)
    ) {
      cb?.(newValue, oldValue);
      oldValue = newValue;
    }
  };
  const effect = new ReactiveEffect(getter, job);
  let oldValue: any;
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }
}
```

### 支持 onCleanup

首先，拓展 ReactiveEffect，使其支持 stop，然后watch 的 callback 增加onCleanup参数，他接受一个函数，并将其保存起来，他会在 stop 执行时（也就是 watch return 出的函数被执行时）或者source 变化时，下一次执行 callback 之前触发：

:::tabs
@tab apiWatch.ts

```ts:apiWatch.ts
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[], cleanup: OnCleanup) => void,
  options?: WatchOptions
) {
  return doWatch(source, cb, options);
}

export function watchEffect(source: WatchEffect) {
  return doWatch(source, null)
}

function doWatch<T>(
  source: WatchSource<T> | WatchSource<T>[] | WatchEffect,
  cb: null | ((newValue: T | T[], oldValue: T | T[], cleanup: OnCleanup) => void),
  options?: WatchOptions
) {
  const { immediate = false, deep = false } = options || {};
  let getter: () => any;
  let isMultiSource = false;
  if (isFunction(source)) {
    getter = source as () => any;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (Array.isArray(source)) {
    isMultiSource = true;
    getter = () => {
      return source.map((s) => {
        if (isRef(s)) {
          return s.value;
        }
        return s();
      });
    };
  } else {
    getter = () => source;
  }

  if (deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }

  let cleanup: () => void
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      fn()
    }
  }

  const job = () => {
    const newValue = effect.run();
    if (
      isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as T[])?.[i])
          )
        : hasChanged(newValue, oldValue)
    ) {
    if (cleanup) {
		cleanup()
	}
      cb?.(newValue, oldValue, onCleanup);
      oldValue = newValue;
    }
  };
  const effect = new ReactiveEffect(getter, job);
  let oldValue: any;
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }

  const unwatch = () => {
    effect.stop()
  }
  return unwatch
}
```

@tab ReactiveEffect.ts

```ts:ReactiveEffct.ts
export class ReactiveEffect<T = any> {
  active = true
  private deferStop?: boolean
  onStop?: () => void 
  parent: ReactiveEffect | undefined = undefined 
  constructor(public fn: () => T, public scheduler?: EffectScheduler | null) {}

  run() {
    if (!this.active) {
      return this.fn()
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      return this.fn()
    } finally {
      activeEffect = this.parent
      this.parent = undefined
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}
```

:::
]]></description><link>https://9999886.xyz/posts/iGHBe7BSyskYCTO_DsQSh</link><guid isPermaLink="true">https://9999886.xyz/posts/iGHBe7BSyskYCTO_DsQSh</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Tue, 26 Aug 2025 09:54:20 GMT</pubDate></item><item><title><![CDATA[Vue Ref]]></title><description><![CDATA[### ref

> 接受一个内部值，返回一个响应式的、可更改的 ref 对象，此对象只有一个指向其内部值的属性 `.value`。
> ref 对象是可更改的，也就是说你可以为 `.value` 赋予新的值。它也是响应式的，即所有对 `.value` 的操作都将被追踪，并且写操作会触发与之相关的副作用。
> 如果将一个对象赋值给 ref，那么这个对象将通过 [reactive()](https://cn.vuejs.org/api/reactivity-core#reactive) 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref，它们将被深层地解包。
> 若要避免这种深层次的转换，请使用 [`shallowRef()`](https://cn.vuejs.org/api/reactivity-advanced.html#shallowref) 来替代。

```ts
function ref<T>(value: T): Ref<UnwrapRef<T>>

interface Ref<T> {
  value: T
}
```

先来一个示例

```ts
import { createApp, h, ref } from 'vueImpl'

const app = createApp({
  setup() {
    const count = ref(0)

    const countObj = ref({
      count: 0,
    })

    return () =>
      h('div', {}, [
        h('p', {}, [`count: ${count.value}`]),
        h('button', { onClick: () => count.value++ }, ['Increment1']),
        h('p', {}, [`countObj: ${countObj.value.count}`]),
        h('button', { onClick: () => countObj.value.count++ }, ['Increment2']),
        h('p', {}, [`countObj: ${countObj.value.count}`]),
        h('button', { onClick: () => countObj.value = { count: 100 } }, ['Update']),
      ])
  },
})

app.mount('#app')
```

1. count给ref传入了原始类型，ref使其具有响应式(.value)，走的是 ref 单独实现的响应式。
2. countObj 给ref传入了对象，ref使其具有响应式(.value)，修改countObj.value.count 时，走的是 reactive 的响应式(如果将一个对象赋值给 ref，那么这个对象将通过 [reactive()](https://cn.vuejs.org/api/reactivity-core#reactive) 转为具有深层次响应式的对象)。
3. 直接修改countObj.value，赋值为另一个对象，这里走的是 ref 的响应式，同时再使新赋值的对象具备响应式。

```ts
export function ref(value?: unknown) {
  return createRef(value)
}

type RefBase<T> = {
  dep?: Dep
  value: T
}

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

function createRef(rawValue: unknown) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue)
}

class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T) {
    // value toReactive => 如果是对象，则转化为reactive
    this._value = toReactive(value)
  }

  get value() {
    // 收集.value依赖
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    // 重新赋值，并触发 trigger
    this._value = toReactive(newVal)
    triggerRefValue(this)
  }
}

export function trackRefValue(ref: RefBase<any>) {
  trackEffects(ref.dep || (ref.dep = createDep()))
}

export function triggerRefValue(ref: RefBase<any>) {
  if (ref.dep) triggerEffects(ref.dep)
}

// 收集依赖，ref执行时，activeEffect为当前组件的ReactiveEffect
export function trackEffects(dep: Dep) {
  if (activeEffect) {
    dep.add(activeEffect)
  }
}

// 修改时，触发更新
export function triggerEffects(dep: Dep | ReactiveEffect[]) {
  const effects = Array.isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

// 触发更新 
export function triggerEffect(effect: ReactiveEffect) {
  if (effect.scheduler) {
    effect.scheduler()
  } else {
    effect.run()
  }
}
```

### shallowRef

> 和 ref() 不同，浅层 ref 的内部值将会原样存储和暴露，并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。
> shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

```ts
const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }
```

```ts
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean,) {
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    this._value = this.__v_isShallow ? newVal : toReactive(newVal)
    triggerRefValue(this)
  }
}

declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }

export function shallowRef<T extends object>(
  value: T,
): T extends Ref ? T : ShallowRef<T>
export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}
```

只需要在`toReactive`时进行判断，如果是 ShallowRef 就不对 Object 进行代理。

### triggerRef

> 强制触发依赖于一个浅层 ref 的副作用，这通常在对浅引用的内部值进行深度变更后使用。

```ts
function triggerRef(ref: ShallowRef): void
```

```ts
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这次变更不应触发副作用，因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)
```

```ts
export function triggerRef(ref: Ref<any>) {
  triggerRefValue(ref)
}
```

这里直接 使用triggerRefValue，相当于 ref.value 触发，触发后数据肯定就更新了。

### toRef

> 可以基于响应式对象上的一个属性，创建一个对应的 ref。这样创建的 ref 与其源属性保持同步：改变源属性的值将更新 ref 的值，反之亦然。

```ts
export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
): ToRef<T[K]>
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue: T[K],
): ToRef<Exclude<T[K], undefined>>
export function toRef(
  source: Record<string, any>,
  key: string,
  defaultValue?: unknown,
): Ref {
  return propertyToRef(source, key!, defaultValue)
}

function propertyToRef(
  source: Record<string, any>,
  key: string,
  defaultValue?: unknown,
) {
  const val = source[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(source, key, defaultValue) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K],
  ) {}

  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }

  get dep(): Dep | undefined {
    return getDepFromReactive(this._object, this._key)
  }
}
```

首先判断目标值是否是 ref，如果是就不用处理，直接返回，如果不是，直接代理 value 的 get 和 set （如果source是 reactive 那自会响应式）

### toRefs

> 将一个响应式对象转换为一个普通对象，这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

```ts
function toRefs<T extends object>(
  object: T
): {
  [K in keyof T]: ToRef<T[K]>
}

type ToRef = T extends Ref ? T : Ref<T>
```

```ts
export type ToRefs<T = any> = {
  [K in keyof T]: ToRef<T[K]>
}

export function toRefs<T extends object>(object: T): ToRefs<T> {
  const ret: any = Array.isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = propertyToRef(object, key)
  }
  return ret
}
```

]]></description><link>https://9999886.xyz/posts/17zw5uAdFiuc5eduFS_vt</link><guid isPermaLink="true">https://9999886.xyz/posts/17zw5uAdFiuc5eduFS_vt</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Wed, 20 Aug 2025 14:31:58 GMT</pubDate></item><item><title><![CDATA[Implement promise]]></title><description><![CDATA[### 简单实现

首先，实现这个最简单的使用方式

```ts
new Promise((resolve, reject) => {
  resolve(1)
})
```

分析一下，Promise 实例接受一个 Function，Function 接受两个参数，分别为 resolve,reject
实现:

```ts
type Fn = (resole: (value: any) => void, reject: (reason: any) => void) => void

class Promise2025 {
  constructor(fn: Fn) {
    fn(this._resolve, this._reject)
  }

  _resolve(value: any) {
  }

  _reject(reason: any) {
  }
}
```

然后，开始为 promise 增加状态，promise 有三种状态，分别是`pending`，`fulfilled`和`rejected`，初始状态为 pending，pending 可以转为 fulfilled(resolve) 和 rejected(reject)，状态不可逆转，fulfilled 和 rejected 也不可以互相转。
定义状态枚举:

```ts
type Fn = (resole: (value: any) => void, reject: (reason: any) => void) => void

enum Status {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected'
}

class Promise2025 {
  constructor(fn: Fn) {
    // 默认为 pending
    this.promiseState = Status.PENDING
    fn(this._resolve, this._reject)
  }

  promiseState: Status

  _resolve(value: any) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
    }
  }
}
```

接下来是存储执行结果，promise 中使用`promiseResult`来存储 resolve 和 reject 的结果，我们只需要在执行 resolve 和 reject 时存一下就OK：

```ts
  promiseResult: any
  
  _resolve(value: any) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
    }
  }
```

这时候应该发现有些不对了，复习一下关于 this 指向的八股文

:::info


`JS`的`this`指向取决于环境。
全局环境中的`this`，直接指向全局对象。
函数环境中的`this`，取决于函数如何被调用：

1. 如果函数是直接调用的，同时又是出于严格模式下，则指向`undefined`，如果不是严格模式，则指向全局对象
2. 如果函数是通过一个对象调用的，则指向对象本身
3. 如果函数是通过一些特殊方法调用的，比如`call`、`apply`、`bind`，通过这些方法调用的话，则指向指定的对象
4. 如果函数是通过`new`调用的，则指向新的实例。
   另外，箭头函数由于没有`this`，所以箭头函数内部的`this`由其外部作用域决定。

   :::

   我们的\_resolve 与\_reject 中，是直接调用的，那么我们在函数内部中直接使用`this.xxx`其实是我们 class 中的 this 的，所以我们需要使用 bind 来给他指定一下 this。

```ts
constructor(fn: Fn) {
    // 默认为 pending
    this.promiseState = Status.PENDING
    // 使用 bind 绑定 this
    fn(this._resolve.bind(this), this._reject.bind(this))
  }
```

这里修改完了后，还需要考虑一个问题，如果我们的 fn 执行报错了怎么办？
在 promise 中，如果 fn 执行报错，那么也是直接触发 reject，我们可以在 fn 执行时使用 try catch 来捕获异常，放到 \_reject 中

```ts
constructor(fn: Fn) {
    // 默认为 pending
    this.promiseState = Status.PENDING
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }
```

### then的简单实现

接下来就到了 then的实现，依旧是一个简单的示例

```ts
new Promise((resolve, reject) => {
  resolve(1)
}).then((res) => {}, (reason) => {})
```

分析一下，这里的 then 接收两个参数，第一个是 resolve 时触发的回调，第二个是 reject 触发的回调。很轻松就可以做到这一点。

```ts
class Promise2025 {
  constructor(fn: Fn) {
    // 默认为 pending
    this.promiseState = Status.PENDING
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  promiseState: Status
  promiseResult: any

  _resolve(value: any) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
    }
  }

  // ++++++++++++++++++++++++++++++++++++++++++++++++
  then(onFulfilled: (value: any) => void, onRejected: (reason: any) => void) {
    if (this.promiseState === Status.FULFILLED) {
      onFulfilled(this.promiseResult)
    }
    if (this.promiseState === Status.REJECTED) {
      onRejected(this.promiseResult)
    }
  }
}
```

测试一下目前的功能:

```ts
promise1.then((res) => {
  console.log(res)
}, (reason) => {
  console.log(reason)
})

const promise2 = new Promise2025((resolve, reject) => {
  reject(2)
})

promise2.then((res) => {
  console.log(res)
}, (reason) => {
  console.log(reason)
})

const promise3 = new Promise2025((resolve, reject) => {
  throw new Error('error')
})

promise3.then((res) => {
  console.log(res)
}, (reason) => {
  console.log(reason)
})
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813111332.png "image.png")

接下来开始实现异步功能，具体就是把then的执行时机放到下一次微任务队列。我们使用[queueMicrotask](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/queueMicrotask)来实现。

```ts
 then(onFulfilled: (value: any) => void, onRejected: (reason: any) => void) {
    if (this.promiseState === Status.FULFILLED) {
      queueMicrotask(() => {
        onFulfilled(this.promiseResult)
      })
    }
    if (this.promiseState === Status.REJECTED) {
      queueMicrotask(() => {
        onRejected(this.promiseResult)
      })
    }
  }
```

测试：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813112635.png "image.png")
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813113041.png "image.png")
可以看到，第二种情况下，then 并没有正常调用。我们在我们的 promise 中打印一下每个阶段的状态：

```ts
type Fn = (resole: (value: any) => void, reject: (reason: any) => void) => void

enum Status {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected'
}

class Promise2025 {
  constructor(fn: Fn) {
    // 默认为 pending
    this.promiseState = Status.PENDING
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  promiseState: Status
  promiseResult: any

  _resolve(value: any) {
    console.log('----------执行 resolve----------');
    
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
    }
  }

  _reject(reason: any) {
    console.log('----------执行 reject----------');
    
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
    }
  }

  then(onFulfilled: (value: any) => void, onRejected: (reason: any) => void) {
    console.log('----------执行 then----------');
    if (this.promiseState === Status.FULFILLED) {
      queueMicrotask(() => {
        onFulfilled(this.promiseResult)
      })
    }
    if (this.promiseState === Status.REJECTED) {
      queueMicrotask(() => {
        onRejected(this.promiseResult)
      })
    }
  }
}

console.log('1')
 new Promise2025((resolve, reject) => {
  setTimeout(() => {
    console.log('--------------fn 执行----------')
    resolve('3')
  }, 0)
}).then(res => {
  console.log(res)
}, err => {})
console.log('4')
```

查看输出
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813113637.png "image.png")
执行时机明显不对，我们的 then 在 fn 之前就开始执行了，此时还没有执行 resolve，promise 的状态还是 pending，所以在 then 什么都不会做。原理也很简单，settimeout 是宏任务，宏任务会在下一个队列才执行。那么我们可以在 then调用时判断 promise 的状态，如果状态为 pending，那么先把回调存起来，等到 resolve 或 reject 调用时再触发。

```ts
type Fn = (resole: (value: any) => void, reject: (reason: any) => void) => void

enum Status {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected'
}

class Promise2025 {
  constructor(fn: Fn) {
    // 设置默认
    this.promiseState = Status.PENDING
    this.promiseResult = null
    // +++++++++++++++++++++++++++++
    this.onFulfilled = []
    this.onRejected = []
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  promiseState: Status
  promiseResult: any
  // ++++++++++++++++++++++
  onFulfilled: Array<(value: any) => void> = []
  onRejected: Array<(reason: any) => void> = []

  _resolve(value: any) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
      // ++++++++++++++++++++++++++++++
      queueMicrotask(() => {
        this.onFulfilled.forEach(fn => fn(value))
      })
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
      // ++++++++++++++++++++++++++++++
      queueMicrotask(() => {
        this.onRejected.forEach(fn => fn(reason))
      })
    }
  }

  then(onFulfilled: (value: any) => void, onRejected: (reason: any) => void) {
  // +++++++++++++++++++++++++++++++++++
    if (this.promiseState === Status.PENDING) {
      this.onFulfilled.push(onFulfilled)
      this.onRejected.push(onRejected)
    }
    if (this.promiseState === Status.FULFILLED) {
      queueMicrotask(() => {
        onFulfilled(this.promiseResult)
      })
    }
    if (this.promiseState === Status.REJECTED) {
      queueMicrotask(() => {
        onRejected(this.promiseResult)
      })
    }
  }
}
```

再测试一下：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813114514.png "image.png")
这下就正常了。为什么onFulfilled是数组呢？因为这样可以支持 then 的多次调用，比如
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813123225.png "image.png")
测试一下我们的 promise：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813123423.png "image.png")
然后来给 then 中的 resolve 和 reject 改为可选参数，如果reject 不为 function 的话且执行到的话，就抛出异常，如果 resolve 不为 function且执行的话，就返回 value。

```ts
then(onFulfilled?: (value: any) => void, onRejected?: (reason: any) => void) {
 // +++++++++++++++++++
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    // ++++++++++++++++++++
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
        throw reason;
    };
    if (this.promiseState === Status.PENDING) {
      this.onFulfilled.push(onFulfilled)
      this.onRejected.push(onRejected)
    }
    if (this.promiseState === Status.FULFILLED) {
      queueMicrotask(() => {
        onFulfilled(this.promiseResult)
      })
    }
    if (this.promiseState === Status.REJECTED) {
      queueMicrotask(() => {
        onRejected(this.promiseResult)
      })
    }
```

### then的链式调用

接下来要实现的是 then 的链式调用，这是手写 promise 最麻烦的部分。

```ts
let p1 = new Promise<number>((resolve, reject) => {
    resolve(100)
})
p1.then(res => {
    console.log('fulfilled', res);
    return 2 * res
}).then(res => {
    console.log('fulfilled', res)
})

// 输出 fulfilled 100 fulfilled 200
const p2 = new Promise<number>((resolve, reject) => {
    resolve(100)
})

p2.then(res => {
    console.log('fulfilled', res);
    return new Promise((resolve, reject) => resolve(3 * res))
}).then(res => {
    console.log('fulfilled', res)
})
// 输出 fulfilled 100 fulfilled 300
```

这块就要根据 [Promise A+规范](https://promisesaplus.com/#notes)来了。
这里简单复制过来。

> 2.2.7then 必须返回一个 Promise。
> 2.2.7.1如果 onFulfilled 或 onRejected 返回值 x，运行 Promise 解决程序 Resolve(promise2, x)。
> 2.2.7.2如果 onFulfilled 或 onRejected 抛出异常 e，promise2 必须以 e 为原因被拒绝。\
> 2.2.7.3如果 onFulfilled 不是一个函数且 promise1 被满足，promise2 必须以与 promise1 相同的值被满足。\
> 2.2.7.4如果 onRejected 不是一个函数且 promise1 被拒绝，promise2 必须以与 promise1 相同的原因被拒绝。
> 2.3Promise 解决过程\
> Promise 解决过程是一个抽象操作，输入一个 Promise 和一个值，我们将其表示为 Resolve(promise, x)。如果 x 是一个 thenable，它尝试让 promise 采纳 x 的状态，假设 x 至少表现得像是一个 Promise。否则，它用值 x 满足 promise。
> 这种对 thenables 的处理允许 Promise 实现进行互操作，只要它们暴露一个符合 Promises/A+ 的 then 方法。它也允许 Promises/A+ 实现能够同化具有合理 then 方法的非符合实现。
> 要运行 Resolve(promise, x)，执行以下步骤：
> 2.3.1如果 promise 和 x 指向同一个对象，则以 TypeError 为原因拒绝 promise。\
> 2.3.2如果 x 是一个 Promise，则采用其状态：\
> 2.3.2.1如果 x 处于挂起状态，promise 必须保持挂起，直到 x 被兑现或拒绝。\
> 2.3.2.2如果或当 x 被兑现时，以相同的值兑现 promise。\
> 2.3.2.3如果或当 x 被拒绝时，用同样的原因拒绝 promise。
> 2.3.3否则，如果 x 是一个对象或函数，\
> 2.3.3.1让 then 成为 x.then。\
> 2.3.3.2如果检索属性 x.then 导致抛出异常 e，则以 e 为理由拒绝 promise。\
> 2.3.3.3如果 then 是一个函数，使用 x 作为 this，第一个参数为 resolvePromise，第二个参数为 rejectPromise，其中：\
> 2.3.3.3.1如果或当 resolvePromise 被用值 y 调用时，运行 Resolve(promise, y)。\
> 2.3.3.3.2如果或当 rejectPromise 被调用并传入理由 r 时，使用 r 拒绝 promise。\
> 2.3.3.3.3如果 resolvePromise 和 rejectPromise 都被调用，或者对同一个参数进行了多次调用，则第一次调用具有优先级，后续的调用将被忽略。\
> 2.3.3.3.4如果调用 then 抛出异常 e，\
> 2.3.3.3.4.1如果 resolvePromise 或 rejectPromise 已经被调用，则忽略它。\
> 2.3.3.3.4.2否则，使用 e 作为原因拒绝 promise。
> 2.3.3.4如果 then 不是一个函数，则用 x 满足 promise。\
> 2.3.4如果 x 不是一个对象或函数，则用 x 满足 promise。
> 如果一个 Promise 使用了一个参与循环 thenable 链的 thenable 来解析，以至于 Resolve(promise, thenable) 的递归性质最终导致 Resolve(promise, thenable) 被再次调用，按照上述算法会导致无限递归。鼓励但不是必须要求实现检测这种递归，并以一个信息量丰富的 TypeError 作为理由来拒绝 promise。

这里借用一下大佬的总结:

1. then方法本身会返回一个新的Promise对象，返回一个新的Promise以后它就有自己的then方法，这样就能实现无限的链式。
2. 不论 promise1 被 resolve() 还是被 reject() 时 promise2 都会执行 Promise 解决过程：Resolve(promise2, x)

先来实现then方法中返回一个新 promise

```ts
then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
        throw reason;
    };

    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(onFulfilled)
        this.onRejected.push(onRejected)
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
          onFulfilled(this.promiseResult)
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          onRejected(this.promiseResult)
        })
      }
    });

    return promise
  }
```

实现不论 promise1 被 resolve() 还是被 reject() 时 promise2 都会执行 Promise 解决过程。

```ts
// promise 的解决过程
function resolvePromise(promise2, x, resolve, reject) {}
....
....
then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
        throw reason;
    };

    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(onFulfilled)
        this.onRejected.push(onRejected)
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
          const x = onFulfilled(this.promiseResult)
          // ++++++++++++++++++++
          resolvePromise(promise, x, resolve, reject)
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          const x = onRejected(this.promiseResult)
          // ++++++++++++++++++++
          resolvePromise(promise, x, resolve, reject)
        })
      }
    });

    return promise
  }
```

这里 promise的解决过程放到resolvePromise中，后续再实现。
实现如果如果 onFulfilled 或 onRejected 抛出异常 e，promise2 必须以 e 为原因被拒绝。

```ts
  then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
        throw reason;
    };

    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(onFulfilled)
        this.onRejected.push(onRejected)
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
        // +++++++++++++++++++++++++++
          try {
            const x = onFulfilled(this.promiseResult)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
        // +++++++++++++++++++++++++++++
          try {
            const x = onRejected(this.promiseResult)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    });

    return promise
  }
```

注意这里的 reject 是 promise2(也就是return 的 promise 的 reject)。
同时修改 pending 状态中的处理。

```ts
class Promise2025<T> {
  constructor(fn: Fn) {
    // 设置默认
    this.promiseState = Status.PENDING
    this.promiseResult = null
    this.onFulfilled = []
    this.onRejected = []
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  promiseState: Status
  promiseResult: any
  onFulfilled: Array<(value: T) => void> = []
  onRejected: Array<(reason: any) => void> = []

  _resolve(value: T) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
      // +++++++++++++++++++++++++
      // queueMicrotask(() => {
        this.onFulfilled.forEach(fn => fn(value))
      // })
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
      // +++++++++++++++++++++++++
      // queueMicrotask(() => {
        this.onRejected.forEach(fn => fn(reason))
      // })
    }
  }

  then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
        throw reason;
    };

    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(() => {
      // +++++++++++++++++++++++++
          queueMicrotask(() => {
            try {
              const x = onFulfilled(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
        this.onRejected.push(() => {
      // +++++++++++++++++++++++++
          queueMicrotask(() => {
            try {
              const x = onRejected(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.promiseResult)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.promiseResult)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    });

    return promise
  }
}
```

如果 `onFulfilled` 不是一个函数且 `promise1` 被满足， `promise2` 必须以与 `promise1` 相同的值被满足

```ts
then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    // -------------------
    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(() => {
          queueMicrotask(() => {
            try {
            // +++++++++++++++
              if (typeof onFulfilled !== 'function') {
                resolve(this.promiseResult)
              } else {
                const x = onFulfilled(this.promiseResult)
                resolvePromise(promise, x, resolve, reject)
              }
            } catch (e) {
              reject(e)
            }
          })
        })
        this.onRejected.push(() => {
          queueMicrotask(() => {
            try {
              const x = onRejected(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
        // +++++++++++++++++
          try {
            if (typeof onFulfilled !== 'function') {
              resolve(this.promiseResult)
            } else {
              const x = onFulfilled(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.promiseResult)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    });

    return promise
  }
```

如果 onRejected 不是一个函数且 promise1 被拒绝， promise2 必须以与 promise1 相同的原因被拒绝。

```ts
then(onFulfilled?: (value: T) => void, onRejected?: (reason: any) => void): any {
    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(() => {
          queueMicrotask(() => {
            try {
              if (typeof onFulfilled !== 'function') {
                resolve(this.promiseResult)
              } else {
                const x = onFulfilled(this.promiseResult)
                resolvePromise(promise, x, resolve, reject)
              }
            } catch (e) {
              reject(e)
            }
          })
        })
        this.onRejected.push(() => {
          queueMicrotask(() => {
            try {
            // +++++++++++++++++++++
              if (typeof onRejected !== 'function') {
                reject(this.promiseResult)
              } else {
                const x = onRejected(this.promiseResult)
                resolvePromise(promise, x, resolve, reject)
              }
            } catch (e) {
              reject(e)
            }
          })
        })
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
          try {
            if (typeof onFulfilled !== 'function') {
              resolve(this.promiseResult)
            } else {
              const x = onFulfilled(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          try {
          // ++++++++++++++++
            if (typeof onRejected !== 'function') {
              reject(this.promiseResult)
            } else {
              const x = onRejected(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
    });

    return promise
  }
```

根据A+规范 2.3，实现 resolvePromise
2.3.1 如果 `promise` 和 `x` 指向同一个对象，则以 `TypeError` 为原因拒绝 `promise`

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
}
```

2.3.2 如果 `x` 是一个 Promise，则采用其状态。如果 `x` 处于挂起状态， `promise` 必须保持挂起，直到 `x` 被兑现或拒绝。如果/当 `x` 被兑现时，以相同的值兑现 `promise` 。如果/当 `x` 被拒绝时，用同样的原因拒绝 `promise` 。

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  }
}
```

2.3.3 如果 `x` 是一个对象或函数。让 `then` 成为 `x.then`，如果检索属性 `x.then` 导致抛出异常 `e` ，则以 `e` 为理由拒绝 `promise`。

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
  // ++++++++++++++++++++++++++
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }
  }
}
```

如果 `then` 是一个函数，使用 `x` 作为 `this` ，第一个参数为 `resolvePromise` ，第二个参数为 `rejectPromise`

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }
    // ++++++++++++++++++++
    if (typeof then === 'function') { 
      then.call(x, y => {
        // 如果/当 `resolvePromise` 被用值 `y` 调用时，运行 `[[Resolve]](promise, y)` 。
        resolvePromise(promise2, y, resolve, reject)
        //如果/当 `rejectPromise` 被调用并传入理由 `r` 时，使用 `r` 拒绝 `promise` 。
      }, reject)
    }
  }
}
```

如果 `resolvePromise` 和 `rejectPromise` 都被调用，或者对同一个参数进行了多次调用，则第一次调用具有优先级，后续的调用将被忽略。

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }
    if (typeof then === 'function') { 
    // ++++++++++++++++++++++
      let called = false
      then.call(x, y => {
        if (called) return
        called = true
        resolvePromise(promise2, y, resolve, reject)
      }, r => {
        if (called) return
        called = true
        reject(r)
      })
    }
  }
}
```

如果调用 `then` 抛出异常 `e`，如果 `resolvePromise` 或 `rejectPromise` 已经被调用，则忽略它，否则，使用 `e` 作为原因拒绝 `promise` 。

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }
    if (typeof then === 'function') { 
      let called = false
      try {
        then.call(x, y => {
          if (called) return
          called = true
          resolvePromise(promise2, y, resolve, reject)
        }, r => {
          if (called) return
          called = true
          reject(r)
        })
      } catch (e) {
      // ++++++++++++++++++++++++++++++
        // 如果 `resolvePromise` 或 `rejectPromise` 已经被调用，则忽略它
        if (called) return
        //否则，使用 `e` 作为原因拒绝 `promise` 。
        reject(e)
      }
    }
  }
}
```

如果 `then` 不是一个函数，则用 `x` 满足 `promise`，如果 `x` 不是一个对象或函数，则用 `x` 满足 `promise` 。

```ts
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
    let then
    try {
      then = x.then
    } catch (e) {
      reject(e)
    }
    if (typeof then === 'function') { 
      let called = false
      try {
        then.call(x, y => {
          if (called) return
          called = true
          resolvePromise(promise2, y, resolve, reject)
        }, r => {
          if (called) return
          called = true
          reject(r)
        })
      } catch (e) {
        // 如果 `resolvePromise` 或 `rejectPromise` 已经被调用，则忽略它
        if (called) return
        //否则，使用 `e` 作为原因拒绝 `promise` 。
        reject(e)
      }
    } else {
      // 如果 `then` 不是一个函数，则用 `x` 满足 `promise`
      resolve(x)
    }
  } else {
    // 如果 x 不是一个对象或函数，则用 x 满足 promise 。
    resolve(x)
  }
}
```

### 结束

这样 promise 就完成了，使用 `promises-aplus-tests`工具测试:
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250813180015.png "image.png")
最后完善一下类型。

```ts
type Fn = (resole: (value: any) => void, reject: (reason: any) => void) => void

enum Status {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected'
}

type Resolve<T> = (value: T | PromiseLike<T>) => void
type Reject = (reason?: any) => void;

class Promise2025<T> {
  constructor(fn: Fn) {
    // 设置默认
    this.promiseState = Status.PENDING
    this.onFulfilled = []
    this.onRejected = []
    try {
      fn(this._resolve.bind(this), this._reject.bind(this))
    } catch (error) {
      this._reject(error)
    }
  }

  promiseState: Status
  promiseResult: T
  onFulfilled: Resolve<T>[] = []
  onRejected: Reject[] = []

  _resolve(value: T) {
    // resolve 后，如果为 pending 就变为 fulfilled，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.FULFILLED
      this.promiseResult = value
        this.onFulfilled.forEach(fn => fn(value))
    }
  }

  _reject(reason: any) {
    // reject 后，如果为 pending 就变为 rejected，不为 pending 就不变（不可逆转）
    if (this.promiseState === Status.PENDING) {
      this.promiseState = Status.REJECTED
      this.promiseResult = reason
      // queueMicrotask(() => {
        this.onRejected.forEach(fn => fn(reason))
      // })
    }
  }

  then(onFulfilled?: (value: T) => T | PromiseLike<T>, onRejected?: (reason: any) => any): any {
    const promise = new Promise2025<T>(async (resolve, reject) => { 
      if (this.promiseState === Status.PENDING) {
        this.onFulfilled.push(() => {
          queueMicrotask(() => {
            try {
              if (typeof onFulfilled !== 'function') {
                resolve(this.promiseResult)
              } else {
                const x = onFulfilled(this.promiseResult)
                resolvePromise(promise, x, resolve, reject)
              }
            } catch (e) {
              reject(e)
            }
          })
        })
        this.onRejected.push(() => {
          queueMicrotask(() => {
            try {
              if (typeof onRejected !== 'function') {
                reject(this.promiseResult)
              } else {
                const x = onRejected(this.promiseResult)
                resolvePromise(promise, x, resolve, reject)
              }
            } catch (e) {
              reject(e)
            }
          })
        })
      }
      if (this.promiseState === Status.FULFILLED) {
        queueMicrotask(() => {
          try {
            if (typeof onFulfilled !== 'function') {
              resolve(this.promiseResult)
            } else {
              const x = onFulfilled(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.promiseState === Status.REJECTED) {
        queueMicrotask(() => {
          try {
            if (typeof onRejected !== 'function') {
              reject(this.promiseResult)
            } else {
              const x = onRejected(this.promiseResult)
              resolvePromise(promise, x, resolve, reject)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
    });

    return promise
  }
}

// promise 的解决过程
function resolvePromise<T>(promise2: Promise2025<T>, x: T | PromiseLike<T>, resolve: Resolve<T>, reject: Reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'))
  }
  if (x instanceof Promise2025) {
    x.then(y => {
      resolvePromise(promise2, y, resolve, reject)
    }, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { 
    let then: PromiseLike<T>['then'] | undefined = undefined
    try {
      then = (x as PromiseLike<T>).then
    } catch (e) {
      reject(e)
    }
    if (typeof then === 'function') { 
      let called = false
      try {
        then.call(x, ((y: T | PromiseLike<T>) => {
          if (called) return
          called = true
          resolvePromise(promise2, y, resolve, reject)
        }), ((r: any) => {
          if (called) return
          called = true
          reject(r)
        }))
      } catch (e) {
        // 如果 `resolvePromise` 或 `rejectPromise` 已经被调用，则忽略它
        if (called) return
        //否则，使用 `e` 作为原因拒绝 `promise` 。
        reject(e)
      }
    } else {
      // 如果 `then` 不是一个函数，则用 `x` 满足 `promise`
      resolve(x)
    }
  } else {
    // 如果 x 不是一个对象或函数，则用 x 满足 promise 。
    resolve(x)
  }
}

module.exports = Promise2025
```

### promise的其他方法

#### promise.all

**`Promise.all()`** 静态方法接受一个 Promise 可迭代对象作为输入，并返回一个 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。当所有输入的 Promise 都被兑现时，返回的 Promise 也将被兑现（即使传入的是一个空的可迭代对象），并返回一个包含所有兑现值的数组。如果输入的任何 Promise 被拒绝，则返回的 Promise 将被拒绝，并带有第一个被拒绝的原因。

```ts
function isPromise(value: any): value is PromiseLike<any> {
  return value instanceof Promise2025 || (typeof value === 'object' && value !== null && typeof value.then === 'function')
}

all(promises: PromiseLike<any>[]) {
    return new Promise((resolve, reject) => {
      if (promises.length === 0) {
        resolve([]);
        return;
      }

      const results = new Array(promises.length);
      let count = 0;

      promises.forEach((p, i) => {
        Promise.resolve(p)
          .then(value => {
            results[i] = value;
            count++;
            if (count === promises.length) {
              resolve(results);
            }
          })
          .catch(reject); // 有一个失败就直接 reject
      });
    });
  }
```

#### promise.any

**`Promise.any()`** 静态方法将一个 Promise 可迭代对象作为输入，并返回一个 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。当输入的任何一个 Promise 兑现时，这个返回的 Promise 将会兑现，并返回第一个兑现的值。当所有输入 Promise 都被拒绝（包括传递了空的可迭代对象）时，它会以一个包含拒绝原因数组的 [`AggregateError`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) 拒绝。

```ts
any(promises: PromiseLike<any>[]) {
    return new Promise((resolve, reject) => {
      if (promises.length === 0) {
        reject(new AggregateError([], 'All promises were rejected'));
        return;
      }

      let count = 0;
      const errors = new Array(promises.length);

      promises.forEach((p, i) => {
        Promise.resolve(p)
          .then(resolve)
          .catch(err => {
            errors[i] = err;
            count++;
            if (count === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          });
      });
    });
  }
```

#### promise.race

**`Promise.race()`** 静态方法接受一个 promise 可迭代对象作为输入，并返回一个 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。这个返回的 promise 会随着第一个 promise 的敲定而敲定。

```ts
race(promises: PromiseLike<T>[]) {
    return new Promise((resolve, reject) => {
      if (promises.length === 0) return; // 永远 pending
      for (const p of promises) {
        Promise.resolve(p).then(resolve, reject);
      }
    });
  }
```

#### promise.allSettled

**`Promise.allSettled()`** 静态方法将一个 Promise 可迭代对象作为输入，并返回一个单独的 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。当所有输入的 Promise 都已敲定时（包括传入空的可迭代对象时），返回的 Promise 将被兑现，并带有描述每个 Promise 结果的对象数组。

```ts
allSettled(promises: PromiseLike<any>[]) {
    return new Promise((resolve) => {
      const result = new Array(promises.length);
      let count = 0;

      promises.forEach((p, i) => {
        Promise.resolve(p)
          .then(value => {
            result[i] = { status: 'fulfilled', value };
          })
          .catch(reason => {
            result[i] = { status: 'rejected', reason };
          })
          .finally(() => {
            count++;
            if (count === promises.length) {
              resolve(result);
            }
          });
      });
    });
  }
```

#### promise.withResolvers

**`Promise.withResolvers()`** 静态方法返回一个对象，其包含一个新的 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) 对象和两个函数，用于解决或拒绝它，对应于传入给 [`Promise()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) 构造函数执行器的两个参数。

```ts
withResolvers() {
    let resolve, reject;
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  }
```

#### promise.try

**`Promise.try()`** 静态方法接受一个任意类型的回调函数（无论其是同步或异步，返回结果或抛出异常），并将其结果封装成一个 [`Promise`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。

```ts
try(fn: Function, ...args: any[]) {
    return new Promise((resolve, reject) => {
      try {
        resolve(fn(...args))
      } catch (err) {
        reject(err);
      }
    });
  }
```

]]></description><link>https://9999886.xyz/posts/A39uH0KBKStbVurbpvjYV</link><guid isPermaLink="true">https://9999886.xyz/posts/A39uH0KBKStbVurbpvjYV</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Wed, 13 Aug 2025 10:22:07 GMT</pubDate></item><item><title><![CDATA[vue的双端对比和最长递增子序列]]></title><description><![CDATA[> 当组件创建和更新时，vue 都会执行内部的 update 函数，该函数在内部调用 render 函数生成虚拟 dom ，组件会指向新树，然后 vue 将新旧两个虚拟 dom 对比，找到差异点，最终更新到真实 dom，对比差异的过程叫 diff，vue 内部通过一个 patch 函数来完成该过程。
> 在对比时，vue 采用深度优先、逐层比较的方式进行比对。在判断两个节点是否相同时，vue 通过虚拟节点的 key 和 tag 来进行判断。具体来说，首先对比根节点，如果相同则将旧节点的真实 dom 的引用挂到新节点，然后根据需要更新属性到真实 dom，然后再对比其子节点。

### 双端对比

对比其子节点时，vue 对每个子节点数组使用了两个指针，分别指向头尾，然后不断向中间靠拢来进行对比，这样做的目的是尽量服用真实 dom。如果发现相同，则进入和根节点一样的对比流程，如果发现不同，则移动真实 dom 到合适的位置。
vue 源码：

```ts
function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (__DEV__) {
    checkDuplicateKeys(newCh)
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        )
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) {
        // New element
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        )
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          )
          oldCh[idxInOld] = undefined
          canMove &&
            nodeOps.insertBefore(
              parentElm,
              vnodeToMove.elm,
              oldStartVnode.elm
            )
        } else {
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    )
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}
```

每轮循环尝试匹配新旧节点的 4 种组合。下面逐个分析。
主要判断顺序：
**头头相同，patch 后指针移动**

```ts
patchVnode(oldStartVnode, newStartVnode, ...)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
```

* 最理想的情况：**新旧列表一致，只是内容变了**。
* 比如：\[A, B, C] → \[A, B, C]，或者 \[A, B] → \[A, B, C]
  直接 patch（更新）两个节点，两个头指针向右推进。

> 什么是 patch？ patchVnode 的主要职责就是对两个 vnode（旧 vnode 和新 vnode）做处理，最终目的是“最小化 DOM 操作”，更新对应的真实 DOM 节点（elm）。

**尾尾相同**

```ts
patchVnode(oldEndVnode, newEndVnode, ...)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
```

* 情况类似上面，只是更新发生在尾部
* 比如：\[A, B, C] → \[X, B, C]，可以从尾部更新 C
  直接 patch（更新）两个节点，两个尾指针向左收缩
  **旧头 = 新尾（节点右移）**

```ts
patchVnode(oldStartVnode, newEndVnode, ...)
    // 移动 DOM
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
```

* **旧头元素被移到新尾部了**，只要移动 DOM。
* 例如：\[A, B, C] → \[B, C, A]，A 从头部移到了尾部。
  patch 后 移动，旧头指针向右推进，新尾指针向左收缩

> 怎么移动？ parent.insertBefore(elm, reference)

**旧尾 = 新头（节点左移**）

```ts
patchVnode(oldEndVnode, newStartVnode, ...)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
```

* **旧尾元素被移到新头部了**。
* 例如：\[A, B, C] → \[C, A, B]，C 从尾部移到了头部。
  **不匹配，则建立 key-to-index map**
  **没有匹配的情况：查找**  **key** **（fallback）**

```ts
if (isUndef(oldKeyToIdx)) {
      oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    }
    const idxInOld = oldKeyToIdx[newStartVnode.key]
    if (isUndef(idxInOld)) {
      // 新节点是全新的，创建插入
      createElm(newStartVnode, ...)
      nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
    } else {
      const vnodeToMove = oldCh[idxInOld]
      if (sameVnode(vnodeToMove, newStartVnode)) {
        patchVnode(vnodeToMove, newStartVnode, ...)
        oldCh[idxInOld] = undefined
        nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
      } else {
        // key 相同但 vnode 不同 → 直接创建新 vnode
        createElm(newStartVnode, ...)
        nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
      }
    }
    newStartVnode = newCh[++newStartIdx]
```

* 前 4 种情况都不匹配，查找 key 是否有一样的。
*
  * 有一样的就拿来 patch + 移动，否则就是新增
    **循环结束后的清理阶段**
* 还有新节点没处理 → 全部插入
* 还有旧节点没处理 → 全部删除

```
oldCh:  [oldStart →     …     ← oldEnd]
newCh:  [newStart →     …     ← newEnd]

1. oldStart == newStart → patch + 向右推进
2. oldEnd == newEnd     → patch + 向左推进
3. oldStart == newEnd   → patch + 移动 oldStart 到末尾
4. oldEnd == newStart   → patch + 移动 oldEnd 到开头
5. fallback（key 查找）：
   - 有 → patch + 移动
   - 无 → create + 插入
```

**当旧头 (oldStartVnode) 等于新尾 (newEndVnode) 时，为什么把节点移动到“旧尾”的后面？不是应该移到“新尾”吗**？

1. 当前 DOM 是旧的，我们还没完成更新。
2. 因为我们是双端比对，并不是立即更新所有节点位置，而是随着指针移动逐步更新。

### 最长递增子序列

vue 源码：

```ts
const patchKeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // prev ending index
  let e2 = l2 - 1 // next ending index

  // 1. sync from start
  // (a b) c
  // (a b) d e
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      break
    }
    i++
  }

  // 2. sync from end
  // a (b c)
  // d e (b c)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = (c2[e2] = optimized
      ? cloneIfMounted(c2[e2] as VNode)
      : normalizeVNode(c2[e2]))
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      break
    }
    e1--
    e2--
  }

  // 3. common sequence + mount
  // (a b)
  // (a b) c
  // i = 2, e1 = 1, e2 = 2
  // (a b)
  // c (a b)
  // i = 0, e1 = -1, e2 = 0
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
      while (i <= e2) {
        patch(
          null,
          (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i])),
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        i++
      }
    }
  }

  // 4. common sequence + unmount
  // (a b) c
  // (a b)
  // i = 2, e1 = 2, e2 = 1
  // a (b c)
  // (b c)
  // i = 0, e1 = 0, e2 = -1
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }

  // 5. unknown sequence
  // [i ... e1 + 1]: a b [c d e] f g
  // [i ... e2 + 1]: a b [e d c h] f g
  // i = 2, e1 = 4, e2 = 5
  else {
    const s1 = i // prev starting index
    const s2 = i // next starting index

    // 5.1 build key:index map for newChildren
    const keyToNewIndexMap: Map<PropertyKey, number> = new Map()
    for (i = s2; i <= e2; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      if (nextChild.key != null) {
        if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
          warn(
            `Duplicate keys found during update:`,
            JSON.stringify(nextChild.key),
            `Make sure keys are unique.`,
          )
        }
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }

    // 5.2 loop through old children left to be patched and try to patch
    // matching nodes & remove nodes that are no longer present
    let j
    let patched = 0
    const toBePatched = e2 - s2 + 1
    let moved = false
    // used to track whether any node has moved
    let maxNewIndexSoFar = 0
    // works as Map<newIndex, oldIndex>
    // Note that oldIndex is offset by +1
    // and oldIndex = 0 is a special value indicating the new node has
    // no corresponding old node.
    // used for determining longest stable subsequence
    const newIndexToOldIndexMap = new Array(toBePatched)
    for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i]
      if (patched >= toBePatched) {
        // all new children have been patched so this can only be a removal
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
      }
      let newIndex
      if (prevChild.key != null) {
        newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {
        // key-less node, try to locate a key-less node of the same type
        for (j = s2; j <= e2; j++) {
          if (
            newIndexToOldIndexMap[j - s2] === 0 &&
            isSameVNodeType(prevChild, c2[j] as VNode)
          ) {
            newIndex = j
            break
          }
        }
      }
      if (newIndex === undefined) {
        unmount(prevChild, parentComponent, parentSuspense, true)
      } else {
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex
        } else {
          moved = true
        }
        patch(
          prevChild,
          c2[newIndex] as VNode,
          container,
          null,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        patched++
      }
    }

    // 5.3 move and mount
    // generate longest stable subsequence only when nodes have moved
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : EMPTY_ARR
    j = increasingNewIndexSequence.length - 1
    // looping backwards so that we can use last patched node as anchor
    for (i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex] as VNode
      const anchorVNode = c2[nextIndex + 1] as VNode
      const anchor =
        nextIndex + 1 < l2
          ? // #13559, fallback to el placeholder for unresolved async component
            anchorVNode.el || anchorVNode.placeholder
          : parentAnchor
      if (newIndexToOldIndexMap[i] === 0) {
        // mount new
        patch(
          null,
          nextChild,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else if (moved) {
        // move if:
        // There is no stable subsequence (e.g. a reverse)
        // OR current node is not among the stable sequence
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          j--
        }
      }
    }
  }
}
```

* c1: 旧 children 列表（VNode\[])
* c2: 新 children 列表（VNode\[])
* container: 当前 DOM 容器
  **从头和尾开始同步相同节点（双端 diff）**

```ts
// sync from start
while (i <= e1 && i <= e2) {
  if (isSameVNodeType(c1[i], c2[i])) {
    patch(...)
    i++
  } else break
}

// sync from end
while (i <= e1 && i <= e2) {
  if (isSameVNodeType(c1[e1], c2[e2])) {
    patch(...)
    e1--
    e2--
  } else break
}
```

**目的**：尽早对比出头尾一致部分，减少中间需要 diff 的范围。

**处理新增或删除节点**

```ts
if (i > e1) {
  // 全是新增节点，直接 addVnodes
  while (i <= e2) insert(...)
} else if (i > e2) {
  // 全是删除节点，直接 removeVnodes
  while (i <= e1) unmount(...)
}
```

**处理中间乱序部分 + 移动逻辑**
**建立 key -> newIndex 映射**

```ts
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
  const nextChild = c2[i]
  keyToNewIndexMap.set(nextChild.key, i)
}
```

**遍历旧 children，尝试复用新节点**

```ts
for (i = s1; i <= e1; i++) {
  const prevChild = c1[i]
  const newIndex = keyToNewIndexMap.get(prevChild.key)
  if (newIndex === undefined) {
    remove(prevChild)
  } else {
    patch(prevChild, c2[newIndex])
    newIndexToOldIndexMap[newIndex - s2] = i + 1
  }
}
```

newIndexToOldIndexMap 是关键数组：

* 值表示：新节点在旧列表中的位置（+1，0表示新插入）
* 后面将用这个数组计算最长递增子序列

#### 为什么需要“最长递增子序列”？

在两个 vnode 列表中（旧列表 c1 和新列表 c2），Vue 想要知道：

* 哪些节点是复用的（即相同类型+相同 key）？
* 哪些需要卸载？（旧节点在新列表中找不到）
* 哪些需要新建？（新节点在旧列表中找不到）
* 哪些复用节点需要移动？
  重点就在于“尽量减少移动操作”。

#### Vue 是怎么使用LIS的？

1. Vue 在 **patchKeyedChildren** 的“第 5 步”中处理复杂的 diff 情况，也就是列表中间有变动的情况。
2. 它建立了一个 newIndexToOldIndexMap 数组，记录新列表每个元素对应的旧位置（没有旧位置的就是新节点，值为 0）。
3. 然后，通过这个数组计算出一个 **最长递增子序列**，这表示：这些节点在新列表中的顺序在旧列表中是“稳定的”，不需要移动。
4. 剩下那些不在 LIS 中的旧节点，就需要执行 move()。

```ts
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}
```

假设

```ts
arr = [5, 3, 4, 8, 6, 7]
```

最长递增子序列是：\[3, 4, 6, 7]，即索引 \[1, 2, 4, 5] → getSequence(arr) 返回 \[1, 2, 4, 5]。
**函数内部变量**

```ts
const p = arr.slice()       // 记录每个元素的前驱索引，用于最后追溯路径
const result = [0]          // 存储构成递增子序列的索引（索引来自 arr）
```

* p\[ i ] : 表示 arr\[i] 的前一个索引是谁（形成路径）
* result: 是当前发现的“最长递增子序列”的索引序列

```ts
for (i = 0; i < len; i++) {
  const arrI = arr[i]
  if (arrI !== 0) {
    ...
  }
}
```

Vue 中传入的 arr 可能含有 0 —— 表示是新节点（没有旧位置），忽略它们。只处理非 0 元素。

```ts
j = result[result.length - 1]   // 最后一个索引
if (arr[j] < arrI) {
  p[i] = j
  result.push(i)
  continue
}
```

* 如果 arrI 比当前 LIS 的最后一个值大，那就可以直接接上 → 更新 p\[i] = j（记录前驱）
* 并把当前 i 加入到 result 中
  如果并不大于，那么

```ts
u = 0
v = result.length - 1
while (u < v) {
  // 取 u 和 v 的中间位置，赋值给 c。>> 1：是右移一位，相当于除以 2（向下取整）
  c = (u + v) >> 1
  if (arr[result[c]] < arrI) {
    u = c + 1
  } else {
    v = c
  }
}
```

* 用 **二分法**找出第一个大于等于 arrI 的位置（贪心+优化）
* 也就是：我们尝试“替换掉更大但没用的尾巴”，保持 result 尽可能小

```ts
if (arrI < arr[result[u]]) {
  if (u > 0) {
    p[i] = result[u - 1]
  }
  result[u] = i
}
```

* 如果找的位置 arr\[result\[u]] 大于 arrI，就替换掉
* p\[i] = result\[u - 1] → 记录当前值接在谁后面

```ts
u = result.length
v = result[u - 1]
while (u-- > 0) {
  result[u] = v
  v = p[v]
}
```

* 倒着从 result 的最后一个索引开始
* 每次向前追溯到 p\[v]（即前驱）
* 最终恢复出完整的 LIS

```ts
// 输入
arr = [2, 5, 3, 7, 4, 8]
// 输出
[0, 2, 4, 5]   // 即 [2, 3, 4, 8]
```

这里最长递增子序列结束，找出**不需要移动的节点序列**，其余的节点则通过 insert 来移动。

* 这些“递增”的新节点，其在旧列表中的位置是有序的；
* 说明它们在 DOM 中本来就是按顺序的；
* 所以我们只移动其他“打乱顺序”的节点；
* 能够达到最小移动代价。

```ts
<!-- 初始 DOM -->
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>


-------------

<div>A</div>
<div>C</div>
<div>B</div>
<div>D</div>
```

* A 和 D 没变
* B 和 C 顺序互换了
* 最长递增子序列是 \[A, D]

]]></description><link>https://9999886.xyz/posts/JWR1LflMIdRvILI6HNPkH</link><guid isPermaLink="true">https://9999886.xyz/posts/JWR1LflMIdRvILI6HNPkH</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Mon, 28 Jul 2025 13:57:16 GMT</pubDate></item><item><title><![CDATA[事件循环]]></title><description><![CDATA[### 什么是进程

进程就是程序运行所需要的专属内存空间。每个应用至少有一个进程，进程之间相互独立，双方同意下可以通信。

### 什么是线程

用来运行代码的就是线程，线程是 **CPU 调度的最小单位**。一个进程至少有一个线程，进程开启后就会创建一个线程运行代码，该线程就是主线程。主线程结束了，整个进程也结束了。一个进程中可以包含多个线程。

### 浏览器有哪些进程和线程

浏览器是一个多进程多线程的应用程序。浏览器为了避免相互影响，为了减少连环崩溃的几率，当启动浏览器后，它会自动启动多个进程。
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250727132945.png "image.png")
渲染进程启动后，会开启一个渲染主线程，负责执行 HTML、CSS、JS 代码。
默认情况下，每一个标签页一个单独的渲染进程（后续可能改为每个站点一个进程或其他模式）。
**事件循环就发生在渲染主线程。**

### 渲染主线程是怎么工作的

渲染主线程的任务包括单不限于：

* 解析 HTML
* 解析 CSS
* 计算样式
* 布局
* 处理图层
* 执行 js
* 执行事件处理函数
* 执行计时器的回调
  如何调度这些任务？队列

1. 最开始时，渲染主线程会进入一个无限循环
2. 每一次循环会检查消息队列中是否有任务存在，有的话就取第一个开始执行，执行完成后进入下一次循环。如果没有后续，则进入休眠模式。
3. 其他所有线程，可以随时向队列中添加任务，如果此时主线程在休眠，则会进行唤醒以继续循环执行任务。
   这个过程，就是事件循环。

### 什么是异步

代码执行过程中，会遇到一些无法立即处理的任务，比如

* 计时完成后需要执行的任务 - 计时器 定时器
* 网络通讯完成后执行的任务 - fetch ajax
* 用户操作后需要执行的任务 - addEventListener
  为了避免主线程阻塞，浏览器选择用异步来处理这个问题。

### 任务有优先级吗

任务没有优先级，但是任务队列有优先级。

* 每个任务都有一个任务类型，同一个类型的任务必须在一个队列，不同类型的任务可以分属于不同的队列，在一次事件循环中，浏览器可以根据实际情况从不同的队列中取出任务执行。
* 浏览器必须准备好一个微队列，微队列种的任务优先所有其他任务执行

> 随着浏览器的复杂度急剧提升，W3C 不再使用宏任务的说法。

在目前 chrome 的实现中，至少包含了以下队列：

* 延时队列：存放计时器到达后的回调任务 优先级：中
* 交互队列：存放用户操作后的回调：优先级：高
* 微队列：存放需要最快执行的任务，优先级：最高

### Q\&A

#### 为什么渲染进程不使用多个线程来处理这些事情？

因为 DOM、CSSOM 和 JS 都是强依赖状态的，必须保证线程安全；多线程会引入复杂的锁机制和同步问题，得不偿失。

#### 如何理解 js 的异步

js 是一门单线程的语言，这是因为它运行在浏览器的渲染主线程中，渲染主线程只有一个。
而渲染主线程承担着很多工作，比如渲染页面，执行 js 等等。
如果使用同步的方式，就很有可能导致主线程阻塞，从而导致消息队列中的很多其他任务都无法执行，比如等待一个三秒的定时器。这样一来，浏览器就卡死了。
所以浏览器采用异步的方式来避免，具体做法是当某些任务发生时，比如计算器、网络、事件监听，主线程将任务交给其他线程来处理，转而执行后续其他任务，当其他线程完成时，将事先传递的回调函数包装成任务，加入消息队列的末尾，等待主线程调度执行。

#### 阐述一下 js 的事件循环

事件循环又叫做消息循环，是浏览器渲染主线程的工作方式。
在 chrome 中，它开启一个不会结束的 for 循环，每次循环从消息队列中取出第一个任务执行，而其他线程只需要在合适的时候将任务放到队列末尾即可。
过去吧消息队列简单分为宏队列和微队列，这种说法目前已经无法满足复杂的浏览器环境，现在根据 W3C 官方的解释，每个任务有不同的类型，同类型的任务必须在同一个队列，不同的任务可以属于不同的队列。不同任务队列有不同的优先级，在一个事件循环中，有浏览器自行决定去哪个队列的任务，但是浏览器必须有一个微队列，微队列的任务一定具有最高的优先级，必须优先调度执行。

#### js中的计时器能做到精确计时吗？

不能。

1. 计算机硬件无法做到精确计时。
2. js 计时器最终调用的是操作系统的函数，操作系统的计时函数本身就有少量偏差。
3. 按照 w3c 的标准，浏览器实现计时器时，如果嵌套层级超过 5 层，则会带有 4 毫秒的最少事件，这样在计时事件少于4 毫秒时有带来了偏差。
4. 受事件循环的影响，计时器回调函数只能在主线程空闲时运行，因此又有偏差。

]]></description><link>https://9999886.xyz/posts/SIscisH2TcT16JOaHhNTE</link><guid isPermaLink="true">https://9999886.xyz/posts/SIscisH2TcT16JOaHhNTE</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Sun, 27 Jul 2025 06:36:35 GMT</pubDate></item><item><title><![CDATA[浏览器缓存]]></title><description><![CDATA[### 什么是浏览器缓存？

> 浏览器缓存(Brower Caching)是浏览器对之前请求过的文件进行缓存，以便下一次访问时重复使用，节省带宽，提高访问速度，降低服务器压力

http缓存机制主要在http响应头中设定，响应头中相关字段为**Expires**、**Cache-Control**、**Last-Modified**、**Etag**。

### 缓存的类别

浏览器缓存分为强缓存和协商缓存

本质区别在于 强缓存是**不需要发送HTTP请求的**, 而协商缓存需要.也**就是在发送HTTP请求之前, 浏览器会先检查一下强缓存, 如果命中直接使用，否则就进入下一步。**

#### 强缓存

浏览器不会像服务器发送任何请求，直接从本地缓存中读取文件并返回Status Code: 200 OK

浏览器检查强缓存的方式主要是判断这两个字段:

* HTTP/1.0时期使用的是**Expires**;
* HTTP/1.1使用的是**Cache-Control**
  (expires中文意思有效期, cache-control中文意思缓存管理)

Expires 是一个具体的时间
Cache-Control: max-age=300

表示的是这个资源在响应之后的300s内过期, 也就是5分钟之内再次获取这个资源会直接使用缓存.

Cache-Control:no-store 不缓存

Cache-Control:public 可以被多用户缓存，包括终端和CDN等中级代理服务器

Cache-Control:private 只能被客户端或浏览器缓存

Cache-Control:no-cache no-cache 总是会命中网络，因为在释放浏览器的缓存副本（除非服务器的响应的文件已更新）之前，它必须与服务器重新验证，不过如果服务器响应允许使用缓存副本，网络只会传输文件报头：文件主体可以从缓存中获取，而不必重新下载

Cache-Control: must-revalidate must-revalidate 需要一个关联的 max-age 指令；上文我们把它设置为 10 分钟。如果说 no-cache 会立即向服务器验证，经过允许后才能使用缓存的副本，那么 must-revalidate 更像是一个具有宽期限的 no-cache。情况是这样的，在最初的十分钟浏览器**不会**向服务器重新验证，但是就在十分钟过去的那一刻，它又到服务器去请求，如果服务器没什么新东西，它会返回 304 并且新的 Cache-Control 报头应用于缓存的文件 —— 我们的十分钟再次开始。如果十分钟后服务器上有了一个新的文件，我们会得到 200 的响应和它的报文，那么本地缓存就会被更新。

##### Expires 和 Cache-control的对比

* Expires产于HTTP/1.0,Cache-control产于HTTP/1.1;
* Expires设置的是一个具体的时间,Cache-control 可以设置具体时常还有其它的属性;
* 两者同时存在,Cache-control的优先级更高;
* 在不支持HTTP/1.1的环境下,Expires就会发挥作用, 所以先阶段的存在是为了做一些兼容的处理.

**若是设置了** **Expires** **, 但是服务器的时间与浏览器的时间不一致的时候(比如你手动修改了本地的时间), 那么就可能会造成缓存失效, 因此这种方式强缓存方式并不是很准确, 它也因此在** **HTTP/1.1** **中被摒弃了**

#### 协商缓存

如果没有命中强缓存，向服务器发送请求，服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存，如果命中，则返回304状态码并带上新的response header通知浏览器从缓存中读取资源；

协商缓存可以通过设置两种 HTTP Header 实现：Last-Modified 和 ETag对比：ETag更精确，性能上Last-Modified好点

* Last-Modified:\
  http1.0\
  原理：浏览器第一次访问资源时，服务器会在response头里添加Last-Modified时间点，这个时间点是服务器最后修改文件的时间点，然后浏览器第二次访问资源时，检测到缓存文件里有Last-Modified，就会在请求头里加If-Modified-Since，值为Last-Modified的值，服务器收到头里有If-Modified-Since，就会拿这个值和请求文件的最后修改时间作对比，如果没有变化，就返回304，如果小于了最后修改时间，说明文件有更新，就会返回新的资源，状态码为200
* ETag:\
  http1.1\
  原理：与Last-Modified类似，只是Last-Modified返回的是最后修改的时间点，而ETag是每次访问服务器都会返回一个新的token，第二次请求时，该值埋在请求头里的If-None-Match发送给服务器，服务器在比较新旧的token是否一致，一致则返回304通知浏览器使用本地缓存，不一致则返回新的资源，新的ETag，状态码为200

### 网页获取缓存（三级缓存）

从浏览器发起HTTP请求到获得请求结果, 可以分为以下几个过程:

1. 浏览器第一次发起HTTP请求, 在浏览器缓存中没有发现请求的缓存结果和缓存标识
2. 因此向服务器发起HTTP请求, 获得该请求的结果还有缓存规则(也就是Last-Modified或者ETag)
3. 浏览器把响应内容存入Disk Cache, 把响应内容的引用存入Memory Cache
4. 把响应内容存入Service Worker的Cache Storage(如果Service Worker的脚本调用了cache.put())

下一次请求相同资源的时候:

1. 调用Service Worker的fetch事件响应
2. 查看memory Cache
3. 查看disk Cache. 这里细分为:

* 有强缓存且未失效, 则使用强缓存, 不请求服务器, 返回的状态码都是200
* 有强缓存且已失效, 使用协商缓存判断, 是返回304还是200(读取缓存还是重新获取)

### LRU 缓存淘汰

LRU（Least recently used，最近最少使用）算法根据数据的历史访问记录来进行**淘汰**数据，其核心思想是“如果数据最近被访问过，那么将来被访问的几率也更高”。**Vue 的** **keep-alive** **组件中就用到了此算法**

整个流程大致为：

1. 新加入的数据插入到第一项
2. 每当缓存命中（即缓存数据被访问），则将数据提升到第一项
3. 当缓存数量满的时候，将最后一项的数据丢弃

```js
class LRUCahe {
  constructor(capacity) {
    this.cache = new Map();
    this.capacity = capacity; // 最大缓存容量
  }

  get(key) {
    if (this.cache.has(key)) {
      const temp = this.cache.get(key);
      this.cache.delete(key);
      this.cache.set(key, temp);

      return temp;
    }
    return undefined;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

// before
const cache = new Map();

// after
const cache = new LRUCahe(50);
```

### Q\&A

#### 怎么不使用本地缓存 使用协商缓存

Cache-Control

* no-cache 不使用本地缓存。需要使用协商缓存。
* 可以在客户端存储资源，**每次都必须去服务端做新鲜度校验**，来决定从服务端获取新的资源（200）还是使用客户端缓存（304）。也就是所谓的协商缓存

#### 怎么禁用本地缓存

* no-store直接禁止浏览器缓存数据，每次请求资源都会向服务器要完整的资源， 类似于 network 中的 disabled cache。**永远都不要在客户端存储资源**，永远都去原始服务器去获取资源。

#### 用户行为对缓存的影响

F5 刷新的时候，会暂时禁用强缓存
Ctrl + F5 强制刷新的时候，会暂时禁用强缓存和协商缓存

#### 强制不用任何缓存

Cache-control: no-store
]]></description><link>https://9999886.xyz/posts/kvhdiGRAQ7dXBjG5rlbKv</link><guid isPermaLink="true">https://9999886.xyz/posts/kvhdiGRAQ7dXBjG5rlbKv</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 24 Jul 2025 04:24:19 GMT</pubDate></item><item><title><![CDATA[Vue Template]]></title><description><![CDATA[### 目标

实现Vue的"template"功能。将

```ts
const app = createApp({ template: `<p class="hello">Hello World</p>` })
```

转换为:

```ts
const app = createApp({ render() { return h('p', { class: 'hello' }, ['Hello World']) }, })
```

### 简单的实现

先进行一个简易版的实现：

```ts:parse.ts
// 使用正则来提取
export const baseParse = (
  content: string,
): { tag: string; props: Record<string, string>; textContent: string } => {
  const matched = content.match(/<(\w+)\s+([^>]*)>([^<]*)<\/\1>/)
  if (!matched) return { tag: '', props: {}, textContent: '' }

  const [_, tag, attrs, textContent] = matched

  const props: Record<string, string> = {}
  attrs.replace(/(\w+)=["']([^"']*)["']/g, (_, key: string, value: string) => {
    props[key] = value
    return ''
  })

  return { tag, props, textContent }
}
```

```ts:codegen.ts
export const generate = ({
  tag,
  props,
  textContent,
}: {
  tag: string
  props: Record<string, string>
  textContent: string
}): string => {
  return `return () => {
  const { h } = vueImpl;
  return h("${tag}", { ${Object.entries(props)
    .map(([k, v]) => `${k}: "${v}"`)
    .join(', ')} }, ["${textContent}"]);
}`
}
```

> 实际上，Vue 有两种类型的编译器．\
> 一种是在运行时（在浏览器中）运行的编译器，另一种是在构建过程中（如 Node.js）运行的编译器．\
> 具体来说，运行时编译器负责编译 template 选项或作为 HTML 提供的模板，而构建过程编译器负责编译 SFC（或 JSX）．\
> 我们当前实现的 template 选项属于前者．
> 需要注意的重要一点是，两个编译器共享公共处理．\
> 这个公共部分的源代码在 `compiler-core` 目录中实现．\
> 运行时编译器和 SFC 编译器分别在 `compiler-dom` 和 `compiler-sfc` 目录中实现．

这时候可以进行简单的编译：

```ts
const app = createApp({ template: `<p class="hello">Hello World</p>` })
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250722163233.png "image.png")

### 更完整的实现

要解析更复杂的 template，简单的正则是不够的。Vue中的做法是**AST**（抽象语法树）。
目前，parse 函数的返回值如下
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250722163907.png "image.png")
仅仅这些是不够的，让我们来拓展它：

```ts:ast.ts
// 这表示节点的类型。
// 应该注意的是，这里的 Node 不是指 HTML Node，而是指这个模板编译器处理的粒度。
// 所以，不仅 Element 和 Text，Attribute 也被视为一个 Node。
// 这与 Vue.js 的设计一致，在将来实现指令时会很有用。
export const enum NodeTypes {
  ELEMENT,
  TEXT,
  ATTRIBUTE,
}

// 所有 Node 都有 type 和 loc。
// loc 代表位置，保存关于这个 Node 在源代码（模板字符串）中对应位置的信息。
// （例如，哪一行和行上的哪个位置）
export interface Node {
  type: NodeTypes
  loc: SourceLocation
}

// Element 的 Node。
export interface ElementNode extends Node {
  type: NodeTypes.ELEMENT
  tag: string // 例如 "div"
  props: Array<AttributeNode> // 例如 { name: "class", value: { content: "container" } }
  children: TemplateChildNode[]
  isSelfClosing: boolean // 例如 <img /> -> true
}

// ElementNode 拥有的 Attribute。
// 它可以表达为只是 Record<string, string>，
// 但它被定义为像 Vue 一样具有 name(string) 和 value(TextNode)。
export interface AttributeNode extends Node {
  type: NodeTypes.ATTRIBUTE
  name: string
  value: TextNode | undefined
}

export type TemplateChildNode = ElementNode | TextNode

export interface TextNode extends Node {
  type: NodeTypes.TEXT
  content: string
}

// 关于位置的信息。
// Node 有这个信息。
// start 和 end 包含位置信息。
// source 包含实际代码（字符串）。
export interface SourceLocation {
  start: Position
  end: Position
  source: string
}

export interface Position {
  offset: number // 从文件开始
  line: number
  column: number
}
```

```ts:parse.ts
export interface ParserContext {
  // 原始模板字符串
  readonly originalSource: string

  source: string

  // 此解析器正在读取的当前位置
  offset: number
  line: number
  column: number
}

function createParserContext(content: string): ParserContext {
  return {
    originalSource: content,
    source: content,
    column: 1,
    line: 1,
    offset: 0,
  }
}
```

解析的顺序是 parseChildren -> parseText -> parseElement

#### parseChildren

```ts:parse
export const baseParse = (
  content: string,
): { children: TemplateChildNode[] } => {
  const context = createParserContext(content)
  const children = parseChildren(context, []) // 解析子节点
  return { children: children }
}

function parseChildren(
  context: ParserContext,

  // 由于 HTML 具有递归结构，我们将祖先元素保持为堆栈，并在每次嵌套到子元素中时推送它们。
  // 当找到结束标签时，parseChildren 结束并弹出祖先。
  ancestors: ElementNode[],
): TemplateChildNode[] {
  const nodes: TemplateChildNode[] = []

  while (!isEnd(context, ancestors)) {
    const s = context.source
    let node: TemplateChildNode | undefined = undefined

    if (s[0] === '<') {
      // 如果 s 以 "<" 开头且下一个字符是字母，则将其解析为元素。
      if (/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors) // TODO: 稍后实现这个。
      }
    }

    if (!node) {
      // 如果不匹配上述条件，则将其解析为 TextNode。
      node = parseText(context) // TODO: 稍后实现这个。
    }

    pushNode(nodes, node)
  }

  return nodes
}

// 确定解析子元素的 while 循环结束的函数
function isEnd(context: ParserContext, ancestors: ElementNode[]): boolean {
  const s = context.source

  // 如果 s 以 "</" 开头且祖先的标签名跟随，它确定是否有闭合标签（parseChildren 是否应该结束）。
  if (startsWith(s, '</')) {
    for (let i = ancestors.length - 1; i >= 0; --i) {
      if (startsWithEndTagOpen(s, ancestors[i].tag)) {
        return true
      }
    }
  }

  return !s
}

function startsWith(source: string, searchString: string): boolean {
  return source.startsWith(searchString)
}

function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
  // 如果 Text 类型的节点是连续的，它们会被合并。
  if (node.type === NodeTypes.TEXT) {
    const prev = last(nodes)
    if (prev && prev.type === NodeTypes.TEXT) {
      prev.content += node.content
      return
    }
  }

  nodes.push(node)
}

function last<T>(xs: T[]): T | undefined {
  return xs[xs.length - 1]
}

function startsWithEndTagOpen(source: string, tag: string): boolean {
  return (
    startsWith(source, '</') &&
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
    /[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
  )
}
```

#### parseText

```ts:parse
function parseText(context: ParserContext): TextNode {
  // 读取直到 "<"（无论它是开始还是结束标签），并根据读取了多少字符计算 Text 数据结束点的索引。
  const endToken = '<'
  let endIndex = context.source.length
  const index = context.source.indexOf(endToken, 1)
  if (index !== -1 && endIndex > index) {
    endIndex = index
  }

  const start = getCursor(context) // 用于 loc

  // 根据 endIndex 的信息解析 Text 数据。
  const content = parseTextData(context, endIndex)

  return {
    type: NodeTypes.TEXT,
    content,
    loc: getSelection(context, start),
  }
}

// 根据内容和长度提取文本。
function parseTextData(context: ParserContext, length: number): string {
  const rawText = context.source.slice(0, length)
  advanceBy(context, length)
  return rawText
}

// -------------------- 以下是实用程序（也在 parseElement 等中使用） --------------------

function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  const { source } = context
  advancePositionWithMutation(context, source, numberOfCharacters)
  context.source = source.slice(numberOfCharacters)
}

function advanceSpaces(context: ParserContext): void {
  const match = /^[\t\r\n\f ]+/.exec(context.source);
  if (match) {
    advanceBy(context, match[0].length);
  }
}

// 虽然有点长，但它只是计算位置。
// 它破坏性地更新作为参数接收的 pos 对象。
function advancePositionWithMutation(
  pos: Position,
  source: string,
  numberOfCharacters: number = source.length,
): Position {
  let linesCount = 0
  let lastNewLinePos = -1
  for (let i = 0; i < numberOfCharacters; i++) {
    if (source.charCodeAt(i) === 10 /* newline char code */) {
      linesCount++
      lastNewLinePos = i
    }
  }

  pos.offset += numberOfCharacters
  pos.line += linesCount
  pos.column =
    lastNewLinePos === -1
      ? pos.column + numberOfCharacters
      : numberOfCharacters - lastNewLinePos

  return pos
}

function getCursor(context: ParserContext): Position {
  const { column, line, offset } = context
  return { column, line, offset }
}

function getSelection(
  context: ParserContext,
  start: Position,
  end?: Position,
): SourceLocation {
  end = end || getCursor(context)
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset),
  }
}
```

#### parseElement

```ts:parse.ts
const enum TagType {
  Start,
  End,
}

function parseElement(
  context: ParserContext,
  ancestors: ElementNode[],
): ElementNode | undefined {
  // 开始标签。
  const element = parseTag(context, TagType.Start) // TODO:

  // 如果它是像 <img /> 这样的自闭合元素，我们在这里结束（因为没有子元素或结束标签）。
  if (element.isSelfClosing) {
    return element
  }

  // 子元素。
  ancestors.push(element)
  const children = parseChildren(context, ancestors)
  ancestors.pop()

  element.children = children

  // 结束标签。
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End) // TODO:
  }

  return element
}

function parseTag(context: ParserContext, type: TagType): ElementNode {
  // 标签打开。
  const start = getCursor(context)
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  const tag = match[1]

  advanceBy(context, match[0].length)
  advanceSpaces(context)

  // 属性。
  let props = parseAttributes(context, type)

  // 标签关闭。
  let isSelfClosing = false

  // 如果下一个字符是 "/>"，它是一个自闭合标签。
  isSelfClosing = startsWith(context.source, '/>')
  advanceBy(context, isSelfClosing ? 2 : 1)

  return {
    type: NodeTypes.ELEMENT,
    tag,
    props,
    children: [],
    isSelfClosing,
    loc: getSelection(context, start),
  }
}

// 解析整个属性（多个属性）。
// 例如 `id="app" class="container" style="color: red"`
function parseAttributes(
  context: ParserContext,
  type: TagType,
): AttributeNode[] {
  const props = []
  const attributeNames = new Set<string>()

  // 继续读取直到标签结束。
  while (
    context.source.length > 0 &&
    !startsWith(context.source, '>') &&
    !startsWith(context.source, '/>')
  ) {
    const attr = parseAttribute(context, attributeNames)

    if (type === TagType.Start) {
      props.push(attr)
    }

    advanceSpaces(context) // 跳过空格。
  }

  return props
}

type AttributeValue =
  | {
      content: string
      loc: SourceLocation
    }
  | undefined

// 解析单个属性。
// 例如 `id="app"`
function parseAttribute(
  context: ParserContext,
  nameSet: Set<string>,
): AttributeNode {
  // 名称。
  const start = getCursor(context)
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  const name = match[0]

  nameSet.add(name)

  advanceBy(context, name.length)

  // 值
  let value: AttributeValue = undefined

  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    advanceSpaces(context)
    advanceBy(context, 1)
    advanceSpaces(context)
    value = parseAttributeValue(context)
  }

  const loc = getSelection(context, start)

  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      loc: value.loc,
    },
    loc,
  }
}

// 解析属性的值。
// 此实现允许解析值，无论它们是单引号还是双引号。
// 它只是提取引号中包含的值。
function parseAttributeValue(context: ParserContext): AttributeValue {
  const start = getCursor(context)
  let content: string

  const quote = context.source[0]
  const isQuoted = quote === `"` || quote === `'`
  if (isQuoted) {
    // 引用值。
    advanceBy(context, 1)

    const endIndex = context.source.indexOf(quote)
    if (endIndex === -1) {
      content = parseTextData(context, context.source.length)
    } else {
      content = parseTextData(context, endIndex)
      advanceBy(context, 1)
    }
  } else {
    // 未引用
    const match = /^[^\t\r\n\f >]+/.exec(context.source)
    if (!match) {
      return undefined
    }
    content = parseTextData(context, match[0].length)
  }

  return { content, loc: getSelection(context, start) }
}
```

进行测试：

```ts:example.ts
import { createApp, h, reactive } from 'vueImpl'

const app = createApp({
  template: `
    <div class="container" style="text-align: center">
      <h2>Hello!</h2>
      <img
        width="150px"
        src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1200px-Vue.js_Logo_2.svg.png"
        alt="Vue.js Logo"
      />
      <p><b>vue</b> is the minimal Vue.js</p>

      <style>
        .container {
          height: 100vh;
          padding: 16px;
          background-color: #becdbe;
          color: #2c3e50;
        }
      </style>
    </div>
  `,
})
app.mount('#app')
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250722171146.png "image.png")

这样我们的 ast 就解析正常了

#### 基于 ast 生成渲染函数

```ts:codegen.ts
import { ElementNode, NodeTypes, TemplateChildNode, TextNode } from './ast'

export const generate = ({
  children,
}: {
  children: TemplateChildNode[]
}): string => {
  return `return function render() {
  const { h } = vueImpl;
  return ${genNode(children[0])};
}`
}

const genNode = (node: TemplateChildNode): string => {
  switch (node.type) {
    case NodeTypes.ELEMENT:
      return genElement(node)
    case NodeTypes.TEXT:
      return genText(node)
    default:
      return ''
  }
}

const genElement = (el: ElementNode): string => {
  return `h("${el.tag}", {${el.props
    .map(({ name, value }) => `${name}: "${value?.content}"`)
    .join(', ')}}, [${el.children.map(it => genNode(it)).join(', ')}])`
}

const genText = (text: TextNode): string => {
  return `\`${text.content}\``
}
```

结果：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250722171522.png "image.png")

本次代码git: <https://github.com/ryne6/vue-impl/commit/de8b664a85b7e0f05a8fd03e80d7e2287c4c6f4b>

相关链接：<https://github.com/chibivue-land/chibivue>
]]></description><link>https://9999886.xyz/posts/Xpe3B_x_S3By3CirrXN5V</link><guid isPermaLink="true">https://9999886.xyz/posts/Xpe3B_x_S3By3CirrXN5V</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Tue, 22 Jul 2025 09:19:25 GMT</pubDate></item><item><title><![CDATA[Keyv iterator 报错ErrorReply  ERR syntax error]]></title><description><![CDATA[Nest 中使用 Keyv 增加缓存

```ts
try {
      // 不传递任何参数
      const val = await this.cacheManager.stores[0].iterator('')
      console.log(val)

      // 使用 try/catch 包装迭代过程
      try {
        for await (const entry of val) {
          console.log('Entry:', entry)
        }
      } catch (iterError) {
        console.error('Iterator error:', iterError)
      }
    } catch (error) {
      Logger.error('Cache iteration error:', error)
    }
```

报错`ErrorReply: ERR syntax error`

解决方案： redis 升级到 7
]]></description><link>https://9999886.xyz/posts/h3TES5n3DiVG8sjUZqg1l</link><guid isPermaLink="true">https://9999886.xyz/posts/h3TES5n3DiVG8sjUZqg1l</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Tue, 22 Jul 2025 06:28:57 GMT</pubDate></item><item><title><![CDATA[Vue Reactivity System]]></title><description><![CDATA[### What is the Reactivity System?

> Reactive objects are JavaScript proxies that behave like normal objects. The difference is that Vue can track property access and changes on reactive objects.\
> 响应式对象是像普通对象一样行为的 JavaScript 代理。不同之处在于 Vue 可以跟踪响应式对象上的属性访问和变化。

> One of Vue's most distinctive features is its modest Reactivity System. The state of a component is composed of reactive JavaScript objects. When the state changes, the view is updated.\
> Vue 最显著的特点之一是其简洁的响应式系统。组件的状态由响应式 JavaScript 对象组成。当状态变化时，视图会更新。

### Proxy

> **Proxy** 对象用于创建一个对象的代理，从而实现基本操作的拦截和自定义（如属性查找、赋值、枚举、函数调用等）。

[Proxy](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)是响应式的关键。

```ts:demo.ts
const o = new Proxy(
  { value: 1, value2: 2 },

  {
    get(target, key, receiver) {
      console.log(`target:${target}, key: ${key}`)
      return target[key]
    },
    set(target, key, value, receiver) {
      console.log('hello from setter')
      target[key] = value
      return true
    },
  },
)

```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250606162850.png "image.png")

### Reflect

> **Reflect** 是一个内置的对象，它提供拦截 JavaScript 操作的方法。这些方法与 [proxy handler](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) 的方法相同。`Reflect` 不是一个函数对象，因此它是不可构造的。

[Reflect](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect) 可以完成对对象的基本操作

#### 什么是对象的基本操作？

目前所有的操作(调用、语法)都是间接的使用对象的基本操作。间接操作可能会导致一些其他问题。

1. `Object.keys()`拿不到无法枚举的值，那么就可以使用`Reflect.ownKeys()`。
2. `Reflect` 可以 `Reflect(target, key, otherTarget)`
   解决了什么问题?

```ts
const obj = {
	a: 1,
	b: 2,
	get c() {
	  return this.a + this.b
	}
}

// 如果直接 获取的话 那么 获取 target['c']时，get c 中的 this是 `obj`，而不是被 Proxy 代理的新对象。那么 get c 中 访问 this.a、this.b 时，不会触发 a、b 的 getter。 如果使用 Reflect 的话，直接 Reflect.get(target, key, receiver)即可。
```

```ts
import { track, trigger } from './effect'
import { reactive } from './reactive'

export const mutableHandlers: ProxyHandler<object> = {
  get(target: object, key: string | symbol, receiver: object) {
    track(target, key)

    const res = Reflect.get(target, key, receiver)
    // reactive 只支持对象
    if (res !== null && typeof res === 'object') {
      return reactive(res)
    }

    return res
  },

  set(target: object, key: string | symbol, value: unknown, receiver: object) {
    let oldValue = (target as any)[key]
    Reflect.set(target, key, value, receiver)
    // 检查值是否已更改
    if (hasChanged(value, oldValue)) {
      trigger(target, key)
    }
    return true
  },
}

const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)
```

[receiver](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#receiver)

### Vue 的响应式系统核心概念

Vue.js 的响应式系统涉及 `target`，`Proxy`，`Reflect`, `ReactiveEffect`，`Dep`，`track`，`trigger`，`targetMap` 和 `activeEffect`（目前是 `activeSub`）这几个概念

| **名称**         | **说明**                      |
| :------------- | :-------------------------- |
| target         | 响应式目标对象                     |
| targetMap      | 存储所有响应式目标及其依赖的全局 WeakMap    |
| ReactiveEffect | 包装更新逻辑的类（如 updateComponent） |
| activeEffect   | 当前正在执行的 ReactiveEffect      |
| Dep            | 存储一个属性对应的所有 ReactiveEffect  |
| track          | 依赖收集函数                      |
| trigger        | 依赖触发函数                      |

```ts:targetMap
targetMap: WeakMap
  └─ target1: Map (KeyToDepMap)
      ├─ key1: Dep(Set of ReactiveEffect)
      └─ key2: Dep(...)
  └─ target2: ...
```

```ts
type Target = any // 响应式目标
type TargetKey = any // 目标拥有的任何键

const targetMap = new WeakMap<Target, KeyToDepMap>() // 在此模块中定义为全局变量

type KeyToDepMap = Map<TargetKey, Dep> // 目标的键和效果的映射

type Dep = Set<ReactiveEffect> // dep 有多个 ReactiveEffects

class ReactiveEffect {
  constructor(
    // 这里，您给出想要实际应用为效果的函数（在这种情况下是 updateComponent）
    public fn: () => T,
  ) {}
}
```

也就是说 `targetMap` 中，`key`是 `target`， 每一个 `target`对应的是他们自己的依赖总`Map`，即 `KeyToDepMap`, `KeyToDepMap`是`target`中每个`key`的依赖集合，集合中每一项都是`ReactiveEffect`

:::tabs
@tab baseHandler.ts

```ts:baseHandler.ts
import { track, trigger } from './effect'
import { reactive } from './reactive'

export const mutableHandlers: ProxyHandler<object> = {
  get(target: object, key: string | symbol, receiver: object) {
    track(target, key)

    const res = Reflect.get(target, key, receiver)
    // reactive 只支持对象
    if (res !== null && typeof res === 'object') {
      return reactive(res)
    }

    return res
  },

  set(target: object, key: string | symbol, value: unknown, receiver: object) {
    let oldValue = (target as any)[key]
    Reflect.set(target, key, value, receiver)
    // 检查值是否已更改
    if (hasChanged(value, oldValue)) {
      trigger(target, key)
    }
    return true
  },
}

const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)
```

@tab dep.ts

```ts:dep.ts
import { type ReactiveEffect } from './effect'

export type Dep = Set<ReactiveEffect>

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep: Dep = new Set<ReactiveEffect>(effects)
  return dep
}
```

@tab effect.ts

```ts:effect.ts
import { Dep, createDep } from './dep'

type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {

  // fn 指 updateComponent()
  constructor(public fn: () => T) {}

  run() {
    /**
     * 时间线：
        1. effect.run() 调用
        2. activeEffect = this (设置为当前effect)
        3. this.fn() 开始执行 (updateComponent)
        4. componentRender() 执行 
        5. 访问 state.count
        6. 触发 Proxy get 拦截器
        7. 调用 track(target, 'count')
        8. 此时 activeEffect 有值
        9. dep.add(activeEffect) 执行
        10. componentRender() 执行完毕
        11. updateComponent() 执行完毕
        12. activeEffect = parent (恢复为undefined)
     */
    let parent: ReactiveEffect | undefined = activeEffect
    activeEffect = this
    const res = this.fn()
    activeEffect = parent
    return res
  }
}

// 收集
export function track(target: object, key: unknown) {
  // targetMap ->  depMap -> dep
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }
  

  if (activeEffect) {
    dep.add(activeEffect)
  }
}

// 执行
export function trigger(target: object, key?: unknown) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)

  if (dep) {
    const effects = [...dep]
    for (const effect of effects) {
      effect.run()
    }
  }
}
```

@tab reactive.ts

```ts:reactive.ts
import { mutableHandlers } from './baseHandler'

export function reactive<T extends object>(target: T): T {
  const proxy = new Proxy(target, mutableHandlers)
  return proxy as T
}
```

@tab apiCreateApp.ts

```ts:apiCreateApp.ts
import { ReactiveEffect } from '../reactivity'
import { Component } from './component'
import { RootRenderFunction } from './renderer'

export interface App<HostElement = any> {
  mount(rootContainer: HostElement | string): void
}

export type CreateAppFunction<HostElement> = (
  rootComponent: Component,
) => App<HostElement>

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent) {
    const app: App = {
      mount(rootContainer: HostElement) {
        const componentRender = rootComponent.setup!()
        const updateComponent = () => {
          const vnode = componentRender()
          render(vnode, rootContainer)
        }
        // ++++++++++
        const effect = new ReactiveEffect(updateComponent)
        effect.run()
      },
    }

    return app
  }
}
```

:::

### Q\&A

#### 为什么 Vue 不做更细粒度的 vnode 局部更新？

Vue 模板（或 JSX/render 函数）在编译成 render 函数后，其实是一个**完整的函数体**：

```ts
render() {
  return h('div', [
    h('span', this.foo),
    h('span', this.bar)
  ])
}
```

在运行时你没法很容易做到：

> 只重新执行 this.foo 对应的 vnode 部分（h('span', this.foo)）而不执行整个 render。

因为：

* render() 是 JS 函数，不是声明式模板的结构树；
* Vue 不会单独给 this.foo 对应的 vnode 打补丁逻辑，它只能重新执行整个 render，再做 diff；
* 模板里没有静态的「区域」概念，只有函数运行出来的 vnode 树。

手动拆区域的代价和复杂度反而更高
如果 Vue 要追踪“哪段模板依赖了 foo”，那必须：

* 模板每一段都挂一个独立的 effect
* 管理更新粒度变得极其复杂
* 每个响应式变量都要绑定一堆小函数

这等于 **把整个模板切成一堆小组件**，还不如开发者自己手动提成组件来的清晰可控。

Diff 本身已经是按区域执行的优化
虽然 render() 会重新跑一次，但 **diff 过程是区域化执行的**：

* render() -> 新 vnode 树
* patch(oldVNode, newVNode) 会在 vnode 层做精细比较（如 patchText、patchProps、patchChildren）
* 如果你用了 v-for，它还会用到**双端 diff + 最长递增子序列优化移动**
  也就是说，即使整个组件 render 重跑，**Vue 会确保 DOM 操作尽可能最小化**。

]]></description><link>https://9999886.xyz/posts/JTDcJHG8aLI4E6zB4ovS4</link><guid isPermaLink="true">https://9999886.xyz/posts/JTDcJHG8aLI4E6zB4ovS4</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Tue, 10 Jun 2025 06:17:19 GMT</pubDate></item><item><title><![CDATA[DI and DIP 依赖注入和依赖注入原则]]></title><description><![CDATA[> **依赖注入**是一种设计模式，核心思想是：**不要在类内部创建依赖，而是通过外部传入**，这样可以减少耦合、提高测试性和灵活性。

### 不使用依赖注入的例子

```ts
class EmailService {
  send(email: string, message: string) {
    console.log(`发送邮件到 ${email}: ${message}`);
  }
}

class UserController {
  private emailService = new EmailService(); // 直接创建，耦合很强
  
  notifyUser(email: string) {
    this.emailService.send(email, "欢迎加入！");
  }
}
```

### 使用依赖注入的例子

```ts
class EmailService {
  send(email: string, message: string) {
    console.log(`发送邮件到 ${email}: ${message}`);
  }
}

class UserController {
  constructor(private emailService: EmailService) {}

  notifyUser(email: string) {
    this.emailService.send(email, "欢迎加入！");
  }
}

// 使用方式
const emailService = new EmailService();
const userController = new UserController(emailService);
```

* UserController 不再依赖于具体实现，易于替换或测试。
* 更符合“单一职责”和“模块化”的思路。

> **依赖倒置原则** > **高层模块**（业务逻辑）不应该依赖**低层模块**（工具类），二者都应该依赖于**抽象**。**抽象不应该依赖细节**，**细节应该依赖抽象**。

```ts
// 抽象接口
interface MessageService {
  send(to: string, message: string): void;
}

// 低层模块实现
class EmailService implements MessageService {
  send(to: string, message: string): void {
    console.log(`发送邮件给 ${to}: ${message}`);
  }
}

// 高层模块
class UserController {
  constructor(private messageService: MessageService) {}

  notifyUser(email: string) {
    this.messageService.send(email, "欢迎！");
  }
}

// 使用
const service = new EmailService();
const controller = new UserController(service);
```

* 高层模块（UserController）只依赖接口（MessageService），与具体实现（EmailService）无关。
* 更容易扩展，比如换成短信服务 SMSService，无需改动 UserController。

### 实例

:::tabs

@tab renderer.ts

```ts:renderer.ts
export interface RendererOptions<HostNode = RendererNode> {
  setElementText(node: HostNode, text: string): void
}

export interface RendererNode {
  [key: string]: any
}

export interface RendererElement extends RendererNode {}

export type RootRenderFunction<HostElement = RendererElement> = (
  message: string,
  container: HostElement,
) => void

export function createRenderer(options: RendererOptions) {
  const { setElementText: hostSetElementText } = options

  const render: RootRenderFunction = (message, container) => {
    hostSetElementText(container, message)
  }

  return { render }
}
```

@tab apiCreateApp.ts

```ts:apiCreateApp.ts
import { Component } from './component'
import { RootRenderFunction } from './renderer'

export interface App<HostElement = any> {
  mount(rootContainer: HostElement | string): void
}

export type CreateAppFunction<HostElement> = (
  rootComponent: Component,
) => App<HostElement>

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent) {
    const app: App = {
      mount(rootContainer: HostElement) {
        const message = rootComponent.render!()
        render(message, rootContainer)
      },
    }

    return app
  }
}
```

@tab component.ts

```ts:component.ts
import type { ComponentOptions } from './componentOptions'

export type Component = ComponentOptions
```

@tab componentOptions.ts

```ts:componentOptions.ts
export type ComponentOptions = {
  render?: Function
}
```

@tab nodeOps.ts

```ts:nodeOps.ts
import { RendererOptions } from '../runtime-core/renderer'

export const nodeOps: RendererOptions<Node> = {
  setElementText(node, text) {
    node.textContent = text
  },
}
```

@tab runtime-dom/index.ts

```ts:runtime-dom/index.ts
import { createAppAPI, CreateAppFunction } from '../runtime-core'
import { createRenderer } from '../runtime-core/renderer'
import { nodeOps } from './nodeOps'

const { render } = createRenderer(nodeOps)

const _createApp = createAppAPI(render)

export const createApp = ((...args) => {
  const app = _createApp(...args)
  const { mount } = app
  app.mount = (selector: string) => {
    const container = document.querySelector(selector)
    if (!container) return
    mount(container)
  }

  return app
}) as CreateAppFunction<Element>
```

:::

`createRenderer` 接收了`setElementText` 并以`setElementText` 实现并返回了 `render`(这里`render` 是操作`DOM`相关的操作)， `render` 接受参数并调用 传入进来的`setElementText` ，这样实际的操作就与`creatRenderer`无关。

`createAppAPI` 接收了 `render`，并在返回的对象的方法中使用了`render`，这样顺序就是 `app(createApp)` -> `render(createRenderer)` -> `createRenderer ` 的参数

### GPT解析实例

runtime-dom/index.ts
    ↓
createRenderer(nodeOps)
    ↓
  render 函数（通过注入 nodeOps 的 setElementText 实现）
    ↓
createAppAPI(render)
    ↓
createApp().mount(selector)
    ↓
rootComponent.render() → render(message, container)

#### DI（依赖注入）

createRenderer(options) **不自己创建 DOM 操作函数**，而是通过参数注入 setElementText。也就是说：**渲染逻辑依赖于外部注入的底层实现**（DOM 还是 Canvas，甚至是 SSR 都无所谓，只要实现同样的接口就行）。

```ts
createRenderer(nodeOps)
```

#### DIP（依赖倒置原则）

这体现在以下几点：

* createRenderer 并不依赖于 DOM 或具体的实现细节，而是依赖于抽象接口 RendererOptions。
* createAppAPI 依赖的也是抽象的 render 函数，不关心具体渲染方式。
* 高层模块（如 createAppAPI）依赖的是抽象接口，而不是底层细节。
*

🔧 **也就是说：**

> 高层模块 createAppAPI 和底层实现 nodeOps.setElementText 都依赖于 RendererOptions 这个抽象。

这就是典型的 **依赖倒置**。

**✅ 这种设计的好处**

1. **解耦**：逻辑与具体实现完全分离，可以轻松替换渲染逻辑（例如服务端渲染/Canvas 渲染/WebGL 渲染）。
2. **扩展性强**：想换底层实现，只要实现同样接口就行。
3. **测试友好**：可以 mock RendererOptions 来测试 createRenderer 的行为。
4. **灵活组合**：上层代码无需修改即可适配多种运行环境（DOM、Node.js、Native 等）。

**✅ 如果要总结一句话：**

> 这套设计用 createRenderer 做了渲染层的抽象，并通过依赖注入（DI）和依赖倒置原则（DIP）将 DOM 操作从业务逻辑中解耦出来，从而构建出一个高度可扩展和可复用的 UI 渲染引擎。

### Q & A

Q: 依赖倒置 就是尽可能把依赖抽离到更下一层？
A: 依赖倒置不是“放到底层”，而是“依赖抽象，而不是依赖具体实现”

传统的设计是这样的：
高层模块（业务逻辑）
      ↓ 依赖
低层模块（数据库/文件系统/DOM/网络请求等）
高层模块直接依赖底层模块，一旦底层改变，高层也要改，**耦合强，灵活性差**。

而“依赖倒置”后：
高层模块 → 依赖接口/抽象 ← 底层模块实现接口

🔁 **这就是“倒置”**：原本是高层依赖低层，现在是两者都依赖抽象。

来源: <https://book.chibivue.land/10-minimum-example/015-package-architecture.html#di-and-dip>
]]></description><link>https://9999886.xyz/posts/7KcczmA6rD7hJYiixeD0R</link><guid isPermaLink="true">https://9999886.xyz/posts/7KcczmA6rD7hJYiixeD0R</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 05 Jun 2025 09:24:54 GMT</pubDate></item><item><title><![CDATA[nest集成cache-module实践]]></title><description><![CDATA[### 依赖

```json:package.json
    "@nestjs/cache-manager": "^3.0.1",
    "cache-manager": "^6.4.3",
    "@keyv/redis": "^4.4.0",
```

### 集成

`app.module.ts`中注册，这里使用`redis`作为缓存的存储。

```ts:app.module.ts
import { createKeyv } from '@keyv/redis'
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager'
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { APP_INTERCEPTOR } from '@nestjs/core'

import { AppController } from './app.controller'
import { RedisModule } from './common/redis/redis.module'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    RedisModule,
    CacheModule.registerAsync({
      useFactory: async (configService: ConfigService) => {
        const host = configService.get('REDIS_HOST') || '127.0.0.1'
        const port = configService.get('REDIS_PORT') || 6379
        const password = configService.get('REDIS_PASSWORD')
        return {
          stores: [
            createKeyv(
              {
                url: `redis://${host}:${port}`,
                password,
              },
              {
                namespace:
                  configService.get('CACHE_NAMESPACE') || 'request_cache_',
              },
            ),
          ],
          ttl: configService.get('CACHE_TTL') || 60000,
        }
      },
      isGlobal: true,
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

```

这样缓存就会在全局生效

然后实现部分接口不进行缓存，以及修改某些数据后，删除已经缓存的部分

#### skipCache

可以基于`CacheInterceptor` 拓展一个`CustomCacheInterceptor`来进行过滤

```ts:custom-cache.interceptor.ts
import { CacheInterceptor } from '@nestjs/cache-manager'
import { ExecutionContext, Injectable, Logger } from '@nestjs/common'

@Injectable()
export class CustomCacheInterceptor extends CacheInterceptor {
  private logger = new Logger('CustomCacheInterceptor')

  trackBy(context: ExecutionContext): string | undefined {
    try {
      const request = context.switchToHttp().getRequest()
      // 只缓存 GET 请求
      if (request.method !== 'GET') {
        return undefined
      }
      const handler = context.getHandler()
      const shouldSkipCache = Reflect.getMetadata('skipCache', handler) === true
      if (shouldSkipCache) {
        return undefined
      }

      const url = request.url
      return url
    } catch (error) {
      this.logger.error(error)
      return undefined
    }
  }
}

```

这里实现了只缓存 `GET`请求，并通过`Reflect.getMetadata`来获取`skipCache`，
`shipCache`为 `true`则跳过缓存。

`skipCache`的设置可以自定义一个装饰器来做

```ts:cache.decorator.ts
import { applyDecorators, SetMetadata } from '@nestjs/common'

export function CustomCache(skipCache: boolean = false) {
  applyDecorators(SetMetadata('skipCache', skipCache))
}

```

在不需要缓存的`controller`中设置
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250523162253.png "image.png")

#### 删除已经缓存的部分

在这之前我们先拓展 `cache.decorator.ts` 和 `custom-cache.interceptor.ts`来支持自定义前缀，方便后面删除

:::tabs
@tab custom-cache.interceptor.ts

```ts:custom-cache.interceptor.ts
import { CacheInterceptor } from '@nestjs/cache-manager'
import { ExecutionContext, Injectable, Logger } from '@nestjs/common'

@Injectable()
export class CustomCacheInterceptor extends CacheInterceptor {
  private logger = new Logger('CustomCacheInterceptor')

  trackBy(context: ExecutionContext): string | undefined {
    try {
      const request = context.switchToHttp().getRequest()
      // 只缓存 GET 请求
      if (request.method !== 'GET') {
        return undefined
      }
      const handler = context.getHandler()
      const shouldSkipCache = Reflect.getMetadata('skipCache', handler) === true
      if (shouldSkipCache) {
        return undefined
      }

      const customCacheKey =
        Reflect.getMetadata('customCacheKey', handler) || ''

      const url = request.url
      return `${customCacheKey}:${url}`
    } catch (error) {
      this.logger.error(error)
      return undefined
    }
  }
}

```

@tab cache.decorator.ts

```ts:cache.decorator.ts
import { applyDecorators, SetMetadata } from '@nestjs/common'

export function CustomCache(key: string | boolean) {
  if (typeof key === 'boolean' && key === false) {
    return applyDecorators(SetMetadata('skipCache', true))
  } else {
    return applyDecorators(SetMetadata('customCacheKey', key))
  }
}
```

:::

这样修改后，controller 就可以这样:
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250523162748.png "image.png")

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250523162902.png "image.png")

这样就可以根据`custormKey`来批量删除

实现一个`cache.service.ts`来查询、删除缓存:

```ts:helper.cache.service.ts
import { Cache } from 'cache-manager'

import { CACHE_MANAGER } from '@nestjs/cache-manager'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class HelperCacheService {
  @Inject(CACHE_MANAGER)
  private cacheManager: Cache

  @Inject(ConfigService)
  private configService: ConfigService

  private logger = new Logger('HelperCacheService')
  async getCacheAllKeys(): Promise<string[]> {
    const keys: string[] = []
    try {
      const val = await this.cacheManager.stores[0].iterator(
        this.configService.get('CACHE_NAMESPACE') || 'request_cache_',
      )
      for await (const [key] of val) {
        keys.push(key)
      }
      return keys
    } catch (error) {
      this.logger.error('Cache iteration error:', error)
      return []
    }
  }

  async clearCache() {
    const keys = await this.getCacheAllKeys()
    await this.clearCacheByKeys(keys)
  }

  async clearCacheByPrefix(prefix: string) {
    const keys = await this.getCacheAllKeys()
    const cacheKeys = keys.filter((key) => key.startsWith(prefix))
    await this.clearCacheByKeys(cacheKeys)
  }

  async clearCacheByKeys(keys: string[]) {
    if (keys.length === 0) return

    try {
      await Promise.all(
        keys.map((key) => this.cacheManager.stores[0].delete(key)),
      )
      this.logger.log(`Cleared ${keys.length} cache entries`)
    } catch (error) {
      this.logger.error('Failed to clear cache by keys:', error)
    }
  }

  async getCacheByKeys(keys: string[]) {
    const values: any[] = []
    for (const key of keys) {
      const value = await this.cacheManager.stores[0].get(key)
      values.push(value)
    }
    return values
  }

  async getCacheByPrefix(prefix: string) {
    const keys = await this.getCacheAllKeys()
    const values: any[] = []
    for (const key of keys) {
      if (key.startsWith(prefix)) {
        const value = await this.cacheManager.stores[0].get(key)
        values.push(value)
      }
    }
    return values
  }
}

```

:::warn
使用 `this.cacheManager.stores[0].iterator`时，应当注意`redis`版本不能过低。实测`redis:5-alpine` 不行，`redis:7-alpine`可以
:::

使用示例:

:::tabs

@tab post.controller.ts

```ts:post.controller.ts
import { IncomingMessage } from 'node:http'
import { FastifyRequest } from 'fastify'

import { Controller, Get, Param, Query, Req } from '@nestjs/common'

import { CustomCache } from '~/common/decorators/cache.decorator'

import { PostFindAllDto, PostPageDto } from './post.dto'
import { PostService } from './post.service'

@Controller('post')
export class PostController {
  constructor(private readonly postService: PostService) {}

  @Get()
  @CustomCache('post-list')
  getList(@Query() query: PostPageDto) {
    return this.postService.getList(query)
  }

  @Get('findAll')
  @CustomCache('post-findAll')
  findAll(@Query() query: PostFindAllDto) {
    return this.postService.findAll(query)
  }

  @Get(':id')
  getDetail(@Param('id') id: string, @Req() req: IncomingMessage) {
    return this.postService.getDetail(id, req)
  }

  @Get(':id/read')
  @CustomCache(false)
  async incrementReadCount(
    @Param('id') id: string,
    @Req() req: FastifyRequest,
  ) {
    return await this.postService.incrementReadCount(id, req)
  }
}

```

@tab post.service.ts

```ts:post.service.ts
import { IncomingMessage } from 'node:http'
import { FastifyRequest } from 'fastify'
import { catchError, firstValueFrom } from 'rxjs'

import { HttpService } from '@nestjs/axios'
import {
  BadRequestException,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Post, Prisma } from '@prisma/client'

import { PrismaService } from '~/common/db/prisma.service'
import { RedisService } from '~/common/redis/redis.service'
import { HelperCacheService } from '~/helpers/helper.cache.service'
import { ImageModel, ImageService } from '~/helpers/helper.image.service'
import { WebEventsGateway } from '~/modules/gateway/web/event.gateway'
import { createPageResult } from '~/shared/page.result'
import {
  AnonymousCookieKey,
  auth,
  AuthCookieKey,
  hasAuthCookie,
} from '~/utils/auth'
import { isDev } from '~/utils/env'

import { SocketMessageType } from '../gateway/constant'
import { ActivityService, ActivityType } from '../statistics/activity.service'
import { PostDto, PostFindAllDto, PostPageDto } from './post.dto'

const TestIntroduction =
  '测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介,测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介测试简介'

@Injectable()
export class PostService {
  private readonly prisma: PrismaService

  @Inject(HelperCacheService)
  private helperCacheService: HelperCacheService

  constructor(
    prisma: PrismaService,
    redis: RedisService,
    httpService: HttpService,
    configService: ConfigService,
    webEventsGateway: WebEventsGateway,
    imageService: ImageService,
  ) {
    this.prisma = prisma
    this.redis = redis
    this.httpService = httpService
    this.configService = configService
    this.webEventsGateway = webEventsGateway
    this.imageService = imageService
  }

  async getDetail(id: string, req: IncomingMessage) {
    const post = await this.prisma.post.findUnique({
      where: { id, isDeleted: false },
      include: {
        categories: {
          select: {
            category: {
              select: {
                id: true,
                name: true,
              },
            },
          },
        },
        _count: {
          select: {
            likes: true,
          },
        },
        images: true,
      },
    })
    if (!post) {
      throw new NotFoundException('post not found or deleted')
    }

    const transformedPost = {
      ...post,
      categories: post.categories.map((pc) => pc.category),
      likeCount: post._count.likes,
    }

    return transformedPost
  }

  async delete(id: string, req: IncomingMessage) {
    const post = await this.prisma.post.findUnique({
      where: { id, isDeleted: false },
    })

    if (!post) {
      throw new NotFoundException()
    }

    await this.prisma.post.update({
      where: { id },
      data: { isDeleted: true, categories: { deleteMany: {} } },
    })

    this.webEventsGateway.emitMessage({
      type: SocketMessageType.Post_DELETED,
      data: {
        postId: post.id,
      },
    })

    await this.clearPostCache(id)
  }

  async create(data: PostDto, req: IncomingMessage) {
    const { categoryIdList, draftId, ...postData } = data

    try {
      const post = await this.prisma.post.create({
        data: {
          ...postData,
          categories: {
            create: categoryIdList.map((id) => ({
              category: {
                connect: { id },
              },
            })),
          },
          draft: draftId
            ? {
                connect: { id: draftId },
              }
            : undefined,
          introduction: isDev() ? TestIntroduction : undefined,
        },
      })

      await this.clearPostCache()
      return true
    } catch (error) {
      Logger.error(error)
      throw new BadRequestException('Failed to create post')
    }
  }

  async update(data: PostDto, req: IncomingMessage) {
    const { id, categoryIdList, ...postData } = data

    if (!id) {
      throw new BadRequestException('id is required')
    }

    const post = await this.prisma.post.findUnique({
      where: { id, isDeleted: false },
    })

    if (!post) {
      throw new NotFoundException('post not found or deleted')
    }

    try {
      await this.prisma.$transaction(async (tx) => {
        await tx.postCategory.deleteMany({
          where: { postId: id },
        })

        return tx.post.update({
          where: { id, isDeleted: false },
          data: {
            ...postData,
            categories: {
              create: categoryIdList.map((categoryId) => ({
                category: { connect: { id: categoryId } },
              })),
            },
            introduction: post.introduction,
          },
        })
      })

      await this.clearPostCache(id)
    } catch (error) {
      Logger.error(error)
      throw new BadRequestException('Failed to update post')
    }
  }

  async clearPostCache(id?: string) {
    await this.helperCacheService.clearCacheByPrefix('post-list')
    await this.helperCacheService.clearCacheByPrefix('post-findAll')
    if (id) {
      await this.helperCacheService.clearCacheByKeys([`:/post/${id}`])
    }
  }
\
}

```

:::
]]></description><link>https://9999886.xyz/posts/JI4nqEPEkRSH8vAqOvqB2</link><guid isPermaLink="true">https://9999886.xyz/posts/JI4nqEPEkRSH8vAqOvqB2</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 29 May 2025 08:36:59 GMT</pubDate></item><item><title><![CDATA[obsidian + 阿里云oss配置]]></title><description><![CDATA[#### 购买阿里云对象存储oss服务

#### 新建bucket

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629151445.png "image.png")

#### 新建子用户

右上角头像 -> 访问控制 -> 左侧 身份管理 -> 用户 -> 创建用户（勾选OpenApi 调用访问） 存一下两个key
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629151635.png "image.png")

#### bucket授权

bucket列表 -> 点击名字进入详情 -> 权限控制 -> bucket授权策略 -> bucket授权策略tab -> 新增授权
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629151917.png "image.png")

#### 配置PickGo

[下载地址](https://github.com/Molunerfinn/PicGo/releases)
tips：下载后打开报错 **“PicGo”已损坏，无法打开。 你应该将它移到废纸篓。**
执行命令：

```
sudo spctl --master-disable
xattr -cr /Applications/PicGo.app
```

[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)

配置：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629152353.png "image.png")
配置后可以上传一张图片测试

#### 配置obsidian

第三方插件市场中下载  [image-auto-upload-plugin](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Frenmu123%2Fobsidian-image-auto-upload-plugin%2Fblob%2Fmaster%2Freadme-zh.md "https://github.com/renmu123/obsidian-image-auto-upload-plugin/blob/master/readme-zh.md") ，启用后直接使用默认配置即可。
]]></description><link>https://9999886.xyz/posts/9zlMwAWt5O7USdIq-GkIY</link><guid isPermaLink="true">https://9999886.xyz/posts/9zlMwAWt5O7USdIq-GkIY</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 15 May 2025 08:03:36 GMT</pubDate></item><item><title><![CDATA[nuxt + useDark暗黑模式闪屏问题]]></title><description><![CDATA[# 问题

每次刷新时，页面闪屏，比如设置了黑色刷新初始为白色， 然后又变为黑色

# 原因

useDark 默认存储在 local storage 中，而 ssr 状态下获取不到 local storage

# 处理

```ts:nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          innerHTML: `
            (function() {
              const isDark = localStorage.getItem('darkMode') === 'true' || 
                (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches);
              document.documentElement.classList.toggle('dark', isDark);
            })();
          `,
          type: 'text/javascript',
        },
      ],
    },
  },
})
```

# 后续问题

Hydration class mismatch

# 原因

虽然解决了闪屏问题，但是 useDark 在 ssr 时初始化的值还是一个错误的值（获取不到 local storage）

# 解决

使用 seesion 来存储

```ts
const isDark = useDark({
    storageKey: 'vueuse-color-scheme',
    storage: {
      getItem(key: string) {
        if (import.meta.server) {
          // 服务端从 headers 中获取 cookie
          const cookieStr = useRequestHeaders(['cookie'])?.cookie || ''
          return cookieStr.split('; ').find(row => row.startsWith(key))?.split('=')[1] || null
        }
        return document.cookie.split('; ').find(row => row.startsWith(key))?.split('=')[1] || null
      },
      setItem(key: string, value: string) {
        document.cookie = `${key}=${value};path=/;max-age=${60 * 60 * 24 * 30}`
      },
    } as Storage,
  })
```

```ts
// nuxt.config.ts
script: [
        {
          innerHTML: `
            (function() {
              var getCookie = function(name) {
                return document.cookie.split('; ').find(row => row.startsWith(name))?.split('=')[1] || null;
              };
              var darkMode = getCookie('vueuse-color-scheme');
              if (darkMode === 'dark') {
                document.documentElement.classList.add('dark');
              } else {
                document.documentElement.classList.remove('dark');
              }
            })();
          `,
          type: 'text/javascript',
        },
      ],
```

]]></description><link>https://9999886.xyz/posts/GT14KLNnU3E7x4NljTtCM</link><guid isPermaLink="true">https://9999886.xyz/posts/GT14KLNnU3E7x4NljTtCM</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 15 May 2025 06:30:14 GMT</pubDate></item><item><title><![CDATA[Jenkins自动部署]]></title><description><![CDATA[涉及`docker` `jenkins`

服务器信息： centos 7.6

# 安装docker

```sh
yum install gcc c++
sudo yum remove docker
```

## 安装软件包和国内的镜像仓库

```sh
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
```

## 下载阿里云仓库

```sh
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
```

##  install docker

```sh
sudo yum install docker-ce docker-ce-cli containerd.io -y
```

## 启动docker

```sh
sudo systemctl start docker
```

## 腾讯云配置镜像加速源

```sh
vim /etc/docker/daemon.json
```

添加配置：

```sh
{   "registry-mirrors": [       "https://mirror.ccs.tencentyun.com" ] 
}
```

## 重启docker

```sh
sudo systemctl restart docker
```

# 安装Jenkins

## 安装git

```sh
sudo yum install git
```

## 安装jdk

```sh
cd /home 

mkdri jdk

cd jdk

通过wget下载压缩包  (去掉 \)
wget \ --no-check-certificate \ --no-cookies \ --header \ "Cookie: oraclelicense=accept-securebackup-cookie" \ http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.tar.gz

解压
tar xvf jdk-8u131-linux-x64.tar.gz

配置环境变量
vim /etc/profile

文件最后追加
export JAVA_HOME=/home/jdk/jdk1.8.0_131 export CLASSPATH=$:CLASSPATH:$JAVA_HOME/lib/ export PATH=$PATH:$JAVA_HOME/bin

srouce一下：
source /etc/profile

检查是否成功：
java -version
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629164606.png "image.png")

## 使用docker拉取Jenkins镜像

```sh
docker pull jenkins/jenkins:2.426.2-lts
```

## 创建挂载目录

```sh
mkdir -p /usr/docker/jenkins_data

启动
docker run -d -p 8082:8080 -p 50000:50000 -v /usr/docker/jenkins_data:/var/jenkins_home  -v /etc/localtime:/etc/localtime -v /usr/bin/docker:/usr/bin/docker     -v /var/run/docker.sock:/var/run/docker.sock   --restart=on-failure  -u 0 --name myjenkins jenkins/jenkins:2.426.2-lts
```

:::info
启动指令解析：

-d ：后台运行容器

-p：端口映射， 左边是本地端口，右边是docker容器端口 ，8080是Jenkins Web 界面的工作端口,50000是JNLP（Java Network Launch Protocol）工作端口。这个端口用于 Jenkins 节点和主控节点之间的通信。

-v ：目录挂载，将主机上的 /usr/docker/jenkins\_data 目录挂载到容器内的 /var/jenkins\_home 目录，用于持久化 Jenkins 的数据。/etc/localtime:/etc/localtime：将本地主机上的时区信息文件挂载到容器内的 /etc/localtime 文件中，确保容器内的时间与主机上的时间一致

-v /usr/bin/docker:/usr/bin/docker: 将主机上的 /usr/bin/docker 文件挂载到容器中的 /usr/bin/docker，这样容器内的 Jenkins 可以直接使用宿主机上的 Docker 命令。在使用 GitLab/Jenkins 等 CI 软件的时候需要使用 Docker 命令来构建镜像，需要在容器中使用 Docker 命令；通过将宿主机的 Docker 共享给容器

-v /var/run/docker.sock:/var/run/docker.sock: 将主机上的 Docker socket 文件挂载到容器中的相同位置，这样容器内的 Jenkins 可以与宿主机上的 Docker 引擎进行通信。

–restart=on-failure：设置容器的重启策略为在容器以非零状态退出（异常退出）时重启。

-u 0：将容器内进程的用户身份设置为 root 用户，等同于-u root。

–name myjenkins：给容器指定一个名称为 myjenkins。
:::

## 验证Jenkins容器是否启动成功

```sh
docker ps
```

## 查看Jenkins初始密码

```sh
docker logs myjenkins

输出内容中Please use the following password to proceed to installation下一行就是
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629165334.png "image.png")

也可以在文件中查看

```sh
cat /usr/docker/jenkins_data/secrets/initialAdminPassword 

e 这就是
```

## 修改插件源

```sh
cd /usr/docker/jenkins_data/updates

替换default.json文件中指定的源

sed -i 's|http://updates.jenkins-ci.org/|https://mirrors.tuna.tsinghua.edu.cn/jenkins/|g' default.json

sed -i 's|http://www.google.com/|https://www.baidu.com/|g' default.json

```

## 修改下载地址

```sh
vim /usr/docker/jenkins_data/hudson.model.UpdateCenter.xml 

修改xml文件为：

<?xml version='1.1' encoding='UTF-8'?>
<sites>
  <site>
    <id>default</id>
    <url>http://mirror.esuni.jp/jenkins/updates/update-center.json</url>
  </site>
</sites>

```

## 登录Jenkins web页面

服务器IP:8082
如下图：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629170446.png "image.png")

注意：因为是**docker**启动，所以目录`/var/jenkins_home/secrets/initialAdminPassword` 中的`var/jenkins_home`要替换为我们的实际映射地址:`/usr/docker/jenkins_data/`

进入后安装推荐的插件：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629170729.png "image.png")

创建用户选择使用admin登录
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629171122.png "image.png")
点击左侧 Manage Jenkins -> Plugins -> 左侧 Available Plugins 安装插件

:::tip 一些常用插件

Locale（中文插件）

Gitlab（拉取 gitlab 中的源代码）

Maven Integration（maven构建工具）

Publish Over SSH（远程推送工具）

Role-based Authorization Strategy（权限管理）

Deploy to container（自动化部署工程所需要插件，部署到容器插件）

git parameter（用户参数化构建过程里添加git类型参数）

安装NodeJS插件
:::

## 配置NodeJS

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629172111.png "image.png")

拉动滚动条到最下方，点击新增NodeJS

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629172252.png "image.png")

## 配置github

点击manage Jenkins时会提醒配置 URL 进入那个页面，找到github

在凭据下方点击添加
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629174007.png "image.png")

去github 获取秘钥

github -> setting -> developer setting -> personal access tokens -> tokens(classic)
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629174416.png "image.png")
生成完成后 复制到jenkins secret
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629174528.png "image.png")

然后点击高级，生成一个hook url
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629174627.png "image.png")

在github中找到目标仓库，点击setting
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629174736.png "image.png")

jekins那边记得点击保存

## 配置前端项目

前端项目中新建DockerFIle:

```sh
FROM nginx
COPY dist/ /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
```

`/nginx/default.conf`

```sh
server {  
    listen       80;  
    server_name  localhost;  
  
    #charset koi8-r;  # 如果不需要这个字符集，可以注释掉  
    access_log  /var/log/nginx/host.access.log  main;  
    error_log  /var/log/nginx/error.log  error;  
  
    location / {  
        root   /usr/share/nginx/html;  
        index  index.html index.htm;  
    }  
  
    #error_page  404              /404.html;  # 如果需要自定义404页面，可以取消注释  
  
    # redirect server error pages to the static page /50x.html  
    #  
    error_page   500 502 503 504  /50x.html;  
    location = /50x.html {  
        root   /usr/share/nginx/html;  
    }  
}


```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629194000.png "image.png")

## jenkins 创建项目

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629175135.png "image.png")
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629175151.png "image.png")

点击确定

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629175239.png "image.png")

项目url： github项目地址

源码管理：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629175428.png "image.png")

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20240629175539.png "image.png")

Build Steps 中点击增加构建步骤， 选择执行shell

输入命令后点击保存

```sh
npm install npm run build rm -rf /home/nginx/html/* docker stop vueApp docker rm vueApp docker build -t vuenginxcontainer . docker image ls | grep vuenginxcontainer docker run \ -p 3000:80 \ -d --name vueApp \ vuenginxcontainer
```

```sh
npm install npm run build rm -rf /home/nginx/html/* docker build -t vuenginxcontainer . docker image ls | grep vuenginxcontainer docker run \ -p 3000:80 \ -d --name vueApp \ vuenginxcontainer
```

]]></description><link>https://9999886.xyz/posts/uhvYkJ6HEZzyubMfwX1NQ</link><guid isPermaLink="true">https://9999886.xyz/posts/uhvYkJ6HEZzyubMfwX1NQ</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 15 May 2025 06:28:14 GMT</pubDate></item><item><title><![CDATA[使用 Docker 部署 NestJs]]></title><description><![CDATA[## 本地测试

目录：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250509132916.png "image.png")

```Dockerfile
FROM node:22-alpine as builder

WORKDIR /app
COPY . .

ENV CI=true

RUN npm install -g pnpm
RUN npm install -g pm2

RUN pnpm install

RUN cd apps/core && npx prisma generate

RUN pnpm run build

COPY ecosystem.config.js .

COPY script/deploy.sh .

RUN chmod +x deploy.sh

EXPOSE 2333

CMD ["./deploy.sh"]

```

```sh:deploy.sh
#!/bin/sh

echo "Running database migrations..."
(cd apps/core && npx prisma migrate deploy)
echo "Starting application with PM2..."
pm2-runtime ecosystem.config.js

```

::: info

```
修改阿里云镜像
"registry-mirrors": [
	"https://registry.cn-hangzhou.aliyuncs.com"
]
```

:::

构建镜像：

```sh
sudo docker build -t nexo-admin .
```

报错：

```sh
sudo docker build -t nexo-admin .
Password:
[+] Building 0.3s (2/3)                                                                               docker:desktop-linux
[+] Building 1.3s (2/3)                                                                               docker:desktop-linux
[+] Building 1.4s (3/3) FINISHED                                                                      docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                  0.0s
 => => transferring dockerfile: 253B                                                                                  0.0s
 => [internal] load .dockerignore                                                                                     0.0s
 => => transferring context: 2B                                                                                       0.0s
 => ERROR [internal] load metadata for docker.io/library/node:20-alpine                                               1.3s
------
 > [internal] load metadata for docker.io/library/node:20-alpine:
------
Dockerfile:1
--------------------
   1 | >>> FROM node:20-alpine
   2 |     
   3 |     RUN npm install -g pnpm
--------------------
ERROR: failed to solve: node:20-alpine: error getting credentials - err: exit status 1, out: ``
```

pull node 镜像即可

```sh
sudo docker pull node:22-alpine
```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250509140447.png "image.png")
这样就这个镜像就是正常的

配置 docker-compose

```yml:docker-compose.yml
version: '1.0'
services:
  mysql:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: nexo-admin
      MYSQL_DATABASE: nexo
      MYSQL_USER: nexo
      MYSQL_PASSWORD: nexo-admin
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - '3306:3306'

  redis:
    image: redis:5-alpine
    restart: always
    command: redis-server --requirepass nexo-admin
    volumes:
      - redis-data:/data
    ports:
      - '6379:6379'

  nexo-core:
    image: nexo-core:latest
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '2333:2333'
    depends_on:
      - mysql
      - redis
    environment:
      - DATABASE_URL=mysql://root:nexo-admin@mysql:3306/nexo?schema=public
      - REDIS_HOST=redis
volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
```

配置完成后，使用命令启动:

```sh
docker-compose up -d
```

查看启动是否正常：
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250509163112.png "image.png")

## 服务器部署

安装 `docker` [链接](https://juejin.cn/post/7483049212315484195?searchId=20250513155504523CFAAAD90D7F2DFC43#heading-3)

使用`github action`上传代码

```yaml:.github/workflows/deploy.yaml
name: Deploy to Server

on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Deploy to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ vars.SSH_HOST }}
          username: ${{ vars.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          source: ./
          target: ~/nexo-core/
          strip_components: 0
          timeout: 120s
          overwrite: true
          rm: true

```

服务器信息在代码仓库中设置
![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250513220500.png "image.png")

服务器中运行`docker compose up -d` 启动项目。可以在服务器单独配置一个`.env`文件放项目的环境变量

更新：

```sh:deploy.sh
#!/bin/bash                                                                                                                               
set -e                                                                                                                                                         
# 颜色定义                                                                                                                                                      

GREEN='\033[0;32m'                                                                                                                                                

RED='\033[0;31m'                                                                                                                                                  

NC='\033[0m'                                                                                                                                                      

# 路径配置                                                                                                                                                        

ENV_SOURCE=~/env/core-env                                                                                                                                         

ENV_TARGET=~/nexo-core/apps/core/.env                                                                                                                             

PROJECT_DIR=~/nexo-core                                                                                                                                           

start_time=$(date +%s)                                                                                                                                            

echo -e "${GREEN}📁 开始部署 nexo-core${NC}"                                                                                                                      

# 检查文件                                                                                                                                                        

if [ ! -f "$ENV_SOURCE" ]; then                                                                                                                                   

  echo -e "${RED}❌ 环境变量文件不存在：$ENV_SOURCE${NC}"                                                                                                         

  exit 1                                                                                                                                                          

fi                                                                                                                                                                

if [ ! -d "$PROJECT_DIR" ]; then                                                                                                                                  

  echo -e "${RED}❌ 项目目录不存在：$PROJECT_DIR${NC}"                                                                                                            

  exit 1                                                                                                                                                          

fi                                                                                                                                                                

# 拷贝 .env                                                                                                                                                       

echo -e "${GREEN}✅ 拷贝环境文件...${NC}"                                                                                                                         

sudo cp "$ENV_SOURCE" "$ENV_TARGET"                                                                                                                               

# 构建镜像                                                                                                                                                        

cd "$PROJECT_DIR"                                                                                                                                                 

echo -e "${GREEN}🔨 构建镜像中...${NC}"                                                                                                                           

docker compose build --no-cache nexo-core || {                                                                                                                    

  echo -e "${RED}❌ 镜像构建失败！${NC}"                                                                                                                          

  exit 1                                                                                                                                                          

}                                                                                                                                                                 

# 启动服务                                                                                                                                                        

echo -e "${GREEN}🚀 启动服务...${NC}"                                                                                                                             

docker compose up -d nexo-core || {                                                                                                                               

  echo -e "${RED}❌ 服务启动失败！${NC}"                                                                                                                          

  exit 1                                                                                                                                                          

}                                                                                                                                                                 

# 删除 dangling 镜像                                                                                                                                              

echo -e "${GREEN}🧹 清理无用镜像...${NC}"                                                                                                                         

dangling_images=$(docker images -f "dangling=true" -q)                                                                                                            

if [ -n "$dangling_images" ]; then                                                                                                                                

  docker rmi $dangling_images                                                                                                                                     

  echo -e "${GREEN}✅ 已删除 dangling 镜像。${NC}"                                                                                                                

else                                                                                                                                                              

  echo -e "${GREEN}ℹ️ 无 dangling 镜像可清理。${NC}"                                                                                                               

fi                                                                                                                                                                

end_time=$(date +%s)                                                                                                                                              

echo -e "${GREEN}✅ 部署完成，用时 $((end_time - start_time)) 秒。${NC}"
```

修改`github action`

```yaml:.github/workflows/deploy.yaml
name: Deploy to Server

on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Deploy to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ vars.SSH_HOST }}
          username: ${{ vars.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          source: ./
          target: ~/nexo-core/
          strip_components: 0
          timeout: 120s
          overwrite: true
          rm: true

      - name: Execute deployment script
        uses: appleboy/ssh-action@master
        with:
          host: ${{ vars.SSH_HOST }}
          username: ${{ vars.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            sh ~/deploy/nexo-core-deploy.sh

```

![image.png](https://obsidian-img-bsuooo.oss-cn-beijing.aliyuncs.com/note/20250513222759.png "image.png")
]]></description><link>https://9999886.xyz/posts/6qsNUbSgcDjvDaapxi0y8</link><guid isPermaLink="true">https://9999886.xyz/posts/6qsNUbSgcDjvDaapxi0y8</guid><dc:creator><![CDATA[ryne]]></dc:creator><pubDate>Thu, 15 May 2025 05:54:31 GMT</pubDate></item></channel></rss>