普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月24日首页

项目实践3:一个冲突引起的灾难

作者 巧_
2025年10月24日 11:06

几天没写代码了,把项目主分支拉一下,发现一对冲突,想起来之前有人和我说过如果冲突不解决就会直接将本地的覆盖远程的,想着也没有写什么,就直接覆盖呗,忘记之前才格式化完prettier已经提交了,就点了一下右边可视化界面的按钮,把冲突提交直接放到了咱暂存提交上去了,导致冲突没解决,全是报错,还手欠的补了一刀,推到远程了,然后好死不死,有个伙伴还在我刚推完的基础上拉了一下,交了一个新的提交上去,天呐,好不容易直接完main,发现系统运行不了了呵呵,吓得我赶紧报备:

image.png

本来我是打算悄悄解决的,一顿慌乱操作下,没屁用,我那点知识在紧急情况下根本不值得一提: 首先,把reset和revert搞混了, 其次,命令一直记错,还有就是处理方式不对;

image.png

现在来统一科普一下:有现成的我就直接引用了,谢谢作者:blog.csdn.net/qq_41914181…

在我好不容易搞好之后,鸡哥和我说: image.png

image.png

下面是我常用的git记录,还有一些后续加上:

 git reset HEAD这通常用于撤销暂存区的修改(默认使用 --mixed 模式),即把已经 git add 到暂存区的内容退回到工作区,而工作区的文件内容不会被修改。但是如果是--hard模式,那就是工作区的也被撤销了

示例:如果你不小心 git add 了某个文件,想取消暂存,就可以用:

git reset HEAD <文件名>  # 撤销单个文件的暂存
git reset HEAD          # 撤销所有文件的暂存

ls -al 用长格式列出当前文件夹下的所有文件包括隐藏文件

image.png

pwd 就会输出当前目录的绝对路径

image.png

git commit -m "消息" 提交暂存区内容并设置消息

cp -r 循环复制目录及以下内容

vi  文件名 image.png

image.png

点击i进入插入模式,可以编辑

image.png

esc退出编辑模式进入命令模式,:wq保存并退出

 git add 后列出所有文件名(空格分隔):

git add file1.txt file2.js dir/file3.py  # 添加 file1.txt、file2.js、dir 目录下的 file3.py

git add . 会递归添加当前目录下所有修改过的文件、新文件(但会自动忽略 .gitignore 中指定的文件):

git add -u # 仅更新已跟踪文件的修改到暂存区

git mv <旧文件名> <新文件名>可一步完成重命名操作

解释一下,这个git log默认是本地仓库的当前分支的日志,如果git log --all就是当前本地分支和远程所有分支

git log --oneline简短描述提交的commit日志

image.png

git log -n4最近四个提交的commit日志

image.png

git branch -v本地有多少分支

git checkout -b teamp abcd 是 Git 中一个创建并切换新分支的命令,作用是 基于 abcd 这个 commit(或其他引用)创建名为 teamp 的新分支,并立即切换到该分支

cat 文件名输出文件名的所有内容

Git 本地仓库操作指南:将未提交文件复刻至新分支(无需关联远端)

2025年10月24日 09:52

Git 本地仓库操作指南:将未提交文件复刻至新分支(无需关联远端)

在日常开发中,我们常会遇到这样的场景:本地仓库已有开发项目,存在未提交的修改内容,既不想将这些内容直接提交到当前分支,也无需上传至远端仓库,仅需在本地新建分支并将未提交文件完整复刻过去。此时可通过以下步骤高效完成操作,全程仅涉及本地仓库交互,无需依赖远端服务。

一、确认当前未提交的更改内容

在进行分支操作前,首先需明确当前工作区和暂存区中未提交的文件详情,避免遗漏或误操作。打开终端,进入本地项目仓库目录,执行以下命令:

bash

git status

执行后终端会输出两类关键信息:

  1. 已修改但未暂存的文件:标注为 “modified: 文件名”,表示文件已修改但未通过git add加入暂存区;
  2. 未跟踪的文件:标注为 “untracked files: 文件名”,表示新创建的文件尚未被 Git 跟踪。通过该命令可清晰掌握需复刻的内容范围,确保后续操作针对性。

二、新建分支并自动切换(保留未提交内容)

Git 的工作区和暂存区具有 “分支共享” 特性 —— 未提交的修改不会与特定分支绑定,切换分支时会自动跟随到新分支。利用这一特性,我们可通过单条命令完成 “新建分支 + 切换分支”,同时保留未提交文件。

在终端执行以下命令:

bash

git switch -c 新分支名称
  • 命令解析:git switch用于切换分支,-c(全称 create)是 “新建分支” 的参数,紧跟的 “新分支名称” 需自定义(建议遵循项目命名规范,如feature/local-devfix/uncommitted-code);
  • 示例:若需新建名为local-copy-branch的分支,命令为git switch -c local-copy-branch

执行成功后,终端会提示 “Switched to a new branch ' 新分支名称 '”,此时已切换至新分支,且第一步中确认的未提交文件(包括已修改未暂存、未跟踪文件)已完整保留在新分支的工作区 / 暂存区中。

三、在新分支中提交未提交文件

切换到新分支后,需将未提交文件正式提交至新分支的本地仓库,确保这些内容被 Git 持久化跟踪(仅本地生效)。

1. 暂存文件

根据需求选择暂存方式:

  • 暂存所有未提交文件(包括已修改和未跟踪文件):

    bash

    git add .
    

    注意:.代表当前目录,该命令会递归暂存当前仓库下所有未暂存 / 未跟踪的修改,适合需完整复刻所有内容的场景。

  • 选择性暂存指定文件:若无需复刻全部内容,可单独指定文件名暂存,示例:

    bash

    git add 文件名1 文件名2
    

暂存后可再次执行git status验证,此时文件会标注为 “staged: 文件名”,表示已成功加入暂存区。

2. 提交至本地仓库

执行提交命令,为此次提交添加清晰的描述信息(便于后续查看提交历史):

bash

git commit -m "提交说明:将原分支未提交文件复刻至新分支"
  • 提交说明建议:需简洁明了,标注核心操作,如 “feat: 复刻原分支未提交的用户模块代码至 local-copy-branch”;
  • 执行结果:终端会输出提交摘要,包括提交 ID、修改文件数量、新增 / 删除代码行数等,提示 “1 file changed, 2 insertions (+), 1 deletion (-)” 即表示提交成功。

至此,未提交文件已正式存储在新分支的本地仓库中,新分支具备完整的复刻内容。

四、可选操作:切换回原分支(保持原分支纯净)

若后续仍需在原分支开发,可切换回原分支,且原分支会保持创建新分支前的状态 —— 即不包含新分支中提交的内容,确保原分支历史不被干扰。

执行切换命令:

bash

git switch 原分支名称
  • 示例:若原分支为mainmaster,命令为git switch main
  • 状态验证:切换后执行git status,会发现原分支中已无之前的未提交内容,回到未创建新分支时的初始状态。

操作效果与注意事项

最终效果

  • 新分支:包含所有未提交的修改(已通过git commit持久化),可在新分支中继续开发或备份内容;
  • 原分支:保持纯净,无新增提交,不影响原有开发进度;
  • 全程无远端交互:所有操作仅在本地仓库完成,无需git push等远端命令,适合离线开发或本地临时分支需求。

注意事项

  1. 若存在 “暂存区 + 工作区混合修改”(部分文件已git add,部分未暂存),切换分支后两种状态会完整保留,提交时需注意暂存区内容是否正确;
  2. 新分支名称避免与本地已存在的分支重名,若重名会提示 “fatal: A branch named ' 新分支名称 ' already exists”,需更换名称或删除原有分支(删除命令:git branch -d 分支名);
  3. 若未提交内容中包含大型文件或敏感信息,无需担心泄露 —— 全程本地操作,无任何内容上传至远端,安全性可控。

通过以上步骤,可在不依赖远端仓库的前提下,高效实现 “未提交文件本地分支复刻”,既保证了当前分支的纯净性,又能妥善保存未提交内容,适配本地临时开发、代码备份等场景需求。

昨天 — 2025年10月23日首页
昨天以前首页

正确的 .gitignore 配置

作者 tiantian_cool
2025年10月22日 09:27
# Xcode 用户数据
**/xcuserdata/
*.xcodeproj/xcuserdata/
*.xcworkspace/xcuserdata/

# Xcode 构建文件
build/
DerivedData/

# CocoaPods - 只忽略 Pods 目录,不忽略 Podfile 和 Podfile.lock
Pods/

# macOS
.DS_Store

# 其他
*.swp
*~

提交代码时

git add Podfile Podfile.lock .gitignore
git commit -m "Update dependencies"
git push

执行 pod install 后,.xcodeproj 文件被修改了,产生了待提交的内容。

原因分析

当你运行 pod install 时,CocoaPods 会:

  1. ✅ 在 Pods/ 目录下载依赖库(已被 .gitignore 忽略)
  2. ⚠️ 修改 .xcodeproj/project.pbxproj 文件,添加对 Pods 的引用

首次克隆项目后

# 1. 克隆项目
git clone <your-repo-url>
cd 项目目录

# 2. 安装依赖
pod install

# 3. 提交 .xcodeproj 的修改(如果有)
git add eWordMedical.xcodeproj/project.pbxproj
git commit -m "Update project configuration after pod install"
git push

为什么会有这些修改? 可能的原因:

  1. 路径差异:不同电脑上的绝对路径不同
  2. CocoaPods 版本:不同版本的 CocoaPods 生成的配置略有差异
  3. 首次安装:如果项目之前没有正确提交 .xcodeproj

这样做的好处:

  • 保持项目文件与实际配置一致
  • 团队其他成员拉取后可以直接编译

预防措施 为了减少这种情况,团队应该 统一 CocoaPods 版本

# 查看当前版本
pod --version

# 在 Gemfile 中锁定版本(可选)
gem 'cocoapods', '~> 1.15'

确保 .xcworkspace 也被提交

# .xcworkspace 应该提交(包含工作区配置)
git add xxxx.xcworkspace

在 .gitignore 中只忽略用户数据

# 只忽略用户数据,不忽略项目文件
**/xcuserdata/
*.xcworkspace/xcuserdata/

⚠️ 可能冲突的情况

只有在以下情况会冲突:

  1. 同时修改项目结构

    • 你:添加了新文件 A
    • 同事:添加了新文件 B
    • 两个人都修改了 .xcodeproj
    • 结果:Git 合并冲突 ❌
  2. 同时更新依赖

    • 你:更新了 Alamofire 版本
    • 同事:更新了 SnapKit 版本
    • 两个人都修改了 Podfile.lock 和 .xcodeproj
    • 结果:需要手动合并 ⚠️

CocoaPods 库中的代码有报错问题每次我都需要手动修改为了防止每次都修改以下修改 使用 Podfile 的 post_install 钩子自动修复

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == 'CountdownLabel'  # 替换为你的 Pod 名称
      target.build_configurations.each do |config|
        # 自动修复感叹号问题
        Dir.glob("Pods/CountdownLabel/**/*.swift").each do |file|
          contents = File.read(file)
          # 将 as !TimeZone 替换为 as? TimeZone
          new_contents = contents.gsub(/as !TimeZone/, 'as? TimeZone')
          File.write(file, new_contents) if contents != new_contents
        end
      end
    end
  end
end

从零实现富文本编辑器#8-浏览器输入模式的非受控DOM行为

作者 WindRunnerMax
2025年10月20日 11:02

浏览器输入模式的非受控DOM行为

先前我们在选区模块的基础上,通过浏览器的组合事件来实现半受控的输入模式,这是状态同步的重要实现之一。在这里我们要关注于处理浏览器复杂DOM结构默认行为,以及兼容IME输入法的各种输入场景,相当于我们来Case By Case地处理输入法和浏览器兼容的行为。

从零实现富文本编辑器项目的相关文章:

概述

在整个编辑器系列最开始的时候,我们就提到了ContentEditable的可控性以及浏览器兼容性问题,特别是结合了React作为视图层的模式下,状态管理以及DOM的行为将变得更不可控,这里回顾一下常见的浏览器的兼容性问题:

  • 在空contenteditable编辑器的情况下,直接按下回车键,在Chrome中的表现是会插入<div><br></div>,而在FireFox(<60)中的表现是会插入<br>IE中的表现是会插入<p><br></p>
  • 在有文本的编辑器中,如果在文本中间插入回车例如123|123,在Chrome中的表现内容是123<div>123</div>,而在FireFox中的表现则是会将内容格式化为<div>123</div><div>123</div>
  • 同样在有文本的编辑器中,如果在文本中间插入回车后再删除回车,例如123|123->123123,在Chrome中的表现内容会恢复原本的123123,而在FireFox中的表现则是会变为<div>123123</div>
  • 在同时存在两行文本的时候,如果同时选中两行内容再执行("formatBlock", false, "P")命令,在Chrome中的表现是会将两行内容包裹在同个<p>中,而在FireFox中的表现则是会将两行内容分别包裹<p>标签。
  • ...

由于我们的编辑器输入是依靠浏览器提供的组合事件,自然无法规避相关问题。编辑器设计的视图结构是需要严格控制的,这样我们才能根据一定的规则实现视图与选区模式的同步。依照整体MVC架构的设计,当前编辑器的视图结构设计如下:

<div data-block="true" >
  <div data-node="true">
    <span data-leaf="true"><span data-string="true">inline</span></span>
    <span data-leaf="true"><span data-string="true">inline2</span></span>
  </div>
</div>

那么如果在ContentEdiable输入时导致上述的结构被破坏,我们设计的编辑器同步模式便会出现问题。因此为了解决类似的问题,我们就需要实现脏DOM检查,若是出现破坏性的节点结构,就需要尝试修复DOM结构,甚至需要调度React来重新渲染严格的视图结构。

然而,如果每次输入或者选区变化等时机都进行DOM检查和修复,势必会影响编辑器整体性能或者输入流畅性,并且DOM检查和修复的范围也需要进行限制,否则同样影响性能。因此在这里我们需要对浏览器的输入模式进行归类,针对不同的类型进行不同的DOM检查和修复模式。

行内节点

DOM结构与Model结构的同步在非受控的React组件中变得复杂,这其实也就是部分编辑器选择自绘选区的原因之一,可以以此避免非受控问题。那么非受控的行为造成的主要问题可以比较容易地复现出来,假设此时存在两个节点,分别是inline类型和text类型的文本节点:

inline|text

此时我们的光标在inline后,假设schema中定义的inline规则是不会继承前个节点的格式,那么接下来如果我们输入内容例如1,此时文本就变成了inline|1text。这个操作是符合直觉的,然而当我们在上述的位置唤醒IME输入中文内容时,这里的文本就变成了错误的内容。

inline中文|中文text

这里的差异可以比较容易地看出来,如果是输入的英文或者数字,即不需要唤醒IME的受控输入模式,1这个字符是会添加到text文本节点前。而唤醒IME输入法的非受控输入模式,则会导致输入的内容不仅出现在text前,而且还会出现在inline节点的后面,这部分显然是有问题的。

这里究其原因还是在于非受控的IME问题,在输入英文时我们的输入在beforeinput事件中被阻止了默认行为,因此不会触发浏览器默认行为的DOM变更。然而当前在唤醒IME的情况下,DOM的变更行为是无法被阻止的,因此此时属于非受控的输入,这样就导致了问题。

此时由于浏览器的默认行为,inline节点的内容会被输入法插入“中文”的文本,这部分是浏览器对于输入法的默认处理。而当我们输入完成后,数据结构Model层的内容是会将文本放置于text前,这部分则是编辑器来控制的行为,这跟我们输入非中文的表现是一致的,也是符合预期表现的。

那么由于我们的immutable设计,再加上性能优化策略的memo以及useMemo的执行,即使在最终的文本节点渲染加入了脏DOM检测也是不够的,因为此时完全不会执行rerender。这就导致React原地复用了当前的DOM节点,因此造成了IME输入的DOM变更和Model层的不一致。

const onRef = (dom: HTMLSpanElement | null) => {
  if (props.children === dom.textContent) return void 0;
  const children = dom.childNodes;
  // If the text content is inconsistent due to the modification of the input
  // it needs to be corrected
  for (let i = 1; i < children.length; ++i) {
    const node = children[i];
    node && node.remove();
  }
  // Guaranteed to have only one text child
  if (isDOMText(dom.firstChild)) {
    dom.firstChild.nodeValue = props.children;
  }
};

而如果我们直接将leafReact.memo以及useMemo移除,这个问题自然是会消失,然而这样就会导致编辑器的性能下降。因此我们就需要考虑尽可能检查到脏DOM的情况,实际上如果是在input事件或者MutationObserver中处理输入的纯非受控情况,也需要处理脏DOM的问题。

那么我们可以明显的想到,当行状态发生变更时,我们就直接检查当前行的所有leaf节点,然后对比文本内容,如果存在不一致的情况则直接进行修正。如果直接使用querySelector的话显然不够优雅,我们可以借助WeakMap来映射叶子状态到DOM结构,以此来快速定位到需要的节点。

然后在行节点的状态变更后,在处理副作用的时候检查脏DOM节点,并且由于我们的行状态也是immutable的,因此也不需要担心性能问题。此时检查的执行是O(N)的算法,而且检查的范围也会限制在发生rerender的行中,具体检查节点的方法自然也跟上述onRef一致。

const leaves = lineState.getLeaves();
for (const leaf of leaves) {
  const dom = LEAF_TO_TEXT.get(leaf);
  if (!dom) continue;
  const text = leaf.getText();
  // 避免 React 非受控与 IME 造成的 DOM 内容问题
  if (text === dom.textContent) continue;
  editor.logger.debug("Correct Text Node", dom);
  const nodes = dom.childNodes;
  for (let i = 1; i < nodes.length; ++i) {
    const node = nodes[i];
    node && node.remove();
  }
  if (isDOMText(dom.firstChild)) {
    dom.firstChild.nodeValue = text;
  }
}

这里需要注意的是,脏节点的状态检查是需要在useLayoutEffect时机执行的,因为我们需要保证执行的顺序是先校正DOM再更新选区。如果反过来的话就会导致一个问题,先更新的选区依然停留在脏节点上,此时再校正会由于DOM节点变化导致选区的丢失,表现是选区会在inline的最前方。

leaf rerender -> line rerender -> line layout effect -> block layout effect

此外,这里的实现在首次渲染并不需要检查,此时不会存在脏节点的情况,因此初始化渲染的时候我们可以直接跳过检查。以这种策略来处理脏DOM的问题,还可以避免部分其他可能存在的问题,零宽字符文本的内容暂时先不处理,如果再碰到类似的情况是需要额外的检查的。

其实换个角度想,这里的问题也可能是我们的选区策略是尽可能偏左侧的查找,如果在这种情况将其校正到右侧节点可能也可以解决问题。不过因为在空行的情况下我们的末尾\n节点并不会渲染,因此这样的策略目前并不能彻底解决问题,而且这个处理方式也会使得编辑器的选区策略变得更加复杂。

[inline|][text] => [inline][|text]

这里还需要关注下ReactHooks调用时机,在下面的例子中,从真实DOM中得到onRef执行顺序是最前的,因此在此时进行首次DOM检查是合理的。而后续的Child LayoutEffect就类似于行DOM检查,在修正过后在Parent LayoutEffect中更新选区是符合调度时机方案。

Child onRef
Child useLayoutEffect
Parent useLayoutEffect
Child useEffect
Parent useEffect
// https://playcode.io/react
import React from 'react';
const Child = () => {
  const [,forceUpdate] = React.useState({});
  const onRef = () => console.log("Child onRef");
  React.useEffect(() => console.log("Child useEffect"));
  React.useLayoutEffect(() => console.log("Child useLayoutEffect"));
  return <button ref={onRef} onClick={() => forceUpdate({})}>Update</button>
}
export function App(props) {
  React.useEffect(() => console.log("Parent useEffect"));
  React.useLayoutEffect(() => console.log("Parent useLayoutEffect"));
  return <Child></Child>;
}

包装节点

关于包装节点的问题需要我们先聊一下这个模式的设计,现在实现的富文本编辑器是没有块结构的,因此实现任何具有嵌套的结构都是个复杂的问题。在这里我们原本就不会处理诸如表格类的嵌套结构,但是例如blockquote这种wrapper级结构我们是需要处理的。

类似的结构还有list,但是list我们可以完全自己绘制,但是blockquote这种结构是需要具体组合才可以的。然而如果仅仅是blockquote还好,在inline节点上使用wrapper是更常见的实现,例如a标签的包装在编辑器的实现模式中就是很常规的行为。

具体来说,在我们将文本分割为bolditalicinline节点时,会导致DOM节点被实际切割,此时如果嵌套<a>节点的话,就会导致hover后下划线等效果出现切割。因此如果能够将其wrapper在同一个<a>标签的话,就不会出现这种问题。

但是新的问题又来了,如果仅仅是单个key来实现渲染时嵌套并不是什么复杂问题,而同时存在多个需要wrapperkey则变成了令人费解的问题。如下面的例子中,如果将34单独合并b,外层再包裹a似乎是合理的,但是将34先包裹a后再合并5b也是合理的,甚至有没有办法将67一并合并,因为其都存在b标签。

1 2 3  4  5 6  7 8 9 0
a a ab ab b bc b c c c

思来想去,我最终想到了个简单的实现,对于需要wrapper的元素,如果其合并listkeyvalue全部相同的话,那么就作为同一个值来合并。那么这种情况下就变的简单了很多,我们将其认为是一个组合值,而不是单独的值,在大部分场景下是足够的。

1 2 3  4  5 6  7 8 9 0
a a ab ab b bc b c c c
12 34 5 6 7 890

不过话又说回来,这种wrapper结构是比较特殊的场景下才会需要的,在某些操作例如缩进这个行为中,是无法判断究竟是要缩进引用块还是缩进其中的文字。这个问题在很多开源编辑器中都存在,特别是扁平化的数据结构设计例如Quill编辑器。

其实也就是在没有块结构的情况下,对于类似的行为不好控制,而整体缩进这件事配合list在大型文档中也是很合理的行为,因此这部分实现还是要等我们的块结构编辑器实现才可以。当然,如果数据结构本身支持嵌套模式,例如Slate就可以实现。

后续在wrap node实现的a标签来实现输入时,又出现了上述类似inline-code的脏DOM问题。以下面的DOM结构来看,看似并不会有什么问题,然而当光标放置于超链接这三个字后唤醒IME输入中文时,会发现输入“测试输入”这几个字会被放置于直属div下,与a标签平级。

<div contenteditable>
  <a href="https://www.baidu.com"><span>超链接</span></a>
  <span>文本</span>
</div>
<div contenteditable>
  <a href="https://www.baidu.com"><span>超链接</span></a>
  测试输入
  <span>文本</span>
</div>

在这种情况下我们先前实现的脏DOM检测就失效了,因为检查脏DOM的实现是基于data-leaf实现的。此时浏览器的输入表现会导致我们无法正确检查到这部分内容,除非直接拿data-node行节点来直接判断,这样的实现自然不够好。

说到这里,先前我发现飞书文档的实现是a标签渲染的leaf,而wrap的包装实现是使用的span直接处理的,并且额外增加了样式来实现hover效果。直接使用span包裹就不会出现上述问题,而内部的a标签虽然会导致同样的问题,但是在leaf下可以触发脏DOM检查。

<div contenteditable>
  <span>
    <a href="https://www.baidu.com"><span>超链接</span></a>
    测试输入
  </span>
  <span>文本</span>
</div>

因此就可以在先前的脏DOM检查基础上解决了问题,而本质上类似的行为就是浏览器默认处理的结果,不同的浏览器处理结果可能都不一样。目前看起来是浏览器认为a标签的结构应该是属于inline的实现,也就是类似我们的inline-code实现,理论上倒却是并没有什么问题,由此我们需要自己来处理这些非受控的问题。

实际上Quill本身也会出现这个问题,同样也是脏DOM的处理。而slate并不会出现这个问题,这里处理方案则是通过DOM规避了问题,在a标签两端放置额外的&nbsp节点,以此来避免这个问题。当然还引入了额外的问题,引入了新的节点,目前看起来转移光标需要受控处理。

<!-- https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/inlines.tsx -->
<div contenteditable>
  <a href="https://www.baidu.com"
    ><span contenteditable="false" style="font-size: 0">&nbsp;</span
    ><span>超链接测试输入</span
    ><span contenteditable="false" style="font-size: 0">&nbsp;</span></a
  ><span>文本</span>
</div>

浏览器兼容性

在后续浏览器的测试中,重新出现了上述提到的a标签问题,此时并不是由于包装节点引起的,因此问题变得复杂了很多,主要是各个浏览器的兼容性的问题。类似于行内代码块,本质上还是浏览器IME非受控导致的DOM变更问题,但是在浏览器表现差异很大,下面是最小的DEMO结构。

<div contenteditable>
  <span data-leaf><a href="#"><span data-string>在[:]后输入:</span></a></span><span data-leaf>非链接文本</span>
</div>

在上述示例的a标签位置的最后的位置上输入内容,主流的浏览器的表现是有差异的,甚至在不同版本的浏览器上表现还不一致:

  • Chrome中会在a标签的同级位置插入文本类型的节点,效果类似于<a></a>"text"内容。
  • Firefox中会在a标签内插入span类型的节点,效果类似于<a></a><span data-string>text</span>内容。
  • Safari中会将a标签和span标签交换位置,然后在a标签上同级位置加入文本内容,类似<span><a></a>"text"</span>
<!-- Chrome -->
<span data-leaf="true">
  <a href="https://www.baidu.com"><span data-string="true">超链接</span></a>
  "文本"
</span>

<!-- Firefox -->
 <span data-leaf="true">
  <a href="https://www.baidu.com"><span data-string="true">超链接</span></a>
  <span data-string="true">文本</span>
</span>

<!-- Safari -->
 <span data-leaf="true">
  <span data-string="true">
    <a href="https://www.baidu.com">超链接</a>
    "文本"
    ""
  </span>
</span>

因此我们的脏DOM检查需要更细粒度地处理,仅仅对比文本内容显然是不足以处理的,我们还需要检查文本的内容节点结构是否准确。其实最开始我们是仅处理了Chrome下的情况,最简单的办法就是在leaf节点下仅允许存在单个节点,存在多个节点则说明是脏DOM

for (let i = 1; i < nodes.length; ++i) {
  const node = nodes[i];
  node && node.remove();
}

但是后来发现在编辑时会把Embed节点移除,这里也就是因为我们错误地把组合的div节点当成了脏DOM,因此这里就需要更细粒度地处理了。然后考虑检查节点的类型,如果是文本的节点类型再移除,那么就可以避免Embed节点被误删的问题。

for (let i = 1; i < nodes.length; ++i) {
  const node = nodes[i];
  isDOMText(node) && node.remove();
}

虽然看起来是解决了问题,然而在后续就发现了FirefoxSafari下的问题。先来看Firefox的情况,这个节点并非文本类型的节点,在脏DOM检查的时候就无法被移除掉,这依然无法处理Firefox下的脏DOM问题,因此我们需要进一步处理不同类型的节点。

// data-leaf 节点内部仅应该存在非文本节点, 文本类型单节点, 嵌入类型双节点
for (let i = 1; i < nodes.length; ++i) {
  const node = nodes[i];
  // 双节点情况下, 即 Void/Embed 节点类型时需要忽略该节点
  if (isHTMLElement(node) && node.hasAttribute(VOID_KEY)) {
    continue;
  }
  node.remove();
}

Safari的情况下就更加复杂,因为其会将a标签和span标签交换位置,这样就导致了DOM结构性造成了破坏。这种情况下我们就必须要重新刷新DOM结构,这种情况下就需要更加复杂地处理,在这里我们加入forceUpdate以及TextNode节点的检查。

其实在飞书文档中也是采用了类似的做法,飞书文档的a标签在唤醒IME输入后,同样会触发脏DOM的检查,然后飞书文档会直接以行为基础ReMount当前行的所有leaf节点,这样就可以避免复杂的脏DOM检查。我们这里实现更精细的leaf处理,主要是避免不必要的挂载。

const LeafView: FC = () => {
  const { forceUpdate, index: renderKey } = useForceUpdate();
  LEAF_TO_REMOUNT.set(leafState, forceUpdate);
  return (<span key={renderKey}></span>);
}

if (isDOMText(dom.firstChild)) {
  // ...
} else {
  const func = LEAF_TO_REMOUNT.get(leaf);
  func && func();
}

这里需要注意的是,我们还需要处理零宽字符类型的情况。当Embed节点前没有任何节点,即位于行首时,输入中文后同样会导致IME的输入内容被滞留在Embed节点的零宽字符上,这点与上述的inline节点是类似的,因此这部分也需要处理。

const zeroNode = LEAF_TO_ZERO_TEXT.get(leaf);
const isZeroNode = !!zeroNode;
const textNode = isZeroNode ? zeroNode : LEAF_TO_TEXT.get(leaf);
const text = isZeroNode ? ZERO_SYMBOL : leaf.getText();
const nodes = textNode.childNodes;

到这里,我们的脏DOM检查已经能够处理大部分情况了,整体的模式都是React在行DOM结构计算完成后,浏览器渲染前进行处理。针对于文本节点以及a标签的检查,需要检查文本与状态的关系,以及严格的DOM结构破坏后的需要直接Remount组件。

// 文本节点内部仅应该存在一个文本节点, 需要移除额外节点
for (let i = 1; i < nodes.length; ++i) {
  const node = nodes[i];
  node && node.remove();
}
// 如果文本内容不合法, 通常是由于输入的脏 DOM, 需要纠正内容
if (isDOMText(textNode.firstChild)) {
  // Case1: [inline-code][caret][text] IME 会导致模型/文本差异
  // Case3: 在单行仅存在 Embed 节点时, 在节点最前输入会导致内容重复
  if (textNode.firstChild.nodeValue === text) return false;
  textNode.firstChild.nodeValue = text;
  } else {
  // Case2: Safari 下在 a 节点末尾输入时, 会导致节点内外层交换
  const func = LEAF_TO_REMOUNT.get(leaf);
  func && func();
  if (process.env.NODE_ENV === "development") {
    console.log("Force Render Text Node", textNode);
  }
}

而针对于额外的文本节点,即本章节中重点提到的浏览器兼容性问题,我们需要严格地控制leaf节点下的DOM结构。如果仅存在单个文本节点的情况下,是符合设计的结构,而如果是存在多个节点,除了Void/Embed节点的情况外,则说明DOM结构被破坏了,这里我们就需要移除掉多余的节点。

// data-leaf 节点内部仅应该存在非文本节点, 文本类型单节点, 嵌入类型双节点
for (let i = 1; i < nodes.length; ++i) {
  const node = nodes[i];
  // 双节点情况下, 即 Void/Embed 节点类型时需要忽略该节点
  if (isHTMLElement(node) && node.hasAttribute(VOID_KEY)) {
    continue;
  }
  // Case1: Chrome a 标签内的 IME 输入会导致同级的额外文本节点类型插入
  // Case2: Firefox a 标签内的 IME 输入会导致同级的额外 data-string 节点类型插入
  node.remove();
}

样式组合渲染

由于我们的编辑器是以immutable提高渲染性能,因此在文本节点变更时若是需要存在连续的格式处理,例如inline-code的样式实现,就会出现组件不重新渲染问题。具体表现是若是存在多个连续的code节点,最后一个节点长度为1,删除最后这个节点时会导致前一个节点无法刷新样式。

[inline][c]|

这个问题的原因是我们的className是在渲染leaf节点时动态计算的,具体的逻辑如下所示。如果前一个节点不存在或者前一个节点不是inline-code,则添加inline-code-start类属性,类似的需要在最后一个节点加入inline-code-end类属性。

if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
  context.classList.push(INLINE_CODE_START_CLASS);
}
context.classList.push("block-kit-inline-code");
if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
  context.classList.push(INLINE_CODE_END_CLASS);
}

这个情况同样类似于Dirty DOM的问题,由于删除的节点长度为1,因此前一个节点的LeafState并没有变更,因此不会触发React的重新渲染。这里我们就需要在行节点渲染时进行纠正,这里的执行倒是不需要像上述检查那样同步执行,以异步的effect执行即可。

/**
 * 编辑器行结构布局计算后异步调用
 */
public didPaintLineState(lineState: LineState): void {
  for (let i = 0; i < leaves.length; i++) {
    if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
      node && node.classList.add(INLINE_CODE_START_CLASS);
    }
    if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
      node && node.classList.add(INLINE_CODE_END_CLASS);
    }
  }
}

虽然看起来已经解决了问题,然而在React中还是存在一些问题,主要的原因此时的DOM处理是非受控的。类似于下面的例子,由于React在处理style属性时,只会更新发生变化的样式属性,即使整体是新对象,但具体值与上次渲染时相同,因此React不会重新设置这个样式属性。

// https://playcode.io/react
import React from "react";
export function App() {
  const el = React.useRef();
  const [, setState] = React.useState(1);
  const onClick = () => {
    el.current && (el.current.style.color = "blue");
  }
  console.log("Render App")
  return (
    <div>
      <div style={{ color:"red" }} ref={el}>Hello React.</div>
      <button onClick={onClick}>Color Button</button>
      <button onClick={() => setState(c => ++c)}>Rerender Button</button>
    </div>
  );
}

因此,在上述的didPaintLineState中我们主要是classList添加类属性值,即使是LeafState发生了变更,React也不会重新设置类属性值,因此这里我们还需要在didPaintLineState变更时删除非必要的类属性值。

public didPaintLineState(lineState: LineState): void {
  for (let i = 0; i < leaves.length; i++) {
    if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
      node && node.classList.add(INLINE_CODE_START_CLASS);
    } else {
      node && node.classList.remove(INLINE_CODE_START_CLASS);
    }
    if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
      node && node.classList.add(INLINE_CODE_END_CLASS);
    } else {
      node && node.classList.remove(INLINE_CODE_END_CLASS);
    }
  }
}

总结

在先前我们实现了半受控的输入模式,这个输入模式同样是目前大多数富文本编辑器的主流实现方式。在这里我们关注于浏览器ContentEdiable模式输入的默认行为造成的DOM结构问题,并且通过脏DOM检查的方式来修正这些问题,以此来保持编辑器的严格DOM结构。

当前我们主要关注的是编辑器文本的输入问题,即如何将键盘输入的内容写入到编辑器数据模型中。而接下来我们需要关注于输入模式结构化变更的受控处理,即回车、删除、拖拽等操作的处理,这些操作同样也是基于输入相关事件实现的,而且通常会涉及到文本的结构变更,属于输入模式的补充。

每日一题

参考

草梅 Auth 1.10.1 发布与浏览器自动化工具 | 2025 年第 42 周草梅周报

作者 草梅友仁
2025年10月19日 22:53

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


本周依旧在开发 草梅 Auth 中。

你也可以直接访问官网地址:auth.cmyr.dev/ Demo 站:auth-demo.cmyr.dev/ 文档地址:auth-docs.cmyr.dev/

本周 草梅 Auth 发布了 1.10.1 版本。

本周的主要改动是修复了人机验证相关的逻辑的一些错误,优化验证码体验。

如果想了解如何部署和使用项目,可以参考文档的内容,也欢迎补充文档缺失的内容。

如果你对草梅 Auth 感兴趣,欢迎参与开发和测试。


最近研究了下浏览器自动化,发现了个有趣的工具——browserbase/stagehand,可以使用 AI 大模型来操控浏览器。

20251019215236399.png

使用方法也很简单,可以用官方脚手架生成。

npx create-browser-app
# 按照 CLI 提示进入项目目录并添加您的 API 密钥。然后运行示例脚本。
cd my-stagehand-app # Enter the project directory
cp .env.example .env  # Add your API keys
npm start # Run the example script

也可以手动安装依赖

pnpm i @browserbasehq/stagehand playwright
# 如果没有安装 playwright 需执行下面这条命令,以安装对应的浏览器
npx playwright install

然后再编写脚本即可。

import "dotenv/config";
import { Stagehand } from "@browserbasehq/stagehand";

async function main() {
    const stagehand = new Stagehand({
        env: "BROWSERBASE",
    });

    await stagehand.init();

    console.log(`Stagehand Session Started`);
    console.log(
        `Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`
    );

    const page = stagehand.page;

    await page.goto("https://stagehand.dev");

    const extractResult = await page.extract(
        "Extract the value proposition from the page."
    );
    console.log(`Extract result:\n`, extractResult);

    const actResult = await page.act("Click the 'Evals' button.");
    console.log(`Act result:\n`, actResult);

    const observeResult = await page.observe("What can I click on this page?");
    console.log(`Observe result:\n`, observeResult);

    const agent = await stagehand.agent({
        instructions:
            "You're a helpful assistant that can control a web browser.",
    });

    const agentResult = await agent.execute(
        "What is the most accurate model to use in Stagehand?"
    );
    console.log(`Agent result:\n`, agentResult);

    await stagehand.close();
}

main().catch((err) => {
    console.error(err);
    process.exit(1);
});

可以看到脚本中是直接用自然语言来描述的,因此简化了浏览器自动化脚本的编写。

所以现在无需考虑什么 XPath 或者 selector 了,直接用自然语言描述就行。

接下来一段时间会研究下如何把发布周报的过程给自动化一下,毕竟整个发布操作的重复度其实是非常高的,很适合自动化。

GitHub Release

caomei-auth

v1.10.1 - 2025-10-18 20:08:40

摘要: 版本 1.10.1 (2025-10-18) 摘要:

本次更新主要包含以下错误修复:

  1. 验证码组件:

    • 更新以支持新的 vue-recaptcha 插件
    • 修正了组件导入路径问题
  2. 构建配置:

    • 更新了项目构建配置
  3. Nuxt 相关:

    • 修复了 vue-recaptcha-v3 的转译条件问题
    • 优化了 Google reCAPTCHA 插件的加载逻辑
    • 将 vue-recaptcha 插件添加到 Nuxt 配置并设置了相关选项

本次更新主要针对验证码功能和构建配置进行了多项修复和优化。

cmyr-template-cli

v1.41.6 - 2025-10-19 02:40:06

摘要: [1.41.6]版本更新摘要:

Bug 修复:

  • 在 package.json 文件中新增了 homepage、repository 和 bugs 三个字段

本次更新主要解决了 package.json 配置文件缺少必要字段的问题,添加了项目主页、代码仓库和问题反馈的相关链接信息,便于用户更好地了解和参与项目开发。

最新 GitHub 加星仓库

  • CaoMeiYouRen starred Second-Me - 2025-10-14 11:26:30 训练 AI 自我提升,扩展能力,连接世界 主要编程语言:Python GitHub 星标数:14424

其他博客或周刊推荐

阮一峰的网络日志

阿猫的博客

潮流周刊

二丫讲梵的学习周刊

总结

本周的更新和动态如上所示。感谢您的阅读! 您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址:blog.cmyr.ltd/archives/20…
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

pnpm monorepo 联调:告别 --global 参数

作者 JinSo
2025年10月19日 15:08

前言

在之前的文章《pnpm monorepo 联调方案》中,我详细介绍了如何使用 pnpm linkpnpm link --global 来解决 monorepo 环境下的调试难题。

时间过去了一段时间,pnpm 也在不断演进。最近在使用过程中,我发现了一个有趣的变化:执行 pnpm link 时不再需要添加 --global 参数,同时 pnpm 会自动创建 pnpm-workspace.yaml 文件。这引起了我的好奇心,决定深入研究一下 pnpm 10.x 版本中 link 功能的最新变化。

pnpm 10.x 中关于 link 的变更

最大的变化就是 去除了 --global 参数

之前我们这样操作:

# 库中
pnpm link --global

# 项目中
pnpm link --global <pkg>

现在直接:

# 库中
pnpm link

# 项目中
pnpm link <pkg>

看起来是小改动,但其实是把之前的 pnpm link --global 的行为直接变成了 pnpm link 的默认行为。

从官方文档也能看出来,现在的 pnpm link 描述和之前 9.x 版本的 pnpm link --global 完全一样。

image0.png

image1.png

取消链接还是用 pnpm unlink

pnpm unlink <pkg>

实际使用体验

在库中执行 link

现在在基础库中执行 pnpm link,会直接链接到全局:

cd ~/packages/core
pnpm install
pnpm link

# 输出示例
❯ pnpm link
 WARN  The package @easy-editor/core, which you have just pnpm linked, has the following peerDependencies specified in its package.json:
  - mobx@^6.13.5
The linked in dependency will not resolve the peer dependencies from the target node_modules.
This might cause issues in your project. To resolve this, you may use the "file:" protocol to reference the local dependency.
√ The modules directory at "C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules" will be removed and reinstalled from scratch. Proceed? (Y/n) · true
Recreating C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules

在项目中链接库

在项目中链接也更直接了:

cd ~/projects/my-project
pnpm link @easy-editor/core

# 输出示例
❯ pnpm link @easy-editor/core
 WARN  The package @easy-editor/core, which you have just pnpm linked, has the following peerDependencies specified in its package.json:
  - mobx@^6.13.5
The linked in dependency will not resolve the peer dependencies from the target node_modules.
This might cause issues in your project. To resolve this, you may use the "file:" protocol to reference the local dependency.
√ The modules directory at "D:\\Programming\\EasyEditor\\EasyDashboard\\node_modules" will be removed and reinstalled from scratch. Proceed? (Y/n) · true
Recreating D:\\Programming\\EasyEditor\\EasyDashboard\\node_modules
dependencies:
    + @easy-editor/core 0.0.0 <- C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@easy-editor\\core

自动生成的 workspace 配置

这是个新发现,链接完成后,pnpm 会自动在项目根目录生成 pnpm-workspace.yaml

overrides:
  '@easy-editor/core': link:C:/Users/user/AppData/Local/pnpm/global/5/node_modules/@easy-editor/core

这样就能清楚地看到当前项目链接了哪些包,比之前透明多了。

多库联调的改进

之前需要给每个库都加 --global,现在直接:

# 在各个库中
cd ~/packages/core && pnpm link
cd ~/packages/designer && pnpm link
cd ~/packages/utils && pnpm link

# 在项目中
cd ~/projects/my-project
pnpm link @myorg/core
pnpm link @myorg/designer
pnpm link @myorg/utils

对于 workspace:* 这种依赖,pnpm 还是会自动处理,和之前一样智能。

前后对比

pnpm 9.x pnpm 10.x
全局链接 pnpm link --global pnpm link
项目链接 pnpm link --global <pkg> pnpm link <pkg>
workspace 配置 手动管理 自动生成
操作复杂度 需要记住加 --global 更简单直接

总结

pnpm 10.x 的这个改动虽然看起来不大,但确实让联调操作更简单了。不用再记那个 --global 参数,直接 pnpm link 就完事。

加上自动生成的 workspace 配置文件,整个链接状态也更透明了。如果你还在用旧版本,建议升级试试,体验确实有提升。


参考资料

  1. pnpm link | pnpm 官方文档
  2. "pnpm link --global" in v10 behaves differently from v9, breaking global linking.

KuiklyUI Pager 架构设计完整分析

作者 风冷
2025年10月17日 19:39

KuiklyUI Pager 架构设计完整分析

1. 架构概述

KuiklyUI Pager是KuiklyUI框架中的核心组件,负责页面的创建、生命周期管理、视图渲染、事件分发和跨平台适配。Pager架构采用了清晰的分层设计和模块化思想,结合响应式编程模式,为跨平台应用提供统一的页面管理机制。

核心设计理念

  • 分层架构:将页面管理、视图渲染、事件处理等功能解耦为独立层次
  • 响应式编程:利用ReactiveObserver实现数据与视图的响应式更新
  • 模块化设计:支持核心模块和外部模块的注册与管理
  • 跨平台适配:通过BridgeManager实现与原生平台的通信

2. 核心组件与关系

组件概览

classDiagram
    class IPager {
        +pageData: PagerData
        +lifecycleScope: CoroutineScope
        +registerModule(module: IModule)
        +bindValueChange(observer: ReactiveObserver)
        +onCreatePager()
        +pageDidAppear()
        +pageDidDisappear()
    }
    
    class Pager {
        +pagerData: PagerData
        -modules: MutableMap<String, IModule>
        -pagerEventObservers: MutableList<IPagerEventObserver>
        +registerModule(module: IModule)
        +onCreatePager()
        +calculateLayout()
        +dispatchViewEvent()
    }
    
    class PagerManager {
        -pagerMap: MutableMap<String, IPager>
        -pagerNameMap: MutableMap<String, String>
        +createPager()
        +destroyPager()
        +getPager()
    }
    
    class PagerData {
        +platform: String
        +isAndroid: Boolean
        +isIOS: Boolean
        +safeAreaInsets: SafeAreaInsets
        +windowSize: Size
    }
    
    class IPagerEventObserver {
        +onPagerEvent(eventName: String, data: Any?)
    }
    
    class IPagerLayoutEventObserver {
        +onPagerWillCalculateLayoutFinish()
    }
    
    IPager <|-- Pager
    PagerManager "1" -- "*" IPager
    Pager "1" -- "1" PagerData
    Pager "1" -- "*" IPagerEventObserver
    IPagerEventObserver <|-- IPagerLayoutEventObserver

组件说明

IPager 接口

定义了页面的核心能力接口,包括:

  • 生命周期管理:onCreatePager、pageDidAppear、pageDidDisappear等生命周期方法
  • 模块管理:registerModule、unregisterModule、getModule等模块注册与获取方法
  • 事件处理:addViewEventObserver、dispatchViewEvent等事件观察与分发方法
  • 数据绑定:bindValueChange、unbindValueChange等响应式数据绑定方法
  • 布局操作:createBody、calculateLayout等视图创建与布局计算方法
Pager 抽象类

是IPager接口的主要实现,提供了页面管理的核心逻辑:

  • 模块管理实现:维护modules集合,处理模块的注册、初始化和销毁
  • 生命周期实现:实现页面生命周期方法,协调模块的生命周期
  • 视图事件处理:维护nativeRefViewMap,实现视图事件的分发
  • 布局计算:实现calculateLayout方法,包含布局计算循环和最大重试逻辑
  • 任务调度:利用TaskManager进行异步任务调度
PagerManager 单例

是Pager的管理中心,采用单例模式:

  • Pager实例管理:通过pagerMap和pagerNameMap管理所有Pager实例
  • 页面创建与销毁:提供createPager和destroyPager方法
  • 页面检索:提供getPager方法根据ID或名称获取Pager实例
  • 响应式观察者管理:管理全局的ReactiveObserver
  • 页面路由注册:支持页面路由信息的注册
PagerData 类

封装了页面相关的所有数据:

  • 设备信息:platform、isAndroid、isIOS等平台标识
  • 页面参数:通过params属性存储页面传递参数
  • 安全区域信息:safeAreaInsets存储刘海屏等异形屏的安全区域
  • 窗口尺寸:windowSize、screenSize等视图尺寸信息
  • 响应式属性:采用ReactiveObserver实现数据变化的响应式通知

3. 页面生命周期管理

生命周期流程

sequenceDiagram
    participant PM as PagerManager
    participant P as Pager
    participant M as Modules
    participant V as View
    
    PM->>P: createPager()
    P->>P: onCreatePager()
    P->>M: module.init()
    P->>P: createBody()
    P->>P: calculateLayout()
    P->>P: pageDidAppear()
    P->>M: module.onPageAppear()
    P->>V: renderView()
    
    Note over P: 页面处于活动状态
    
    P->>P: pageDidDisappear()
    P->>M: module.onPageDisappear()
    PM->>P: destroyPager()
    P->>M: module.destroy()

生命周期方法详解

  1. 创建阶段

    • onCreatePager():页面创建时调用,初始化页面数据和组件
    • createBody():创建页面的根视图节点
    • calculateLayout():计算页面布局
  2. 可见阶段

    • pageDidAppear():页面显示在屏幕上时调用
    • 触发所有模块的onPageAppear()方法
  3. 不可见阶段

    • pageDidDisappear():页面从屏幕上消失时调用
    • 触发所有模块的onPageDisappear()方法
  4. 销毁阶段

    • destroyPager():销毁页面实例
    • 释放所有资源和模块

4. 核心机制分析

4.1 模块管理机制

Pager架构的模块化设计允许灵活扩展功能,包括核心模块和外部模块:

// 模块注册示例
fun registerModule(module: IModule) {
    if (!modules.containsKey(module.moduleName)) {
        modules[module.moduleName] = module
        module.init(this)
    }
}

// 核心模块初始化
private fun initCoreModules() {
    registerModule(NotifyModule())
    registerModule(MemoryCacheModule())
    registerModule(BackPressModule())
    // ...其他核心模块
}

4.2 事件处理机制

Pager采用观察者模式实现事件分发,支持多种类型的事件:

  1. 页面事件:通过IPagerEventObserver接口分发
  2. 布局事件:通过IPagerLayoutEventObserver接口分发
  3. 视图事件:通过nativeRefViewMap分发原生视图事件
// 页面事件处理示例
fun handlePagerEvent(eventName: String, data: Any?) {
    when (eventName) {
        THEME_DID_CHANGED -> {
            // 主题变化处理
            calculateLayout()
        }
        ROOT_VIEW_SIZE_CHANGED -> {
            // 根视图尺寸变化处理
            updateWindowSize()
            calculateLayout()
        }
        // ...其他事件处理
    }
}

4.3 布局计算机制

Pager实现了一套完整的布局计算流程,包含最大重试逻辑以防止布局循环:

fun calculateLayout() {
    var retryCount = 0
    val maxRetryCount = 4
    
    while (retryCount < maxRetryCount) {
        // 布局计算逻辑
        
        if (!needRetryLayout) {
            break
        }
        retryCount++
    }
    
    // 通知观察者布局计算完成
    notifyLayoutEventObservers {
        it.onPagerWillCalculateLayoutFinish()
    }
}

4.4 响应式更新机制

Pager通过ReactiveObserver实现数据与视图的响应式更新:

// 数据绑定示例
fun bindValueChange(observer: ReactiveObserver) {
    observers.add(observer)
}

// 响应式属性更新
fun updateReactiveProperty(value: Any) {
    // 更新值
    // 通知所有观察者
    observers.forEach {
        it.onValueChanged(value)
    }
}

4.5 跨平台通信机制

Pager通过BridgeManager实现与原生平台的通信:

// 跨平台通信示例
fun callNativeMethod(methodName: String, params: Map<String, Any?>): Any? {
    return BridgeManager.callNative(methodName, params)
}

5. 视图系统交互

5.1 视图树构建

Pager通过createBody方法构建视图树:

// 视图树构建示例
override fun createBody(): FlexNode {
    return FlexNode().apply {
        style.flexDirection = FlexDirection.COLUMN
        style.width = Dimension(100f, DimensionType.PERCENT)
        style.height = Dimension(100f, DimensionType.PERCENT)
        
        // 添加子视图
        addChild(createContentNode())
    }
}

fun renderView() {
    // 将FlexNode渲染为原生视图
    // 更新nativeRefViewMap
}

5.2 原生视图引用管理

Pager维护nativeRefViewMap来管理原生视图引用,用于事件分发:

// 原生视图引用管理
private val nativeRefViewMap = mutableMapOf<String, Any>()

fun registerNativeView(refId: String, view: Any) {
    nativeRefViewMap[refId] = view
}

fun getNativeView(refId: String): Any? {
    return nativeRefViewMap[refId]
}

6. 性能优化机制

6.1 延迟加载

Pager实现了模块和视图的延迟加载机制:

// 延迟任务调度
fun scheduleLazyTask(task: () -> Unit) {
    addLazyTaskUtilEndCollectDependency(object : LazyTask {
        override fun run() {
            task()
        }
    })
}

6.2 布局优化

Pager通过最大重试次数限制布局计算循环:

// 布局计算中的最大重试逻辑
var retryCount = 0
val maxRetryCount = 4

while (retryCount < maxRetryCount && needRetryLayout) {
    // 布局计算
    retryCount++
}

6.3 模块懒加载

核心模块在页面创建时初始化,而外部模块可以按需注册:

// 核心模块初始化
private fun initCoreModules() {
    // 注册核心模块
}

// 外部模块注册接口
fun registerModule(module: IModule) {
    // 注册外部模块
}

6.4 性能监控

Pager集成了PerformanceModule进行性能监控:

// 性能监控示例
fun startPerformanceMonitor(tag: String) {
    val performanceModule = getModule<PerformanceModule>(PerformanceModule.MODULE_NAME)
    performanceModule?.startMonitor(tag)
}

fun endPerformanceMonitor(tag: String) {
    val performanceModule = getModule<PerformanceModule>(PerformanceModule.MODULE_NAME)
    performanceModule?.endMonitor(tag)
}

7. 平台适配特性

7.1 平台检测

PagerData中包含平台检测信息,方便开发者编写平台特定代码:

// 平台检测
val isAndroid = platform == PLATFORM_ANDROID
val isIOS = platform == PLATFORM_IOS
val isOhos = platform == PLATFORM_OHOS

// 使用示例
if (pagerData.isAndroid) {
    // Android平台特定代码
} else if (pagerData.isIOS) {
    // iOS平台特定代码
}

7.2 安全区域适配

PagerData包含安全区域信息,支持刘海屏等异形屏适配:

// 安全区域信息
val safeAreaInsets = SafeAreaInsets(
    top = 44f,     // iOS刘海高度
    bottom = 34f,  // iOS底部安全区高度
    left = 0f,
    right = 0f
)

// 使用示例
bodyNode.style.paddingTop = Dimension(pagerData.safeAreaInsets.top, DimensionType.POINT)

8. 开发实践与最佳实践

8.1 创建自定义页面

// 自定义页面示例
class MyCustomPage : Pager() {
    override fun onCreatePager() {
        super.onCreatePager()
        
        // 初始化页面数据
        initPageData()
        
        // 注册自定义模块
        registerModule(MyCustomModule())
    }
    
    override fun createBody(): FlexNode {
        return FlexNode().apply {
            style.flexDirection = FlexDirection.COLUMN
            style.width = Dimension(100f, DimensionType.PERCENT)
            style.height = Dimension(100f, DimensionType.PERCENT)
            
            // 构建UI
            addChild(createHeader())
            addChild(createContent())
            addChild(createFooter())
        }
    }
    
    private fun initPageData() {
        // 初始化数据
    }
    
    // 生命周期方法重写
    override fun pageDidAppear() {
        super.pageDidAppear()
        // 页面显示时的逻辑
    }
    
    override fun pageDidDisappear() {
        super.pageDidDisappear()
        // 页面隐藏时的逻辑
    }
}

8.2 使用模块系统

// 模块使用示例
class MyCustomModule : IModule {
    override val moduleName: String = "MyCustomModule"
    
    private lateinit var pager: IPager
    
    override fun init(pager: IPager) {
        this.pager = pager
        // 初始化模块资源
    }
    
    override fun onPageAppear() {
        // 页面显示时执行
    }
    
    override fun onPageDisappear() {
        // 页面隐藏时执行
    }
    
    override fun destroy() {
        // 释放模块资源
    }
}

// 在页面中使用模块
val myModule = getModule<MyCustomModule>("MyCustomModule")
myModule?.doSomething()

8.3 响应式数据绑定

// 响应式数据绑定示例
class MyData {
    var title by mutableStateOf("Default Title")
    var count by mutableStateOf(0)
}

// 在页面中绑定数据
val myData = MyData()

// 绑定到视图
bindValueChange(object : ReactiveObserver {
    override fun onValueChanged(value: Any) {
        when (value) {
            is String -> {
                // 更新标题视图
            }
            is Int -> {
                // 更新计数视图
            }
        }
    }
})

// 更新数据触发视图更新
myData.title = "New Title"
myData.count = 42

9. 总结

KuiklyUI Pager架构设计体现了现代跨平台UI框架的核心思想,通过清晰的分层设计、模块化架构、响应式编程和跨平台适配,为开发者提供了强大而灵活的页面管理能力。Pager架构的主要优势包括:

  1. 高度模块化:支持核心模块和外部模块的注册与管理,便于功能扩展
  2. 响应式更新:通过ReactiveObserver实现数据与视图的响应式更新
  3. 完善的生命周期:提供完整的页面生命周期管理
  4. 跨平台适配:支持Android、iOS、鸿蒙等多平台适配
  5. 性能优化:包含延迟加载、布局优化、性能监控等机制

通过合理利用Pager架构的特性,开发者可以构建高性能、可维护的跨平台应用。

一个专业的前端如何在国内安装 `bun`

作者 Legend80s
2025年10月11日 14:42

image.png

本文以 macOS 为例,但思路也适用于 Windows 系统。

本文思路适用于任何使用 bash 安装的命令。学会后一劳永逸!

对于 bun 我们有多种安装方式,可以使用现有的包管理器比如 npm i -g bun

但是有个麻烦事:npm 一般是通过 nvm 安装的,如果 nvm 切换版本,则无法使用 buncommand not found: bun),还得继续 npm i 安装一遍,颇为麻烦。

所以 bun 官方一般推荐通过 shell 脚本的方式安装,以下安装命令来自 bun 官网 bun.com/docs/instal…

curl -fsSL https://bun.com/install | bash # for macOS, Linux, and WSL

[!TIPS] 其实 Windows 系统安装了 git bash 上述命令也是可用的。

但是如果直接运行我们会发现超时报错。通过下载安装脚本 bun.com/install 和搜索关键词 github 我们在 101 行发现:

GITHUB=${GITHUB-"https://github.com"}

github_repo="$GITHUB/oven-sh/bun"

原因很清楚了国内无法访问 github,修复也很简单找一个 proxy 即可,这里我用的是 gh-proxy.com/ (2025-09 可用)。而且可以预留了环境变量 GITHUB

GITHUB=${GITHUB-"https://github.com"} 意思是如果环境变量存在 GITHUB 则使用,否则兜底到 "https://github.com",对于 JS 的默认值(GITHUB = GITHUB || "https://github.com")。

第一步:移除无用包

可选。主要是为了删除无用包减少磁盘浪费,以及避免冲突。切换到曾经安装过 bun 的 node.js 版本。

nvm use 20 # 切换到之前安装过 bun 的 node.js 版本
npm uninstall -g bun

第二步:替换成可用 proxy

因为这里 bun 做得特别好,相对于 pnpm 安装脚本来说(pnpm 安装详见我的上一篇文章)预留了环境变量 GITHUB,也就是我们可以灵活替换 github 地址,相当于给了我们留了一个『后门』,于细微初见架构。故我们有三种安装 bun 的方式,从简单到容易。

方式一:环境变量 + 远程脚本

充分利用环境变量特点。

GITHUB='https://gh-proxy.com/https://github.com' curl -fsSL https://bun.com/install | bash # for macOS, Linux, and WSL

方式二:环境变量 + 本地脚本

下载 bun.com/install 到本地并取名 bun-install.sh 并修改成如下:

[!TIPS] 注意:.sh 结尾可选,因为 bash 命令会将其当做脚本运行,这里稳妥起见还是 .sh 结尾。

GITHUB='https://gh-proxy.com/https://github.com' bash ~/Downloads/bun-install.sh

会自动将下载地址改成(macOS 系统下):

# 此处无需我们修改,只是阐述背后发生的事情
- https://github.com/oven-sh/bun/releases/latest/download/bun-darwin-aarch64.zip
+ https://gh-proxy.com/https://github.com/oven-sh/bun/releases/latest/download/bun-darwin-aarch64.zip

执行效果:

######################################################################### 100.0%
bun was installed successfully to ~/.bun/bin/bun

Added "~/.bun/bin" to $PATH in "~/.zshrc"

To get started, run:

  exec /bin/zsh
  bun --help

确实看到 .zshrc 文件尾部增加了:

# bun completions
[ -s "/Users/legend80s/.bun/_bun" ] && source "/Users/legend80s/.bun/_bun"

# bun
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"

方式三:修改本地脚本

- GITHUB=${GITHUB-"https://github.com"}
+ GITHUB=${GITHUB-"https://gh-proxy.com/https://github.com"}

然后执行:

bash ~/Downloads/bun-install.sh

效果

重新开一个 terminal 让更新后的 .zshrc 生效或者直接 source .zshrc 然后,

试试 bun -v 输出 1.2.22(2025-09-27)。

再试试 bunx bunx ydd -e -s -c=a bun

一样成功 🎉。

总结

本文我们学会了不修改 shell 脚本安装开源命令,得益于环境变量以及默认值的灵活性。

啥时候给 pnpm 的脚本提个 issue 或 PR 让它也增加环境变量这样就更方便了。

Git和GitHub终极秘籍:50个命令让你从新手秒变专家

作者 微芒不朽
2025年10月8日 22:11

一、基本的 Git 命令

  1. 初始化一个新的 git 存储库

    git init
    
  2. 克隆现有存储库

    git clone https://github.com/user/repo.git
    
  3. 检查您的存储库的状态

    git status
    
  4. 暂存所有提交更改

    git add .
    
  5. 用有意义的信息做出承诺

    git commit -m "Add feature X with proper validation"
    
  6. 将提交推送到远程分支

    git push origin main
    
  7. 从远程提取最新更改

    git pull origin main
    
  8. 创建一个新分支并切换到它

    git checkout -b feature/new-ui
    
  9. 切换分支

    git checkout develop
    
  10. 在本地删除分支

    git branch -d feature/old-branch
    

二、GitHub 特定的命令和技巧

  1. 使用 GitHub CLI 创建新存储库

    gh repo create my-new-project --public --description "A new repository"
    
  2. 在 GitHub 页面上打开当前存储库

    gh repo view --web
    
  3. 列出 repo 的所有拉取请求

    gh pr list
    
  4. 在本地签出拉取请求

    gh pr checkout 42
    
  5. 通过 CLI 创建拉取请求

    gh pr create --title "Add new login flow" --body "Implemented OAuth2 login" --base main --head feature/login
    

三、分支与合并

  1. 将分支合并到当前分支中

    git merge feature/new-ui
    
  2. 将当前分支变基到 main

    git rebase main
    
  3. 如果发生冲突,则中止合并或变基

    git merge --abort
    

四、撤消和重置

  1. 提交前取消暂存文件

    git reset HEAD <file>
    
  2. 丢弃文件中未暂存的更改

    git checkout -- <file>
    
  3. 通过创建新提交来恢复提交

    git revert <commit_hash>
    
  4. 重置提交和更改(危险:谨慎使用)

    git reset --hard HEAD~1
    

五、有用的 Git 别名示例

  1. 快速状态命令

    git config --global alias.s 'status -s'
    
  2. 用图表和颜色记录一条线

    git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
    

六、管理远程存储库

  1. 添加远程源

    git remote add origin https://github.com/user/repo.git
    
  2. 更改远程源的 URL

    git remote set-url origin git@github.com:user/repo.git
    
  3. 移除遥控器

    git remote remove origin
    

七、标记发布

  1. 创建带注释的标签

    git tag -a v1.0 -m "Version 1.0 release"
    
  2. 将标签推送到遥控器

    git push origin v1.0
    
  3. 删除远程标签

    git push --delete origin v1.0
    

八、GitHub Actions 片段

  1. Node.js项目的基本工作流程

    name: Node CI
    
    on: [push, pull_request]
    
    jobs:
      build:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-node@v3
            with:
              node-version: '16'
          - run: npm install
          - run: npm test
    
  2. 在多个 Node 版本上运行测试

    strategy:
      matrix:
        node-version: [12, 14, 16]
    
    steps:
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
    
  3. GitHub Actions 中的缓存依赖项

    - name: Cache NPM modules
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-npm-
    
  4. 上传工件

    - uses: actions/upload-artifact@v3
      with:
        name: my-artifact
        path: path/to/file
    
  5. 在 GitHub Actions 中通知 Slack

    - name: Slack Notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        fields: repo,message,commit,author
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    

九、GitHub CLI 片段

  1. 列出当前用户的存储库

    gh repo list --limit 10
    
  2. 创建一个新问题

    gh issue create --title "Bug report" --body "There is an issue with login"
    
  3. 查看拉取请求详细信息

    gh pr view 123 --web
    
  4. 关闭问题

    gh issue close 42
    
  5. 将自己分配给一个问题

    gh issue edit 42 --add-assignee @me
    

十、GitHub Markdown 片段

  1. 任务列表语法

    - [x] Item 1 done
    - [ ] Item 2 pending
    
  2. 表格示例

    | Feature  | Supported |
    | -------- | --------- |
    | Login    | ✅        |
    | Signup   | ❌        |
    
  3. 嵌入图像

    ![Alt text](https://via.placeholder.com/150)
    

十一、合作与审查

  1. 通过 CLI 合并拉取请求

    gh pr merge 123 --squash --delete-branch
    
  2. 请求团队成员审核

    gh pr review 123 --request "username"
    
  3. 查看 PR 中的更改

    gh pr diff 123
    

十二、使用 GitHub Actions 实现自动化

  1. PR 上的自动标签

    name: Label PRs
    
    on:
      pull_request:
        types: [opened, synchronize]
    
    jobs:
      label:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/labeler@v3
    
  2. 自动关闭过时问题

    name: Close stale
    
    on:
      schedule:
        - cron: '0 0 * * 0'
    
    jobs:
      stale:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/stale@v3
    

十三、Git 提示与技巧

  1. 显示目录中每个文件的上次提交

    git ls-files | xargs -I{} git log -1 --format="%h %ad %an" -- {}
    
  2. 以交互方式压缩最后 N 个提交

    git rebase -i HEAD~3
    

alien-signals 系列 —— 认识下一代响应式框架

作者 JinSo
2025年10月8日 16:16

前段时间,社区掀起了一股 alien-signals 的热潮,其惊人的性能表现让许多开发者印象深刻。作为一名前端开发者,我深入研究了它的源码,探索其性能优势的秘密。

本文将开启一个全新的系列,深入剖析 alien-signals 的使用方法、实现原理和性能优化等内容。让我们一起揭开 alien-signals 的神秘面纱。

为什么要关注 alien-signals?

在深入技术细节之前,让我们先看看 alien-signals 在性能基准测试中的表现。根据 js-reactivity-benchmark 的数据,alien-signals 在各项性能指标上都遥遥领先:

image.png

各响应式框架性能对比,alien-signals 在各项指标上遥遥领先

这样的性能表现,加上 Vue 3.6 即将采用 alien-signals 重构响应式系统的消息,使得它成为了前端社区的焦点。

Signals 概念

如果对 Signals 概念还不熟悉,推荐先阅读我之前的文章:理解 Signal 是如何工作的,其中详细介绍了 Signals 的基本原理和工作机制。

简单来说,Signals 是一种响应式编程模式,它允许我们创建可观察的值和计算属性,当依赖发生变化时自动更新。

响应式库

先简单了解一下什么是响应式框架:

Reactivity is the future of JS frameworks! Reactivity allows you to write lazy variables that are efficiently cached and updated, making it easier to write clean and fast code. —— 引自Super Charging Fine-Grained Reactive Performance · milomg.dev

响应式是 JS 框架的未来!响应性允许您编写有效缓存和更新的惰性变量,从而更轻松地编写干净、快速的代码。

而且响应式库是 Solid、Qwik、Vue 和 Svelte 等现代 Web 组件框架的核心。在某些情况下,您可以将细粒度的响应式状态管理添加到其他库,例如 Lit 和 React。

关于响应式算法的深入探讨,推荐阅读 深入探索细粒度响应式框架的性能优化 ,其中详细对比了 MobX、Preact Signals 和 Reactively 三种主流算法的实现原理。

响应式框架的目标

响应式库的目标是在响应式函数的源发生变化时运行响应式函数。

此外,响应式库还应该是:

  • 高效:永远不要过度执行响应式元素(如果它们的源没有改变,请不要重新运行)
  • 无故障:永远不要允许用户代码看到只有一些响应式元素更新的中间状态(当你运行响应式元素时,每个源都应该更新)

响应式框架的分类

在深入 alien-signals 之前,我们需要了解响应式框架的基本分类。

  • 按执行时机分类

    响应式框架根据执行时机可以分为两大类:

    1. Lazy(惰性) :只在结果被访问时才进行计算,延迟执行,按需计算的思想能有效减少冗余计算。

      • 代表:Solid.js、Preact Signals、Vue、alien-signals
    2. Eager(即时性) :数据变化时立即计算,实时响应,但可能导致频繁计算的性能问题。

      • 代表:MobX

    💡 关于 Solid 的 Signals 的原理,可以参考我的另一篇文章:Solid 之旅 —— Signal 响应式原理

  • 按更新传播算法分类

    根据 维基百科对响应式编程的定义,更新传播算法可以分为三种模式:

    1. Push-based(推送模型)

      • 相当于 Eager 模型
      • 依赖项变化时立即推送完整的变化信息给订阅者
      • 类似于服务端向客户端的 SSE 推送机制
    2. Pull-based(拉取模型)

      • 相当于 Lazy 模型
      • 只在需要时(如读取 computed 值)才拉取依赖项的变化
      • 类似于客户端向服务端的轮询机制
    3. Push-pull hybrid(推拉混合模型)

      • 结合了推送和拉取的优点
      • Push 阶段:依赖项变化时推送脏标记,通知订阅者需要更新
      • Pull 阶段:订阅者在需要时拉取具体的变化值

Vue Signals 进化论

要理解 alien-signals 的由来,我们需要回顾 Vue 响应式系统的演进历程。这部分内容深受 Vue Signals 进化论系列文章 的启发。

  • Vue 3.5:借鉴 Preact Signals

    Vue 3.5 的响应式重构直接受到了 Preact Signals 的启发,主要借鉴了两个核心设计:

    1. 版本计数(Version Counting) :用于快速判断依赖是否发生变化
    2. 双向链表结构:高效管理依赖关系

    这些设计思想将在后续的系列文章中详细解析。

  • Vue 3.6:拥抱 alien-signals

    alien-signals 在继承 Preact Signals 优秀设计的基础上,进行了大量性能优化:

    • 延续双向链表设计:保持了高效的依赖管理
    • 简化节点属性:减少内存占用
    • 贴近 Vue 的命名规范:更好的生态融合
    • 极致的性能优化:包括模拟递归调用栈、内存对齐、位运算等技巧

    虽然这些优化在一定程度上牺牲了代码的可读性,但换来的是卓越的运行时性能。

使用

基础 API 使用

alien-signals 提供了简洁直观的 API:

import { signal, computed, effect } from 'alien-signals';

// 创建响应式信号
const count = signal(1);

// 创建计算属性  
const doubleCount = computed(() => count() * 2);

// 创建副作用
effect(() => {
  console.log(`Count is: ${count()}`);
}); 
// 输出: Count is: 1

// 读取计算属性
console.log(doubleCount()); 
// 输出: 2

// 更新信号值
count(2); 
// 触发 effect,输出: Count is: 2

// 计算属性自动更新
console.log(doubleCount()); 
// 输出: 4

Effect Scope

alien-signals 还提供了作用域管理功能,方便批量管理副作用:

import { signal, effect, effectScope } from 'alien-signals';

const count = signal(1);
// 创建作用域
const stopScope = effectScope(() => {
  effect(() => {
    console.log(`Count in scope: ${count()}`);
  });
  // 输出: Count in scope: 1
});

count(2);
// 输出: Count in scope: 2

// 停止作用域内的所有副作用
stopScope();

count(3);
// 不再有输出,因为 effect 已被清理

架构设计

了解了基本使用后,让我们简单地了解 alien-signals 的架构设计。alien-signals 采用了优雅的双层架构设计:

1. System 层(响应式系统核心)

提供了 createReactiveSystem() 工厂函数,用于构建自定义响应式 API:

import { createReactiveSystem } from './system.js';

const {
  link,  // 建立依赖关系
  unlink,  // 解除依赖关系
  propagate,  // 传播更新
  checkDirty,  // 检查脏状态
  endTracking,  // 结束依赖追踪
  startTracking,  // 开始依赖追踪
  shallowPropagate,  // 浅层传播
} = createReactiveSystem({
  update,  // 更新回调
  notify,  // 通知回调
  unwatched,  // 未观察状态回调
});

2. Surface API 层(面向用户的 API)

基于 System 层构建的标准响应式 API,包括 signal、computed、effect 等。您可以参考 alien-signals 的实现 来构建自己的响应式 API。

总结

本文介绍了 alien-signals 的背景、核心概念以及基本使用方法。我们了解到:

  • alien-signals 是当前性能最优秀的响应式框架之一
  • 它继承了 Preact Signals 的优秀设计,并进行了极致的性能优化
  • Vue 3.6 即将采用它来重构响应式系统,足见其价值
  • 其双层架构设计既保证了性能,又提供了良好的扩展性

在下一篇文章中,我将深入 alien-signals 的 system 层,详细剖析其响应式机制的实现原理,包括依赖追踪、更新传播、脏检查等核心算法。敬请期待!

参考资料

  1. Vue Signals 进化论(v3.5):Preact 重构启示录
  2. Vue Signals 进化论(v3.6):Alien Signals 终局之战?
  3. Reactive programming - Wikipedia
  4. Super Charging Fine-Grained Reactive Performance
  5. stackblitz/alien-signals: 👾 The lightest signal library
❌
❌