普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月29日首页

每日一题-转换字符串的最小成本 I🟡

2026年1月29日 00:00

给你两个下标从 0 开始的字符串 sourcetarget ,它们的长度均为 n 并且由 小写 英文字母组成。

另给你两个下标从 0 开始的字符数组 originalchanged ,以及一个整数数组 cost ,其中 cost[i] 代表将字符 original[i] 更改为字符 changed[i] 的成本。

你从字符串 source 开始。在一次操作中,如果 存在 任意 下标 j 满足 cost[j] == z  、original[j] == x 以及 changed[j] == y 。你就可以选择字符串中的一个字符 x 并以 z 的成本将其更改为字符 y

返回将字符串 source 转换为字符串 target 所需的 最小 成本。如果不可能完成转换,则返回 -1

注意,可能存在下标 ij 使得 original[j] == original[i]changed[j] == changed[i]

 

示例 1:

输入:source = "abcd", target = "acbe", original = ["a","b","c","c","e","d"], changed = ["b","c","b","e","b","e"], cost = [2,5,5,1,2,20]
输出:28
解释:将字符串 "abcd" 转换为字符串 "acbe" :
- 更改下标 1 处的值 'b' 为 'c' ,成本为 5 。
- 更改下标 2 处的值 'c' 为 'e' ,成本为 1 。
- 更改下标 2 处的值 'e' 为 'b' ,成本为 2 。
- 更改下标 3 处的值 'd' 为 'e' ,成本为 20 。
产生的总成本是 5 + 1 + 2 + 20 = 28 。
可以证明这是可能的最小成本。

示例 2:

输入:source = "aaaa", target = "bbbb", original = ["a","c"], changed = ["c","b"], cost = [1,2]
输出:12
解释:要将字符 'a' 更改为 'b':
- 将字符 'a' 更改为 'c',成本为 1 
- 将字符 'c' 更改为 'b',成本为 2 
产生的总成本是 1 + 2 = 3。
将所有 'a' 更改为 'b',产生的总成本是 3 * 4 = 12 。

示例 3:

输入:source = "abcd", target = "abce", original = ["a"], changed = ["e"], cost = [10000]
输出:-1
解释:无法将 source 字符串转换为 target 字符串,因为下标 3 处的值无法从 'd' 更改为 'e' 。

 

提示:

  • 1 <= source.length == target.length <= 105
  • sourcetarget 均由小写英文字母组成
  • 1 <= cost.length== original.length == changed.length <= 2000
  • original[i]changed[i] 是小写英文字母
  • 1 <= cost[i] <= 106
  • original[i] != changed[i]

🔥别再用递归了!WeakMap 的影子索引“让树不再是树!”

作者 vilan_微澜
2026年1月28日 23:57

一、前言

大家好,我是微澜。今天来分享一个基于 WeakMap 实现的快速对树形结构数据进行增删改查操作useTree hook函数,它是基于JavaScriptWeakMap 特性,在不改动原始数据的前提下,实现了一套 O(1) 查找的影子索引结构,这个影子其实就是对象的引用地址,让树形数据操作像操作数组一样简单!

二、为什么选择 WeakMap?

1. 非侵入性 (Non-invasive)

通过 WeakMap 在内存中构建了一套 Node -> Parent 的映射。原始数据对象保持纯净,没有任何多余属性,完美支持序列化。

2. 内存安全 (Memory Safety)

这是最关键的一点。WeakMap 对键(对象)是弱引用的。

  • 如果你删除了原始树中的某个节点,且没有其他地方引用它,垃圾回收器(GC)会自动清理索引表中的对应项。

  • 这种特性非常适合处理大型动态树,完全不用担心手动维护索引带来的内存泄漏。

3、不同实现方案对比

在开始之前,我们先看看为什么选择 WeakMap。我们可以通过一个综合对比来看看操作树形数据不同方案的差异:

维度 / 方案 传统递归遍历 节点注入 parent Map 缓存方案 WeakMap 优化方案 (本项目)
初始化复杂度 O(N2)O(N^2) (双层循环) O(N)O(N) O(N)O(N) O(N)O(N) (单次递归)
溯源时间复杂度 O(N)O(N) O(1)O(1) O(D)O(D) O(D)O(D) (D为深度,极快)
数据污染 高(直接修改对象) 无(外部映射,保持纯净)
内存管理 一般 一般 (强引用,需手动清理) 优秀(弱引用自动 GC)
序列化友好度 友好 极差(循环引用) 友好 友好
适用场景 极小数据量 中等数据量 中等数据量 大规模数据、高频转换

三、核心实现原理

1. 整体架构图

我们通过一个外部的 WeakMap 来存储节点与其父级的对应关系。这种设计最大的好处是:“不改变原始树结构,不产生循环引用,且能实现 O(1) 的溯源。”

graph TD
    A[原始树形数据] --> B{useTree Hook}
    B -- "1. 递归扫描" --> C[WeakMap 存储库]

    subgraph "WeakMap 存储策略"
    C -- "Key: 根节点" --> D["Value: 整个 Tree 数组 (方便删除)"]
    C -- "Key: 子节点" --> E["Value: 直接父节点对象 (实现溯源)"]
    end

    F[业务请求: 查找/删除] --> C
    C -- "返回父级引用" --> G[完成操作]

2. 数据映射图解 (核心逻辑)

为了让大家更直观地看到 WeakMap 里到底存了什么,我们看下面这个例子:

示例数据:

const list = [
  {
    id: 1, name: '掘金',
    children: [ { id: 11, name: '前端组' } ]
  },
  {
    id: 2, name: '社区',
    children: []
  }
];

WeakMap 内部映射关系:

节点 (Key) 映射值 (Value) Value类型 存储逻辑说明
{ id: 11, name: '前端组' } { id: 1, name: '掘金', children: [{ id: 11, name: '前端组' }] } Object (父节点) 非根节点:指向它的直接父对象
{ id: 1, name: '掘金', children: [{ id: 11, name: '前端组' }] } [{ id: 1, name: '掘金', children: [{ id: 11, name: '前端组' }] }, { id: 2, name: '社区', children: [] }] Array (根数组) 根节点:指向它所属的整棵树数组
{ id: 2, name: '社区', children: [] } [{ id: 1, name: '掘金', children: [{ id: 11, name: '前端组' }] }, { id: 2, name: '社区', children: [] }] Array (根数组) 根节点:指向它所属的整棵树数组

💡 核心秘诀: 以上设计了一套巧妙的判断规则:如果节点是根节点,其映射值就是数组;如果节点是子节点,其映射值就是父对象。 这样在执行 removeChild 时,我们只需要判断 Array.isArray(parent),就能自动切换“从数组中删除”根节点或“从 children 属性中删除”子节点的逻辑,极其优雅!


四、useTree()实现方法逐一拆解

接下来,我们看看 useTree 内部每个方法的具体实现原理。

1. initTree - 初始化索引

功能:递归遍历整棵树,建立 WeakMap 映射。

// 数据索引对象
const _treeWeakMap = new WeakMap<TreeNode, TreeNode | TreeNode[]>();

const useTree = (
  props: Props = {
    // 外部数据对象字段映射
    treeNodeProp: {
      value: 'value',
      label: 'label',
      children: 'children',
    }
  }
){
    // 初始化树结构索引
    function initTree(list, parent){...}
}
  // 初始化树结构索引
  function initTree(list: TreeNode[], parent?: TreeNode) {
    list.forEach((item) => {
      // 根节点的父级为完整的树数据,在删除根节点时需要通过完整的数组删除
      _treeWeakMap.set(item, parent || list);
      if (item[treeNodeProp.children]) {
        // 删除子节点只需要通过对应子节点的children数组删除
        initTree(item[treeNodeProp.children], item);
      }
    });
  }

原理:在组件挂载或数据更新时调用。通过这种“差异化映射”,我们让每个节点都拥有了一个指向其“归属集合”的指针,也就是指向了父级的引用地址

以上文的表格列出的示例数据为例,调用initTree方法初始化后,可以得到以下对应关系:

graph LR
    subgraph Keys [节点对象 - Key]
        K11("前端组 (id:11)")
        K1("掘金 (id:1)")
        K2("社区 (id:2)")
    end

    subgraph Values [映射值 - Value]
        V_Obj1["父节点对象 (id:1)"]
        V_Arr["根数据数组 [Tree]"]
    end

    K11 -- "指向直接父级" --> V_Obj1
    K1 -- "指向所属根数组" --> V_Arr
    K2 -- "指向所属根数组" --> V_Arr

    style K11 fill:#f9f,stroke:#333
    style K1 fill:#bbf,stroke:#333
    style K2 fill:#bbf,stroke:#333
    style V_Arr fill:#dfd,stroke:#333
    style V_Obj1 fill:#fff,stroke:#333

2. _setParent - 节点索引设置

功能:建立节点与其所属容器(父节点对象或根数组)的映射关系。

  // 设置节点的父节点映射。为防止用户错误使用,不向外暴露,内部使用
  function _setParent(node: TreeNode, parent: TreeNode | TreeNode[]) {
    _treeWeakMap.set(node, parent);
  }

原理:这是对 WeakMap.set 的一层简单封装。通过这种方式,我们将节点对象引用作为 Key,其父级引用作为 Value 存入索引库。通过_前缀标记为私有是为了防止外部直接篡改映射关系,保证索引的单一可靠性。

3. getParent - 获取父节点

功能:获取指定节点的直接父节点。

  // 获取节点的父节点
  function getParent(node: TreeNode) {
    return _treeWeakMap.get(node);
  }
  • 原理:利用 WeakMap.get 直接获取。时间复杂度为 O(1)

4. getParents - 获取全路径父节点

功能:递归向上检索父级,获取从当前节点到根部的所有父节点数组。

  // 获取节点的所有父节点
  function getParents(item: TreeNode, parentList: (TreeNode | string)[] = [], key?: string): (TreeNode | string)[] {
    const parent = _treeWeakMap.get(item);
    // 递归到根节点数组时停止
    if (parent && !_isRootNode(parent)) {
      parentList.push(key ? parent[key] : parent);
      getParents(parent, parentList, key);
    }
    return parentList;
  }
  
  // 判断是否根节点,下文都用这个方法判断是否根节点
  function _isRootNode(node: TreeNode | TreeNode[]) {
    return Array.isArray(node);
  }

原理:通过节点自身的引用地址在WeakMap中查找父级。相比于DFS(深度优先遍历)全树,这种垂直爬升的方式效率极高。 什么是垂直爬升

类似于:

show code is me.parent.parent. ......

就这样,连祖宗十八代都能给你扒出来!

5. addChild - 动态添加节点

功能:向指定节点添加子节点,并自动更新索引。

  // 添加子节点或根节点,节点不存在.children时,代表添加到根节点数组
  function addChild(node: TreeNode, child: TreeNode) {
    if(_isRootNode(node)) {
      // 根节点数组直接添加
      const i = node.push(child) - 1;
      /**
       * 为新增加的子节点构建一个WeakMap索引,指向父节点
       * 注意:注册为key的引用地址是经过proxy处理后的节点
       */
      node[i] && _setParent(node[i], node);
    } else {
      // 非根节点,添加到children数组
      if (!node[treeNodeProp.children]) {
        node[treeNodeProp.children] = [];
      }
      const i = node[treeNodeProp.children].push(child) - 1;
      // 和上面设置根节点同理
      node.children[i] && _setParent(node.children[i], node);
    }
  }

原理:在操作原始数据的同时,同步更新 WeakMap索引。也就是为新增的对象建立父级索引。

注意:这里不能把新添加的child对象当做WeakMapKey,因为child只是一个外部传入的一个临时变量,还没有和整棵树建立联系,需要在添加到树当中后,获取树当中的对象地址作为Key建立索引。

6. removeChild - 删除指定节点

功能:无痛删除任意节点,无需手动遍历查找。

  // 删除指定子节点
  function removeChild(node: TreeNode) {
    // 找到父节点,通过父节点删除
    const parent = getParent(node);
    if(!parent) {
      // 没有找到父级节点,抛出错误
      throw new Error('没有找到父级节点!');
    }
    if(_isRootNode(parent)) {
      // 删除根节点
      const index = parent.findIndex((item: TreeNode) => item === node);
      if(index >= 0) {
        parent.splice(index, 1);
      } else {
        // 没有找到要删除的根节点,抛出错误
        throw new Error('没有找到要删除的根节点!');
      }
    } else {
      // 通过找到父级删除自己
      parent.children = parent.children.filter((item: TreeNode) => item !== node);
    }
  }

原理:这是该方案最精妙的地方!通过 initTree 建立的差异化映射,我们只需要简单的 Array.isArray 判断,就能准确找到节点所在的“容器”,实现秒删。


五、完整代码实现

代码不多,却能快速~实现对树形数据的增删改查操作。

// useTree.ts

type Props = {
  // 树形数据字段名映射类型
  treeNodeProp: TreeNodeProp
}

// 树节点属性映射类型
type TreeNodeProp = {
  value: string;
  label: string;
  children: string;
}

// 数节点
type TreeNode = any

/**
 * 树结构索引数据
 * key为节点本身,value为父节点或根节点数组(即整棵树)
 * 如果value为Array,代表根节点数组
 * 如果value为Object,代表子节点
 */
const _treeWeakMap = new WeakMap<TreeNode, TreeNode | TreeNode[]>();

// 使用weakmap构建树形数据数据索引
export const useTree = (
  props: Props = {
    // 外部数据对象字段映射
    treeNodeProp: {
      value: 'value',
      label: 'label',
      children: 'children',
    }
  }
) => {
  const { treeNodeProp } = props;

  // 设置节点的父节点映射。为防止用户错误使用,不向外暴露,内部使用
  function _setParent(node: TreeNode, parent: TreeNode | TreeNode[]) {
    _treeWeakMap.set(node, parent);
  }

  // 判断是否根节点
  function _isRootNode(node: TreeNode | TreeNode[]) {
    return Array.isArray(node);
  }

  // 初始化树结构索引
  function initTree(list: TreeNode[], parent?: TreeNode) {
    list.forEach((item) => {
      // 根节点的父级为完整的树数据,在删除根节点时需要通过完整的数组删除
      _treeWeakMap.set(item, parent || list);
      if (item[treeNodeProp.children]) {
        // 删除子节点只需要通过对应子节点的children数组删除
        initTree(item[treeNodeProp.children], item);
      }
    });
  }

  // 添加子节点或根节点,节点不存在.children时,代表添加到根节点数组
  function addChild(node: TreeNode, child: TreeNode) {
    if(_isRootNode(node)) {
      // 根节点数组直接添加
      const i = node.push(child) - 1;
      /**
       * 为新增加的子节点构建一个WeakMap索引,指向父节点
       * 注意:注册为key的引用地址是经过proxy处理后的节点
       */
      node[i] && _setParent(node[i], node);
    } else {
      // 非根节点,添加到children数组
      if (!node[treeNodeProp.children]) {
        node[treeNodeProp.children] = [];
      }
      const i = node[treeNodeProp.children].push(child) - 1;
      // 和上面设置根节点同理
      node.children[i] && _setParent(node.children[i], node);
    }
  }

  // 删除指定子节点
  function removeChild(node: TreeNode) {
    // 找到父节点,通过父节点删除
    const parent = getParent(node);
    if(!parent) {
      // 没有找到父级节点,抛出错误
      throw new Error('没有找到父级节点!');
    }
    if(_isRootNode(parent)) {
      // 删除根节点
      const index = parent.findIndex((item: TreeNode) => item === node);
      if(index >= 0) {
        parent.splice(index, 1);
      } else {
        // 没有找到要删除的根节点,抛出错误
        throw new Error('没有找到要删除的根节点!');
      }
    } else {
      // 通过找到父级删除自己
      parent.children = parent.children.filter((item: TreeNode) => item !== node);
    }
  }

  // 获取节点的父节点
  function getParent(node: TreeNode) {
    return _treeWeakMap.get(node);
  }

  // 获取节点的所有父节点
  function getParents(item: TreeNode, parentList: (TreeNode | string)[] = [], key?: string): (TreeNode | string)[] {
    const parent = _treeWeakMap.get(item);
    // 递归到根节点数组时停止
    if (parent && !_isRootNode(parent)) {
      parentList.push(key ? parent[key] : parent);
      getParents(parent, parentList, key);
    }
    return parentList;
  }

  // 获取节点的所有父节点label数组
  function getParentLabels(item: TreeNode, labelList: string[] = []): string[] {
    return getParents(item, labelList, treeNodeProp.label) as string[];
  }

  // 获取节点的所有父节点value数组
  function getParentValues(item: TreeNode, valueList: string[] = []): string[] {
    return getParents(item, valueList, treeNodeProp.value) as string[];
  }

  return {
    getParents,
    getParentLabels,
    getParentValues,
    getParent,
    initTree,
    addChild,
    removeChild,
  };
};

六、总结

通过 WeakMap 实现的 useTree,核心优势在于解耦。它将“业务数据”与“层级关系”分离开来,既保证了数据的纯净,又获得了极高的查找性能。

WeakMap 作为一个容易被忽视的原生特性,在处理这类关联关系映射时有着天然的优势。

如果你也在为复杂树结构的管理发愁,不妨尝试下这种“影子索引”的思路。

如有不足或可优化的地方,欢迎在评论区交流讨论,如果觉得有用,点个👍也挺好的!👏

如果你在处理大型树形表格或复杂的组织架构,这个 Hook 绝对是你的提效神器!

七、预览和源码地址

多场景演示 (Demo)

项目源码地址

github:https://github.com/java6688/Tree_By_WeakMap

构建无障碍组件之Link Pattern

作者 anOnion
2026年1月28日 23:18

Link Pattern 详解:构建无障碍链接组件

链接是 Web 页面中最核心的导航元素,它将用户从一个资源引导到另一个资源。根据 W3C WAI-ARIA Link Pattern 规范,正确实现的链接组件不仅要提供清晰的导航功能,更要确保所有用户都能顺利访问,包括依赖屏幕阅读器等辅助技术的用户。本文将深入探讨 Link Pattern 的核心概念、实现要点以及最佳实践。

一、链接的定义与核心功能

链接是一个允许用户导航到另一个页面、页面位置或其他资源的界面组件。链接的本质是超文本引用,它告诉用户这里有你可能感兴趣的另一个资源。与按钮执行动作不同,链接的作用是导航,这是两者最本质的区别。

在实际开发中,浏览器为原生 HTML 链接提供了丰富的功能支持,例如在新窗口中打开目标页面、将目标 URL 复制到系统剪贴板等。因此,应尽可能使用 HTML a 元素创建链接

二、何时需要自定义链接实现

在某些情况下,需要使用非 a 元素实现链接功能,例如:

  • 图片作为导航入口
  • 使用 CSS 伪元素创建的可视化链接
  • 复杂的 UI 组件中需要链接行为的元素

根据 WAI-ARIA 规范,当必须使用非 a 元素时,需要手动添加必要的 ARIA 属性和键盘支持。

三、键盘交互规范

键盘可访问性是 Web 无障碍设计的核心要素之一。链接组件必须支持完整的键盘交互,确保无法使用鼠标的用户也能顺利操作。根据 Link Pattern 规范:

回车键是激活链接的主要方式。当用户按下回车键时,链接被触发执行导航操作。

上下文菜单(可选):按 Shift + F10 键可以打开链接的上下文菜单,提供复制链接地址、在新窗口中打开等选项。

操作系统 打开上下文菜单
Windows Shift + F10Menu
macOS Control + 点击

四、WAI-ARIA 角色、状态和属性

正确使用 WAI-ARIA 属性是构建无障碍链接组件的技术基础。

角色声明是基础要求。非 a 元素的链接需要将 role 属性设置为 link,向辅助技术表明这是一个链接组件。

示例:使用 span 元素模拟链接:

<span
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')">
  示例网站
</span>

可访问名称是链接最重要的可访问性特征之一。链接必须有可访问的名称,可以通过元素文本内容、aria-label 或 alt 属性提供。

示例 1:使用 img 元素作为链接时,通过 alt 属性提供可访问名称:

<img
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')"
  src="logo.png"
  alt="示例网站" />

示例 2:使用 aria-label 为链接提供可访问名称:

<span
  tabindex="0"
  role="link"
  class="text-link"
  onclick="goToLink(event, 'https://example.com/')"
  onkeydown="goToLink(event, 'https://example.com/')"
  aria-label="访问示例网站"
  >🔗</span
>

焦点管理需要使用 tabindex="0",将链接元素包含在页面 Tab 序列中,使其可通过键盘聚焦。

五、完整示例

以下是使用不同元素实现链接的完整示例:

<!-- 示例 1:span 元素作为链接 -->
<span
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')">
  W3C 网站
</span>

<!-- 示例 2:img 元素作为链接 -->
<img
  tabindex="0"
  role="link"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')"
  src="logo.svg"
  alt="W3C 网站" />

<!-- 示例 3:使用 aria-label 的链接 -->
<span
  tabindex="0"
  role="link"
  class="link-styled"
  onclick="goToLink(event, 'https://w3.org/')"
  onkeydown="goToLink(event, 'https://w3.org/')"
  aria-label="W3C 网站"
  >🔗</span
>

<script>
  function goToLink(event, url) {
    if (event.type === 'keydown' && event.key !== 'Enter') {
      return;
    }
    window.open(url, '_blank');
  }
</script>

<style>
  .link-styled {
    color: blue;
    text-decoration: underline;
    cursor: pointer;
  }
  .link-styled:focus {
    outline: 2px solid blue;
    outline-offset: 2px;
  }
</style>

六、最佳实践

6.1 优先使用原生元素

尽可能使用原生 HTML a 元素创建链接。浏览器为原生链接提供了丰富的功能和更好的兼容性,无需额外添加 ARIA 属性。

<!-- 推荐做法 -->
<a
  href="https://example.com/"
  target="_blank"
  >访问示例</a
>

<!-- 不推荐做法 -->
<span
  role="link"
  tabindex="0"
  >访问示例</span
>

6.2 正确处理键盘事件

自定义链接需要同时处理 onclick 和 onkeydown 事件,确保用户可以通过回车键激活链接。

element.addEventListener('keydown', function (e) {
  if (e.key === 'Enter') {
    e.preventDefault();
    // 执行导航操作
    window.location.href = this.dataset.url;
  }
});

6.3 提供视觉反馈

链接应该有明确的视觉样式,让用户能够识别这是一个可交互的元素。同时,应该提供键盘焦点样式。

a,
[role='link'] {
  color: #0066cc;
  text-decoration: underline;
  cursor: pointer;
}

/* 焦点状态:仅对键盘 Tab 导航显示焦点框,鼠标点击时不显示 */
a:focus-visible,
[role='link']:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
  border-radius: 2px;
}

/* 悬停状态:加深颜色并加粗下划线,提供鼠标交互反馈 */
a:hover,
[role='link']:hover {
  color: #004499;
  text-decoration-thickness: 2px;
}

/* 已访问状态:使用紫色标识用户已访问的链接 */
a:visited,
[role='link']:visited {
  color: #551a8b;
}

/* 激活状态:点击瞬间的颜色变化 */
a:active,
[role='link']:active {
  color: #ff0000;
}

6.4 避免过度使用 ARIA

WAI-ARIA 有一条重要原则:没有 ARIA 比糟糕的 ARIA 更好。在某些情况下,错误使用 ARIA 可能会导致比不使用更糟糕的可访问性体验。只有在确实需要时才使用自定义链接实现。

七、链接与按钮的区别

在 Web 开发中,正确区分按钮和链接至关重要。

特性 链接 按钮
功能 导航到其他资源 触发动作
HTML 元素 <a> <button><input type="button">
键盘激活 Enter Space、Enter
role 属性 link button
典型用途 页面跳转、锚点导航 提交表单、打开对话框

八、总结

构建无障碍的链接组件需要关注多个层面的细节。从语义化角度,应优先使用原生 HTML a 元素;从键盘交互角度,必须支持回车键激活;从 ARIA 属性角度,需要正确使用 role="link" 和可访问名称。

WAI-ARIA Link Pattern 为我们提供了清晰的指导方针,遵循这些规范能够帮助我们创建更加包容和易用的 Web 应用。每一个正确实现的链接组件,都是构建无障碍网络环境的重要一步。

type-challenges(ts类型体操): 7 - 对象属性只读

作者 fxss
2026年1月28日 22:45

7 - 对象属性只读

by Anthony Fu (@antfu) #简单 #built-in #readonly #object-keys

题目

不要使用内置的Readonly<T>,自己实现一个。

泛型 Readonly<T> 会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly) 的。

也就是不可以再对该对象的属性赋值。

例如:

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

在 Github 上查看:tsch.js.org/7/zh-CN

代码

/* _____________ 你的代码 _____________ */

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

关键解释:

  • readonly 关键字用于将属性设置为只读,即不能对其进行赋值操作;
  • [P in keyof T] 用于遍历泛型 T 的所有属性键;
  • T[P] 用于获取属性 P 的类型。

相关知识点

readonly

  • 核心作用:标记后,目标(属性 / 数组 / 元组)只能在初始化阶段赋值(比如接口实例化、类构造函数、变量声明时),后续任何修改操作都会被 TS 编译器拦截报错;
  • 运行时特性:readonly 仅做编译时检查,不会生成任何额外 JS 代码,也无法真正阻止运行时的修改(比如通过类型断言绕开的话,运行时仍能改);
  • const 的区别:const 是变量层面的不可重新赋值(但变量指向的对象 / 数组内部属性仍可改),readonly 是属性 / 类型层面的不可修改(变量本身可重新赋值,除非变量也用 const)。

常用使用场景:

  1. 作用于接口 / 类型别名的属性(最基础)
// 定义带只读属性的接口
interface User {
  readonly id: number; // 只读属性:只能初始化赋值,后续不可改
  name: string; // 普通属性:可修改
}

// 初始化时赋值(合法)
const user: User = { id: 1, name: "张三" };

// 尝试修改只读属性(报错)
user.id = 2; // ❌ 报错:无法分配到 "id",因为它是只读属性
// 修改普通属性(合法)
user.name = "李四"; // ✅ 合法
  1. 作用于类的属性: 类中使用 readonly 标记属性,只能在声明时构造函数中赋值,后续无法修改
class Person {
  readonly id: number; // 只读属性
  name: string;

  // 构造函数中给 readonly 属性赋值(唯一合法的后续赋值方式)
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  updateInfo() {
    this.id = 100; // ❌ 报错:id 是只读属性
    this.name = "王五"; // ✅ 合法
  }
}

const person = new Person(1, "赵六");
person.id = 2; // ❌ 报错:只读属性不可修改
  1. 作用于数组 / 元组(只读数组): readonly 可标记数组为 “只读数组”,禁止修改数组元素、调用 push/pop 等修改方法
// 方式1:使用 readonly 修饰数组类型
const arr1: readonly number[] = [1, 2, 3];
arr1.push(4); // ❌ 报错:readonly 数组不存在 push 方法
arr1[0] = 10; // ❌ 报错:无法修改只读数组的元素

// 方式2:使用 ReadonlyArray<T> 类型(等价于 readonly T[])
const arr2: ReadonlyArray<string> = ["a", "b"];
arr2.pop(); // ❌ 报错

// 作用于元组(只读元组)
type Point = readonly [number, number];
const point: Point = [10, 20];
point[0] = 30; // ❌ 报错:只读元组元素不可修改
  1. 结合 keyof + in 批量创建只读类型(映射类型)
interface Product {
  name: string;
  price: number;
  stock: number;
}

// 批量创建只读版本的 Product(TS 内置的 Readonly<T> 就是这么实现的)
type ReadonlyProduct = {
  readonly [K in keyof Product]: Product[K];
};

const product: ReadonlyProduct = { name: "手机", price: 2999, stock: 100 };
product.price = 3999; // ❌ 报错:price 是只读属性

// TS 内置了 Readonly<T>,可直接使用(无需手动写映射类型)
const product2: Readonly<Product> = { name: "电脑", price: 5999, stock: 50 };
product2.stock = 60; // ❌ 报错
  1. 只读索引签名:如果类型使用索引签名,也可以标记为 readonly,禁止通过索引修改属性
// 只读索引签名:只能读取,不能修改
type ReadonlyDict = {
  readonly [key: string]: number;
};

const dict: ReadonlyDict = { a: 1, b: 2 };
dict["a"] = 3; // ❌ 报错:索引签名是只读的
console.log(dict["b"]); // ✅ 合法:仅读取

keyof

keyof 操作符用于获取对象类型的所有属性名(包括索引签名),并将其转换为联合类型。

例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoKeys = keyof Todo // "title" | "description" | "completed"

in

in 操作符用于遍历联合类型中的每个成员。

例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoKeys = 'title' | 'description' | 'completed'

type TodoPreview = {
  [P in TodoKeys]: Todo[P]
}
// TodoPreview 类型为:
// {
//   title: string
//   description: string
//   completed: boolean
// }

测试用例

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]

interface Todo1 {
  title: string
  description: string
  completed: boolean
  meta: {
    author: string
  }
}

相关链接

分享你的解答:tsch.js.org/7/answer/zh… 查看解答:tsch.js.org/7/solutions 更多题目:tsch.js.org/zh-CN

下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。

实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。

前端功能点

python dijkstra

作者 nriB8ZIB57
2023年12月24日 17:26

Problem: 100156. 转换字符串的最小成本 I

[TOC]

思路

python dijkstra

解题方法

python dijkstra

Code

###Python3

class Solution:
    def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
        ans=0
        g=[[] for _ in range(26)]
        for i,j,c in zip(original,changed,cost):
            heappush(g[ord(i)-ord('a')],[c,ord(j)-ord('a')])
        d=defaultdict(int)
        def dijkstra(x,y):#dijkstra
            distant=0
            vis=set()
            q=[]
            q.append([0,x])
            while q:
                c,temp=heappop(q)
                if temp==y:
                    return c
                if temp in vis:
                    continue
                vis.add(temp)
                for cc,xx in g[temp]:
                    heappush(q,[c+cc,xx])
            return -1
        for i,j in zip(source,target):
            if i!=j:
                if (ord(i)-ord('a'),ord(j)-ord('a')) in d:
                    ans+=d[ord(i)-ord('a'),ord(j)-ord('a')]
                else :
                    res=dijkstra(ord(i)-ord('a'),ord(j)-ord('a'))
                    if res==-1:
                        return -1
                    ans+=res
                    d[ord(i)-ord('a'),ord(j)-ord('a')]=res
        return ans

Floyd 算法(Python/Java/C++/Go)

作者 endlesscheng
2023年12月24日 12:17

建图,从 $\textit{original}[i]$ 向 $\textit{changed}[i]$ 连边,边权为 $\textit{cost}[i]$。

然后用 Floyd 算法求图中任意两点最短路,得到 $\textit{dis}$ 矩阵,原理请看 带你发明 Floyd 算法!包含为什么循环顺序是 $kij$ 的讲解。

这里得到的 $\textit{dis}[i][j]$ 表示字母 $i$ 通过若干次替换操作变成字母 $j$ 的最小成本。

最后累加所有 $\textit{dis}[\textit{original}[i]][\textit{changed}[i]]$,即为答案。如果答案为无穷大,返回 $-1$。

本题视频讲解

###py

class Solution:
    def minimumCost(self, source: str, target: str, original: List[str], changed: List[str], cost: List[int]) -> int:
        dis = [[inf] * 26 for _ in range(26)]
        for i in range(26):
            dis[i][i] = 0

        for x, y, c in zip(original, changed, cost):
            x = ord(x) - ord('a')
            y = ord(y) - ord('a')
            dis[x][y] = min(dis[x][y], c)

        for k in range(26):
            for i in range(26):
                if dis[i][k] == inf:
                    continue  # 巨大优化!
                for j in range(26):
                    dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j])

        ans = sum(dis[ord(x) - ord('a')][ord(y) - ord('a')] for x, y in zip(source, target))
        return ans if ans < inf else -1

###java

class Solution {
    public long minimumCost(String source, String target, char[] original, char[] changed, int[] cost) {
        final int INF = Integer.MAX_VALUE / 2;
        int[][] dis = new int[26][26];
        for (int i = 0; i < 26; i++) {
            Arrays.fill(dis[i], INF);
            dis[i][i] = 0;
        }
        for (int i = 0; i < cost.length; i++) {
            int x = original[i] - 'a';
            int y = changed[i] - 'a';
            dis[x][y] = Math.min(dis[x][y], cost[i]);
        }
        for (int k = 0; k < 26; k++) {
            for (int i = 0; i < 26; i++) {
                if (dis[i][k] == INF) {
                    continue; // 巨大优化!
                }
                for (int j = 0; j < 26; j++) {
                    dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]);
                }
            }
        }

        long ans = 0;
        for (int i = 0; i < source.length(); i++) {
            int d = dis[source.charAt(i) - 'a'][target.charAt(i) - 'a'];
            if (d == INF) {
                return -1;
            }
            ans += d;
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long minimumCost(string source, string target, vector<char>& original, vector<char>& changed, vector<int>& cost) {
        const int INF = 0x3f3f3f3f;
        int dis[26][26];
        memset(dis, 0x3f, sizeof(dis));
        for (int i = 0; i < 26; i++) {
            dis[i][i] = 0;
        }
        for (int i = 0; i < cost.size(); i++) {
            int x = original[i] - 'a';
            int y = changed[i] - 'a';
            dis[x][y] = min(dis[x][y], cost[i]);
        }
        for (int k = 0; k < 26; k++) {
            for (int i = 0; i < 26; i++) {
                if (dis[i][k] == INF) {
                    continue; // 巨大优化!
                }
                for (int j = 0; j < 26; j++) {
                    dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
                }
            }
        }

        long long ans = 0;
        for (int i = 0; i < source.length(); i++) {
            int d = dis[source[i] - 'a'][target[i] - 'a'];
            if (d == INF) {
                return -1;
            }
            ans += d;
        }
        return ans;
    }
};

###go

func minimumCost(source, target string, original, changed []byte, cost []int) (ans int64) {
const inf = math.MaxInt / 2
dis := [26][26]int{}
for i := range dis {
for j := range dis[i] {
if j != i {
dis[i][j] = inf
}
}
}
for i, c := range cost {
x := original[i] - 'a'
y := changed[i] - 'a'
dis[x][y] = min(dis[x][y], c)
}
for k := range dis {
for i := range dis {
if dis[i][k] == inf {
continue // 巨大优化!
}
for j := range dis {
dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j])
}
}
}

for i, b := range source {
d := dis[b-'a'][target[i]-'a']
if d == inf {
return -1
}
ans += int64(d)
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m+|\Sigma|^3)$,其中 $n$ 为 $\textit{source}$ 的长度,$m$ 为 $\textit{cost}$ 的长度,$|\Sigma|$ 为字符集合的大小,本题中字符均为小写字母,所以 $|\Sigma|=26$。
  • 空间复杂度:$\mathcal{O}(|\Sigma|^2)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Floyd 求最短路

作者 tsreaper
2023年12月24日 12:13

解法:Floyd 求最短路

本题使用到了 Floyd 求最短路算法。关于这个算法,我给力扣专栏写过一篇文章进行了详细的介绍,欢迎有兴趣的读者阅读:https://zhuanlan.zhihu.com/p/623757829

由于本题只能将单个字符改为其它字符,所以不同位置之间的修改互不影响,那么答案就是把 source[i] 改成 target[i] 的代价加起来,即 $\sum\limits_{i = 1}^n g(s_i, t_i)$,其中 $g(s_i, t_i)$ 是把字符 $s_i$ 改成 $t_i$ 的最小代价。

我们把每个字母看成一个点,如果能把字母 $x$ 改成字母 $y$,就从 $x$ 到 $y$ 连一条长度为 cost[i] 的有向边。这样对于任意两个字母 $x$ 和 $y$,把 $x$ 改成 $y$ 的最小代价就是从 $x$ 到 $y$ 的最短路。

由于我们需要知道任意两个点之间的最短路,所以可以使用 Floyd 算法。

复杂度 $\mathcal{O}(n + m + \Sigma^3)$,其中 $n$ 是 source 的长度,$m$ 是 original 的长度,$\Sigma$ 是字符集的大小,即 $26$。

参考代码(c++)

###c++

class Solution {
public:
    long long minimumCost(string source, string target, vector<char>& original, vector<char>& changed, vector<int>& cost) {
        const int INF = 1e9;
        // 建有向图
        long long g[26][26];
        for (int i = 0; i < 26; i++) for (int j = 0; j < 26; j++) g[i][j] = INF;
        for (int i = 0; i < 26; i++) g[i][i] = 0;
        for (int i = 0; i < original.size(); i++) {
            int x = original[i] - 'a', y = changed[i] - 'a';
            g[x][y] = min(g[x][y], 1LL * cost[i]);
        }

        // floyd 求最短路
        for (int k = 0; k < 26; k++) for (int i = 0; i < 26; i++) for (int j = 0; j < 26; j++)
            g[i][j] = min(g[i][j], g[i][k] + g[k][j]);

        long long ans = 0;
        // 把每个位置的修改代价加起来
        for (int i = 0; i < source.size(); i++) {
            int x = source[i] - 'a', y = target[i] - 'a';
            if (x != y) {
                // x 不能变成 y,无解
                if (g[x][y] >= INF) return -1;
                // 否则答案增加把 x 改成 y 的最小代价
                ans += g[x][y];
            }
        }
        return ans;
    }
};
昨天 — 2026年1月28日首页

实测苹果博主十件套:每月 ¥38 ,能替代剪映和 PS 吗?

作者 苏伟鸿
2026年1月28日 22:06

一个小红书博主的工作流,大抵都是如此:选好题目,写好脚本,启动拍摄,再经调色、剪辑、配乐等后期处理,设计封面后上传平台发布。

只是还没动手呢,创作工具的收费首先就是一个坎:

剪映专业版,一年 499 元;WPS 会员,一年 138 元;更别提创作者很难避开的 Adobe 家族,光 PhotoShop 和 Lightroom 两个修图的一年就要超 1000 元——还没开始赚钱,奖金两千多块就先花出去了。

同样带有明显「创意」属性的苹果,最近把自己旗下的创作工具打了包,每个月只要 38 元,相当于少喝一杯星巴克,就能用上苹果专业生产力「十件套」。

爱范儿提前上手了 Apple Creator Studio 套件,一起来看看这个「白菜价」的全家桶,都有哪些好东西。

影音图文,一套搞定

「十件套」看起来很东西很多,其实一点都不复杂, 无非就是用来处理视频、音乐、图像、文档四种类型的工具。

Final Cut Pro 在专业影视生产领域久负盛名——奥斯卡最佳影片《寄生虫》就是用它剪辑。它属于苹果创意工具中的「亲儿子」,单买一个就要 1998 元,有大半台 Mac mini 那么贵。

这个剪辑平台的特性就是「磁性时间线」,能够自动补位、对齐,加上同样是自家产的 M 芯片深度优化和后台渲染,用 FCP 剪视频可以用行云流水形容。

苹果最强的能力自然还是「生态」,Creator 全家桶之间也是互通的。用 Final Cut Pro 剪出来的视频,可以直接联动 Motion 制作特效,然后再使用 Compressor 导出,成片体积小画质也够好。

▲ Motion

从剪辑、特效到导出,Creator 提供了一条龙的工作流,在与爱范儿的专访中,苹果产品营销总监 Brent Chiu-Watson 解释了这套系统的工作逻辑:

我们相信,技术应该让创意自由流动,在你需要的时候,以最合适的形式出现。

或许对于大众来说,Logic Pro 这个名字稍显陌生。这是一个专业的音频制作工具,升级版的 Garage Band,不少知名音乐人,例如 Billie Eilish,都用它来作曲、编曲。

即使你完全不懂乐理,不会乐器,也能借助 Logic Pro AI 工具的辅助,做出想要风格的音乐。

如果你是专业的音乐人和表演者,有直播、录制现场的需求,MainStage 就是一个趁手利器,帮助用户随手调用录音棚级别的音效。

音乐和视频创作曾经被视作相对独立的两个创意领域,但在 Creator Studio 系列中,Final Cut Pro 内置了 Logic Pro 的节拍,导入配乐之后,软件会自动分析节奏,在时间线上标记每一个拍点,剪辑视频时,画面会自动吸附这些拍点,任何人都能成为「卡点狂魔」。

视频、音频工具都齐活了,还差一个能对标 PS 的图像处理工具,于是苹果在不久前收购了 Pixelmator Pro,现在也加入了 Creator 订阅中。

苹果选择 Pixelmator Pro 的原因,正是看中了这个平台将复杂技术隐藏在优雅、简洁界面之下的能力。

比如说,用户想要做海报、产品设计,可以直接使用 Pixelmator Pro 提供的大量模板,快速上手这个平台。

除了基础和专业的图片创作和编辑工具,Pixelator Pro 还有一些非常实用小功能,例如一键将不清晰的图片进行超分辨率处理,以往都需要专门的工具。

总的来说,大部分甚至部分专业的图像设计需求,Pixelmator Pro 都是能够满足的。

上面提到的 Final Cut Pro、Logic Pro、Pixelmator Pro「御三家」,单独买断要 3600 元,即使冲着它们订阅也已经值回票价。

虽然 iWork 四件套现在基本免费,也带着一些独占功能加入了 Creator 套装之中。

订阅版本的 iWork 区别不大,亮点是一系列的 AI 功能,可惜国行版 AI 还在「准备中」。

国内订阅用户能用上的独占功能,主要是一些新的模板——特别是 Keynote,不少新模板都更精美更有格调。

终于,人人都能创作

相信不少在观望的小伙伴,心中都有一个问题:

这个全新的订阅版,会影响买断版吗?

首先,这些工具的买断版本,苹果会继续在 Mac 平台上提供,功能也基本一致,只有极少数高级功能是订阅独占。

不过,iPad 的 Final Cut Pro 和 Logic Pro 此前都是订阅制,老用户可以继续保持之前的订阅方案,但不再接受新用户。

这里建议这两个平台的老 iPad 用户,转移到全新的 Creator 方案上——同样是一个月 38 元,当然是订全家桶更划算啦。

至于 iPad 的 Pixelmator 其实是另一个应用,和 Creator 的 Pixelmator Pro 不一样,也会继续提供。

所以,该不该订阅呢?

如果你是一个领域非常垂直的专业老手,此前也已经买断了需要的软件,那其实可以不用订阅。

特别是如果你已经捆绑了剪映、Adobe 生态,已经构建好了一套熟悉的工作流,其实也不一定要省这笔钱去转投苹果阵营,毕竟效率才是最重要的。

比如我上班的时候是爱范儿的编导,下班之后也是一名小博主,无论是工作还是生活,我都习惯在手机上剪片修图,这时候 Apple Creator Studio 就帮不上忙了,只能回去继续用 Adobe Lightroom。

真心希望苹果能加把劲,整几个好用的 iPhone 生产力工具出来,能加入订阅就更好了。

而对于那些正打算购买 Final Cut Pro、Logic Pro 等平台的用户,以及摩拳擦掌想要成为一名博主、设计师、音乐人的创作者新人,那我强烈建议可以订阅一个月试试看,38 块钱怎么玩都不算亏。

这两年的「国补」和教育优惠,将 Mac 的价格打到史无前例的低位,这批新用户,当然也会愿意用这个低价,尝试一下苹果这些知名度很高的专业创作工具——对于学生来说更是如此。

Apple Creator Studio 不只是一个简单的「打包」,更是一个蕴含更多可能性的「钥匙」:在这个内容生产高度个体化的时代,每个人都可以不止是单一类型的创作者。

创作者的身份发生改变,创作工具也理应跟进,苹果顺应了变革的潮流,Apple 全球产品营销副总裁 Bob Borchers 告诉爱范儿:

我们的目标是尽可能广泛地激励和加速创造力。我们希望给他们工具和能力,让他们更高效地做正在做的事,也能探索以前没想过的事。

这半个多月以来,我每天上班都在用这个十件套,最大的感触就是,个人创作的门槛,真的被拉到了很低。

哪怕在 10 年前,这些专业工具都还是影视公司、音乐工作室的入场券,买一套好电脑配几套好软件,就意味着上万元的成本,「想做」和真正「能做」之间隔着真金白银,对于个人来说更是如此。

而现在,四五千就能买到一台轻便、做工精致,同时也很强大的苹果电脑,再花 38 订阅,一个小型但足够专业的个人工作室就成型了。

人人都能成为创作者,终于不再是一个美好的期待。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


React Native新架构之iOS端初始化源码分析

2026年1月28日 21:41

React Native新架构之iOS端初始化源码分析

前言

注意,本文是基于React Native 0.83版本源码进行分析。有了前面几篇Android端分析文章打基础,再来理解iOS端就易如反掌了。总的来说,iOS端的实现要比Android端简单太多了,这是因为Android端的Java/Kotlin 都是运行在Java的虚拟机环境中,其与C++通信要经过JNI,并不如OC与C++互调那么简单直接。此外,RN基于Android的Gradle构建机制,也使问题更加复杂。

初始化流程

React Native的iOS端相比于安卓端要简单很多。现在我们基于Swift入口来分析一下初始化流程。首先找到RN源码工程中的测试工程,打开源码react-native/private/helloworld/ios/HelloWorld/AppDelegate.swift

import React                          // React Native 核心模块
import ReactAppDependencyProvider     // Codegen 生成的依赖提供者
import React_RCTAppDelegate           // AppDelegate 相关类
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  var reactNativeDelegate: ReactNativeDelegate?
  var reactNativeFactory: RCTReactNativeFactory?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    // 1: 创建 ReactNativeDelegate
    let delegate = ReactNativeDelegate()
    // 2: 创建 RCTReactNativeFactory
    let factory = RCTReactNativeFactory(delegate: delegate)
    // 3: 配置依赖提供者(Codegen 生成的模块)
    delegate.dependencyProvider = RCTAppDependencyProvider()

    // 保持强引用
    reactNativeDelegate = delegate
    reactNativeFactory = factory

    // 4: 配置开发菜单(仅 DEBUG 模式)
    #if DEBUG
    let devMenuConfiguration = RCTDevMenuConfiguration(
      devMenuEnabled: true,
      shakeGestureEnabled: true,
      keyboardShortcutsEnabled: true
    )
    reactNativeFactory?.devMenuConfiguration = devMenuConfiguration
    #endif

    // 5: 创建主窗口
    window = UIWindow(frame: UIScreen.main.bounds)

    // 6: 启动 React Native
    factory.startReactNative(
      withModuleName: "HelloWorld",
      in: window,
      launchOptions: launchOptions
    )

    return true
  }
}

class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
  // 旧架构兼容(新架构中会调用 bundleURL)
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    self.bundleURL()
  }

  // 提供 JS Bundle 的 URL
  override func bundleURL() -> URL? {
    #if DEBUG
    // 开发模式:从 Metro 开发服务器加载
    RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
    #else
    // 生产模式:从 App Bundle 加载
    Bundle.main.url(forResource: "main", withExtension: "jsbundle")
    #endif
  }
}

Swift 新架构使用 @main 注解作为应用入口,无需 main.mmain.swift 文件。ReactNativeDelegate 继承自 RCTDefaultReactNativeFactoryDelegate,负责提供 Bundle URL 和自定义配置。

这里RCTAppDependencyProvider是Objective-C++代码,这是为了方便与C++互调。源码tcode/react-native/packages/react-native/Libraries/AppDelegate/RCTReactNativeFactory.mm

@interface RCTReactNativeFactory () <
    RCTComponentViewFactoryComponentProvider, // Fabric 组件提供者
    RCTHostDelegate,                          // RCTHost 代理
    RCTJSRuntimeConfiguratorProtocol,         // JS Runtime 配置
    RCTTurboModuleManagerDelegate>            // TurboModule 管理器代理
@end

@implementation RCTReactNativeFactory

- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate
{
  return [self initWithDelegate:delegate releaseLevel:Stable];
}

- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate releaseLevel:(RCTReleaseLevel)releaseLevel
{
  if (self = [super init]) {
    self.delegate = delegate;
    [self _setUpFeatureFlags:releaseLevel];
    // 设置默认颜色空间
    [RCTColorSpaceUtils applyDefaultColorSpace:[self defaultColorSpace]];
    RCTEnableTurboModule(YES);

    // 创建 RCTRootViewFactory
    self.rootViewFactory = [self createRCTRootViewFactory];
    // 设置第三方 Fabric 组件提供者
    [RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
  }

  return self;
}

可以看到RCTReactNativeFactory的构造逻辑非常简单,但它实现了四个关键协议,这使得它可以作为多个子系统的代理。

先重点看一下createRCTRootViewFactory方法的实现,这是一个关键方法,同时设置了大量的回调 Block 来桥接代理模式:

- (RCTRootViewFactory *)createRCTRootViewFactory
{
  __weak __typeof(self) weakSelf = self;
  // 1. 创建 Bundle URL 提供者 Block
  RCTBundleURLBlock bundleUrlBlock = ^{
    auto *strongSelf = weakSelf;
    return strongSelf.bundleURL;
  };

  // 2. 创建配置对象(新架构默认全部启用)
  RCTRootViewFactoryConfiguration *configuration =
      [[RCTRootViewFactoryConfiguration alloc] initWithBundleURLBlock:bundleUrlBlock
                                                       newArchEnabled:YES
                                                   turboModuleEnabled:YES
                                                    bridgelessEnabled:YES];

  // 省略旧架构代码......

  // 3.配置根视图自定义回调
  configuration.customizeRootView = ^(UIView *_Nonnull rootView) {
    [weakSelf.delegate customizeRootView:(RCTRootView *)rootView];
  };

  // 4.配置 Bundle URL 获取回调
  configuration.sourceURLForBridge = ^NSURL *_Nullable(RCTBridge *_Nonnull bridge)
  {
    // 新架构:直接使用 bundleURL,不再依赖 bridge
    return [weakSelf.delegate bundleURL];
  };

  // 5.配置 Bundle 加载回调(支持进度)
  if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) {
    configuration.loadSourceForBridgeWithProgress =
        ^(RCTBridge *_Nonnull bridge,
          RCTSourceLoadProgressBlock _Nonnull onProgress,
          RCTSourceLoadBlock _Nonnull loadCallback) {
          // 新架构:使用 loadBundleAtURL 替代旧的 loadSourceForBridge
          [weakSelf.delegate loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:loadCallback];
        };
  }

  // 6.配置无进度的 Bundle 加载回调
  if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) {
    configuration.loadSourceForBridge = ^(RCTBridge *_Nonnull bridge, RCTSourceLoadBlock _Nonnull loadCallback) {
      // 新架构:使用 loadBundleAtURL,进度回调为空
      [weakSelf.delegate loadBundleAtURL:self.bundleURL
                              onProgress:^(RCTLoadingProgress *progressData) {
                              }
                              onComplete:loadCallback];
    };
  }

  // 7.设置 JS Runtime 代理
  configuration.jsRuntimeConfiguratorDelegate = self;
  // 8.创建并返回 RCTRootViewFactory
  return [[RCTRootViewFactory alloc] initWithTurboModuleDelegate:self hostDelegate:self configuration:configuration];
}

这里的大概流程是非常清楚的,主要就是设置了四个协议的代理。我们将流程绘制成一个关系图:

┌─────────────────────────────────────────────────────────────────────────┐
│                        RCTReactNativeFactory                             │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │ 实现协议:                                                          │  │
│  │  • RCTTurboModuleManagerDelegate    → TurboModule 类/实例查找      │  │
│  │  • RCTHostDelegate                  → Host 生命周期 + Bundle 加载  │  │
│  │  • RCTJSRuntimeConfiguratorProtocol → JS Runtime 工厂创建         │  │
│  │  • RCTComponentViewFactoryComponentProvider → Fabric 组件注册     │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                    │                                     │
│                                    ▼                                     │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │ createRCTRootViewFactory() 中设置:                                 │  │
│  │  • initWithTurboModuleDelegate: self  ← RCTTurboModuleManagerDelegate│
│  │  • hostDelegate: self                 ← RCTHostDelegate              │
│  │  • jsRuntimeConfiguratorDelegate: self ← RCTJSRuntimeConfiguratorProtocol│
│  └───────────────────────────────────────────────────────────────────┘  │
│                                    │                                     │
│                                    ▼                                     │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │ 初始化时设置:                                                       │  │
│  │  • thirdPartyFabricComponentsProvider = self                       │  │
│  │    ← RCTComponentViewFactoryComponentProvider                      │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

RCTTurboModuleManagerDelegate

主要负责 TurboModule 的类查找和实例创建,源码react-native/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.h

@class RCTBridgeProxy;
@class RCTTurboModuleManager;
@class RCTDevMenuConfigurationDecorator;

@protocol RCTTurboModuleManagerDelegate <NSObject>

/**
 * 给定模块名称,返回其实际类。如果返回 nil,则执行基本的 ObjC 类查找
 */
- (Class)getModuleClassFromName:(const char *)name;

/**
 * 给定一个模块类,为其提供一个实例。如果返回值为 nil,则使用默认初始化器
 */
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass;

@optional

/**
 * 此方法用于获取一个工厂对象,该对象可以创建 `facebook::react::TurboModule` 实例。
 * 实现 `RCTTurboModuleProvider` 接口的类必须是 Objective-C 类,以便我们可以使用代码生成工具对其进行动态初始化。
 */
- (id<RCTModuleProvider>)getModuleProvider:(const char *)name;

/**
 * 创建一个 TurboModule 实例,而无需依赖任何 ObjC++ 模块实例
 */
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:
                                                          (std::shared_ptr<facebook::react::CallInvoker>)jsInvoker;

- (void)installJSBindings:(facebook::jsi::Runtime &)runtime;

- (void)invalidate;

@end

再来看看RCTReactNativeFactory 中的实现:

#pragma mark - RCTTurboModuleManagerDelegate

- (Class)getModuleClassFromName:(const char *)name
{
#if RN_DISABLE_OSS_PLUGIN_HEADER
  return RCTTurboModulePluginClassProvider(name);
#else
  // 先尝试从 delegate 获取
  if ([_delegate respondsToSelector:@selector(getModuleClassFromName:)]) {
    Class moduleClass = [_delegate getModuleClassFromName:name];
    if (moduleClass != nil) {
      return moduleClass;
    }
  }
  return RCTCoreModulesClassProvider(name);
#endif
}

- (nullable id<RCTModuleProvider>)getModuleProvider:(const char *)name
{
  if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) {
    return [_delegate getModuleProvider:name];
  }
  return nil;
}

// 创建纯 C++ TurboModule
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
  if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) {
    return [_delegate getTurboModule:name jsInvoker:jsInvoker];
  }

  return facebook::react::DefaultTurboModules::getTurboModule(name, jsInvoker);
}

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
#if USE_OSS_CODEGEN
  if (self.delegate.dependencyProvider == nil) {
    [NSException raise:@"ReactNativeFactoryDelegate dependencyProvider is nil"
                format:@"Delegate must provide a valid dependencyProvider"];
  }
#endif
  if ([_delegate respondsToSelector:@selector(getModuleInstanceFromClass:)]) {
    id<RCTTurboModule> moduleInstance = [_delegate getModuleInstanceFromClass:moduleClass];
    if (moduleInstance != nil) {
      return moduleInstance;
    }
  }
  // 使用默认方式创建(通过 dependencyProvider)
  return RCTAppSetupDefaultModuleFromClass(moduleClass, self.delegate.dependencyProvider);
}

RCTHostDelegate

主要负责 RCTHost 的生命周期和 Bundle 加载。源码react-native/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h

NS_ASSUME_NONNULL_BEGIN

@class RCTFabricSurface;
@class RCTHost;
@class RCTModuleRegistry;
@class RCTDevMenuConfiguration;

@protocol RCTTurboModuleManagerDelegate;

typedef NSURL *_Nullable (^RCTHostBundleURLProvider)(void);

// Runtime API

@protocol RCTHostDelegate <NSObject>
// Host 启动完成通知
- (void)hostDidStart:(RCTHost *)host;

@optional
// 需要在主线程初始化的模块列表
- (NSArray<NSString *> *)unstableModulesRequiringMainQueueSetup;

// 加载 Bundle(支持进度回调)
- (void)loadBundleAtURL:(NSURL *)sourceURL
             onProgress:(RCTSourceLoadProgressBlock)onProgress
             onComplete:(RCTSourceLoadBlock)loadCallback;

@end


NS_ASSUME_NONNULL_END

查看RCTReactNativeFactory 中的实现:

#pragma mark - RCTHostDelegate

- (void)hostDidStart:(RCTHost *)host
{
  if ([_delegate respondsToSelector:@selector(hostDidStart:)]) {
    [_delegate hostDidStart:host];
  }
}

- (NSArray<NSString *> *)unstableModulesRequiringMainQueueSetup
{
#if RN_DISABLE_OSS_PLUGIN_HEADER
  return RCTTurboModulePluginUnstableModulesRequiringMainQueueSetup();
#else
  return self.delegate.dependencyProvider
      ? RCTAppSetupUnstableModulesRequiringMainQueueSetup(self.delegate.dependencyProvider)
      : @[];
#endif
}

RCTJSRuntimeConfiguratorProtocol

主要负责创建 JS Runtime 工厂,源码react-native/packages/react-native/Libraries/AppDelegate/RCTJSRuntimeConfiguratorProtocol.h

NS_ASSUME_NONNULL_BEGIN

// Forward declarations for umbrella headers.
// In implementations, import `<react/runtime/JSRuntimeFactoryCAPI.h>` to obtain the actual type.
typedef void *JSRuntimeFactoryRef;

@protocol RCTJSRuntimeConfiguratorProtocol

// 创建 JS Runtime 工厂(通常返回 Hermes)
- (JSRuntimeFactoryRef)createJSRuntimeFactory;

@end

NS_ASSUME_NONNULL_END

查看RCTReactNativeFactory 中的实现:

#pragma mark - RCTJSRuntimeConfiguratorProtocol

- (JSRuntimeFactoryRef)createJSRuntimeFactory
{
  // 委托给用户的 delegate
  return [_delegate createJSRuntimeFactory];
}

RCTComponentViewFactoryComponentProvider

主要负责提供第三方 Fabric 组件,源码react-native/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.h

/**
 * 可用于向 Fabric 提供第三方组件的协议。
 * Fabric 将检查此映射表,以确定是否存在需要注册的组件。
 */
@protocol RCTComponentViewFactoryComponentProvider <NSObject>

/**
 * 返回一个第三方组件字典,其中 `key` 是组件处理程序,`value` 是一个符合 `RCTComponentViewProtocol` 协议的类
 */
- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents;

@end

查看RCTReactNativeFactory 中的实现:

#pragma mark - RCTComponentViewFactoryComponentProvider

- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
  // 先尝试从 delegate 获取
  if ([_delegate respondsToSelector:@selector(thirdPartyFabricComponents)]) {
    return _delegate.thirdPartyFabricComponents;
  }
  // 回退到 dependencyProvider(Codegen 生成)
  return self.delegate.dependencyProvider ? self.delegate.dependencyProvider.thirdPartyFabricComponents : @{};
}

RCTDefaultReactNativeFactoryDelegate

以上协的很多方法的实现,其实也是代理给RCTReactNativeFactory 中的delegate对象。此delegate也就是我们最开始自定义的ReactNativeDelegate实例。其也是继承自RCTDefaultReactNativeFactoryDelegate,现在分析一下此类的实现,源码react-native/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm

@implementation RCTDefaultReactNativeFactoryDelegate

@synthesize dependencyProvider;
// 抽象方法:子类必须实现
- (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge
{
  [NSException raise:@"RCTBridgeDelegate::sourceURLForBridge not implemented"
              format:@"Subclasses must implement a valid sourceURLForBridge method"];
  return nil;
}

- (UIViewController *)createRootViewController
{
  return [UIViewController new];
}

- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController
{
  rootViewController.view = rootView;
}

- (JSRuntimeFactoryRef)createJSRuntimeFactory
{
#if USE_THIRD_PARTY_JSC != 1
  return jsrt_create_hermes_factory();
#endif
}

- (void)customizeRootView:(RCTRootView *)rootView
{
  // Override point for customization after application launch.
}

- (RCTColorSpace)defaultColorSpace
{
  return RCTColorSpaceSRGB;
}

- (NSURL *_Nullable)bundleURL
{
  [NSException raise:@"RCTAppDelegate::bundleURL not implemented"
              format:@"Subclasses must implement a valid getBundleURL method"];
  return nullptr;
}

- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
  return (self.dependencyProvider != nullptr) ? self.dependencyProvider.thirdPartyFabricComponents : @{};
}

- (void)hostDidStart:(RCTHost *)host
{
}

- (NSArray<NSString *> *)unstableModulesRequiringMainQueueSetup
{
  return (self.dependencyProvider != nullptr)
      ? RCTAppSetupUnstableModulesRequiringMainQueueSetup(self.dependencyProvider)
      : @[];
}

- (nullable id<RCTModuleProvider>)getModuleProvider:(const char *)name
{
  NSString *providerName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
  return (self.dependencyProvider != nullptr) ? self.dependencyProvider.moduleProviders[providerName] : nullptr;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
  return facebook::react::DefaultTurboModules::getTurboModule(name, jsInvoker);
}

#pragma mark - RCTArchConfiguratorProtocol

- (BOOL)newArchEnabled
{
  return YES;
}

- (BOOL)bridgelessEnabled
{
  return YES;
}

- (BOOL)fabricEnabled
{
  return YES;
}

- (BOOL)turboModuleEnabled
{
  return YES;
}

- (Class)getModuleClassFromName:(const char *)name
{
  return nullptr;
}

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
  return nullptr;
}

- (void)loadSourceForBridge:(RCTBridge *)bridge
                 onProgress:(RCTSourceLoadProgressBlock)onProgress
                 onComplete:(RCTSourceLoadBlock)loadCallback
{
  [RCTJavaScriptLoader loadBundleAtURL:[self sourceURLForBridge:bridge] onProgress:onProgress onComplete:loadCallback];
}

@end

React Native 启动

对于以上四个协议以及代理,我们已经有了大概认识,现在应该把视线拉回AppDelegate中的初始化流程,继续分析startReactNative方法的实现,它对应的OC方法名应该是startReactNativeWithModuleName

- (void)startReactNativeWithModuleName:(NSString *)moduleName
                              inWindow:(UIWindow *_Nullable)window
                         launchOptions:(NSDictionary *_Nullable)launchOptions
{
  [self startReactNativeWithModuleName:moduleName inWindow:window initialProperties:nil launchOptions:launchOptions];
}

- (void)startReactNativeWithModuleName:(NSString *)moduleName
                              inWindow:(UIWindow *_Nullable)window
                     initialProperties:(NSDictionary *_Nullable)initialProperties
                         launchOptions:(NSDictionary *_Nullable)launchOptions
{
  // 1. 通过 RootViewFactory 创建根视图
  UIView *rootView = [self.rootViewFactory viewWithModuleName:moduleName
                                            initialProperties:initialProperties
                                                launchOptions:launchOptions
                                         devMenuConfiguration:self.devMenuConfiguration];
  // 2. 创建 RootViewController
  UIViewController *rootViewController = [_delegate createRootViewController];
  // 3. 设置根视图
  [_delegate setRootView:rootView toRootViewController:rootViewController];
  // 4. 配置窗口并显示
  window.rootViewController = rootViewController;
  [window makeKeyAndVisible];
}

可以看到,流程十分清楚,我们继续跟踪viewWithModuleName方法实现。

RCTHost (ReactHost)初始化

查看源码react-native/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm

- (UIView *)viewWithModuleName:(NSString *)moduleName
             initialProperties:(NSDictionary *)initProps
                 launchOptions:(NSDictionary *)launchOptions
          devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration
{
  // 1. 初始化 ReactHost
  [self initializeReactHostWithLaunchOptions:launchOptions devMenuConfiguration:devMenuConfiguration];

  // 2. 创建 Fabric Surface
  RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName
                                                        initialProperties:initProps ? initProps : @{}];
   // 3. 创建 SurfaceHostingProxyRootView
  RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView =
      [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];

  surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
  if (_configuration.customizeRootView != nil) {
    // 4. 自定义根视图
    _configuration.customizeRootView(surfaceHostingProxyRootView);
  }
  return surfaceHostingProxyRootView;
}


- (void)initializeReactHostWithLaunchOptions:(NSDictionary *)launchOptions
                        devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration
{
  // Enable TurboModule interop by default in Bridgeless mode
  RCTEnableTurboModuleInterop(YES);
  RCTEnableTurboModuleInteropBridgeProxy(YES);

  [self createReactHostIfNeeded:launchOptions devMenuConfiguration:devMenuConfiguration];
  return;
}

- (void)createReactHostIfNeeded:(NSDictionary *)launchOptions
           devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration
{
  if (self.reactHost) {
    return;
  }
  self.reactHost = [self createReactHost:launchOptions devMenuConfiguration:devMenuConfiguration];
}


- (RCTHost *)createReactHost:(NSDictionary *)launchOptions
        devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration
{
  __weak __typeof(self) weakSelf = self;
  RCTHost *reactHost =
      [[RCTHost alloc] initWithBundleURLProvider:self->_configuration.bundleURLBlock
                                    hostDelegate:_hostDelegate
                      turboModuleManagerDelegate:_turboModuleManagerDelegate
                                jsEngineProvider:^std::shared_ptr<facebook::react::JSRuntimeFactory>() {
                                  return [weakSelf createJSRuntimeFactory];
                                }
                                   launchOptions:launchOptions
                            devMenuConfiguration:devMenuConfiguration];
  // 设置 Bundle URL 提供者
  [reactHost setBundleURLProvider:^NSURL *() {
    return [weakSelf bundleURL];
  }];
  // 启动 ReactHost
  [reactHost start];
  return reactHost;
}
RCTInstance 初始化

继续跟踪RCTHoststart方法。源码react-native/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm

- (void)start
{
  // 1. 设置 Bundle URL
  if (_bundleURLProvider) {
    [self _setBundleURL:_bundleURLProvider()];
  }

  // 2. 设置 Inspector Target(用于调试)
  auto &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance();
  if (inspectorFlags.getFuseboxEnabled() && !_inspectorPageId.has_value()) {
    _inspectorTarget =
        facebook::react::jsinspector_modern::HostTarget::create(*_inspectorHostDelegate, [](auto callback) {
          RCTExecuteOnMainQueue(^{
            callback();
          });
        });
    __weak RCTHost *weakSelf = self;
    _inspectorPageId = facebook::react::jsinspector_modern::getInspectorInstance().addPage(
        "React Native Bridgeless",
        /* vm */ "",
        [weakSelf](std::unique_ptr<facebook::react::jsinspector_modern::IRemoteConnection> remote)
            -> std::unique_ptr<facebook::react::jsinspector_modern::ILocalConnection> {
          RCTHost *strongSelf = weakSelf;
          if (!strongSelf) {
            // This can happen if we're about to be dealloc'd. Reject the connection.
            return nullptr;
          }
          return strongSelf->_inspectorTarget->connect(std::move(remote));
        },
        {.nativePageReloads = true, .prefersFuseboxFrontend = true});
  }
  if (_instance) {
    RCTLogWarn(
        @"RCTHost should not be creating a new instance if one already exists. This implies there is a bug with how/when this method is being called.");
    [_instance invalidate];
  }

  // 3. 创建 RCTInstance
  _instance = [[RCTInstance alloc] initWithDelegate:self
                                   jsRuntimeFactory:[self _provideJSEngine]
                                      bundleManager:_bundleManager
                         turboModuleManagerDelegate:_turboModuleManagerDelegate
                                     moduleRegistry:_moduleRegistry
                              parentInspectorTarget:_inspectorTarget.get()
                                      launchOptions:_launchOptions
                               devMenuConfiguration:_devMenuConfiguration];
  // 4. 通知代理 Host 已启动
  [_hostDelegate hostDidStart:self];
}

继续查看RCTInstance的构造实现,源码react-native/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm

- (instancetype)initWithDelegate:(id<RCTInstanceDelegate>)delegate
                jsRuntimeFactory:(std::shared_ptr<facebook::react::JSRuntimeFactory>)jsRuntimeFactory
                   bundleManager:(RCTBundleManager *)bundleManager
      turboModuleManagerDelegate:(id<RCTTurboModuleManagerDelegate>)tmmDelegate
                  moduleRegistry:(RCTModuleRegistry *)moduleRegistry
           parentInspectorTarget:(jsinspector_modern::HostTarget *)parentInspectorTarget
                   launchOptions:(nullable NSDictionary *)launchOptions
            devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration
{
  if (self = [super init]) {
    // 性能监控初始化
    _performanceLogger = [RCTPerformanceLogger new];
    registerPerformanceLoggerHooks(_performanceLogger);
    [_performanceLogger markStartForTag:RCTPLReactInstanceInit];

    // 保存外部传入的关键依赖
    _delegate = delegate;
    _jsRuntimeFactory = jsRuntimeFactory;
    _appTMMDelegate = tmmDelegate;
    _jsThreadManager = [RCTJSThreadManager new];

    // 开发菜单配置
    _devMenuConfigurationDecorator =
#if RCT_DEV_MENU
        [[RCTDevMenuConfigurationDecorator alloc] initWithDevMenuConfiguration:devMenuConfiguration];
#else
        nil;
#endif

    _parentInspectorTarget = parentInspectorTarget;

    // JS 模块调用器配置(设置 Bridgeless 模式下的 JS 模块方法调用器)
    {
      __weak __typeof(self) weakSelf = self;
      [_bridgeModuleDecorator.callableJSModules
          setBridgelessJSModuleMethodInvoker:^(
              NSString *moduleName, NSString *methodName, NSArray *args, dispatch_block_t onComplete) {
            [weakSelf callFunctionOnJSModule:moduleName method:methodName args:args];
            if (onComplete) {
              [weakSelf
                  callFunctionOnBufferedRuntimeExecutor:[onComplete](facebook::jsi::Runtime &_) { onComplete(); }];
            }
          }];
    }
    _launchOptions = launchOptions;

    // 内存警告监听
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_handleMemoryWarning)
                                                 name:UIApplicationDidReceiveMemoryWarningNotification
                                               object:nil];
    // 启动核心初始化
    [self _start];
  }
  return self;
}

接下来,我们来看整个初始化过程的核心方法_start实现:

- (void)_start
{
  // 1.设置定时器系统
  auto objCTimerRegistry = std::make_unique<ObjCTimerRegistry>();
  auto timing = objCTimerRegistry->timing;
  auto *objCTimerRegistryRawPtr = objCTimerRegistry.get();

  auto timerManager = std::make_shared<TimerManager>(std::move(objCTimerRegistry));
  objCTimerRegistryRawPtr->setTimerManager(timerManager);

  __weak __typeof(self) weakSelf = self;
  auto onJsError = [=](jsi::Runtime &runtime, const JsErrorHandler::ProcessedError &error) {
    [weakSelf _handleJSError:error withRuntime:runtime];
  };

  // 2.创建 ReactInstance (C++ 层)
  _reactInstance = std::make_unique<ReactInstance>(
      _jsRuntimeFactory->createJSRuntime(_jsThreadManager.jsMessageThread),
      _jsThreadManager.jsMessageThread,
      timerManager,
      onJsError,
      _parentInspectorTarget);
  _valid = true;

  // 3.设置 RuntimeExecutor
  RuntimeExecutor bufferedRuntimeExecutor = _reactInstance->getBufferedRuntimeExecutor();
  timerManager->setRuntimeExecutor(bufferedRuntimeExecutor);

  auto jsCallInvoker = make_shared<RuntimeSchedulerCallInvoker>(_reactInstance->getRuntimeScheduler());
  // 省略旧架构......

  // 4.创建 TurboModuleManager
  _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridgeProxy:bridgeProxy
                                                     bridgeModuleDecorator:_bridgeModuleDecorator
                                                                  delegate:self
                                                                 jsInvoker:jsCallInvoker
                                             devMenuConfigurationDecorator:_devMenuConfigurationDecorator];

#if RCT_DEV
  /**
    * 实例化 DevMenu 会产生副作用,即通过 RCTDevMenu 注册
    * CMD + d、CMD + i 和 CMD + n 的快捷键。 
    * 因此,启用 TurboModules 时,我们必须手动创建此NativeModule。
   */
  [_turboModuleManager moduleForName:"RCTDevMenu"];
#endif // end RCT_DEV

  // 初始化 RCTModuleRegistry,以便 TurboModules 可以引用其他 TurboModules
  [_bridgeModuleDecorator.moduleRegistry setTurboModuleRegistry:_turboModuleManager];

  if (ReactNativeFeatureFlags::enableEagerMainQueueModulesOnIOS()) {
    /**
     * 某些原生模块需要在主线程上捕获 UIKit 对象。
     * 因此,请在此处的主队列上开始初始化这些模块。JavaScript 线程将等待这些模块初始化完成后,才会执行 JavaScript 代码包。
     */
    NSArray<NSString *> *modulesRequiringMainQueueSetup = [_delegate unstableModulesRequiringMainQueueSetup];

    std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
    std::shared_ptr<std::condition_variable> cv = std::make_shared<std::condition_variable>();
    std::shared_ptr<bool> isReady = std::make_shared<bool>(false);

    _waitUntilModuleSetupComplete = ^{
      std::unique_lock<std::mutex> lock(*mutex);
      cv->wait(lock, [isReady] { return *isReady; });
    };

    // TODO(T218039767): 将性能日志记录功能集成到主队列模块初始化过程中
    RCTExecuteOnMainQueue(^{
      for (NSString *moduleName in modulesRequiringMainQueueSetup) {
        [self->_bridgeModuleDecorator.moduleRegistry moduleForName:[moduleName UTF8String]];
      }

      RCTScreenSize();
      RCTScreenScale();
      RCTSwitchSize();

      std::lock_guard<std::mutex> lock(*mutex);
      *isReady = true;
      cv->notify_all();
    });
  }

  RCTLogSetBridgelessModuleRegistry(_bridgeModuleDecorator.moduleRegistry);
  RCTLogSetBridgelessCallableJSModules(_bridgeModuleDecorator.callableJSModules);

  // 5.创建 ContextContainer
  auto contextContainer = std::make_shared<ContextContainer>();
  [_delegate didCreateContextContainer:contextContainer];
  // 插入核心模块
  contextContainer->insert(
      "RCTImageLoader", facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTImageLoader"]));
  contextContainer->insert(
      "RCTEventDispatcher",
      facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTEventDispatcher"]));
  contextContainer->insert("RCTBridgeModuleDecorator", facebook::react::wrapManagedObject(_bridgeModuleDecorator));
  contextContainer->insert(RuntimeSchedulerKey, std::weak_ptr<RuntimeScheduler>(_reactInstance->getRuntimeScheduler()));
  contextContainer->insert("RCTBridgeProxy", facebook::react::wrapManagedObject(bridgeProxy));

  // 6.创建 SurfacePresenter
  _surfacePresenter = [[RCTSurfacePresenter alloc]
        initWithContextContainer:contextContainer
                 runtimeExecutor:bufferedRuntimeExecutor
      bridgelessBindingsExecutor:std::optional(_reactInstance->getUnbufferedRuntimeExecutor())];

  // 这使得模块中的 RCTViewRegistry 能够通过其 viewForReactTag 方法返回 UIView 对象
  __weak RCTSurfacePresenter *weakSurfacePresenter = _surfacePresenter;
  [_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^UIView *(NSNumber *reactTag) {
    RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter;
    if (strongSurfacePresenter == nil) {
      return nil;
    }

    return [strongSurfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue];
  }];

  // 7.创建DisplayLink (用于调用计时器回调函数)
  _displayLink = [RCTDisplayLink new];

  auto &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance();

  ReactInstance::JSRuntimeFlags options = {
      .isProfiling = inspectorFlags.getIsProfilingBuild(),
      .runtimeDiagnosticFlags = [RCTInstanceRuntimeDiagnosticFlags() UTF8String]};

  // 8.初始化 JS Runtime 并加载 Bundle
  _reactInstance->initializeRuntime(options, [=](jsi::Runtime &runtime) {
    __strong __typeof(self) strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }

    // 安装 TurboModule JS 绑定
    [strongSelf->_turboModuleManager installJSBindings:runtime];
    // 绑定 Native Logger
    facebook::react::bindNativeLogger(runtime, [](const std::string &message, unsigned int logLevel) {
      _RCTLogJavaScriptInternal(static_cast<RCTLogLevel>(logLevel), [NSString stringWithUTF8String:message.c_str()]);
    });

    // 安装 Native Component Registry 绑定
    RCTInstallNativeComponentRegistryBinding(runtime);

    // 通知代理 Runtime 已初始化
    [strongSelf->_delegate instance:strongSelf didInitializeRuntime:runtime];

    // 设置 DisplayLink
    id<RCTDisplayLinkModuleHolder> moduleHolder = [[RCTBridgelessDisplayLinkModuleHolder alloc] initWithModule:timing];
    [strongSelf->_displayLink registerModuleForFrameUpdates:timing withModuleHolder:moduleHolder];
    [strongSelf->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];

    // 尝试同步加载bundle包,如果失败则回退到异步加载。
    [strongSelf->_performanceLogger markStartForTag:RCTPLScriptDownload];
    [strongSelf _loadJSBundle:[strongSelf->_bridgeModuleDecorator.bundleManager bundleURL]];
  });

  [_performanceLogger markStopForTag:RCTPLReactInstanceInit];
}
JS Bundle 加载

继续查看_loadJSBundle方法的实现:

- (void)_loadJSBundle:(NSURL *)sourceURL
{
#if RCT_DEV_MENU && __has_include(<React/RCTDevLoadingViewProtocol.h>)
  {
    // 显示加载视图
    id<RCTDevLoadingViewProtocol> loadingView =
        (id<RCTDevLoadingViewProtocol>)[_turboModuleManager moduleForName:"DevLoadingView"];
    [loadingView showWithURL:sourceURL];
  }
#endif

  __weak __typeof(self) weakSelf = self;
  // 通过代理加载 Bundle
  [_delegate loadBundleAtURL:sourceURL
      onProgress:^(RCTLoadingProgress *progressData) {
        __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }

#if RCT_DEV_MENU && __has_include(<React/RCTDevLoadingViewProtocol.h>)
        id<RCTDevLoadingViewProtocol> loadingView =
            (id<RCTDevLoadingViewProtocol>)[strongSelf->_turboModuleManager moduleForName:"DevLoadingView"];
        [loadingView updateProgress:progressData];
#endif
      }
      onComplete:^(NSError *error, RCTSource *source) {
        __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }

        if (error) {
          [strongSelf handleBundleLoadingError:error];
          return;
        }
        // _loadScriptFromSource 函数的回调函数需要 DevSettings 模块,因此需要提前进行初始化。
        RCTDevSettings *const devSettings =
            (RCTDevSettings *)[strongSelf->_turboModuleManager moduleForName:"DevSettings"];

        // 加载脚本
        [strongSelf _loadScriptFromSource:source];
        // 仅在开发环境中启用热模块重载功能。
        [strongSelf->_performanceLogger markStopForTag:RCTPLScriptDownload];
        [devSettings setupHMRClientWithBundleURL:sourceURL];
#if RCT_DEV
        [strongSelf _logOldArchitectureWarnings];
#endif
      }];
}


- (void)_loadScriptFromSource:(RCTSource *)source
{
  std::lock_guard<std::mutex> lock(_invalidationMutex);
  if (!_valid) {
    return;
  }

  auto script = std::make_unique<NSDataBigString>(source.data);
  const auto *url = deriveSourceURL(source.url).UTF8String;

  auto beforeLoad = [waitUntilModuleSetupComplete = self->_waitUntilModuleSetupComplete](jsi::Runtime &_) {
    if (waitUntilModuleSetupComplete) {
      waitUntilModuleSetupComplete();
    }
  };
  auto afterLoad = [](jsi::Runtime &_) {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTInstanceDidLoadBundle" object:nil];
  };
  // 在 ReactInstance 中执行脚本
  _reactInstance->loadScript(std::move(script), url, beforeLoad, afterLoad);
}
Surface 创建与启动

现在,我们把视线拉回viewWithModuleName方法中,继续分析后面的createSurfaceWithModuleName方法的实现。源码RCTHost.mm

- (RCTFabricSurface *)createSurfaceWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary *)properties
{
  return [self createSurfaceWithModuleName:moduleName mode:DisplayMode::Visible initialProperties:properties];
}

- (RCTFabricSurface *)createSurfaceWithModuleName:(NSString *)moduleName
                                             mode:(DisplayMode)displayMode
                                initialProperties:(NSDictionary *)properties
{
  // 1. 创建 FabricSurface
  RCTFabricSurface *surface = [[RCTFabricSurface alloc] initWithSurfacePresenter:self.surfacePresenter
                                                                      moduleName:moduleName
                                                               initialProperties:properties];
  // 2. 设置显示模式
  surface.surfaceHandler.setDisplayMode(displayMode);

  // 3. 附加 Surface
  [self _attachSurface:surface];

  __weak RCTFabricSurface *weakSurface = surface;
  // 在 JS Bundle 执行完成后,使用BufferedRuntimeExecutor启动Surface
  [_instance callFunctionOnBufferedRuntimeExecutor:[weakSurface](facebook::jsi::Runtime &_) { [weakSurface start]; }];
  return surface;
}

总结

初始化的大概流程:

┌─────────────────────────────────────────────────────────────────┐
                        Swift 应用层                              
  ┌─────────────────────────────────────────────────────────────┐│
    AppDelegate                                                 ││
      ├── reactNativeDelegate: ReactNativeDelegate             ││
      └── reactNativeFactory: RCTReactNativeFactory             ││
  └─────────────────────────────────────────────────────────────┘│
  ┌─────────────────────────────────────────────────────────────┐│
    ReactNativeDelegate : RCTDefaultReactNativeFactoryDelegate  ││
      ├── bundleURL()  URL?                                    ││
      └── dependencyProvider: RCTAppDependencyProvider          ││
  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                               
                               
┌─────────────────────────────────────────────────────────────────┐
                        Factory                                 
  ┌─────────────────────────────────────────────────────────────┐│
    RCTReactNativeFactory                                       ││
      ├── delegate: RCTReactNativeFactoryDelegate               ││
      ├── rootViewFactory: RCTRootViewFactory                   ││
      └── startReactNative(withModuleName:in:)                  ││
  └─────────────────────────────────────────────────────────────┘│
  ┌─────────────────────────────────────────────────────────────┐│
    RCTRootViewFactory                                          ││
      ├── reactHost: RCTHost                                    ││
      ├── view(withModuleName:)  UIView                        ││
      └── createReactHost()  RCTHost                           ││
  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                               
                               
┌─────────────────────────────────────────────────────────────────┐
                         Host                                   
  ┌─────────────────────────────────────────────────────────────┐│
    RCTHost                                                     ││
      ├── instance: RCTInstance                                 ││
      ├── bundleManager: RCTBundleManager                       ││
      ├── start()                                               ││
      └── createSurface(withModuleName:)  RCTFabricSurface     ││
  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                               
                               
┌─────────────────────────────────────────────────────────────────┐
                       Instance                                 
  ┌─────────────────────────────────────────────────────────────┐│
    RCTInstance                                                 ││
      ├── reactInstance: ReactInstance (C++)                    ││
      ├── turboModuleManager: RCTTurboModuleManager             ││
      ├── surfacePresenter: RCTSurfacePresenter                 ││
      ├── displayLink: RCTDisplayLink                           ││
      └── _start()                                              ││
  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                               
                               
┌─────────────────────────────────────────────────────────────────┐
                        C++ Runtime                             
  ┌─────────────────────────────────────────────────────────────┐│
    ReactInstance (C++)                                         ││
      ├── jsRuntime: jsi::Runtime (Hermes)                      ││
      ├── runtimeScheduler: RuntimeScheduler                    ││
      ├── timerManager: TimerManager                            ││
      ├── initializeRuntime()                                   ││
      └── loadScript()                                          ││
  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

*ST铖昌:预计2025年净利润9500万元-1.24亿元,公司股票存在可能被终止上市的风险

2026年1月28日 20:57
36氪获悉,*ST铖昌公告,*ST铖昌预计2025年归属于上市公司股东的净利润为9500万元–12400万元,上年同期为亏损3111.79万元,同比扭亏为盈。公司股票因2024年度经审计净利润为负值且扣除后营业收入低于3亿元,自2025年4月24日起被实施退市风险警示。若公司2025年度出现《深圳证券交易所股票上市规则》第9.3.12条规定的相关情形,公司股票将面临被终止上市的风险。公司已披露股票可能被终止上市的风险提示公告,并计划更换会计师事务所以推进2025年度审计工作。

苹果据悉今年9月只上iPhone18 Pro系列和首款阔折叠Fold

2026年1月28日 20:42
市场消息称,苹果iPhone 18预计明年Q1推出,9月只上iPhone18 Pro系列和首款阔折叠iPhone Fold,基本都是万元机档位。苹果今年秋季发布的所有新款iPhone都配备12GB内存,涵盖iPhone 18 Pro、iPhone 18 Pro Max以及iPhone Fold(折叠屏 iPhone),与 iPhone 17 Pro系列所采用的12GB内存保持一致。(财联社)

银锡铜价格大涨,电子元器件掀涨价潮

2026年1月28日 20:37
近段时间,电子元器件行业出现普遍性价格上涨。与以往主要由短期供需变化引发的波动不同,本轮自2025年底开始、在2026年初全面启动的涨价潮,其广度与深度均属罕见。国内外主要厂商均已陆续发布涨价通知,涨幅从5%到30%不等。业内人士表示,与以往消费电子需求主导的行业周期不同,此轮电子元器件价格上涨,需求的核心拉动力来自 AI服务器、新能源汽车和高端工业应用三大领域,高端应用需求的强劲支撑,让涨价从“可选项”变为“必选项”。(央视财经)

mri@1.2.0源码阅读

作者 米丘
2026年1月28日 18:23

mri(全称 Minimalist CLI Argument Parser)的核心作用是解析命令行传入的参数,把用户在终端输入的命令行参数(比如 --port 3000-v)转换成易于在代码中使用的 JavaScript 对象。

有什么特点?

  1. 超轻量:体积只有几百 KB,无依赖,适合对包体积敏感的 Node.js/ 前端工具开发;
  2. 易用性:API 极简,几行代码就能完成参数解析;
  3. 常用场景:前端工程化工具(如自定义构建脚本、本地开发服务器、CLI 工具)中解析命令行参数。

index文件

mri-1.2.0/src/index.js

export default function (args, opts) {
  args = args || []; // 命令行参数数组(比如 process.argv.slice(2))
  opts = opts || {}; // 解析配置(别名、默认值、类型等)

  var k, arr, arg, name, val, out={ _:[] }; // out 解析结果对象
  var i=0, j=0, idx=0, len=args.length;

  // 标记是否配置了别名、严格模式、默认值
  const alibi = opts.alias !== void 0;
  const strict = opts.unknown !== void 0;
  const defaults = opts.default !== void 0;

  opts.alias = opts.alias || {};
  opts.string = toArr(opts.string); // 字符串类型的参数名数组
  opts.boolean = toArr(opts.boolean); // 布尔类型的参数名数组

  // 别名双向绑定
  if (alibi) {
    for (k in opts.alias) {
      arr = opts.alias[k] = toArr(opts.alias[k]);
      for (i=0; i < arr.length; i++) {
        (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
      }
    }
  }
  // 布尔 / 字符串类型补全
  // 布尔类型:把别名也加入布尔列表
  for (i=opts.boolean.length; i-- > 0;) {
    arr = opts.alias[opts.boolean[i]] || [];
    for (j=arr.length; j-- > 0;) opts.boolean.push(arr[j]);
  }

  // 字符串类型:同理,把别名也加入字符串列表
  for (i=opts.string.length; i-- > 0;) {
    arr = opts.alias[opts.string[i]] || [];
    for (j=arr.length; j-- > 0;) opts.string.push(arr[j]);
  }

  // 默认值关联类型
  if (defaults) {
    for (k in opts.default) {
      name = typeof opts.default[k];
      arr = opts.alias[k] = opts.alias[k] || [];
      if (opts[name] !== void 0) {
        opts[name].push(k);
        for (i=0; i < arr.length; i++) {
          opts[name].push(arr[i]);
        }
      }
    }
  }

  // 遍历解析命令行参数
  const keys = strict ? Object.keys(opts.alias) : [];

  for (i=0; i < len; i++) {
    arg = args[i]; // 当前处理的参数(比如 '--port'、'-p=3000')

    // 1. 遇到 "--" 终止解析,后续参数全部放入 out._
    if (arg === '--') {
      out._ = out._.concat(args.slice(++i));
      break;
    }

    // 2. 计算参数前的 "-" 数量(比如 --port 是 2 个,-p 是 1 个)
    for (j=0; j < arg.length; j++) {
      if (arg.charCodeAt(j) !== 45) break; // "-"
    }

    // 3. 无 "-":非选项参数,放入 out._
    if (j === 0) {
      out._.push(arg);

      // 4. 以 "no-" 开头(比如 --no-open):布尔值取反
    } else if (arg.substring(j, j + 3) === 'no-') {
      name = arg.substring(j + 3);
      if (strict && !~keys.indexOf(name)) {
        return opts.unknown(arg);
      }
      out[name] = false;

      // 5. 正常选项参数(比如 --port=3000、-p3000、-p 3000)
    } else {
      // 找到 "=" 的位置(分割参数名和值)
      for (idx=j+1; idx < arg.length; idx++) {
        if (arg.charCodeAt(idx) === 61) break; // "="
      }

      // 取值:有=则取=后的值;无=则取下一个参数(如果下一个不是-开头)
      name = arg.substring(j, idx);
      val = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);
      arr = (j === 2 ? [name] : name);

      // 遍历解析(比如 -pv 会拆成 p 和 v 分别处理)
      for (idx=0; idx < arr.length; idx++) {
        name = arr[idx];
        if (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);
        // 把解析后的值存入 out(toVal 是核心工具函数)
        toVal(out, name, (idx + 1 < arr.length) || val, opts);
      }
    }
  }

  // 1. 补全默认值:如果参数未解析到,用默认值填充
  if (defaults) {
    for (k in opts.default) {
      if (out[k] === void 0) {
        out[k] = opts.default[k];
      }
    }
  }

  // 2. 同步别名:比如解析到 -p=3000,自动给 out.port 也赋值 3000
  if (alibi) {
    for (k in out) {
      arr = opts.alias[k] || [];
      while (arr.length > 0) {
        out[arr.shift()] = out[k];
      }
    }
  }

  return out;
}

toArr

function toArr(any) {
return any == null ? [] : Array.isArray(any) ? any : [any];
}

toVal

function toVal(out, key, val, opts) {
  var x,
    old = out[key],
    nxt = !!~opts.string.indexOf(key)
      ? val == null || val === true
        ? ''
        : String(val)
      : typeof val === 'boolean'
        ? val
        : !!~opts.boolean.indexOf(key)
          ? val === 'false'
            ? false
            : val === 'true' ||
              (out._.push(((x = +val), x * 0 === 0) ? x : val), !!val)
          : ((x = +val), x * 0 === 0)
            ? x
            : val;
  out[key] =
    old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
}

示例


const mri = require('mri');
const args = mri(process.argv.slice(2));
console.log(args);

image.png

vue3自定义指令合集-单例v-tooltip

作者 fubobo
2026年1月28日 18:17

背景

在实际项目中,Tooltip 往往会遇到这些问题:

  • 大多数使用组件形式实现(有时候项目中对于tooltip没有过多要求,使用UI组件的话,需要包一层组件,有点臃肿)
  • 每个元素一个 Tooltip,实例过多,性能差
  • 页面滚动 / 容器滚动后,Tooltip 位置不更新
  • Tooltip 抖动、闪烁、频繁销毁重建
  • 指令解绑不彻底,事件和监听器泄漏

目标

  • 全局只创建 一个 Tooltip 实例
  • 使用指令 v-tooltip 即插即用
  • 支持 top / right / bottom / left
  • 自动跟随目标元素位置变化
  • 无闪烁、无内存泄漏
  • TypeScript 类型安全

造个轮子,代码放在仓库了,各位大佬自取:gitee.com/Gitfubobo/v…


使用方式

<button v-tooltip="'删除后无法恢复'">删除</button>
<button v-tooltip.right="'更多操作'">操作</button>

image.png


整体设计思路

Tooltip 用单例统一管理,指令只负责绑定 DOM,这里的单例是指DOM单例,页面上只存在一个DOM

核心拆分

  • TooltipSingleton

    • 负责 Tooltip 的创建、更新、销毁
  import tooltipV from './tooltip.vue';
  
  
  /**
     * @Author: fubobo
     * @Description: 单例
     * @return {TooltipSingleton}
     */
    public static getInstance(): TooltipSingleton {
        if (!TooltipSingleton.instance) {
            TooltipSingleton.instance = new TooltipSingleton();
        }
        return TooltipSingleton.instance;
    }

    // 创建dom挂载tooltip组件
    private createInstance() {
        if (!this.tooltipInstance) {
            this.rootEl = document.createElement('div');
            document.body.appendChild(this.rootEl);
            this.app = createApp(tooltipV);
            this.tooltipInstance = this.app.mount(this.rootEl);
        }
    }
  • v-tooltip 指令

    • 负责 DOM 事件绑定与生命周期
 // 指令对象
 const tooltipDirective: Directive<HTMLElement, string> = {
    mounted(el, binding) {
        TooltipSingleton.getInstance().mount(el, binding);
    },
    updated(el, binding) {
        TooltipSingleton.getInstance().updated(el, binding);
    },
    unmounted(el) {
        TooltipSingleton.getInstance().unmount(el);
    },
};
  • 位置监听工具

    • 负责监听滚动 / resize / DOM 变化
  /**
     * @Author: fubobo
     * @Description: 根据目标元素和tooltip进行位置计算
     * @return 坐标
     * @param {HTMLElement} targetEl
     * @param {Placement} placement
     */
    private calculatePosition(targetEl: HTMLElement, placement: Placement) {
        ....
        return { x, y };
    }

/**
 * 监听元素距离窗口顶部的距离变化
 * @param element 目标元素
 * @param callback 距离变化回调
 * @param options 配置项
 * @returns 停止监听的函数
 */
const observeElementViewportTop = (
    element: HTMLElement,
    callback: PositionCallback,
    options?: {
        throttle?: number; // 节流时间(默认 100ms)
        observeAncestors?: boolean; // 是否监听祖先元素尺寸变化
    }
) => {
    ....
    return () => {
        window.removeEventListener('scroll', handleWindowEvent);
        window.removeEventListener('resize', handleWindowEvent);
        mutationObserver.disconnect();
        resizeObserver.disconnect();
    };
};

为什么一定要用「单例 Tooltip」

如果每个 v-tooltip 都创建一个组件:

  • DOM 数量指数级增长
  • 每个 Tooltip 都监听 scroll / resize
  • 事件难以统一管理

带来的好处

  • 整个应用 只存在一个 Tooltip
  • hover 不同元素 → 只是更新内容和位置
  • 性能稳定、结构清晰

Tooltip 实例的创建与销毁

创建(只会执行一次)

private createInstance() {
  this.rootEl = document.createElement('div');
  document.body.appendChild(this.rootEl);
  this.app = createApp(tooltipV);
  this.tooltipInstance = this.app.mount(this.rootEl);
}

销毁(最后一个指令卸载时)

private destroyInstance() {
  this.app.unmount();
  document.body.removeChild(this.rootEl);
}

Tooltip 定位计算逻辑

核心思路:

使用目标元素位置 + Tooltip 自身尺寸计算最终坐标

const tooltipRect = tooltip.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
switch (placement) {
  case 'top':
    x = targetRect.x + targetRect.width / 2 - tooltipRect.width / 2;
    y = targetRect.y - tooltipRect.height - 10;
}

监听「元素位置变化」

仅靠 mouseenter 是完全不够的:

  • 页面滚动
  • 容器滚动
  • DOM transform
  • 父级尺寸变化

解决方案:多维度监听

observeElementViewportTop(el, callback, {
  observeAncestors: true,
});

监听来源包括:

监听方式 解决问题
scroll / resize 页面和窗口变化
MutationObserver class / style 改变
ResizeObserver 元素或父级尺寸变化

📌 Tooltip 始终贴着目标元素,不会错位

关键点

  • Tooltip 自身维护 hover 状态
  • 延迟隐藏(100ms)
  • 体验更自然

使用 WeakMap 优化性能

  • 不阻止 GC
  • DOM 销毁后自动释放
  • 从根源避免内存泄漏
class TooltipSingleton {
    ...
    private eventHandlers = new WeakMap<
        HTMLElement,
        {
            enter: EventListener;
            leave: EventListener;
        }
    >(); // 存储元素的事件处理器(弱引用防止内存泄漏)
    private directiveInfo = new WeakMap<
        HTMLElement,
        {
            text: string;
            placement: Placement;
        }
    >(); // 存储元素关联的指令信息(弱引用)
}

解决了哪些问题

问题 是否解决
Tooltip 重复创建
滚动后位置错乱
hover 闪烁
内存泄漏
TypeScript 类型不安全
指令更新无效
支持DOM传值

代码仓库

框框造个轮子,代码放在仓库了,各位大佬自取:gitee.com/Gitfubobo/v…

JS-面试必考:手动实现一个标准的 Ajax 请求(XMLHttpRequest)

2026年1月28日 18:15

前言

虽然 fetch API 在现代开发中非常流行,但 XMLHttpRequest (XHR) 依然是理解 Web 网络编程的基石。无论是底层的 Ajax 封装,还是需要细粒度监控上传/下载进度的场景,XHR 依然不可替代。

一、 什么是 Ajax?

Ajax(Asynchronous JavaScript And XML) 并不是一种单一的编程语言,而是一套技术的组合。其核心是在不重新加载整个网页的情况下,通过与服务器进行异步数据交换,实现页面的局部刷新

  • 核心优势:提升用户体验,减少网络带宽占用。
  • 实现基础:JavaScript 和浏览器原生的 XMLHttpRequest 对象。

二、 XHR 操作的“五步法”逻辑

1. 创建对象

let xhr = new XMLHttpRequest();

2. 注册回调函数 (事件驱动)

XHR 是基于事件驱动的,我们需要在请求发送前定义好如何处理各种状态。

  • onreadystatechange:监控请求的生命周期(状态 0-4)。
  • onerror:处理网络层面的错误(如断网或 DNS 解析失败)。
  • ontimeout:处理响应超时,需配合 xhr.timeout 使用。
  • onprogress:数据传输进度监控。

3. 配置请求参数

通过 open() 方法初始化请求:

  • method:请求方法(GET, POST, PUT 等)。
  • url:请求地址。
  • async:是否异步(默认 true)。注意: 设为 false 会阻塞浏览器主线程,导致页面假死。

4. 设置属性与请求头

  • responseType:设置期望的返回格式(如 json, blob)。
  • setRequestHeader():手动添加自定义请求头(必须在 open 之后,send 之前调用)。

5. 发送请求

  • send(data):正式发起请求。如果是 GET 请求,传 null;如果是 POST,传具体数据。

三、 详解 readyState:五种状态的演变

readyState 是排查 Ajax 问题的关键,它记录了请求的每一步进展:

取值 状态名称 描述
0 UNSENT 初始化状态,对象已创建,尚未调用 open()
1 OPENED 已调用 open(),尚未调用 send(),可设置 Header
2 HEADERS_RECEIVED 已调用 send(),接收到了响应头和状态码
3 LOADING 正在下载响应体,responseText 已包含部分数据
4 DONE 数据接收完成,请求生命周期结束

四、 进度监控:如何实现进度条?

XHR 相比 fetch 的一大优势就是对进度的原生支持。通过 onprogress 事件,我们可以计算出下载百分比:

  • lengthComputable:布尔值,表示服务器是否返回了 Content-Length
  • loaded:已接收的字节数。
  • total:总字节数。

五、 代码实现:封装一个标准的 Ajax 函数

/**
 * 基于 XHR 封装的数据获取函数
 * @param {string} URL 请求地址
 */
function GetWebData(URL) {
  const xhr = new XMLHttpRequest();

  // 1. 状态监控:只在 DONE 阶段处理逻辑
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) { 
      // 兼容性判断:2xx 范围和 304 缓存均认为成功
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        console.log("请求成功:", xhr.response);
      } else {
        console.error("请求失败, 状态码:", xhr.status);
      }
    }
  };

  // 2. 进度监控
  xhr.onprogress = function (event) {
    if (event.lengthComputable) {
      let percent = (event.loaded / event.total) * 100;
      console.log(`下载进度: ${percent.toFixed(2)}%`);
    }
  };

  // 3. 异常与超时处理
  xhr.timeout = 5000; 
  xhr.ontimeout = () => console.error("请求超时!");
  xhr.onerror = () => console.error("网络异常或跨域错误");

  // 4. 配置与发送
  xhr.open("GET", URL, true);
  xhr.responseType = "json"; // 自动解析 JSON
  xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

  xhr.send(null);
}
❌
❌