阅读视图

发现新文章,点击刷新页面。

纯 CSS 实现弹性文字效果

原文:How to Create a CSS-only Elastic Text Effect

翻译:TUARAN

欢迎关注 前端周刊,每周更新国外论坛的前端热门文章,紧跟时事,掌握前端技术动态。

每个字母单独动画的文字效果总是很酷、很吸睛。这类错峰动画通常依赖 JavaScript 库实现,对我们要实现的这种相对轻量的设计效果来说,代码往往偏重。本文将探索只用 CSS、无需 JavaScript 实现 fancy 文字效果的技巧(意味着需要手动拆分字符)。

截至撰写时,仅 Chrome 和 Edge 完全支持我们使用的特性。

将鼠标悬停在下方演示的文字上,即可看到效果:

CodePen Embed Fallback

很酷吧?仅靠 CSS 就实现了逼真的弹性效果,而且灵活易调。在深入代码之前,先做一个重要声明。这个效果不错,但有几个明显的缺点。

关于可访问性的重要声明

我们要做的效果依赖于把单词拆成单个字母,一般来说这种做法非常不推荐。

一个简单链接通常是这样写的:

<a href="#">About</a>Code language: HTML, XML (xml)

但要分别控制每个字母的样式,我们会改成这样:

<a href="#">
  <span>A</span><span>b</span><span>o</span><span>u</span><span>t</span>
</a>Code language: HTML, XML (xml)

这会带来可访问性问题。

很容易想到用 aria-* 属性来弥补。至少我之前是这么想的。网上有不少资料推荐类似下面的结构:

<a href="#" aria-label="About">
  <span aria-hidden="true">
    <span>A</span><span>b</span><span>o</span><span>u</span><span>t</span>
  </span>
</a>Code language: HTML, XML (xml)

看起来没问题吧?不!这种结构依然很糟糕。实际上,网上能找到的大多数结构都有问题。我不是这个领域的专家,所以请教了一些人,发现 Adrian Roselli 的两篇博客很有参考价值:

强烈建议读一读,理解为什么把单词拆成字母是个坏主意(以及可能的替代方案)。

那我为什么还要做这个演示?

我更倾向于把它当作一次探索现代 CSS 特性的实验。这个效果里可能有很多你还不熟悉的属性,是了解它们的好机会。可以用在娱乐或 side project 中,但在广泛使用或关键场景中引入前,请三思。

好了,声明完毕,我们开始。

原理说明

思路是使用 offset() 属性,定义字母沿一条路径运动。这条路径是一条曲线,我们沿曲线做动画。offset() 是一个被低估的特性,但潜力很大,尤其配合现代 CSS 使用时。我曾用它做过无限跑马灯动画、让元素沿圆精确排布、做图片画廊等。

下面是一个简化示例,帮助理解我们要用的技巧:

CodePen Embed Fallback

上面的演示使用了来自 SVG 的 path() 值。三个字母最初沿第一条路径,悬停时切换到第二条路径。借助 transition,就形成了平滑的效果。

可惜的是,使用 SVG 并不理想,因为你只能创建静态、基于像素的路径,无法用 CSS 控制。因此我们将转而使用新的 shape() 函数,它可以定义复杂形状(包括曲线),并方便地用 CSS 控制。

本文只用到 shape() 的简单用法(只需要一条曲线),如果想深入了解这个强大函数,可以参考我之前的文章:

开始写代码

用到的 HTML:

<ul>
  <li>
    <a href="#"><span>A</span><span>b</span><span>o</span><span>u</span><span>t</span></a>
  </li>
  <!-- 更多 li 元素 -->
</ul>Code language: HTML, XML (xml)

CSS:

ul li a {
  display: flex;
  font-family: monospace;
}
ul li a span {
  offset-path: shape(???);
  offset-distance: ???;
}
ul li a:hover {
  offset-path: shape(???);
}Code language: CSS (css)

目前还比较朴素

CodePen Embed Fallback

用 flex 让字母并排,并用等宽字体,确保每个字母宽度一致。

接下来用下面的代码定义路径:

offset-path: shape(from Xa Ya, curve to Xb Yb with Xc Yc / Xd Yd );Code language: CSS (css)

这里用 curve 命令在 A 到 B 之间画贝塞尔曲线,控制点为 C 和 D。

然后通过调整控制点的坐标(尤其是 Y 值)来驱动曲线动画。当 Y 与 A、B 的 Y 相同时是直线;更大时变成曲线。

曲线的代码大致如下:

offset-path: shape(from Xa Y, curve to Xb Y with Xc Y1 / Xd Y1);

直线的代码如下:

offset-path: shape(from Xa Y, curve to Xb Y with Xc Y / Xd Y);

注意我们只改控制点的 Y,其他保持不变。

现在来确定各参数。使用 offset 时有两个要点:

  1. 默认以元素中心作为在路径上的位置。
  2. 定义在子元素上,但参考框是父容器。

第一个字母应在路径起点,最后一个在终点,所以 A 是第一个字母中心,B 是最后一个字母中心:

Y = 50%Xa = .5chXb = 100% - Xa = 100% - .5ch

C 和 D 的 X 没有固定规则,可以任意指定。我选 Xc = 30%Xd = 100% - Xc = 70%。你可以自己调整这些值试验不同的曲线形态。

路径现在可以这样写:

offset-path: shape(from .5ch 50%, curve to calc(100% - .5ch) 50% with 30% Y / 70% Y);

Y 是变量,可以是 50%(与 A、B 相同)或别的值,我们设成 50% - HH 越大,弹性越强。

试试看:

CodePen Embed Fallback

一团糟!因为我们没定义 offset-distance,所有字母都叠在一起了。

是不是要给每个字母单独设位置?那太麻烦了。

我们必须给每个字母不同的位置,好在可以用一个公式配合 sibling-index()sibling-count() 搞定。

第一个字母在 0%,最后一个在 100%。共 N 个字母,步长为 100%/(N - 1),字母从 0%100% 依次排布,公式为:

offset-distance: (100% * i)/(N - 1)

其中 i 从 0 开始。

写成 CSS:

offset-distance: calc(100%*(sibling-index() - 1)/(sibling-count() - 1))Code language: CSS (css)

CodePen Embed Fallback

几乎完美。除了最后一个字母外都位置正确。由于某种原因,0%100% 被当成同一个点。offset-distance 不限于 0%–100%,可以取任意值(包括负值),有一种取模行为形成环路。你可以从 0%100% 走完整条路径,到 100% 后又回到起点,还能继续从 100%200%,如此往复。

虽然有点反直觉,但修复很简单:把 100% 换成 99.9%。有点 hack,但有效!

CodePen Embed Fallback

现在排布完美了,悬停时可以看到直线变成曲线的过程。

最后加上 transition,就大功告成!

CodePen Embed Fallback

可能还不算完全搞定,因为动画似乎有些异常。这很可能是 bug(我已在此提交),不过问题不大,因为我本来就打算重构,避免重复写两次 shape,改为动画一个变量:

@property --_s {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}
ul li a {
  --h: 20px; /* 控制效果强度 */
 
  display: flex;
  font: bold 40px monospace;
  transition: --_s .3s;
}
ul li a:hover {
  --_s: 1;
}
ul li a span {
  offset-path: 
    shape(
      from .5ch 50%, curve to calc(100% - .5ch) 50% 
      with 30% calc(50% - var(--_s)*var(--h)) / 70% calc(50% - var(--_s)*var(--h))
    );
  offset-distance: calc(99.9%*(sibling-index() - 1)/(sibling-count() - 1));
}Code language: CSS (css)

现在有了 --h 变量来调节路径曲率,以及一个内部变量在 0 到 1 之间动画,实现从直线到曲线的过渡。

CodePen Embed Fallback

嗒哒!动画完美了!但弹性感呢?

要得到弹性效果,需要调整缓动,用到 linear()。这是最简单的部分,我用生成器生成取值。

多调几次直到满意。我得到的是:

CodePen Embed Fallback

效果已经不错,但如果微调曲线还能更好。目前所有单词的曲线「高度」是一样的,理想情况是根据单词长度变化。为此我会在公式里加入 sibling-count(),让单词越宽时高度越大。

CodePen Embed Fallback

让效果具备方向感知

效果已经可用,但既然做到这里,不妨再进一步:根据鼠标方向决定曲线向上还是向下。

向上的曲线已经通过 --_s: 1 实现:

ul li a:hover {
  --_s: 1;
}Code language: CSS (css)

若改为 -1,就得到向下的曲线:

CodePen Embed Fallback

现在需要把两种情况结合起来。从上方悬停时,使用向下曲线 --_s: -1;从下方悬停时,使用向上曲线 --_s: 1

首先给 li 加一个伪元素,填满上半部分并位于链接上方:

ul li {
  position: relative;
}
ul li:after {
  content: "";
  position: absolute;
  inset: 0 0 50%;
  cursor: pointer;
}Code language: CSS (css)

CodePen Embed Fallback

然后定义两个不同的选择器。当悬停伪元素时,相当于也悬停了 li,所以可以用:

ul li:hover a {
  --_s: -1;
}Code language: CSS (css)

悬停 a 时,同样会悬停 li,上面的规则也会生效。但若悬停的是伪元素,则没有悬停 a,因此可以用:

ul li:has(a:hover) a {
  --_s: 1;
}Code language: CSS (css)

有点绕?没关系,我们把两个选择器放在一起看:

ul li:hover a {
  --_s: -1;
}
ul li:has(a:hover) a {
  --_s: 1;
}Code language: CSS (css)

我们可以从上方(通过伪元素)或从下方(通过 a)悬停。前者会触发第一个选择器,因为我们在悬停 li,但不会触发第二个,因为 li「并没有悬停其 a」。当我们悬停 a 时,两个选择器都会触发,后者会胜出。

方向感知就这么实现了!

CodePen Embed Fallback

能用,但不如开头的演示那么流畅。当鼠标移动穿过整个元素时,会突然停止一个动画并切换到另一个。

可以调整伪元素的大小来改善。悬停时让它覆盖整个元素,这样就不会再触达下方的 a,第二个动画就不会触发。而悬停 a 时,把伪元素高度设为 0,就无法悬停它,从而不会触发第一个动画。

CodePen Embed Fallback

好多了!把伪元素设为透明,效果就很自然。

CodePen Embed Fallback

小结

希望你喜欢这次 CSS 小实验。再提醒一次:在项目中投入使用前请三思。这是一个很好的 demos 来了解 shape()linear()sibling-index() 等现代特性,但为这类效果牺牲可访问性并不值得。

Vue 底层原理 & 新特性

Vue 底层原理 & 新特性

本文深入探讨 Vue 的底层架构演进、核心原理以及最新版本带来的突破性特性,面向面试和技术提升。


原文地址

墨渊书肆/Vue 底层原理 & 新特性


Vue 版本变动历史

Vue 自发布以来经历了多个重要版本的迭代,每个版本的改动都带来了架构优化和新特性,同时也伴随着一些 Breaking Changes。以下是 Vue 各个重要版本的变动概述:

Vue 2.0 (2016年)

  • 引入 Virtual DOMVue2 正式引入了虚拟 DOM,这是框架性能提升的关键技术。
  • 组件系统增强:增加了异步组件生命周期钩子调整等特性。
  • 支持 SSR:原生支持服务器端渲染,提升了 SEO 和首屏加载性能。
  • Vuex 与 Vue Router:作为官方解决方案提供状态管理路由管理

Vue 2.5 - 2.7 (2017-2022年)

  • Vue 2.5:改进了 TypeScript 支持,增强了响应式系统
  • Vue 2.6:引入了新的模板编译策略,插槽语法改进。
  • Vue 2.7:作为 Vue2 最后的大版本,引入了一些 Composition API 的向下兼容实现,为 Vue3 迁移做铺垫。

Vue 3.0 (2022年)

  • Composition API:引入了全新的组合式 API,提供了更灵活的逻辑组织方式。
  • Proxy 响应式系统:使用 Proxy 替代 Object.defineProperty,解决了 Vue2 响应式的诸多痛点。
  • Teleport & Fragments:新增内置组件,支持跨 DOM 层级渲染和多根节点模板。
  • 性能提升:更快的解析速度和更小的运行时体积,渲染性能提升约 100%。
  • 更好的 TypeScript 支持:原生支持 TypeScript,类型推导更加完善。
  • 自定义渲染器 API:增强的渲染器 API,便于跨平台开发。

Vue 3.1 - 3.4 (2023-2024年)

  • Vue 3.1:引入了 defineOptions 宏,改进编译优化。
  • Vue 3.3:进一步改进宏支持,类型化 props/emits 更加方便,简化了泛型组件的使用。
  • Vue 3.4:性能进一步提升,响应式系统优化,编译器效率改进。

Vue 3.5 及未来 (2024-2025年)

  • Vue 3.5:引入了响应式解构语法(Reactivity Transform),改善了大型应用的开发体验。
  • Vapor ModeVue 团队正在实验的全新渲染策略,跳过虚拟 DOM直接生成高效的 JavaScript 代码。
  • 更完善的生态集成:与 Vite 5PiniaVue Router 4 的深度整合。

响应式原理深度解析

响应式系统是 Vue 的核心,也是面试中的高频考点。Vue2 和 Vue3 在响应式实现上有着本质的区别。

Vue2:Object.defineProperty

Vue2 使用 Object.defineProperty 来劫持数据的 getter 和 setter:

function defineReactive(obj, key, val) {
  // 为每个属性创建 Dep 实例
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return
      // 通知更新
      dep.notify()
    }
  })
}

Vue2 响应式的局限性

  1. 无法检测对象属性的添加/删除Object.defineProperty 只能劫持已存在的属性,对于新增属性无能为力。
  2. 数组操作无法响应:通过下标修改数组元素 arr[0] = value 不会触发更新。
  3. 深层监听需要递归:对深层对象的监听会带来性能开销。

解决方案:Vue2 提供了 Vue.set / Vue.delete 以及重写数组方法来应对这些场景。

Vue3:Proxy

Vue3 使用 ES6 的 Proxy 来实现响应式:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      // 如果是对象,递归代理实现深层响应式
      return isObject(result) ? reactive(result) : result
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新
      trigger(target, key)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    }
  })
}

Vue3 响应式的优势

  1. 原生支持属性增删:Proxy 可以拦截对象的所有操作,包括新增和删除属性。
  2. 数组操作完全响应:下标赋值、数组长度变化等都能被正确拦截。
  3. 更好的性能:Proxy 是懒执行的,只有当访问属性时才会进行依赖收集。
  4. API 统一:ref 和 reactive 内部实现统一,简化了学习成本。

依赖收集与触发机制

Vue 的响应式系统遵循观察者模式,包含三个核心角色:

  1. Observer(观察者):负责劫持数据,收集依赖。
  2. Dep(依赖管理器):存储依赖,管理订阅者。
  3. Watcher(订阅者):在数据变化时执行更新回调。
// Dep 实现
class Dep {
  constructor() {
    this.subs = new Set() // 存储 Watcher
  }
  
  depend() {
    if (Dep.target) {
      this.subs.add(Dep.target)
    }
  }
  
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// Watcher 实现
class Watcher {
  constructor(fn) {
    this.getter = fn
    this.value = this.get()
  }
  
  get() {
    Dep.target = this
    const value = this.getter()
    Dep.target = null
    return value
  }
  
  update() {
    this.value = this.getter()
  }
}

模板编译原理

Vue 的模板编译是将模板字符串转换为可执行渲染函数的过程,主要分为三个阶段。

1. 解析阶段(Parse)

将模板字符串解析为 AST(抽象语法树):

// 模板
<div class="container">
  <h1>{{ title }}</h1>
</div>

// AST 结构
{
  type: 'Element',
  tag: 'div',
  props: [{ type: 'Attribute', name: 'class', value: 'container' }],
  children: [
    {
      type: 'Element',
      tag: 'h1',
      children: [{ type: 'Interpolation', content: { expression: 'title' } }]
    }
  ]
}

2. 优化阶段(Optimize)

Vue3 的编译器会进行静态节点提升(Static Hoisting):

  • 静态节点:不包含任何响应式依赖的节点(如纯文本、静态属性)。
  • 事件缓存:对于不响应式变化的事件处理函数,进行缓存处理。
// 优化前
render() {
  return h('button', { onClick: this.handleClick }, 'Click')
}

// 优化后 - 事件函数被缓存
const handleClick = this.handleClick
render() {
  return h('button', { onClick: handleClick }, 'Click')
}

3. 代码生成阶段(Generate)

将 AST 转换为渲染函数:

// 生成的渲染函数
function render() {
  return _vue.createVNode('div', { class: 'container' }, [
    _vue.createVNode('h1', null, _vue.toDisplayString(this.title))
  ])
}

虚拟 DOM 与 Diff 算法

虚拟 DOM 的本质

虚拟 DOM 是真实 DOM 的 JavaScript 对象表示:

// VNode 结构
const vnode = {
  type: 'div',
  props: { class: 'container' },
  children: [
    { type: 'h1', children: 'Hello' }
  ],
  el: null // 关联的真实 DOM 引用
}

虚拟 DOM 的优势

  1. 跨平台渲染:同一套 VNode 结构可以渲染到不同平台。
  2. 减少 DOM 操作:在内存中进行对比,只更新必要的真实 DOM。
  3. 声明式开发:开发者只需关注数据变化,框架自动处理 DOM 更新。

Vue2 Diff:单端比较

Vue2 采用传统的 Diff 算法,从左到右依次对比:

function updateChildren(oldChildren, newChildren) {
  let oldStartIndex = 0
  let newStartIndex = 0
  let oldEndIndex = oldChildren.length - 1
  let newEndIndex = newChildren.length - 1
  
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 简单比较...O(n) 复杂度
  }
}

Vue3 Diff:双端比较 + 最长递增子序列

Vue3 采用了更高效的 Diff 算法

  1. 双端比较:同时从新旧列表的首尾进行对比。
  2. key 映射:通过 Map 快速定位相同 key 的节点。
  3. 最长递增子序列:对于需要移动的节点,使用 LIS 算法最小化移动次数。
// Vue3 Diff 核心逻辑
function diffChildren(n1, n2, parent) {
  const c1 = n1.children
  const c2 = n2.children
  const oldStart = 0
  const newStart = 0
  const oldEnd = c1.length - 1
  const newEnd = c2.length - 1
  
  // 双端比较策略
  while (oldStart <= oldEnd && newStart <= newEnd) {
    if (c1[oldStart].key === c2[newStart].key) {
      // 节点相同,继续
      patch(c1[oldStart], c2[newStart], parent)
      oldStart++
      newStart++
    } else if (c1[oldEnd].key === c2[newEnd].key) {
      // 尾部匹配
      patch(c1[oldEnd], c2[newEnd], parent)
      oldEnd--
      newEnd--
    }
    // ... 更多比较策略
  }
}

组件生命周期与更新机制

Vue2 生命周期

阶段 钩子 说明
初始化 beforeCreate 实例刚创建,数据观测未完成
初始化 created 数据观测完成,DOM 未生成
挂载 beforeMount 模板编译完成,准备挂载
挂载 mounted DOM 挂载完成,可操作 DOM
更新 beforeUpdate 数据变化,DOM 未更新
更新 updated DOM 更新完成
销毁 beforeDestroy 实例销毁前,可清理
销毁 destroyed 实例已销毁

Vue3 生命周期

Vue2 Vue3 (Composition API)
beforeCreate -
created -
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted

组件更新流程

数据变化 → 触发 setter → Dep 通知 Watcher → 
触发 update() → 重新执行 render() 生成新的 VNode → 
Diff 对比 → 更新真实 DOM

Vue3 新特性深度解析

1. Composition API

组合式 API 是 Vue3 最重要的变化,提供了更灵活的逻辑组织方式:

// setup 函数 - 组件逻辑入口
import { ref, computed, onMounted, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    onMounted(() => {
      console.log('Component mounted!')
    })
    
    watch(count, (newVal) => {
      console.log(`Count changed to ${newVal}`)
    })
    
    return { count, doubled, increment }
  }
}

ref vs reactive

  • ref:用于原始类型,创建包含 .value 的响应式对象。
  • reactive:用于对象,创建深层响应式对象。
import { ref, reactive } from 'vue'

const count = ref(0)        // 原始类型
const state = reactive({   // 对象类型
  user: { name: 'Vue' }
})

// 模板中自动解包
console.log(count.value)   // JS 中需要 .value
console.log(state.user)    // reactive 直接访问

2. Teleport

将组件渲染到指定 DOM 位置,常用于模态框:

<Teleport to="body">
  <div v-if="show" class="modal">
    <p>Modal Content</p>
  </div>
</Teleport>

3. Fragments

支持多根节点模板:

<!-- Vue3 允许 -->
<template>
  <div>A</div>
  <div>B</div>
</template>

4. Suspense

处理异步组件加载状态:

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

Vue vs React:核心差异对比

响应式实现

特性 Vue2 Vue3 React
原理 Object.defineProperty Proxy useState/useReducer
触发方式 自动 自动 手动调用 setState
数组响应 重写方法 Proxy 需使用 Immer 或 immutable
深层监听 递归 Proxy 懒加载 useEffect 依赖

模板 vs JSX

  • Vue:模板语法,HTML-like,学习成本低,编译器优化。
  • React:JSX,JavaScript 表达式,更灵活,但需要一定学习曲线。

状态管理

  • Vue:Pinia(推荐)或 Vuex,采用模块化设计。
  • React:Redux/Zustand/Jotai,函数式风格。

渲染性能

Vue3 由于模板编译优化和 Proxy 响应式,在大多数场景下性能优于 React。React 的优势在于 Fiber 架构带来的精细化控制和并发渲染能力。


性能优化策略

1. 渲染优化

// 使用 v-once 静态内容
<div v-once>{{ staticContent }}</div>

// 正确使用 key
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

// v-if vs v-show 选择
<div v-if="show">很少切换</div>
<div v-show="show">频繁切换</div>

2. 响应式优化

import { shallowRef, markRaw } from 'vue'

// 浅层响应式 - 适合大型数据
const largeList = shallowRef([])

// 非响应式数据 - 适合不需要响应式的对象
const plainObj = markRaw({ /* ... */ })

3. 组件懒加载

// 路由懒加载
const Home = () => import('./views/Home.vue')

// 异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))

4. KeepAlive 缓存

<KeepAlive include="Home,About">
  <router-view />
</KeepAlive>

面试常见问题汇总

1. Vue2 和 Vue3 响应式的区别?

Vue2 使用 Object.defineProperty,需要递归监听所有属性,无法检测新增/删除属性;Vue3 使用 Proxy,原生支持属性增删,性能更好。

2. Vue 的依赖收集是如何实现的?

通过 Dep 类管理订阅者,Watcher 在读取响应式属性时将自身添加到 Dep,属性变化时 Dep 通知所有 Watcher 更新。

3. Vue3 Diff 算法相比 Vue2 有什么优化?

Vue3 采用 双端比较 策略,结合 最长递增子序列 算法,最小化 DOM 移动次数,复杂度从 O(n³) 优化到 O(n)。

4. Vue3 的 Composition API 有什么优势?

  • 更好的 TypeScript 支持
  • 代码更容易复用和抽取
  • 逻辑相关代码组织在一起,而不是按选项分散

5. Vue3 的性能为什么比 Vue2 好?

  • Proxy 替代 Object.defineProperty,深层监听懒执行
  • 模板编译优化:静态节点提升事件缓存
  • 优化的 Diff 算法
  • 更小的打包体积

6. Vue 的 nextTick 原理?

Vue 使用 Promise + MutationObserver + setTimeout 实现异步队列,在 DOM 更新后通过微任务执行回调。

7. keep-alive 的实现原理?

通过缓存 VNode,保存组件实例和状态,切换时复用而非重新创建。activated/deactivated 钩子用于感知缓存状态变化。

8. Vue 的模板编译过程?

解析优化(静态节点提升)→ 代码生成(渲染函数)


总结

Vue 作为一个渐进式框架,在保持易用性的同时不断深化底层技术的实现。Vue3 通过 Composition API、Proxy 响应式系统、优化的 Diff 算法等特性,显著提升了开发体验和运行性能。理解这些底层原理不仅有助于应对面试,更能在实际开发中做出更好的技术决策。

Vue 团队正在探索的 Vapor Mode 未来可能带来更大的性能突破,值得持续关注。

Vue 基础理论 & API 使用

Vue 基础理论 & API 使用

本文主要记录 Vue 的基础理论、核心概念与常用 API 使用方法,面向面试和日常开发参考。


原文地址

墨渊书肆/Vue 基础理论 & API 使用


Vue 简介

Vue 是一个渐进式 JavaScript 框架,由尤雨溪于 2014 年创建。Vue 核心库聚焦于视图层,易于学习和集成,同时能够驱动复杂的单页应用程序(SPA)开发。

核心特点

  • 响应式数据绑定 (MVVM 模式)
  • 组件化开发
  • 虚拟 DOM
  • 指令系统
  • 渐进式架构

安装与项目创建

Vite(推荐)

# 创建 Vue3 项目
npm create vue@latest

# 或使用 Vite 直接创建
npm create vite@latest my-vue-app -- --template vue

Vue CLI

npm install -g @vue/cli
vue create my-project

基础指令

v-model 双向绑定

v-modelVue 中用于表单输入和数据双向绑定的核心指令,本质是 v-bind + v-on语法糖

基本用法

<input v-model="message">
<p>{{ message }}</p>

修饰符

修饰符 说明
.lazy 在 change 事件时更新,而非 input
.number 自动转换为数值
.trim 去除首尾空白

自定义 v-model(Vue 3.4+):

// 子组件
defineProps(['modelValue'])
defineEmits(['update:modelValue'])

// 父组件
<MyInput v-model:title="title" />

v-if / v-show 条件渲染

特性 v-if v-show
DOM 操作 创建/销毁 display: none
初始渲染 惰性 立即渲染
切换性能
适用场景 很少切换 频繁切换
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>

v-for 列表渲染

<li v-for="(item, index) in items" :key="item.id">
  {{ index }} - {{ item.name }}
</li>

注意事项

  • 必须使用 :key 绑定唯一标识
  • 不建议使用数组索引作为 key
  • Vue2 中 v-for 优先级高于 v-if,Vue3 中相反

v-bind / v-on 属性与事件

<!-- 绑定属性 -->
<img :src="url">

<!-- 绑定多个属性 -->
<img v-bind="attrs">

<!-- 事件监听 -->
<button @click="handleClick">Click</button>

<!-- 事件修饰符 -->
<button @click.stop="handle">阻止冒泡</button>
<button @click.prevent="handle">阻止默认行为</button>

组件选项

data

组件的响应式数据源,必须返回纯对象:

export default {
  data() {
    return {
      count: 0,
      user: { name: 'Vue' }
    }
  }
}

props

父子组件通信的重要方式,支持类型校验默认值

export default {
  props: {
    // 基础类型
    title: String,
    // 多个类型
    age: [Number, String],
    // 带默认值
    size: {
      type: String,
      default: 'medium'
    },
    // 必需
    id: {
      type: Number,
      required: true
    },
    // 自定义校验
    score: {
      validator: (value) => value >= 0 && value <= 100
    }
  }
}

Vue3 组合式 API

const props = defineProps({
  title: String,
  count: { type: Number, default: 0 }
})

computed 计算属性

缓存计算结果,只在依赖变化时重新计算:

export default {
  data() { return { count: 1 } },
  computed: {
    // 只读
    doubled() { return this.count * 2 },
    // 可写
    plusOne: {
      get() { return this.count + 1 },
      set(val) { this.count = val - 1 }
    }
  }
}

methods 方法

处理业务逻辑,每次渲染都会重新创建:

export default {
  methods: {
    handleClick() { /* ... */ }
  }
}

watch 监听器

监听数据变化并执行回调:

export default {
  data() { return { count: 0 } },
  watch: {
    count(newVal, oldVal) {
      console.log(`变化: ${oldVal}${newVal}`)
    },
    // 深度监听
    'obj.data': {
      handler() { /* ... */ },
      deep: true
    },
    // 立即执行
    name: {
      handler() { /* ... */ },
      immediate: true
    }
  }
}

Composition API

Vue3 引入的组合式 API,提供了更灵活的逻辑组织方式。

ref / reactive 响应式

import { ref, reactive } from 'vue'

// ref - 原始类型
const count = ref(0)
count.value++

// reactive - 对象
const state = reactive({
  user: { name: 'Vue' }
})
state.user.name = 'Vue3'

区别

| 特性 | ref | reactive | | ----- -| ----- | ---------- | | 适用类型 | 任意类型 | 对象/数组 | | 访问方式 | .value | 直接属性 | | 重新赋值 | 响应式 | 替换整个对象 |

toRefs / toRef

将 reactive 对象解构为独立的 ref:

import { reactive, toRefs } from 'vue'

const state = reactive({ name: 'Vue', age: 25 })
const { name, age } = toRefs(state)

// 或创建单个 ref
const nameRef = toRef(state, 'name')

computed() 计算属性

import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

watch / watchEffect

import { ref, watch, watchEffect } from 'vue'

// watch - 显式监听
watch(count, (newVal, oldVal) => { /* ... */ })
watch(() => state.name, (newVal) => { /* ... */ })

// watchEffect - 自动收集依赖
watchEffect(() => {
  console.log(count.value) // 自动追踪
})

执行时机控制

  • watch:默认同步执行
  • watchEffect:默认 pre(在组件更新前)
  • watchPostEffect:在组件更新后执行
  • watchSyncEffect:同步执行

生命周期钩子

import { 
  onMounted, 
  onUpdated, 
  onUnmounted 
} from 'vue'

export default {
  setup() {
    onMounted(() => { console.log('mounted') })
    onUpdated(() => { console.log('updated') })
    onUnmounted(() => { console.log('unmounted') })
  }
}

组件通信

Props / $emit

// 父组件
<Child :count="count" @update="handleUpdate" />

// 子组件
const props = defineProps({ count: Number })
const emit = defineEmits(['update'])
emit('update', props.count + 1)

Provide / Inject

祖先向后代跨级传值

// 祖先组件
provide('key', 'value')

// 后代组件
const value = inject('key')

响应式

// 祖先
const count = ref(0)
provide('count', count)

// 后代 - 修改会影响所有后代
const count = inject('count')

attrs/attrs / listeners

透传属性和事件:

<!-- 透传所有 -->
<Child v-bind="$attrs" v-on="$listeners" />

Pinia 状态管理

Vue3 推荐的状态管理方案:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubled: (state) => state.count * 2
  },
  actions: {
    increment() { this.count++ }
  }
})

内置组件

Transition

为元素添加过渡动画

<Transition name="fade">
  <div v-if="show">Content</div>
</Transition>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

过渡类名

  • v-enter-from / v-leave-from:起始状态
  • v-enter-active / v-leave-active:过渡中
  • v-enter-to / v-leave-to:结束状态

KeepAlive

缓存组件实例:

<KeepAlive include="A,B" exclude="C">
  <component :is="current" />
</KeepAlive>

生命周期

  • activated:激活时
  • deactivated:停用时

Teleport

渲染到指定 DOM 位置:

<Teleport to="#modal-root">
  <div class="modal">Content</div>
</Teleport>

Suspense

处理异步组件(实验性):

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

生命周期

Options API

阶段 钩子 说明
初始化 beforeCreate 实例创建前
初始化 created 数据观测完成
挂载 beforeMount 模板编译完成
挂载 mounted DOM 挂载完成
更新 beforeUpdate 数据变化,DOM 未更新
更新 updated DOM 更新完成
销毁 beforeUnmount 实例销毁前
销毁 unmounted 实例已销毁

父子组件执行顺序

挂载:父 created → 子 created → 子 mounted → 父 mounted

更新:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated

销毁:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted


常用技巧

动态类名

<div :class="{ active: isActive, 'text-center': isCenter }">
<div :class="[activeClass, errorClass]">

条件类名

<div :class="[isActive && 'active']">

动态绑定 style

<div :style="{ color: textColor, fontSize: fontSize + 'px' }">

函数式组件

export default {
  functional: true,
  props: { msg: String },
  render(h, context) {
    return h('div', context.props.msg)
  }
}

异步组件

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Async.vue'),
  loadingComponent: Loading,
  errorComponent: Error,
  delay: 200,
  timeout: 3000
})

面试常见问题

1. v-model 的原理?

本质是 v-bind:value + @input语法糖,监听 input 事件并更新数据。

2. v-for 中 key 的作用?

帮助 Vue 识别节点身份,实现高效的 DOM 复用。推荐使用数据唯一 ID,避免使用数组索引

3. computed 和 watch 的区别?

  • computed:计算属性,依赖变化自动计算,缓存结果
  • watch:监听器,监听数据变化,执行异步或复杂逻辑

4. Vue2 和 Vue3 的区别?

  • 响应式:Object.defineProperty → Proxy
  • API:Options API → Composition API
  • 多根节点:不支持 → 支持
  • 生命周期:beforeDestroy → beforeUnmount

5. 组件通信方式有哪些?

  • props / $emit:父子
  • provide / inject:祖先-后代
  • attrs/attrs / listeners:透传
  • 事件总线:兄弟/任意
  • Pinia/Vuex:全局状态

6. Vue 的响应式原理?

通过 Proxy/Object.defineProperty 劫持数据访问,在 getter 中收集依赖,setter 中触发更新


总结

Vue 以其简洁的 API 和渐进式的设计理念,成为前端开发的主流框架。掌握 Vue 的基础理论、常用 API 以及组件通信方式,是 Vue 开发者的必备技能。Vue3Composition API 提供了更现代化的开发范式,建议在实际项目中优先使用。

构建无障碍组件之Radio group pattern

Radio Group Pattern 详解:构建无障碍单选按钮组件

单选按钮(Radio Button)是表单中用于从一组互斥选项中选择单个项目的控件。本文基于 W3C WAI-ARIA Radio Pattern 规范,详解如何构建无障碍的单选按钮组件。

一、Radio 的定义与核心概念

单选按钮允许用户从一组相关但互斥的选项中选择一个且仅一个选项。当用户选择一个选项时,同组中之前被选中的选项会自动取消选中。

1.1 核心特性

  • 互斥性:同一组内只能有一个选项被选中
  • 预设选中:通常有一个选项默认被选中
  • 分组依赖:通过相同的 name 属性(HTML)或 aria-label(ARIA)进行分组

1.2 与 Checkbox 的区别

特性 Radio Checkbox
选择数量 单选 可多选
互斥性 同组互斥 独立
默认状态 通常预设一个选中 可全部未选中
键盘导航 方向键切换 Tab 切换

二、WAI-ARIA 角色与属性

2.1 基本角色

单选按钮具有 role="radio"

2.2 状态属性

2.3 分组属性

单选按钮必须分组,以便辅助技术理解它们之间的关系:

<div
  role="radiogroup"
  aria-labelledby="group-label">
  <h3 id="group-label">选择支付方式</h3>
  <div
    role="radio"
    aria-checked="true"
    tabindex="0">
    信用卡
  </div>
  <div
    role="radio"
    aria-checked="false"
    tabindex="-1">
    支付宝
  </div>
  <div
    role="radio"
    aria-checked="false"
    tabindex="-1">
    微信支付
  </div>
</div>

2.4 可访问标签

每个单选按钮的可访问标签可以通过以下方式提供:

  • 可见文本内容:直接包含在具有 role="radio" 的元素内的文本
  • aria-labelledby:引用包含标签文本的元素的 ID
  • aria-label:直接在单选按钮元素上设置标签文本

2.5 描述属性

如果包含额外的描述性静态文本,使用 aria-describedby

<div
  role="radio"
  aria-checked="false"
  aria-describedby="option-desc">
  高级会员
</div>
<p id="option-desc">包含所有高级功能,每月 99 元</p>

三、键盘交互规范

3.1 基本键盘操作

当单选按钮获得焦点时:

按键 功能
Space 如果焦点在未选中的单选按钮上,选中该按钮(取消选中同组其他按钮)
Tab 将焦点移动到组内的选中单选按钮;如果组内没有选中按钮,将焦点移动到组内第一个单选按钮

3.2 方向键导航(可选但推荐)

按键 功能
Down Arrow / Right Arrow 将焦点移动到下一个单选按钮,并选中它;如果焦点在最后一个按钮上,将焦点移动到第一个按钮
Up Arrow / Left Arrow 将焦点移动到上一个单选按钮,并选中它;如果焦点在第一个按钮上,将焦点移动到最后一个按钮

四、实现方式

4.1 原生 HTML 实现(推荐)

原生 HTML <input type="radio"> 提供完整的无障碍支持:

<fieldset>
  <legend>选择性别</legend>
  <label>
    <input
      type="radio"
      name="gender"
      value="male"
      checked /></label>
  <label>
    <input
      type="radio"
      name="gender"
      value="female" /></label>
  <label>
    <input
      type="radio"
      name="gender"
      value="other" />
    其他
  </label>
</fieldset>

4.2 ARIA 实现(自定义样式)

<div
  role="radiogroup"
  aria-labelledby="payment-label">
  <h3 id="payment-label">选择支付方式</h3>

  <div
    role="radio"
    aria-checked="true"
    tabindex="0"
    onclick="selectRadio(this)"
    onkeydown="handleKeydown(event, this)">
    <span
      class="radio-icon"
      aria-hidden="true"></span>
    信用卡
  </div>

  <div
    role="radio"
    aria-checked="false"
    tabindex="-1"
    onclick="selectRadio(this)"
    onkeydown="handleKeydown(event, this)">
    <span
      class="radio-icon"
      aria-hidden="true"></span>
    支付宝
  </div>

  <div
    role="radio"
    aria-checked="false"
    tabindex="-1"
    onclick="selectRadio(this)"
    onkeydown="handleKeydown(event, this)">
    <span
      class="radio-icon"
      aria-hidden="true"></span>
    微信支付
  </div>
</div>

<script>
  function selectRadio(selectedRadio) {
    const radioGroup = selectedRadio.closest('[role="radiogroup"]');
    const radios = radioGroup.querySelectorAll('[role="radio"]');

    radios.forEach((radio) => {
      const isSelected = radio === selectedRadio;
      radio.setAttribute('aria-checked', isSelected);
      radio.setAttribute('tabindex', isSelected ? '0' : '-1');
    });
  }

  function handleKeydown(event, radio) {
    const radioGroup = radio.closest('[role="radiogroup"]');
    const radios = Array.from(radioGroup.querySelectorAll('[role="radio"]'));
    const currentIndex = radios.indexOf(radio);

    switch (event.key) {
      case 'ArrowDown':
      case 'ArrowRight':
        event.preventDefault();
        const nextIndex = (currentIndex + 1) % radios.length;
        radios[nextIndex].focus();
        selectRadio(radios[nextIndex]);
        break;
      case 'ArrowUp':
      case 'ArrowLeft':
        event.preventDefault();
        const prevIndex = (currentIndex - 1 + radios.length) % radios.length;
        radios[prevIndex].focus();
        selectRadio(radios[prevIndex]);
        break;
      case ' ':
        event.preventDefault();
        selectRadio(radio);
        break;
    }
  }
</script>

4.3 水平布局的单选按钮组

<fieldset class="radio-group-horizontal">
  <legend>选择评分</legend>
  <div class="radio-options">
    <label class="radio-label">
      <input
        type="radio"
        name="rating"
        value="1" />
      <span>1 星</span>
    </label>
    <label class="radio-label">
      <input
        type="radio"
        name="rating"
        value="2" />
      <span>2 星</span>
    </label>
    <label class="radio-label">
      <input
        type="radio"
        name="rating"
        value="3"
        checked />
      <span>3 星</span>
    </label>
    <label class="radio-label">
      <input
        type="radio"
        name="rating"
        value="4" />
      <span>4 星</span>
    </label>
    <label class="radio-label">
      <input
        type="radio"
        name="rating"
        value="5" />
      <span>5 星</span>
    </label>
  </div>
</fieldset>

4.4 带描述的选项

<fieldset
  role="radiogroup"
  aria-labelledby="plan-label">
  <legend id="plan-label">选择套餐</legend>

  <label class="radio-card">
    <input
      type="radio"
      name="plan"
      value="basic"
      checked />
    <div class="radio-content">
      <strong>基础版</strong>
      <span class="price">¥29/月</span>
      <p class="description">适合个人用户,包含基础功能</p>
    </div>
  </label>

  <label class="radio-card">
    <input
      type="radio"
      name="plan"
      value="pro" />
    <div class="radio-content">
      <strong>专业版</strong>
      <span class="price">¥99/月</span>
      <p class="description">适合小型团队,包含高级功能</p>
    </div>
  </label>

  <label class="radio-card">
    <input
      type="radio"
      name="plan"
      value="enterprise" />
    <div class="radio-content">
      <strong>企业版</strong>
      <span class="price">¥299/月</span>
      <p class="description">适合大型企业,包含全部功能</p>
    </div>
  </label>
</fieldset>

五、常见应用场景

5.1 性别选择

<fieldset>
  <legend>性别</legend>
  <label
    ><input
      type="radio"
      name="gender"
      value="male" />
    男</label
  >
  <label
    ><input
      type="radio"
      name="gender"
      value="female" />
    女</label
  >
  <label
    ><input
      type="radio"
      name="gender"
      value="other" />
    其他</label
  >
  <label
    ><input
      type="radio"
      name="gender"
      value="secret"
      checked />
    保密</label
  >
</fieldset>

5.2 支付方式选择

<fieldset>
  <legend>选择支付方式</legend>
  <label class="payment-option">
    <input
      type="radio"
      name="payment"
      value="credit-card"
      checked />
    <img
      src="credit-card-icon.svg"
      alt="" />
    信用卡
  </label>
  <label class="payment-option">
    <input
      type="radio"
      name="payment"
      value="alipay" />
    <img
      src="alipay-icon.svg"
      alt="" />
    支付宝
  </label>
  <label class="payment-option">
    <input
      type="radio"
      name="payment"
      value="wechat" />
    <img
      src="wechat-icon.svg"
      alt="" />
    微信支付
  </label>
</fieldset>

5.3 主题切换

<fieldset class="theme-selector">
  <legend>选择主题</legend>
  <div class="theme-options">
    <label class="theme-option">
      <input
        type="radio"
        name="theme"
        value="light"
        checked />
      <span class="theme-preview light"></span>
      浅色
    </label>
    <label class="theme-option">
      <input
        type="radio"
        name="theme"
        value="dark" />
      <span class="theme-preview dark"></span>
      深色
    </label>
    <label class="theme-option">
      <input
        type="radio"
        name="theme"
        value="auto" />
      <span class="theme-preview auto"></span>
      跟随系统
    </label>
  </div>
</fieldset>

六、最佳实践

6.1 优先使用原生单选按钮

原生 HTML <input type="radio"> 提供完整的无障碍支持,包括:

  • 自动键盘交互(方向键导航)
  • 自动互斥选择
  • 屏幕阅读器自动播报状态
  • 浏览器原生样式和焦点管理

6.2 始终设置默认选中

为避免用户忘记选择,通常应该预设一个默认选项:

<!-- 推荐:预设默认选项 -->
<fieldset>
  <legend>选择语言</legend>
  <label
    ><input
      type="radio"
      name="language"
      value="zh"
      checked />
    中文</label
  >
  <label
    ><input
      type="radio"
      name="language"
      value="en" />
    English</label
  >
</fieldset>

6.3 使用 fieldset 和 legend 分组

始终使用 <fieldset><legend> 对单选按钮进行语义化分组:

<fieldset>
  <legend>选择尺寸</legend>
  <label
    ><input
      type="radio"
      name="size"
      value="s" />
    S</label
  >
  <label
    ><input
      type="radio"
      name="size"
      value="m"
      checked />
    M</label
  >
  <label
    ><input
      type="radio"
      name="size"
      value="l" />
    L</label
  >
  <label
    ><input
      type="radio"
      name="size"
      value="xl" />
    XL</label
  >
</fieldset>

6.4 提供清晰的视觉指示

确保选中和未选中状态有清晰的视觉区别:

/* 自定义单选按钮样式 */
input[type='radio'] {
  width: 20px;
  height: 20px;
  accent-color: #005a9c;
}

input[type='radio']:focus {
  outline: 2px solid #005a9c;
  outline-offset: 2px;
}

6.5 避免嵌套交互元素

不要在单选按钮标签内嵌套其他交互元素:

<!-- 不推荐 -->
<label>
  <input
    type="radio"
    name="option"
    value="a" />
  选项 A <a href="/details">查看详情</a>
</label>

<!-- 推荐 -->
<div>
  <label>
    <input
      type="radio"
      name="option"
      value="a" />
    选项 A
  </label>
  <a href="/details">查看详情</a>
</div>

6.6 考虑移动端触摸区域

确保单选按钮有足够的触摸区域(至少 44x44px):

.radio-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
  min-height: 44px;
}

七、Radio 与 Select 的选择

场景 推荐组件 原因
选项少于 5 个 Radio 所有选项可见,便于比较
选项多于 7 个 Select 节省空间,避免认知负担
需要显示选项详情 Radio 可以展示描述信息
空间受限 Select 下拉菜单更紧凑
频繁切换 Radio 减少点击次数

八、总结

构建无障碍的单选按钮组件需要关注三个核心:正确的语义化分组(<fieldset><legend>)、清晰的选中状态指示、以及良好的键盘导航支持(方向键切换)。与 Checkbox 不同,Radio 强调互斥选择,适用于需要从一组选项中精确选择单一项目的场景。

遵循 W3C Radio Pattern 规范,我们能够创建既美观又包容的单选按钮组件,为不同能力的用户提供一致的体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

状态提升:前端开发中的状态管理的设计思想

在前端开发中,我们几乎绕不开一个核心问题:状态(state)该放在哪里?

随着项目复杂度的提升,状态的存放位置也会经历一次次“升级”:

子组件 → 父组件 → Hook(组合式函数)→ Pinia(全局状态管理)

这篇文章,我会带你一步步拆解这个“状态提升”的演进过程,并结合VUE代码示例,帮你理解每一次升级背后的动机和设计思想。


一、第一阶段:状态在子组件中(局部状态)

在项目早期,我们通常会把状态直接写在子组件内部。

示例:一个计数器组件

<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

特点

  • 状态封装在组件内部
  • 简单直观
  • 适合完全独立的 UI 组件

问题

如果有两个组件都需要用到这个 count 呢?

比如:

<Counter />
<Display />

Display 组件也想显示这个 count,怎么办?

这时我们就需要第一次升级。


二、第二阶段:从子组件提升到父组件

当多个子组件共享状态时,我们会把状态“提升”到它们的共同父组件。

这和 React 的“状态提升”思想是一致的。

父组件管理状态

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'
import Display from './Display.vue'

const count = ref(0)

const increment = () => {
  count.value++
}
</script>

<template>
  <Counter :count="count" @increment="increment" />
  <Display :count="count" />
</template>

子组件只负责展示和触发

<!-- Counter.vue -->
<script setup>
defineProps({
  count: Number
})

defineEmits(['increment'])
</script>

<template>
  <button @click="$emit('increment')">+1</button>
</template>

优点

  • 状态集中管理
  • 数据流清晰(单向数据流)

缺点

  • 层级一深就会出现:

    • props drilling(层层传参)
    • 事件层层冒泡
  • 父组件变得“臃肿”

当项目规模扩大后,这种方式开始吃力。

于是我们进行第二次升级。


三、第三阶段:从父组件提升到 Hook(组合式函数)

在 Vue 3 中,Composition API 让我们可以把逻辑抽离成 Hook(组合式函数)。

我们把状态抽离到一个独立文件中。

创建一个 useCounter.ts

// useCounter.ts
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  return {
    count,
    increment
  }
}

在组件中使用

<script setup>
import { useCounter } from './useCounter'

const { count, increment } = useCounter()
</script>

优点

  • 逻辑复用
  • 代码结构更清晰
  • 组件变“干净”
  • 可测试性更强

但问题来了

如果两个组件都调用 useCounter()

const a = useCounter()
const b = useCounter()

它们的 count 是:

❌ 不共享的
每调用一次都会创建新的状态实例。

如果我们希望多个组件共享同一个状态怎么办?

这时候,Hook 已经不够用了。

于是我们迎来终极升级。


四、第四阶段:从 Hook 升级到 Pinia

当状态需要在多个页面、多个模块、多个层级中共享时,我们就需要真正的状态管理工具。

在 Vue 生态中,主流选择是:

  • Vuex(旧)
  • Pinia(官方推荐)

这里我们使用 Pinia。


什么是 Pinia?

Pinia 是 Vue 官方推荐的状态管理库,支持 Vue 3,API 设计非常现代化。

它的理念是:

Store = 可复用的全局 Hook


创建一个 Counter Store

// stores/counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  return { count, increment }
})

在组件中使用

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

<template>
  <button @click="counter.increment">
    {{ counter.count }}
  </button>
</template>

关键特性

  • 所有组件共享同一个 store
  • 自动响应式
  • DevTools 支持
  • 模块化管理

状态升级的本质

我们来总结一下这四个阶段:

阶段 状态位置 适用场景 缺点
子组件 组件内部 完全独立组件 无法共享
父组件 父级 局部共享 层级深会混乱
Hook 逻辑抽离 逻辑复用 默认不共享
Pinia 全局 Store 跨页面共享 增加架构复杂度

设计哲学:状态放在哪里?

可以用一句话概括:

状态应该放在“刚好需要它的最上层”

  • 只一个组件用 → 放子组件
  • 两个兄弟组件用 → 放父组件
  • 多个地方用但不共享 → Hook
  • 全局共享 → Pinia

这是一种“按需升级”的架构策略。


不要一开始就上 Pinia

不要直接提升到pinia,这会带来:

  • 不必要的全局耦合
  • 难以维护
  • 状态污染

记住:

全局状态是一种“权力”,不要滥用。

应该从下至上,找到最合适的地方,随着需求的变更,代码也跟随变更。


架构升级的思维模型

这个升级过程,本质上体现的是:

  • 局部化 → 抽象化 → 全局化
  • 组件驱动 → 逻辑驱动 → 状态驱动

这也是现代前端架构演进的核心路线。


结语

Vue 的状态管理不是非黑即白的选择,而是一个“渐进增强”的过程。

当你理解了:

  • 为什么提升状态
  • 什么时候该提升
  • 提升的边界在哪里

你就真正掌握了 Vue 状态管理的设计思想。

思考

  • 如果所有的父子组件都需要这个状态呢?(Provide/Inject)

Claude Code 创始人 Boris 揭秘:团队 10 倍效率技巧

核心观点

当 AI 成为程序员的标配,真正拉开差距的不是工具本身,而是使用方式。

Boris(Claude Code 创始人)首次公开团队内部使用手册,这些技巧来自 Claude Code 核心团队的真实工作场景,可以让工作效率比以前高出 5–10 倍。他的原话颇具代表性:

"我已有 6 个月没写过一行 SQL 代码了。"


十大效率技巧

1. 并行作业:构建多线程作战环境

这是团队给出的 Top 1 建议。

同时开启 3–5 个 git 工作树(worktree),每个运行独立的 Claude 会话:

  • 一个 Claude 负责重构代码
  • 一个 Claude 负责写测试
  • 一个 Claude 负责分析日志

进阶技巧: 给工作树命名并设置 shell 别名(如 zazbzc),一键切换不同任务环境。也可专门维护一个「分析专用」工作树,只用来跑 BigQuery 和看日志。


2. 计划模式(Plan Mode):复杂任务必须先规划

团队铁律:每个复杂任务都从计划模式开始,而不是直接编码。

  • 把精力投入计划阶段,让 Claude 一次性完美实现
  • 进阶玩法: 让一个 Claude 写方案,开第二个 Claude 扮演「主任架构师」审核
  • 避坑指南: 代码一旦跑偏,立刻跳回 Plan Mode 重新规划,而不是反复打补丁

3. CLAUDE.md:让 AI「记住」你的所有规则

这是 Claude 的长期记忆系统,也是普通用户最容易忽视的一点。

每次 Claude 犯错后,在反馈末尾加上:

Update your CLAUDE.md so you don't make this mistake again.

Boris 透露,Claude 非常擅长给自己写规则。持续迭代 CLAUDE.md 文件直到错误率显著下降。有经验的工程师甚至让 Claude 为每个任务/项目维护专门的笔记目录,每次 PR 后更新,并在 CLAUDE.md 中引用这些笔记。


4. 重复的事,一定要变成 Skill

原则:如果某件事每天重复做超过两次,就该把它写成 Skill,提交到 git,在所有项目间复用。

真实案例:

  • /techdebt:每天结束前扫描重复代码
  • 一个 Slash Skill:同步 7 天的 Slack / GDrive / Asana / GitHub 数据
  • 数据工程 Agent:写 dbt、Review、测试

Claude Code 的威力,不在「对话」,而在可复用能力的积累。


5. 90% 的 Bug,Claude 可以自己修

Claude 团队修 Bug 的方式非常「反直觉」——不要去教 Claude 怎么 fix,让 Claude 自己决定。

操作示例:

  • 开 Slack MCP,把一个 Bug 讨论串丢给 Claude,只说一个字:fix
  • 或者直接:Go fix the failing CI tests.(不指定步骤、不微操)
  • 分布式系统中:直接把 docker logs 给 Claude 分析

6. 提升 Prompt 技巧:从「请示」到「挑战」

不要只会求 AI 帮写代码,要学会「挑战」它,像对待高级同事一样协作。

三个改变游戏规则的 Prompt 技巧:

  1. 角色反转审查"证明给我看这能工作",让 Claude 对比主分支和功能分支的行为差异
  2. 优雅重构"已知目前所有条件,把这个垃圾删了,重写一个优雅的方案。"
  3. 详细规格:提前编写详细规格说明,减少歧义——你越具体,输出质量越高

示例 Prompt:"质问我这些变更,直到我通过你的测试才创建 PR。"


7. 终端环境优化:工具配置决定效率上限

Claude Code 团队的偏爱配置:

  • 终端:Ghostty(同步渲染、24-bit 颜色、Unicode)
  • 状态栏:用 /statusline 显示当前 git 分支 + Context 使用率
  • 多任务:tmux / 多 tab,每个标签对应一个任务/worktree,并对标签进行颜色编码和命名

一个被严重低估的技巧:语音输入。 说话比打字快 3 倍,Prompt 质量反而更高。


8. 子代理(Subagents):更多算力,但不污染主上下文

使用原则:

  • 在任何请求后加「使用子代理」,让 Claude 投入更多算力
  • 把单个任务卸载给子代理,保持主代理的上下文窗口干净聚焦
  • 通过 hook 将权限请求路由到 Opus 4.5,自动扫描并批准安全请求,实现权限管理自动化

9. 数据分析:告别手写 SQL 的时代

Claude 团队几乎已经不用手写 SQL

  • 使用 Claude Code 调用 bq 命令行工具即时拉取和分析指标
  • 在代码库中维护 BigQuery 技能,团队每个成员直接在 Claude Code 中进行分析查询
  • 这种方法适用于任何有 CLI、MCP 或 API 的数据库

实战示例:"帮我分析上周用户增长异常的原因"——AI 会自动写 SQL、拉数据、出图表、给结论。


10. 用 Claude 学习:而不只是让它干活

Claude 团队内部的学习用法:

  • /config 中开启「Learning / Explanatory」输出模式,让 Claude 解释改动背后的原因
  • 对于不熟悉的代码,让 Claude 生成可视化 HTML 演示文稿
  • 要求 Claude 绘制新协议和代码库的 ASCII 图表,帮助理解
  • 构建间隔重复学习 Skill(你讲 → 它追问 → 存储)

Claude 不只是生产工具,也是放大理解力的杠杆。


总结

Boris 在开头就强调:使用 Claude Code 没有唯一正确的方式,每个人的设置都不同。

这 10 条技巧只是起点,真正的效率提升来自于:

  • 大胆实验,找到适合自己工作流的方式
  • 持续迭代,不断优化技能和配置
  • 分享交流,从团队中汲取智慧

AI 时代的编程,拼的不是打字速度,而是定义问题的边界以及调度算力的能力。

⚡️ vite-plugin-oxc:从 Babel 到 Oxc,我为 Vite 写了一个高性能编译插件

写在前面

关注前端工具链的人应该都注意到了,Rust 正在「入侵」这个领域。SWC 被 Next.js 采用,Biome 在蚕食 ESLint 和 Prettier 的地盘,而 Oxc 作为新一代 Rust 工具链,性能数据更是夸张——比 SWC 还快好几倍。

更值得关注的是 Vite 的动向。Evan You 团队正在开发 Rolldown,一个用 Rust 重写的 Rollup,底层就是基于 Oxc。按照 roadmap,Rolldown 会逐步集成到 Vite 中,届时整个构建流程都将是 Rust 实现。

既然大势所趋,为什么不提前体验一下?Oxc 的各个模块(oxc-transformoxc-resolveroxc-minify)都已经通过 npm 包发布了,完全可以在当前的 Vite 项目中直接使用。于是我动手写了 vite-plugin-oxc,把 Oxc 的能力接入 Vite 的插件体系,算是在 Rolldown 正式落地前的一次提前尝鲜。

这就是这个插件的由来。

项目源码:github.com/Sunny-117/v…


一、JavaScript 编译工具的前世今生

在聊具体实现之前,有必要回顾一下 JavaScript 编译工具这些年的演进。这不是为了炒冷饭,而是理解这个演进脉络,才能明白为什么 Oxc 的出现是一种必然。

1.1 Babel:开创性的存在

2014 年,当 ES6 规范还在草案阶段,6to5(后来更名为 Babel)横空出世。那时候浏览器对新语法的支持参差不齐,Babel 让开发者可以放心使用箭头函数、解构赋值、类等新特性,然后转译成 ES5 代码跑在老旧浏览器上。

Babel 的架构设计很经典:

源代码 → Parser(解析成 AST)→ Transformer(插件转换)→ Generator(生成代码)

这套架构的优势在于插件系统的灵活性。任何人都可以写一个 Babel 插件,操作 AST 实现自定义的代码转换。十年过去了,Babel 生态里积累了数以万计的插件,覆盖了几乎所有你能想到的代码转换需求。

但问题也很明显:

Babel 是纯 JavaScript 实现的。JavaScript 是单线程、解释执行、带 GC 的语言,天生就不是性能敏感型任务的最佳选择。当项目规模膨胀到几十万行代码时,Babel 的编译时间会变得令人抓狂。我见过一些大型 monorepo 项目,光 Babel 编译就要好几分钟。

另一个痛点是配置复杂度。@babel/preset-env@babel/plugin-transform-runtimecore-jsbrowserslist……这些概念交织在一起,新手很容易迷失在配置地狱里。我至今还记得当年为了搞清楚 useBuiltIns: 'usage'useBuiltIns: 'entry' 的区别,翻了多少遍文档。

1.2 esbuild:用 Go 重写一切

2020 年,esbuild 的出现彻底改变了游戏规则。

作者 Evan Wallace(Figma 联合创始人)用 Go 语言重写了一个 JavaScript/TypeScript 打包器,性能数据令人瞠目结舌:比 Webpack 快 10-100 倍。这不是什么黑魔法,原因很朴素:

  1. Go 是编译型语言,执行效率远高于 JavaScript
  2. Go 的并发模型让 esbuild 可以充分利用多核 CPU
  3. 从零设计,没有历史包袱,数据结构和算法都针对性能优化过
  4. All-in-one,解析、转换、打包、压缩一条龙,减少中间环节的开销

Vite 选择 esbuild 做预构建(pre-bundling)正是看中了这一点。在开发模式下,esbuild 可以在几百毫秒内把 node_modules 里的依赖打包好,让 Vite 的冷启动时间保持在秒级。

但 esbuild 也有它的局限:

  • 不做类型检查。它只剥离 TypeScript 类型,不验证类型正确性。
  • 插件系统相对简单。不像 Babel 那样可以精细操作 AST,esbuild 的插件主要用于自定义模块解析和加载。
  • 不追求 100% 兼容。一些边缘场景的语法转换可能和 Babel 结果不一致。
  • 作者明确表示不会支持某些特性,比如装饰器的旧版实现。

对于大多数项目来说,这些局限不是问题。但在某些场景下,你可能还是得请出 Babel。

1.3 SWC:Rust 阵营的第一枪

2019 年,韩国开发者 Donny(강동윤)用 Rust 启动了 SWC 项目。名字来源于 "Speedy Web Compiler",目标很直接:做一个更快的 Babel 替代品。

SWC 的策略是 兼容 Babel。它实现了大部分 Babel 的转换能力,配置项也尽量保持一致,让迁移成本降到最低。性能方面,SWC 号称比 Babel 快 20 倍以上。

2021 年,SWC 被 Vercel 收购,成为 Next.js 12 的默认编译器。这是一个标志性事件——Rust 编写的前端工具链开始进入主流视野。

SWC 的优势在于:

  • Rust 的性能。编译型语言、零成本抽象、无 GC 停顿。
  • 良好的 Babel 兼容性。支持大部分 Babel 插件的功能。
  • 持续的投入。有 Vercel 背书,项目维护有保障。

但 SWC 也有一些问题。最常被吐槽的是 编译产物的稳定性。早期版本偶尔会出现一些边缘情况的 bug,导致生产环境翻车。另外,SWC 的架构设计主要服务于 Next.js 的需求,作为通用工具使用时,某些场景的支持不够完善。

1.4 工具演进的本质规律

回顾这段历史,可以看到一个清晰的趋势:

时期 代表工具 实现语言 核心特点
2014-2019 Babel JavaScript 开创性、生态丰富、慢
2020-2021 esbuild Go 极致性能、功能精简
2021-2023 SWC Rust 高性能、Babel 兼容
2023-now Oxc Rust 更快、模块化、工具链

这个演进本质上是在解决同一个问题:如何在保证功能的前提下,榨干硬件的每一分性能

JavaScript 天生不适合这类 CPU 密集型任务,所以社区开始用系统级语言重写。Go 和 Rust 之争,目前看来 Rust 略占上风——主要是因为 Rust 的零成本抽象和更精细的内存控制,在极致性能场景下更有优势。


二、Oxc 凭什么更快

Oxc(Oxidation Compiler)是 2023 年开始崭露头角的新项目,作者是 Boshen Chen。相比前辈们,Oxc 有几个独特的特点。

2.1 不只是编译器,是完整工具链

Oxc 的野心不止于一个编译器。它的目标是提供一整套高性能 JavaScript 工具链:

  • oxc-parser:JavaScript/TypeScript 解析器
  • oxc-transform:代码转换器(JSX、TypeScript 等)
  • oxc-resolver:模块路径解析器
  • oxc-minify:代码压缩器
  • oxc-linter:代码检查器(对标 ESLint)
  • oxc-formatter:代码格式化器(对标 Prettier)

每个模块都可以独立使用,通过 npm 包的形式分发(底层是 Rust 编译成 N-API 原生模块)。这种模块化设计让你可以按需引入,而不是大包大揽。

2.2 性能数据

根据 Oxc 官方的 benchmark,在 parser 层面:

  • 比 SWC 快 3 倍
  • 比 Babel parser 快 40+ 倍

在 transformer 层面,处理 TypeScript 的速度大约是 SWC 的 4 倍

这些数字看起来很夸张,但我自己跑过测试,差距确实存在。Oxc 的作者在性能优化上下了很大功夫,比如:

  • 使用 bumpalo 这种 arena allocator 来减少内存分配开销
  • AST 节点设计更紧凑,减少内存占用
  • 大量使用 SIMD 指令加速字符串处理
  • 零拷贝解析,尽量复用源代码字符串

2.3 兼容性策略

Oxc 的兼容性策略比较务实。它不追求 100% 兼容 Babel 的每一个行为,而是覆盖 实际生产中最常用的转换场景

  • TypeScript 类型剥离
  • JSX 转换(classic 和 automatic 两种模式)
  • ES target 降级(async/await、optional chaining 等)
  • React Fast Refresh 注入

对于大多数项目来说,这些功能已经够用了。


三、vite-plugin-oxc 的设计思路

有了前面的背景铺垫,现在进入正题:如何把 Oxc 接入 Vite?

3.1 需求分析

我给自己定的目标是:

  1. 替代 Vite 内置的 esbuild 做代码转换。包括 TypeScript 类型剥离、JSX 转换。
  2. 提供模块解析能力。用 oxc-resolver 替代 Vite 的默认解析逻辑(可选)。
  3. 提供代码压缩能力。用 oxc-minify 在生产构建时压缩代码。
  4. 支持 React Fast Refresh。开发模式下的 HMR 必须正常工作。
  5. 零配置可用。默认配置就能满足大多数项目的需求。

3.2 Vite 插件机制简介

Vite 的插件系统基于 Rollup,但做了一些扩展。一个 Vite 插件本质上是一个对象,包含若干个 hook 函数。和我们这个插件相关的主要有:

  • config:修改 Vite 配置
  • configResolved:配置解析完成后的回调,可以拿到最终配置
  • resolveId:自定义模块 ID 解析
  • load:自定义模块加载
  • transform:代码转换
  • transformIndexHtml:转换 HTML 入口文件
  • generateBundle:生成产物后的回调,可以修改最终产物

另外还有一个关键配置:enforce。它决定插件的执行顺序:

  • enforce: 'pre':在 Vite 核心插件之前执行
  • 不设置:在 Vite 核心插件之后、用户插件之前执行
  • enforce: 'post':在所有插件之后执行

对于代码转换类插件,通常需要设置 enforce: 'pre',这样才能在 Vite 的默认处理之前介入。

3.3 整体架构

┌─────────────────────────────────────────────────────────────┐
│                     vite-plugin-oxc                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │  Transform   │  │   Resolve    │  │     Minify       │   │
│  │  (oxc-       │  │  (oxc-       │  │   (oxc-minify)   │   │
│  │  transform)  │  │  resolver)   │  │                  │   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
│         │                 │                   │             │
│         ▼                 ▼                   ▼             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                React Fast Refresh                    │   │
│  │               (HMR 边界检测 + 运行时)                 │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

这个插件由三个核心功能模块组成,加上一套 React Fast Refresh 的支持逻辑。下面逐一拆解。


四、Transform 模块:代码转换的核心

代码转换是整个插件最核心的功能。它的职责是把 TypeScript、JSX 这些浏览器不认识的语法,转换成标准的 JavaScript。

4.1 基本实现

先看核心代码:

import { transformSync as oxcTransform } from 'oxc-transform'

// 在 transform hook 中
transform(code, id, transformOptions) {
  if (!filter(id) || options.transform === false) return null

  const isJsxFile = /\.[jt]sx$/.test(id)
  const enableRefresh = isDev && options.reactRefresh && isJsxFile

  const result = oxcTransform(id, code, {
    ...transformOpts,
    sourceType: guessSourceType(id, transformOptions?.format),
    sourcemap: options.sourcemap,
    jsx: {
      ...jsxOpts,
      development: isDev,
      refresh: enableRefresh ? {} : undefined,
    },
  })

  if (result.errors.length) {
    throw new SyntaxError(
      result.errors.map((error) => error.message).join('\n')
    )
  }

  return {
    code: result.code,
    map: result.map,
  }
}

oxcTransformoxc-transform 提供的同步转换函数。它接收三个参数:

  1. 文件路径(用于 sourcemap 和错误信息)
  2. 源代码字符串
  3. 转换选项

返回值包含转换后的代码和 sourcemap。

4.2 sourceType 推断

一个容易被忽略的细节是 sourceType 的处理。JavaScript 有两种模块类型:

  • module:ESM,支持 import/export
  • script:传统脚本,支持 CommonJS

如果 sourceType 判断错误,转换结果可能出问题。比如把 ESM 代码当成 script 处理,import 语句就会报语法错误。

我实现了一个 guessSourceType 函数来推断:

export function guessSourceType(
  id: string,
  format?: string
): 'module' | 'script' | undefined {
  // 优先使用上游传递的 format 信息
  if (format === 'module' || format === 'module-typescript') {
    return 'module'
  } else if (format === 'commonjs' || format === 'commonjs-typescript') {
    return 'script'
  }

  // 根据文件扩展名推断
  const moduleFormat = getModuleFormat(id)
  if (moduleFormat) {
    return moduleFormat === 'module' ? 'module' : 'script'
  }
}

export function getModuleFormat(
  id: string
): 'module' | 'commonjs' | 'json' | undefined {
  const ext = path.extname(id)
  switch (ext) {
    case '.mjs':
    case '.mts':
      return 'module'
    case '.cjs':
    case '.cts':
      return 'commonjs'
    case '.json':
      return 'json'
    case '.jsx':
    case '.tsx':
      return 'module' // JSX/TSX 默认当 ESM 处理
  }
}

这里有个约定:.mjs/.mts 是 ESM,.cjs/.cts 是 CommonJS,.jsx/.tsx 默认当 ESM。对于 .js/.ts,则依赖上游的 format 信息或者返回 undefined 让 Oxc 自己判断。

4.3 与 Vite 内置 esbuild 的协同

Vite 默认会用 esbuild 处理 TypeScript 和 JSX。如果我们的插件也处理这些文件,就会重复转换,结果不可预期。

解决方案是在 config hook 里禁用 esbuild 对 JSX/TSX 的处理:

config(_userConfig, { command }) {
  return {
    esbuild: {
      // 让 esbuild 只处理纯 .ts 文件
      include: /\.ts$/,
      // 排除 JSX/TSX,交给我们的插件处理
      exclude: /\.[jt]sx$/,
    },
    optimizeDeps: {
      esbuildOptions: {
        jsx: 'automatic',
      },
    },
  }
}

这样配置后,.ts 文件继续走 esbuild(速度也很快),而 .jsx.tsx.js 走我们的 Oxc 转换。

不过说实话,这个设计有点 trade-off。理想情况下应该完全接管所有 JS/TS 文件的转换,但考虑到 Vite 生态的兼容性,保持这种混合模式可能更稳妥。

4.4 JSX 转换配置

JSX 转换有两种模式:

  1. Classic:转换成 React.createElement 调用
  2. Automatic:转换成 _jsx/_jsxs 调用,自动引入 react/jsx-runtime

React 17+ 推荐使用 automatic 模式,这也是我们插件的默认行为。配置项支持自定义:

const jsxOpts = transformOpts.jsx && typeof transformOpts.jsx === 'object'
  ? transformOpts.jsx
  : {}

oxcTransform(id, code, {
  jsx: {
    ...jsxOpts,
    development: isDev, // 开发模式启用额外的调试信息
    refresh: enableRefresh ? {} : undefined, // Fast Refresh 注入
  },
})

development: true 会在转换结果中加入 __source__self 等调试信息,方便在 React DevTools 里看到组件的源码位置。


五、Resolve 模块:模块解析

模块解析看起来简单,实际上是个大坑。import './foo' 这行代码,到底应该解析成哪个文件?

  • ./foo.js
  • ./foo.ts
  • ./foo/index.js
  • ./foo/index.ts
  • 还是 ./foo.json

这取决于项目配置、Node.js 版本、模块类型等一系列因素。

5.1 为什么要自己做解析

Vite 内部已经有一套解析逻辑,为什么我们还要用 oxc-resolver 再来一套?

两个原因:

  1. 性能。Oxc resolver 是 Rust 实现的,解析速度比 Vite 的 JavaScript 实现更快。在大型项目中,模块解析的开销不可忽视。
  2. 一致性。既然 transform 用了 Oxc,resolver 也用 Oxc,整个工具链的行为会更一致。

当然,这是可选功能,默认开启但可以关闭。

5.2 基本实现

import { ResolverFactory } from 'oxc-resolver'

let resolver: InstanceType<typeof ResolverFactory> | null = null

// 在 configResolved 中初始化
configResolved(config) {
  if (options.resolve !== false) {
    resolver = new ResolverFactory({
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.node'],
      conditionNames: ['import', 'require', 'browser', 'node', 'default'],
      builtinModules: true,
      moduleType: true,
      ...options.resolve,
    })
  }
}

// 在 resolveId hook 中使用
resolveId(id, importer, _resolveOptions) {
  // 处理 React Refresh 虚拟模块
  if (id === '/@react-refresh') {
    return id
  }

  if (!resolver || options.resolve === false) return null

  // 默认跳过 node_modules,除非显式启用
  if (
    !options.resolveNodeModules &&
    id[0] !== '.' &&
    !path.isAbsolute(id)
  ) {
    return null
  }

  try {
    const directory = importer ? path.dirname(importer) : process.cwd()
    const resolved = resolver.sync(directory, id)

    // 处理 Node.js 内置模块
    if (resolved.error?.startsWith('Builtin module')) {
      return {
        id,
        external: true,
        moduleSideEffects: false,
      }
    }

    if (resolved.path) {
      const format = getModuleFormat(resolved.path) || resolved.moduleType || 'commonjs'
      return {
        id: resolved.path,
        format,
      }
    }
  } catch (error) {
    // 解析失败,交给 Vite 的默认逻辑处理
    return null
  }

  return null
}

5.3 性能优化:跳过 node_modules

一个重要的优化是 默认不解析 node_modules

if (
  !options.resolveNodeModules &&
  id[0] !== '.' &&          // 不是相对路径
  !path.isAbsolute(id)       // 不是绝对路径
) {
  return null  // 交给 Vite 处理
}

为什么?因为 Vite 的预构建机制已经把 node_modules 里的依赖处理好了,我们没必要再去解析一遍。只解析项目内的相对路径和绝对路径,性能开销可控。

如果某些场景确实需要解析 node_modules,可以通过配置项开启:

oxc({
  resolveNodeModules: true
})

5.4 内置模块处理

Node.js 有一些内置模块(fspathhttp 等),在浏览器环境是不存在的。当 oxc-resolver 遇到这些模块时,会返回一个特殊的 error:

if (resolved.error?.startsWith('Builtin module')) {
  return {
    id,
    external: true,
    moduleSideEffects: false,
  }
}

把它标记为 external,告诉 Vite 这个模块不需要处理。


六、Minify 模块:代码压缩

生产构建时,代码压缩是必不可少的一环。oxc-minify 提供了和 Terser 类似的压缩能力,但性能更好。

6.1 在 generateBundle 中压缩

代码压缩放在 generateBundle hook 里做:

async generateBundle(_outputOptions, bundle) {
  if (options.minify === false) return

  const { minifySync } = await import('oxc-minify')

  for (const fileName of Object.keys(bundle)) {
    const chunk = bundle[fileName]
    if (chunk.type !== 'chunk') continue

    try {
      const result = minifySync(fileName, chunk.code, {
        ...(options.minify === true ? {} : options.minify),
        sourcemap: options.sourcemap,
      })
      chunk.code = result.code

      // SourceMap 合并...
    } catch (error) {
      this.error(`Failed to minify ${fileName}: ${error}`)
    }
  }
}

这里有几个设计考量:

  1. 为什么在 generateBundle 而不是 transform 因为压缩应该在所有代码转换完成后、输出文件前进行。此时代码已经是最终形态,压缩效果最好。

  2. 为什么用动态 import? oxc-minify 只在生产构建时需要,开发模式下不需要加载这个依赖,动态 import 可以减少冷启动时间。

  3. 为什么跳过非 chunk 类型? bundle 对象里既有 JS chunk,也有 CSS、图片等 asset。我们只压缩 JS。

6.2 SourceMap 合并

压缩代码会改变代码的行列位置,如果项目需要 sourcemap,必须把压缩前后的 sourcemap 合并,才能正确映射到源码。

这个合并逻辑用 @ampproject/remapping 来做:

import remapping, { type EncodedSourceMap } from '@ampproject/remapping'

if (result.map && chunk.map) {
  const minifyMap: EncodedSourceMap = {
    version: 3,
    file: result.map.file,
    sources: result.map.sources,
    sourcesContent: result.map.sourcesContent,
    names: result.map.names,
    mappings: result.map.mappings,
  }
  const chunkMap: EncodedSourceMap = {
    version: 3,
    file: chunk.map.file,
    sources: chunk.map.sources,
    sourcesContent: chunk.map.sourcesContent,
    names: chunk.map.names,
    mappings: chunk.map.mappings,
  }

  // 合并两个 sourcemap
  const merged = remapping([minifyMap, chunkMap], () => null)

  chunk.map = {
    file: merged.file ?? '',
    mappings: merged.mappings as string,
    names: merged.names,
    sources: merged.sources as string[],
    sourcesContent: merged.sourcesContent as string[],
    version: merged.version,
    toUrl() {
      return `data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(this)).toString('base64')}`
    },
  }
}

remapping 函数接收一个 sourcemap 数组,按顺序合并。第一个是最终代码的 map(压缩后),第二个是上一步的 map(压缩前)。合并后的 map 可以从最终代码直接映射回原始源码。

6.3 压缩选项透传

oxc-minify 支持 Terser 风格的压缩选项:

// 默认压缩
oxc({ minify: true })

// 自定义选项
oxc({
  minify: {
    mangle: true,        // 变量名混淆
    compress: {
      dropConsole: true, // 删除 console.log
    },
  }
})

// 禁用压缩
oxc({ minify: false })

这些选项原封不动传给 minifySync,插件层只加了一个 sourcemap 选项的处理。


七、React Fast Refresh:HMR 的核心

React Fast Refresh 是 React 官方的热更新方案,可以在修改组件代码后保留组件状态,只更新改变的部分。要让它正常工作,需要在编译时注入一些运行时代码。

这部分是整个插件最复杂的地方。

7.1 Fast Refresh 的工作原理

Fast Refresh 的基本原理是:

  1. 编译时:在每个模块末尾注入代码,把模块导出的组件注册到 Fast Refresh runtime。
  2. 运行时:当模块热更新时,runtime 对比新旧导出,判断是否可以安全刷新。
  3. 刷新执行:如果可以安全刷新,runtime 触发 React 重新渲染更新后的组件,同时保留状态。

关键在于「安全刷新」的判断。Fast Refresh 只能处理「纯组件变更」的情况。如果模块导出了非组件内容(比如常量、工具函数),且这些内容发生了变化,就必须做完整刷新。

7.2 运行时模块

我实现了一个虚拟模块 /@react-refresh,提供 Fast Refresh 的运行时代码:

const refreshRuntimeCode = `
import RefreshRuntime from 'react-refresh/runtime';

export function injectIntoGlobalHook(globalObject) {
  RefreshRuntime.injectIntoGlobalHook(globalObject);
}

export function register(type, id) {
  RefreshRuntime.register(type, id);
}

export function createSignatureFunctionForTransform() {
  return RefreshRuntime.createSignatureFunctionForTransform();
}

export function performReactRefresh() {
  return RefreshRuntime.performReactRefresh();
}

// 判断是否是 React 组件
export function isLikelyComponentType(type) {
  if (typeof type !== 'function') return false;
  if (type.prototype != null && type.prototype.isReactComponent) return true;
  if (type.$$typeof) return false;
  const name = type.name || type.displayName;
  return typeof name === 'string' && /^[A-Z]/.test(name);
}

// 注册模块导出的组件
export function registerExportsForReactRefresh(filename, moduleExports) {
  for (const key in moduleExports) {
    if (key === '__esModule') continue;
    const exportValue = moduleExports[key];
    if (isLikelyComponentType(exportValue)) {
      RefreshRuntime.register(exportValue, filename + ' export ' + key);
    }
  }
}

// 防抖更新
let enqueueUpdateTimer = null;
function enqueueUpdate() {
  if (enqueueUpdateTimer === null) {
    enqueueUpdateTimer = setTimeout(() => {
      enqueueUpdateTimer = null;
      RefreshRuntime.performReactRefresh();
    }, 16);
  }
}

// 验证刷新边界并触发更新
export function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExports) {
  // 检查导出是否发生不兼容的变化
  for (const key in prevExports) {
    if (key === '__esModule') continue;
    if (!(key in nextExports)) {
      return 'Could not Fast Refresh (export removed)';
    }
  }
  for (const key in nextExports) {
    if (key === '__esModule') continue;
    if (!(key in prevExports)) {
      return 'Could not Fast Refresh (new export)';
    }
  }

  let hasExports = false;
  for (const key in nextExports) {
    if (key === '__esModule') continue;
    hasExports = true;
    const value = nextExports[key];
    if (isLikelyComponentType(value)) continue;
    if (prevExports[key] === nextExports[key]) continue;
    return 'Could not Fast Refresh (non-component export changed)';
  }

  if (hasExports) {
    enqueueUpdate();
  }
  return undefined;
}

export const __hmr_import = (module) => import(/* @vite-ignore */ module);
`

这段代码做了几件事:

  1. 组件注册registerExportsForReactRefresh 遍历模块导出,把看起来像组件的函数注册到 runtime。
  2. 边界验证validateRefreshBoundaryAndEnqueueUpdate 检查新旧导出的差异,判断是否可以安全刷新。
  3. 防抖更新enqueueUpdate 用 16ms 的防抖,避免短时间内多次触发刷新。

7.3 HTML Preamble 注入

Fast Refresh 需要在页面加载之前初始化全局钩子。通过 transformIndexHtml hook 注入:

transformIndexHtml() {
  if (!isDev || !options.reactRefresh) return []

  return [
    {
      tag: 'script',
      attrs: { type: 'module' },
      children: `
import { injectIntoGlobalHook } from "/@react-refresh";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
`,
    },
  ]
}

这段脚本会被插入到 HTML 的 <head> 中,在任何业务代码执行之前运行。它做两件事:

  1. 调用 injectIntoGlobalHook(window) 初始化 runtime。
  2. window 上挂载两个占位函数 $RefreshReg$$RefreshSig$,防止业务代码报错。

7.4 模块尾部代码注入

最后,需要在每个 JSX/TSX 模块末尾注入 HMR 边界检测代码:

if (enableRefresh && transformedCode.includes('$RefreshReg$')) {
  const refreshFooter = `
import * as RefreshRuntime from "/@react-refresh";
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
if (import.meta.hot && !inWebWorker) {
  if (!window.$RefreshReg$) {
    throw new Error(
      "vite-plugin-oxc can't detect preamble. Something is wrong."
    );
  }

  RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
    RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(id)}, currentExports);
    import.meta.hot.accept((nextExports) => {
      if (!nextExports) return;
      const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(
        ${JSON.stringify(id)},
        currentExports,
        nextExports
      );
      if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
    });
  });
}
function $RefreshReg$(type, id) {
  return RefreshRuntime.register(type, ${JSON.stringify(id)} + ' ' + id)
}
function $RefreshSig$() {
  return RefreshRuntime.createSignatureFunctionForTransform();
}
`
  transformedCode = transformedCode + refreshFooter
}

这段代码的逻辑:

  1. 动态导入自身RefreshRuntime.__hmr_import(import.meta.url) 拿到当前模块的导出。
  2. 注册导出:把导出的组件注册到 runtime。
  3. 接受热更新:通过 import.meta.hot.accept 监听更新,拿到新的导出后验证边界。
  4. 判断刷新方式:如果边界验证失败(invalidateMessage 不为空),调用 invalidate 触发完整刷新;否则自动执行 Fast Refresh。

注意这里有个细节:只有当转换后的代码包含 $RefreshReg$ 时才注入。因为 Oxc 只会在检测到组件定义时才插入这些调用,如果模块里没有组件(比如纯工具函数文件),就不需要这套逻辑。

7.5 Web Worker 兼容

代码里有个判断:

const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
if (import.meta.hot && !inWebWorker) {
  // ...
}

Web Worker 环境没有 window 对象,也不支持 HMR,所以需要跳过。


八、文件过滤:控制处理范围

不是所有文件都需要经过 Oxc 处理。CSS、图片、JSON 这些应该跳过。

8.1 Filter 实现

export function createFilter(
  include?: FilterPattern,
  exclude?: FilterPattern
): (id: string) => boolean {
  const includePatterns = normalizePatterns(include)
  const excludePatterns = normalizePatterns(exclude)

  return (id: string) => {
    // 先检查 exclude,命中则跳过
    if (excludePatterns.length > 0) {
      for (const pattern of excludePatterns) {
        if (testPattern(pattern, id)) {
          return false
        }
      }
    }

    // 再检查 include
    if (includePatterns.length === 0) {
      return true // 没有 include 规则则默认处理
    }

    for (const pattern of includePatterns) {
      if (testPattern(pattern, id)) {
        return true
      }
    }

    return false
  }
}

function testPattern(pattern: string | RegExp, id: string): boolean {
  if (typeof pattern === 'string') {
    return id.includes(pattern)
  }
  return pattern.test(id)
}

这个实现遵循一个简单的规则:exclude 优先于 include。如果一个文件同时匹配 include 和 exclude,以 exclude 为准。

8.2 默认配置

include: options.include || [/\.[cm]?[jt]sx?$/],
exclude: options.exclude || [/node_modules/],

默认配置的含义:

  • include: 处理所有 .js.jsx.ts.tsx.mjs.mts.cjs.cts 文件
  • exclude: 跳过 node_modules 目录

[cm]? 这个正则匹配可选的 c(CommonJS)或 m(Module)前缀,覆盖了 Node.js 的各种模块扩展名约定。


九、配置系统设计

一个好的插件应该做到「零配置可用,有需要时可配」。

9.1 类型定义

export interface VitePluginOxcOptions {
  include?: FilterPattern          // 文件包含规则
  exclude?: FilterPattern          // 文件排除规则
  enforce?: 'pre' | 'post'         // 插件执行顺序
  transform?: TransformOptions | false  // 转换选项,false 禁用
  resolve?: NapiResolveOptions | false  // 解析选项,false 禁用
  resolveNodeModules?: boolean     // 是否解析 node_modules
  minify?: MinifyOptions | boolean // 压缩选项
  sourcemap?: boolean              // SourceMap 生成
  reactRefresh?: boolean           // React Fast Refresh
}

每个配置项都可以是具体的选项对象、布尔值、或不设置(使用默认值)。

9.2 选项解析

export function resolveOptions(
  options: VitePluginOxcOptions,
  isDev: boolean
): ResolvedOptions {
  return {
    include: options.include || [/\.[cm]?[jt]sx?$/],
    exclude: options.exclude || [/node_modules/],
    enforce: options.enforce,
    transform: options.transform !== false ? (options.transform || {}) : false,
    resolve: options.resolve !== false ? (options.resolve || {}) : false,
    resolveNodeModules: options.resolveNodeModules || false,
    minify: options.minify !== false ? (options.minify || false) : false,
    sourcemap: options.sourcemap ?? isDev,  // 开发模式默认开启
    reactRefresh: options.reactRefresh ?? true,  // 默认开启
  }
}

几个设计决策:

  1. transformresolve 默认开启,可以传 false 禁用
  2. minify 默认关闭,需要显式传 true 或选项对象开启
  3. sourcemap 根据环境决定,开发模式默认开启,生产模式默认关闭
  4. reactRefresh 默认开启,因为大部分 React 项目都需要

9.3 enforce 处理

enforce 的处理比较特殊:

const plugin: Plugin = {
  name: 'vite-plugin-oxc',
  enforce: 'pre',  // 默认值
  // ...
}

// 如果用户显式设置了 enforce,覆盖默认值
if ('enforce' in rawOptions) {
  plugin.enforce = rawOptions.enforce
}

为什么不直接用 options.enforce || 'pre'?因为用户可能想显式设置 enforce: undefined,表示不要任何 enforce 约束。用 'enforce' in rawOptions 可以区分「没传」和「传了 undefined」两种情况。


十、测试策略

工程化项目离不开测试。这个插件的测试主要覆盖以下场景。

10.1 单元测试结构

import { describe, it, expect, vi, beforeEach } from 'vitest'
import vitePluginOxc from '../src/index'

// Mock oxc-transform
vi.mock('oxc-transform', () => ({
  transformSync: vi.fn((_id: string, code: string, _options?: unknown) => ({
    code: `// Transformed: ${code}`,
    map: null,
    errors: [],
  })),
}))

// Mock oxc-resolver
vi.mock('oxc-resolver', () => ({
  ResolverFactory: class MockResolverFactory {
    sync(_directory: string, id: string) {
      return {
        path: `/resolved/${id}`,
        moduleType: 'module',
      }
    }
  },
}))

// Mock oxc-minify
vi.mock('oxc-minify', () => ({
  minifySync: vi.fn((fileName: string, code: string) => ({
    code: `/* Minified */ ${code.replace(/\s+/g, ' ').trim()}`,
    map: null,
  })),
}))

为什么要 mock Oxc 的依赖?因为:

  1. 隔离测试范围。单元测试关注的是插件的集成逻辑,不是 Oxc 本身的转换行为。
  2. 测试执行速度。原生依赖的加载需要时间,mock 后测试更快。
  3. 确定性。Oxc 的版本更新可能改变输出,mock 可以保证测试稳定。

10.2 核心测试用例

describe('vite-plugin-oxc', () => {
  it('should create plugin with default options', () => {
    const plugin = vitePluginOxc()
    expect(plugin.name).toBe('vite-plugin-oxc')
    expect(plugin.enforce).toBe('pre')
    expect(typeof plugin.transform).toBe('function')
  })

  it('should allow overriding enforce option', () => {
    const pluginPost = vitePluginOxc({ enforce: 'post' })
    expect(pluginPost.enforce).toBe('post')

    const pluginNone = vitePluginOxc({ enforce: undefined })
    expect(pluginNone.enforce).toBeUndefined()
  })
})

describe('generateBundle - oxc-minify integration', () => {
  it('should minify chunk code using oxc-minify', async () => {
    const plugin = vitePluginOxc({ minify: true })
    ;(plugin.configResolved as Function)({ command: 'build' })

    const bundle = {
      'index.js': {
        type: 'chunk',
        code: 'function hello() { console.log("hi"); }',
        map: null,
      },
    }

    await (plugin.generateBundle as Function).call({ error: vi.fn() }, {}, bundle)

    expect(bundle['index.js'].code).toContain('Minified')
  })

  it('should skip minification when minify is false', async () => {
    const plugin = vitePluginOxc({ minify: false })
    ;(plugin.configResolved as Function)({ command: 'build' })

    const originalCode = 'function hello() { console.log("hi"); }'
    const bundle = {
      'index.js': { type: 'chunk', code: originalCode, map: null },
    }

    await (plugin.generateBundle as Function).call({ error: vi.fn() }, {}, bundle)

    expect(bundle['index.js'].code).toBe(originalCode)
  })

  it('should merge sourcemaps when both exist', async () => {
    // 测试 sourcemap 合并逻辑
  })
})

测试覆盖了:

  • 插件创建和默认配置
  • 配置覆盖
  • 压缩功能的开启/关闭
  • SourceMap 合并
  • 错误处理

十一、性能实测

说了这么多理论,实际效果如何?我用一个中型 React 项目做了测试。

11.1 测试环境

  • 项目规模:约 200 个 TypeScript/TSX 文件,5 万行代码
  • 机器配置:MacBook Pro M2,16GB 内存
  • Node.js:v20.10.0

11.2 开发模式冷启动

方案 首次启动时间
Vite 默认(esbuild) 1.2s
vite-plugin-oxc 1.1s

开发模式差距不大,因为 Vite 的预构建已经很快了。

11.3 生产构建

方案 构建时间 产物体积
Vite 默认 18.3s 1.42 MB
vite-plugin-oxc (无压缩) 12.1s 1.58 MB
vite-plugin-oxc (开启压缩) 14.7s 1.39 MB

Transform 阶段提速明显(约 33%),开启 oxc-minify 后总体时间也有优势,且压缩效果略好于默认的 esbuild。

11.4 HMR 响应时间

修改一个组件文件后:

方案 HMR 更新时间
Vite + esbuild 50-80ms
vite-plugin-oxc 40-60ms

HMR 场景下 Oxc 的优势更明显,因为单文件转换时 Oxc 的启动开销比例更低。


十二、踩过的坑

开发过程中遇到了不少问题,记录几个典型的。

12.1 esbuild 的 JSX 处理冲突

最开始没有禁用 esbuild 的 JSX 处理,导致 JSX 被转换了两次,结果代码里出现了奇怪的双重嵌套。

解决方案就是在 config hook 里配置 esbuild 跳过 JSX/TSX:

config() {
  return {
    esbuild: {
      include: /\.ts$/,
      exclude: /\.[jt]sx$/,
    },
  }
}

12.2 SourceMap 合并顺序

第一版 sourcemap 合并写反了顺序:

// 错误写法
const merged = remapping([chunkMap, minifyMap], () => null)

// 正确写法
const merged = remapping([minifyMap, chunkMap], () => null)

remapping 的数组是从「最终代码」到「原始代码」的顺序。压缩后的 map 在前,压缩前的 map 在后。

12.3 React Refresh 的 preamble 时机

Fast Refresh 的 preamble 必须在任何业务代码之前执行。最开始我用 transform hook 在第一个 JSX 文件转换时注入,结果时机不稳定。

后来改用 transformIndexHtml hook,直接往 HTML 里插入 <script>,稳定多了。

12.4 模块格式推断

有些项目混用 ESM 和 CommonJS,如果模块格式判断错误,会导致语法错误或运行时问题。

最后的方案是综合多个信息源:

  1. 上游传递的 format 参数
  2. 文件扩展名(.mjs/.cjs 等)
  3. Oxc resolver 返回的 moduleType
  4. 兜底默认值

12.5 虚拟模块的处理

/@react-refresh 是个虚拟模块,不存在于文件系统。需要在 resolveIdload 两个 hook 里配合处理:

resolveId(id) {
  if (id === '/@react-refresh') {
    return id  // 告诉 Vite 这个 ID 我来处理
  }
}

load(id) {
  if (id === '/@react-refresh') {
    return refreshRuntimeCode  // 返回模块内容
  }
}

十三、未来展望

13.1 Rolldown 的影响

Vite 团队正在开发 Rolldown,一个用 Rust 重写的 Rollup。一旦 Rolldown 成熟,Vite 的整个构建流程都会是 Rust 实现,性能会再上一个台阶。

Rolldown 底层使用的就是 Oxc 的 parser 和 transformer,所以 vite-plugin-oxc 的很多逻辑可能会被 Vite 原生支持。到那时,这个插件的历史使命可能就完成了。

实际上,官方文档里已经提到:

这个包已弃用。请使用 @vitejs/plugin-react,因为 rolldown-vite 已自动启用基于 Oxc 的 Fast Refresh 转换。

这说明方向是对的,只是时机早了一点。

13.2 工具链的 Rust 化趋势

纵观前端工具链的演进,Rust 化是一个明确的趋势:

  • Bundler:Rolldown、Turbopack
  • Compiler:SWC、Oxc
  • Linter:oxlint、Biome
  • Formatter:dprint、Biome
  • Package Manager:pnpm(部分 Rust)、Bun(Zig)

JavaScript 工具用 JavaScript 写的时代正在过去。对于开发者来说,这意味着更快的开发体验,但也意味着参与工具开发的门槛变高了——你得会 Rust。

13.3 这个插件的定位

虽然 Rolldown 出来后这个插件可能就没用了,但它的价值在于:

  1. 作为学习材料。展示了如何把一个 Rust 工具链集成到现有的 JavaScript 生态中。
  2. 作为过渡方案。在 Rolldown 正式发布前,想尝鲜 Oxc 的人可以用这个插件。
  3. 作为参考实现。React Fast Refresh 的集成逻辑,sourcemap 合并的处理,这些代码可以被其他项目借鉴。

十四、总结

回到开头的问题:2026 年了,前端构建为什么还是慢?

答案是:工具链正在追赶硬件的脚步,只是还没追上。

从 Babel 到 esbuild,从 SWC 到 Oxc,每一代工具都在压榨更多性能。vite-plugin-oxc 是我在这条路上的一次尝试——用 Oxc 这套 Rust 工具链,给 Vite 的构建流程提提速。

核心实现其实不复杂:

  • Transform:调用 oxc-transform 做代码转换,处理好 sourceType 推断和 JSX 配置
  • Resolve:用 oxc-resolver 做模块解析,默认跳过 node_modules 保证性能
  • Minify:在 generateBundle 阶段用 oxc-minify 压缩,注意 sourcemap 合并
  • React Fast Refresh:虚拟模块 + HTML preamble + 模块尾部注入,三件套配合

难点在于细节:与 Vite 内置 esbuild 的配合、sourcemap 合并的顺序、模块格式的正确推断、HMR 边界检测的实现……这些东西文档不会告诉你,只能靠踩坑。

这个插件的代码开源在 GitHub 上,欢迎 star 和 PR。虽然它可能很快就会被 Rolldown 取代,但在那之前,希望它能给想了解 Vite 插件开发、Oxc 集成的同学一些参考。

前端工具链的进化永远不会停止。今天是 Oxc,明天可能是更快的东西。作为开发者,保持学习、保持好奇,可能是我们能做的最重要的事。

项目源码:github.com/Sunny-117/v… 欢迎 Star、Issue 和 PR!


参考资料

ArcPy 开发环境搭建

^ 关注我,带你一起学GIS ^

通过上一节ArcPy,一个基于 Python 的 GIS 开发库简介[1],我们知道了ArcPy作为ArcGIS桌面软件随附的一部分,要么集成在ArcGIS DesktopArcGIS Pro中,要么存在于ArcGIS Engine或者ArcGIS Server中。

文中以ArcGIS Pro3.5为例进行讲解。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2026年

系统:Windows 11

ArcGIS Pro:3.5

Python:3.11.11

2. 安装Arcpy

ArcPy包是默认 Python 分布 arcgispro-py3(ArcGIS Pro 和 ArcGIS Server 随附)的一部分。通过克隆 arcgispro-py3 使用 ArcPy 创建环境。 可以使用 ArcGIS Pro 中的软件包管理器,或 Python 命令提示符中的 conda 命令行应用程序来克隆环境。

从 ArcGIS Pro 2.7 开始,当 ArcPy 包版本不冲突时,可将其添加到现有 Python 3 环境中。 要添加 ArcPy,可以使用 conda 从 Anaconda Cloud 上的 Esri 频道安装 ArcPy。 从 Python 命令提示符中,使用适当的版本号运行以下命令:

conda install arcpy=3.6 -c esri

需要注意的是即使ArcPy可以下载安装,但包仍需要ArcGIS Pro,必须安装它才能使用ArcPy

3. 创建环境

3.1. 创建基础环境

随附在ArcGIS Pro、arcgispro-py3中的默认Python环境包含了对用于支持所有ArcGIS Pro Python使用案例的 200 多个软件包的访问权限。在某些情况下,此环境中包含的内容可能远远超出您的需求。 如果你只需要一个简单的环境,即仅包含运行地理处理工具和核心 ArcPy函数所需的最少依赖项的环境,则请使用arcpy-base环境。

arcpy-base明显小于 arcgispro-py3,其中仅包含几十个依赖项。要创建基于arcpy-base的环境,请运行以下conda命令:

conda create -n my-env arcpy-base

凭借一组有限的库,arcpy-base无法完全支持所有基于PythonArcGIS Pro功能。arcpy-base仍可用于运行几乎所有地理处理工具和ArcPy函数,其中包括NumPyGDAL 和Pandas等软件包。

仅使用 arcpy-base将限制对Notebooks(从 ArcGIS Pro 内部和外部)、ArcGIS API for Python 以及许多其他库(包括 matplotlib、pillow、pytest、requests、scipy、sqalachemy 和 swat)的访问权限。

3.2. 克隆arcgispro-py3环境

可以使用ArcGIS Pro包管理器来克隆arcgispro-py3环境。

打开ArcGIS Pro软件,点击Project(工程)。

点击包管理器Package Manager,可以看到当前Python环境已经下载的依赖。

右侧矩形红框查看当前的Python环境,可以点击设置按钮添加Python环境。

可以新建环境,也可以复制环境。

4. 注意事项

官方的建议是不要修改默认环境,否则很可能会影响ArcGIS Pro软件运行。

小小声说一下,我本地为了方便,一直使用的是默认arcgispro-py3环境,到目前为止,还没出现什么问题(何时因为环境问题导致ArcGIS Pro软件无法运行,我还会发文进行说明的)。

参考资料[1] 

ArcPy,一个基于 Python 的 GIS 开发库简介


GIS之路-开发示例数据下载,请在公众号后台回复:vector

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集(全)

GDAL 开发合集(全)

GIS 影像数据源介绍

GeoJSON 数据源介绍

GIS 名词解释

ArcPy,一个基于 Python 的 GIS 开发库简介

GIS 开发库 Turf 介绍

GIS 开发库 GeoTools 介绍

GIS 开发库 GDAL 介绍

地图网站大全

从微信指数看当前GIS框架的趋势

Landsat 卫星数据介绍

OGC:开放地理空间联盟简介

中国地图 GeoJSON 数据集网站介绍

结合OpenSpec 与 Everything-Claude-Code (ECC) 的构建团队工作流程

一、两者定位本质差异

这两个项目解决的是 AI 辅助开发流程中完全不同层面的问题,理解这一点是最关键的。

OpenSpec 解决的是 "做什么"(What) 的问题 —— 它是一个规格驱动开发(Spec-Driven Development)框架。核心理念是:在 AI 写代码之前,先让人和 AI 就需求规格达成共识。它通过 proposal → specs → design → tasks 的制品(artifact)依赖图来管理每一个变更的完整生命周期。

ECC 解决的是 "怎么做"(How) 的问题 —— 它是一个AI 编码助手的配置工具箱。核心理念是:提供一整套生产就绪的 agents、skills、hooks、commands、rules 和 MCP 配置,让 Claude Code(以及 Cursor/OpenCode/Codex)的执行能力最大化。

打个比方:OpenSpec 相当于项目经理的需求管理系统,ECC 相当于开发者的瑞士军刀。


二、核心机制对比

OpenSpec 的核心机制

OpenSpec 的核心是一个 制品依赖图引擎(Artifact DAG)。每个变更(change)被组织为一个独立文件夹,包含四类制品:

proposal.md 记录意图和范围,specs/ 通过 Delta 格式(ADDED/MODIFIED/REMOVED)描述行为变化,design.md 记录技术方案和架构决策,tasks.md 则是带复选框的实施清单。这些制品形成有向无环图的依赖关系,状态通过文件系统存在性自动检测(BLOCKED → READY → DONE)。

它的 OPSX 工作流打破了传统线性阶段的限制,采用流动式操作:你可以在实施过程中随时回头修改设计,不存在阶段门禁。同时它支持 24+ AI 编码工具(Claude Code、Cursor、Windsurf、Gemini CLI、GitHub Copilot、Kiro 等),做到了真正的工具无关性。

特别值得一提的是它的 Delta Spec 概念 —— 不是重写整个规格,而是描述变化量,这对存量项目(brownfield)极其友好。变更完成后通过 archive 操作将 delta 合并回主规格,形成持续演进的系统行为文档。

ECC 的核心机制

ECC 是一套模块化的配置组件库,包含 13 个专用子代理(planner、architect、tdd-guide、code-reviewer、security-reviewer 等),56 个 skills 覆盖前后端模式、持续学习、安全审计等领域,32 个斜杠命令(/plan、/tdd、/code-review、/e2e 等),以及 hooks 系统实现会话记忆持久化、自动格式化、战略性上下文压缩等自动化行为。

它的 Instinct 持续学习系统(v2)是亮点:自动从会话中提取模式,赋予置信度评分,支持跨团队导入导出,并可通过 /evolve 命令将相关直觉聚合为技能。

此外 ECC 的 AgentShield 安全扫描器(1282 个测试、102 条规则)可以扫描你的 AI 配置(CLAUDE.md、hooks、MCP 等)中的漏洞和注入风险,甚至支持三个 Opus 代理的红队/蓝队/审计流水线。


三、各自优缺点

OpenSpec

优点: 工具无关性是它最大的战略优势。支持 24+ AI 工具意味着团队不会被锁定在某个特定 IDE 或 AI 提供商上,前端用 Cursor、后端用 Claude Code 的团队可以共享同一套规格流程。Delta Spec 机制为存量项目提供了优雅的增量规格管理。自定义 schema 能力让团队可以定义自己的制品类型和依赖关系,比如加入 research 步骤。作为 npm 包分发(openspec init 一条命令初始化)降低了团队推广门槛。同时,制品文件夹结构天然支持 Git 版本控制和代码评审。

缺点: 它不涉及代码执行层面的优化,不提供 agent、hook 或 coding skill。对于小型快速迭代任务(hotfix、小 bug)流程可能显得重。需要团队养成写规格的习惯,存在文化适配成本。它也没有 token 优化、上下文管理、会话持久化等运行时增强能力。

ECC

优点: 开箱即用的生产级配置经过了实际产品构建的验证(Anthropic 黑客松获奖作品)。token 优化策略具体且实用(模型选择、thinking token 限制、compaction 阈值),能显著降低成本。多语言 rules 架构(common/ + typescript/ + python/ + golang/)适配不同技术栈。Instinct 学习系统让团队经验可以积累和传承。AgentShield 安全扫描填补了 AI 配置安全性的空白。同时跨平台支持(Claude Code、Cursor、Codex CLI、OpenCode)也相当完善。

缺点: 以 Claude Code 为主要目标,对其他工具的支持虽在扩展但存在功能差距。缺少需求-设计-实施的结构化流程管理,/plan 命令只是调用 planner agent 生成蓝图,不像 OpenSpec 那样有制品依赖图和生命周期管理。配置项繁多(56 skills + 32 commands),新用户需要时间理解哪些组件适合自己。Rules 无法通过插件自动分发(Claude Code 平台限制),需要手动安装。


四、最佳实践姿势

OpenSpec 最佳实践

适合的场景是中大型功能、跨团队协作、需要需求对齐的工作。推荐的流程是:对于明确需求用 /opsx:propose 快速启动然后 /opsx:apply 实施;对于模糊需求先用 /opsx:explore 探索再决定方案;保持每个 change 聚焦于一个逻辑单元;archive 前用 /opsx:verify 验证实现与规格的一致性。

命名规范也很重要:用 add-dark-mode、fix-login-redirect 这种描述性名称,避免 feature-1、wip 这种模糊命名。

ECC 最佳实践

token 管理是关键:默认使用 sonnet,只在复杂架构推理时切换 opus;将 MAX_THINKING_TOKENS 设为 10000,CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 设为 50;MCP 服务器保持 10 个以内,工具总数 80 以内。

工作流模式上:新功能用 /plan → /tdd → /code-review 三步走;修复 bug 先用 /tdd 写失败测试再修复;发布前用 /security-scan + /e2e + /test-coverage 三重验证。任务之间用 /clear 重置上下文,逻辑断点处用 /compact 压缩。


五、面向公司前后端团队的整合方案

基于对两者的深入分析,我推荐的整合策略是 OpenSpec 做流程骨架 + ECC 做执行引擎,两者互补而非替代。

第一层:需求与规格管理(OpenSpec)

在项目根目录执行 openspec init 初始化,所有非 trivial 的功能开发都从 /opsx:propose 或 /opsx:explore 开始。前端和后端开发者在同一个 openspec/changes/ 目录下协作,但各自维护各自领域的 specs(如 specs/api/、specs/ui/)。利用 OpenSpec 的工具无关性,让用不同 IDE 的团队成员共享同一套规格流程。

第二层:编码执行增强(ECC)

在每个开发者的环境中安装 ECC 插件并配置对应语言的 rules(前端装 common/ + typescript/,后端根据栈选择 python/ 或 golang/)。利用 ECC 的 agents 体系:planner 和 architect 辅助 OpenSpec 的 proposal 和 design 阶段,tdd-guide 和 code-reviewer 在 /opsx:apply 实施阶段提供质量保障。配置 token 优化策略统一降本。

第三层:质量与安全闭环

实施完成后,用 /opsx:verify 验证规格一致性(OpenSpec),同时用 ECC 的 /security-scan、/test-coverage、/e2e 进行技术层面验证。用 AgentShield 定期扫描团队的 AI 配置安全性。

第四层:知识积累与传承

利用 ECC 的 Instinct 系统自动学习团队模式,定期 /instinct-export 导出分享。OpenSpec 的 archive 机制自动积累系统行为规格,新人 onboard 时直接阅读 openspec/specs/ 了解系统当前行为。

推荐的完整工作流

一个典型的功能开发流程如下:

  1. PM/开发者启动 → /opsx:explore(OpenSpec,厘清需求)
  2. 需求明确后 → /opsx:propose(OpenSpec,生成 proposal + specs + design + tasks)
  3. 前后端评审制品 → 在 Git PR 中 review openspec/changes/feature-name/
  4. 实施阶段 → /opsx:apply + ECC 的 /tdd/code-review(OpenSpec 管进度,ECC 保质量)
  5. 验证阶段 → /opsx:verify + /security-scan + /e2e
  6. 完成归档 → /opsx:archive(delta specs 合并回主规格)

这套方案的核心价值是:OpenSpec 确保团队在写代码前达成共识(减少返工),ECC 确保写代码时效率和质量最大化(降低成本)。两者结合覆盖了从需求到交付的完整 AI 辅助开发链路。

小米超跑正式亮相! 小米 Vision GT 带着两大「黑科技」来了

今晚小米巴塞罗那发布会的第一个 Ultra,是一辆滑板车。

发布会详尽展示了小米电动滑板车 6 Ultra 全方位的硬件升级,尤其是「Ultra 级」的性能。台上的产品经理强调,超大杯的命名,来源于设计与全地形能力的全面提升。

是的,滑板车也讲究全地形能力……

不过,暂时就先不在这里展开介绍这辆滑板车了,因为今晚真正的重磅产品是——小米 Vision Gran Turismo 概念超跑(下文简称小米 Vision GT)

小米汽车首席设计师李田原上来就说,小米 Vision GT 在直线极速和弯道下压力之间找到了一个完美的平衡点。

对于这辆车的外观,最直观的感受就是「克制」,它并没有很复杂的空气动力学套件。

小米 Vision GT 整车采用自然风雕刻的极简设计理念,车身上的每一个线条和开孔都具备真实的空气动力学功能,没有任何纯粹服务于视觉审美的多余部件。

从侧面观察车身,驾驶舱呈现出悬浮的水滴状结构,外部气流可以直接越过车顶或穿过座舱下方的通道;底部的主动式扰流板会根据行驶状态,实时调节风阻与下压力的平衡。

车身尾部采用了独特的船型结构设计,外围有一个巨大的环形出风口,贯穿式的镂空尾灯很好地融入了出风口结构中。

李田原在现场抛出了三个工程数据:小米 Vision GT 的风阻系数做到了 0.29Cd, 下压力达到 1.2 吨,气动效率高达 4.1。

李田原表示,实现这些亮眼数据依靠两项非常特别的工程设计。

第一项是主动尾流控制系统。

小米在 Vision GT 的尾灯周围布置了密集的微孔,系统会根据车速和转向角度主动向后方喷射高压气流,来「推开」尾部产生的乱流。

第二项是磁悬浮低风阻轮毂盖。

小米 Vision GT 的轮毂外部覆盖了一层半透明的面板,车辆高速行驶时,它会依靠磁力系统保持静止,让两侧的气流顺利通过。与此同时,内部的轮毂会飞速转动,持续为高负荷运转的制动系统提供风冷效果。

视线转向座舱内部。

开超跑通常是一件体力活,运动化的座舱通常很难让人放松下来。为了追求极致的路感反馈。性能车里塞满的总是非常狭窄且僵硬的硬核座椅。

小米为了甩掉这种让人浑身紧绷的刻板印象,为 Vision GT 做了一套舒展的环抱式沙发座舱。

它的座舱没有任何生硬的物理隔断,整个内饰完全连成了一体。李田原称,坐进这辆车里的真实体验更接近于待在一间充满现代感的起居室里,驾驶员既可以挺直腰板去挑战赛道,跑累了也能完全放松地仰靠在宽大的座椅里。

驾驶员前方的方向盘采用 X 型设计,右手边还有一排做工上乘的实体按键。至于智能座舱该有的交互功能,在这里也被处理得非常克制。

作为一辆超跑,小米 Vision GT 的车机界面完全去除了繁杂的菜单层级, 屏幕只会跟随当前的驾驶模式自适应呈现最关键的行驶数据,信息流动的方式非常自然。

在这辆车上,人与车的交互被处理成了日常交谈的舒缓节奏,系统会通过视觉系统感知用户的状态,主动调整座舱的环境氛围。

发布会的最后,小米还展示了一款配套硬件——一套专门为这台跑车打造的模拟器。

虽然咱们买不到小米 Vision GT,但还是可以在《GT 赛车 7》里稍微体验一下的。

带轮子的都关注,欢迎交流。 邮箱:tanjiewen@ifanr.com

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

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


Flutter调试组件:打印任意组件尺寸位置信息 NRenderBox

一、需求来源

当页面元素特别多,比较杂,又必须获取某个组件尺寸位置时,一个个加 GlobalKey 有太麻烦,这是使用一个封装好的组件就特别有用了。然后就有了 NRenderBox 组件,可以打印出子组件的位置及尺寸。

二、使用

NRenderBox(
  child: Container(
    padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(
      color: Colors.transparent,
      border: Border.all(color: Colors.blue),
      borderRadius: BorderRadius.all(Radius.circular(0)),
    ),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        NNetworkImage(
          width: 50,
          height: 60,
          url: AppRes.image.urls.random ?? '',
        ),
        Text("选项"),
      ],
    ),
  ),
)
flutter: NRenderBox rect: Rect.fromLTRB(88.5, 322.0, 157.5, 413.0)

三、NRenderBox源码

import 'package:flutter/material.dart';

/// 点击打印尺寸
class NRenderBox extends StatefulWidget {
  const NRenderBox({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<NRenderBox> createState() => _NRenderBoxState();
}

class _NRenderBoxState extends State<NRenderBox> {
  final renderKey = GlobalKey();

  RenderBox? get renderBox {
    final ctx = renderKey.currentContext;
    if (ctx == null) {
      return null;
    }
    final box = ctx.findRenderObject() as RenderBox?;
    return box;
  }

  Offset? get renderPosition {
    return renderBox?.localToGlobal(Offset.zero);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      key: renderKey,
      onTap: () {
        if (renderBox == null) {
          return;
        }
        final position = renderPosition;
        final size = renderBox!.size;
        final rect = Rect.fromLTWH(position!.dx, position.dy, size.width, size.height);
        debugPrint("$widget rect: $rect");
      },
      child: widget.child,
    );
  }
}

github

top Command in Linux: Monitor Processes in Real Time

top is a command-line utility that displays running processes and system resource usage in real time. It helps you inspect CPU usage, memory consumption, load averages, and process activity from a single interactive view.

This guide explains how to use top to monitor your system and quickly identify resource-heavy processes.

top Command Syntax

The syntax for the top command is as follows:

txt
top [OPTIONS]

Run top without arguments to open the interactive monitor:

Terminal
top

Understanding the top Output

The screen is split into two areas:

  • Summary area — system-wide metrics displayed in the header lines
  • Task area — per-process metrics listed below the header

A typical top header looks like this:

output
top - 14:32:01 up 3 days, 4:12, 2 users, load average: 0.45, 0.31, 0.28
Tasks: 213 total, 1 running, 212 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.2 us, 1.1 sy, 0.0 ni, 95.3 id, 0.2 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 15886.7 total, 8231.4 free, 4912.5 used, 2742.8 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 10374.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1234 www-data 20 0 123456 45678 9012 S 4.3 0.3 0:42.31 nginx
987 mysql 20 0 987654 321098 12345 S 1.7 2.0 3:12.04 mysqld
5678 root 20 0 65432 5678 3456 S 0.0 0.0 0:00.12 sshd

CPU State Percentages

The %Cpu(s) line breaks CPU time into the following states:

  • us — time running user-space (non-kernel) processes
  • sy — time running kernel processes
  • ni — time running user processes with a manually adjusted nice value
  • id — idle time
  • wa — time waiting for I/O to complete
  • hi — time handling hardware interrupt requests
  • si — time handling software interrupt requests
  • st — time stolen from this virtual machine by the hypervisor (relevant on VMs)

High wa typically points to a disk or network I/O bottleneck. High us or sy points to CPU pressure. For memory details, see the free command. For load average context, see uptime .

Task Area Columns

Column Description
PID Process ID
USER Owner of the process
PR Scheduling priority assigned by the kernel
NI Nice value (-20 = highest priority, 19 = lowest)
VIRT Total virtual memory the process has mapped
RES Resident set size — physical RAM currently in use
SHR Shared memory with other processes
S Process state: R running, S sleeping, D uninterruptible, Z zombie
%CPU CPU usage since the last refresh
%MEM Percentage of physical RAM in use
TIME+ Total CPU time consumed since the process started
COMMAND Process name or full command line (toggle with c)

Common Interactive Controls

Press these keys while top is running:

Key Action
q Quit top
h Show help
k Kill a process by PID
r Renice a process
P Sort by CPU usage
M Sort by memory usage
N Sort by PID
T Sort by running time
1 Toggle per-CPU core display
c Toggle full command line
u Filter by user

Refresh Interval and Batch Mode

To change the refresh interval (seconds), use -d:

Terminal
top -d 2

To run top in batch mode (non-interactive), use -b. This is useful for scripts and logs:

Terminal
top -b -n 1

To capture multiple snapshots:

Terminal
top -b -n 5 -d 1 > top-snapshot.txt

Filtering and Sorting

Start top with a user filter:

Terminal
top -u root

To monitor a specific process by PID, use -p:

Terminal
top -p 1234

Pass a comma-separated list to monitor multiple PIDs at once:

Terminal
top -p 1234,5678

To show individual threads instead of processes, use -H:

Terminal
top -H

Thread view is useful when profiling multi-threaded applications — each thread appears as a separate row with its own CPU and memory usage.

Inside top, use:

  • P to sort by CPU
  • M to sort by memory
  • u to show a specific user

For deeper process filtering, combine top with ps , pgrep , and kill .

Quick Reference

Command Description
top Start interactive process monitor
top -d 2 Refresh every 2 seconds
top -u user Show only a specific user’s processes
top -p PID Monitor a specific process by PID
top -H Show individual threads
top -b -n 1 Single non-interactive snapshot
top -b -n 5 -d 1 Collect 5 snapshots at 1-second intervals

Troubleshooting

top is not installed Install the procps package (procps-ng on some distributions), then run top again.

Display refresh is too fast or too slow Use -d to tune the refresh rate, for example top -d 2.

Cannot kill or renice a process You may not own the process. Use sudo or run as a user with sufficient privileges.

FAQ

What is the difference between top and htop? top is available by default on most Linux systems and is lightweight. htop provides a richer interactive UI with color coding, mouse support, and easier navigation, but requires a separate installation.

What does load average mean? Load average shows the average number of runnable or uninterruptible tasks over the last 1, 5, and 15 minutes. A value above the number of CPU cores indicates the system is under pressure. See uptime for more detail.

What is a zombie process? A zombie process has finished execution but its entry remains in the process table because the parent process has not yet read its exit status. A small number of zombie processes is harmless; a growing count may indicate a bug in the parent application.

What do VIRT, RES, and SHR mean? VIRT is the total virtual address space the process has reserved. RES is the actual physical RAM currently in use. SHR is the portion of RES that is shared with other processes (for example, shared libraries). RES minus SHR gives the memory used exclusively by that process.

Can I save top output to a file? Yes. Use batch mode, for example: top -b -n 1 > top.txt.

Conclusion

The top command is one of the fastest ways to inspect process activity and system load in real time. Learn the interactive keys and sort options to diagnose performance issues quickly.

If you have any questions, feel free to leave a comment below.

默茨来华,“敲打”得醒大众奔驰宝马吗?

出品|虎嗅汽车组

作者|邢书博

头图|AI生成


“德国汽车在华销量疲软是战略失误。”


2月25日-26日,德国总理默茨开启访华之旅,在访谈期间罕见德对德国车企提出了批评。

 

随行的商业代表团中,宝马集团董事长齐普策、梅赛德斯-奔驰集团股份公司董事会主席康林松、大众汽车集团管理董事会主席奥博穆这三位德系车企“一把手”同台参访。

 

规格很高,意味着德系车厂商,在华市场疲软问题需要解决,且时间紧迫。


默茨访华来源新华社


针对德国车企认为中国市场不够开放等问题,默兹认为“中国汽车市场已足够开放,允许外资独立设厂,华晨宝马、大众安徽等德方持股比例颇高,但德系车仍然销量不佳。” 公开资料显示,宝马在华合资车占比75%,大众安徽合资公司占比也是75%。德方事实上主导了这两家合资公司的产品研发销售品牌。

 

默兹认为销量不佳的主要原因是德系车在华战略失误:2016年大众推出廉价燃油车战略导致错失电动化机遇,且对插电混动技术不屑一顾。默兹强调说:“德国汽车应承认在插混领域落后,向中国车企学习,尊重中国市场和用户需求,如此才有未来。”

 

《经济学人》载文指出,越来越多的德国企业正在中国推进“本土化”战略:利用中国的供应链,和当地员工共同开发产品,将利润重新投资于中国。

 

德国车企不仅在插混领域缺失话语权,在三电系统、智能化方面也有不足之处;但在机械素质、底盘调教和长期品质等方面依然处于领先地位。


燃油车优势和新能源不足如同天平两端的砝码,德系车该如何取舍才能获得市场与口碑的平衡?

 

宝马奔驰死磕智驾三电,震荡中寻求突破

 

批评归批评,合作成果也要有。报道称默茨访问期间,在汽车领域双方企业达成了十余项商业协议。宝马与宁德时代围绕降低电动汽车整车碳足迹达成合作;奔驰中国与中国科技企业Momenta宣布进一步深化双方在未来出行领域的合作。

 

首当其中的是宝马。

 

“忽略中国广博市场与广阔创新潜力的企业,必将错失全球经济增长和商业成功的重大机遇。”宝马董事长齐普策公开表示。中国是宝马全球第二大销售市场,2025前三季度占比超26%,但同比下滑11.2%,市占率回到了十年前的水平。

 

为了重振中国市场,宝马已在沈阳生产基地累计投资超1200亿元,在华布局四大研发创新基地与三家软件公司,构建起完整的本土化研发、生产与供应体系。

 

一个重要的里程碑是,宝马首款国产新世代车型——新世代BMW iX3长轴距版,将于2026年4月北京车展全球首发。智驾底盘电池都与国产厂商深度绑定。宝马进行了最大程度的本土化研发,希望以此重新赢得中国消费者青睐。

 

慕尼黑车展中展示的新世代BMW iX3


 其最大亮点底盘性能,号称“刹车无感”,看来宝马还是希望在底盘架构和人机操控方面打动人心。这与其一贯的品牌形象相一致。

 

不过对于宝马老车主来说,对智驾还是人驾有自己的看法。

 

“宝马现在语音助手都做得一般。我今早喊了好几遍‘你好宝马,导航到某处’,车机愣是在显示地址后就停滞不动了,无法继续用语音完成导航任务。感觉整个行业陷入了智能化内卷,很多时候忽视了汽车作为交通工具的安全属性。


街访中有宝马车主表示,如果类似语音导航等智能化功能做不好,不如一开始就不要放到车上,反而会增加安全风险。他认为有时智能功能没有反而会提高人的安全意识:“自动大灯开久了忘了驾校学的手动怎么打灯了。有时候需要变道忘了打灯很危险。”车主说。

 

首先普及自动大灯的是奔驰。自适应巡航、车身稳定系统、自动紧急制动等自动化功能也是如此。


可以说燃油时代奔驰不仅是豪华代名词,也是科技先锋队。

 

但在2026年,对奔驰来说,尽快让旗下车型从传统豪华步入科技豪华才是当务之急。

 

2月14日,奔驰发布一项重大人事调整:段建军因个人原因辞去总裁兼首席执行官职务,他的职位自3月1日起由李德思(Daniel Lescow)接任。

 

段建军在去年的奔驰新品发布会上对当下新势力恶意对标表达不满,称“奔驰不怕被对比,对不合理的、错误的对标,奔驰不会放弃做出严正交涉。”。此前余承东在尊界发布会上对标迈巴赫,视频中显示奔驰在雪地中出现打滑甩尾问题。段建军则认为打滑是“人为刻意加大了方向盘角度,延迟回正”。

 

 

“豪华不止一种诠释方式,客户体验最重要。 ”

 

段建军希望通过正面反驳让潜在车主继续认可奔驰的豪华认知和江湖地位。

 

然而事与愿违,强调奔驰品牌魅力并不能让市场认可。2025年,奔驰在中国市场交付了55.19万辆新车,同比下滑19%,这是其销量连续第二年下滑超过10%。说到底,一台台消费者开上路的奔驰车,才是奔驰品牌魅力的来源。品牌的基石还是产品本身。

 

新任奔驰中国掌门人李德思则是产品出身。他被视为“最懂中国的德国人”,曾深度参与Smart品牌的重塑和电动化转型,并主导了奔驰的数字化业务。

 

奔驰近期宣布,2026年奔驰与momenta合作的智驾方案已搭载于全新纯电CLA。2026年,奔驰将于年内落地9款新车型,覆盖高速、城区、泊车全场景。


基于MB.EA专属纯电平台也将于今年发布,告别了过去“BBA油改电”的刻板印象。奔驰还提出了“油电同智”战略,让油车也能和电车一样步入智能化时代。

 

大众发力“油电同智”,胜算大吗?

 

“油电同智”不止奔驰一家在提。大众作为燃油时代无可争议的行业霸主,在电动时代一样有紧迫感。


大众在燃油车方面依旧能打,2025年交付量超257万辆,占中国燃油车市场超22%的份额,蝉联合资车企销量第一、燃油车销量第一,增长了0.6%。但新能源车进展缓慢。新能源车合并销量之后,大众在华整体销量下滑8%,且连续两年下滑。

 

因此,大众提出“油电同智”理念,既是为了海量油车老车主,不改变油车使用习惯也能用上新智驾;也是为了新能源销量,通过老带新实现增长。

 

不过理想很很美好,油车智驾功能还是存在着现实问题。


过去大家都认为电车比油车在智能化上更有优势,因为有大电池。

 

但实际上,电车的大电池只用来给动力系统使用。智驾系统上电动车和燃油车一样,都是使用的车内的低压电池,如12V/48V电源。这为“油电同智“提供了可能性。

 

真正阻碍油车上智驾的,不仅仅是电力系统问题,还有电器架构、市场和品牌等问题。

 

店内实拍大众油电共进标语


具体说来有三个方面的问题——


第一,电器架构上油车先天“生理”缺陷。


燃油车采用分布式电子电气架构,车辆由几十个独立ECU(电子控制单元)分散控制。这就像多个“小作坊”各自为政,算力分散且无法OTA升级。而智驾需要集中式架构和强大的中央计算平台,燃油车在供电电压、散热设计、通信速率上均难以支持高功耗的智驾芯片。动力曲线方面,燃油车智驾匹配也是大问题,短期内很难追上系能源;

 

第二,市场上油车智驾,老百姓不认可。


智驾依赖硬件预埋和软件持续迭代,成本高昂。燃油车销量下滑导致利润摊薄,车企不敢将巨额研发投入在“非走量”的传统车型上。

 

同时消费者普遍认知“智能是新势力的标签”,导致油车即便配了智驾也难以产生溢价,形成“越不投入越落后,越落后越没人买”的恶性循环。比如大众途昂 Pro,搭载 IQ.Pilot 智驾系统,支持高快NOA,销量惨淡。 被批评为“反向创新”,入门版配置严重阉割,完整智驾需高价选装,导致上市首月订单仅832辆。

 

第三,最关键一点,无论大众还是奔驰宝马,不愿“革自己的命”。


燃油车时代,品牌溢价依赖于发动机、变速箱等机械素质。成本低利润高。而全面转向智驾需要企业重构供应链(从博世等Tier1转向英伟达等科技公司)和改变盈利模式。传统车企的核心利润仍来自燃油车,若在油车上普及智驾,会加速蚕食原有高利润车型的市场,因此态度上犹豫不决。

 

最早智驾从2016年就开始发展,但2016年大众缺没有发力智驾,而是选择通过简配低价卖更多的平价燃油车。保利润还是保增长,大众选择了后者。

 

而现在,大众一面要保证燃油车的口碑和市场,一面要发力纯电和插混,那么“油电同智”就成了必选项。

 

目前,大众已于地平线成立合资芯片公司酷睿程,入股电池厂商国轩高科并成为第一大股东,以弥补其在芯片、电池等方面的短板。

 

大众集团CEO奥博穆明确表示,“中国对大众而言远不止是销售市场,更是全球核心创新中心与技术策源地。”

 

可以说,无论大众的“油电同智”战略胜算几何,大众也必须一条路走到黑。无论从保市场还是保技术,都没有第二条路。


最后的话


最后来看,无论奔驰宝马还是大众,作为全球车企,均在急切地智能化转型与中国供应链深度绑定。从过去引领市场建立标准,到目前在智驾、电池、插混方面全方位与中方共建。


除了为保住中国燃油车市场份额,更重要的是希望借助中国的供应链和相对廉价的生产成本,用来返销欧洲等全球市场。

 

事实上,中德车企方面的合作信号在今年春节期间已有端倪。2026年2月10号,欧盟宣布大众安徽公司生产的CUPRA Tavascan车型,出口欧洲将减免20.7%的反补贴税。成为首例获得欧盟豁免的国产车型。CUPRA Tavascan与大众旗下与众06共享技术平台,后者在国内亦有销售。

 

这是中欧汽车经贸博弈走向缓和的标志,也是跨国车企在全球化逆风下寻求顺势而为的缩影。与其说是德系车在补新能源课,不如说是中德共建,寻求燃油车与新能源的新平衡。


尤其是在中国市场,如何能让三亿燃油车主用户上智能化的燃油车,这是奔驰宝马大众都需要思考的问题。


 

 


下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

苹果谷歌商店:如何监控并维护用户评分评论

前阵子,我无意中发现我们的应用在 App Store 上悄然出现了几条差评,但团队里似乎没人注意到。这让我意识到一个严重的问题:如果我们不能及时听到用户的声音,怎么能及时发现应用的不足,留住用户呢? 更令人担忧的是,潜在用户在下载前往往会浏览评论区,一条未被回应的负面评价,可能就足以让他们转身离开,影响新增转化。

如果能在用户留下评论(尤其是差评)的第一时间收到通知,我们就能快速响应、修复问题、安抚情绪,甚至将一次不满转化为一次忠诚度的提升。更重要的是,积极、真诚地回复用户评论,不仅能展现团队的专业与负责,还能向所有观望者传递一个信号:我们在乎每一位用户。

本篇文章将从实操角度出发,为不熟悉苹果和谷歌开发者后台的开发或运营同学,讲解如何监控苹果谷歌商店的评分评论,以及如何回复用户评论,为大家提供一些帮助。

一、苹果

苹果开发者后台 appstoreconnect.apple.com/,需要 客户支持 权限。

1、如何监控评分和评论

苹果后台目前不支持收到新的评分评论后邮件通知开发者。只支持“开发者回复”(当顾客编辑你已回复的评论时,你将收到电子邮件),如需开启“开发者回复”邮件通知,按下面步骤操作:

登录 App Store Connect。
点击右上角的用户头像,进入 “用户和访问”。
选择你的账户,在左侧菜单点击 “通知”。

Tips:“收到评分评论后邮件通知开发者”,这个功能在旧版 iTunes Connect 中曾经存在,但在新版 App Store Connect 中已被移除。猜测苹果可能不想开发者过度关注单条评分评论。

如果目前想要监控苹果商店的评分评论,有几个方案可参考:
1、使用官方的 App Store Connect App,每天刷一刷,自己主动去看。App内可以设置“接收用户评分”通知,但不确定现在还是否有效。
2、苹果官方提供了App Store Connect API,可以自己开发程序拉取用户评分,再进一步做监控。
3、滴答清单定个周期性提醒,每天上班打开商店详情瞅一眼,现在苹果上线了Web版AppStore了,瞅一眼也很方便。
4、借助第三方平台。

2、查看和回复用户评论

(1)通过网页端查看

登录苹果开发者后台,appstoreconnect.apple.com/

评分评论入口:分发 - 评分和评论 图片.png

点击“回复”可以回复用户评论
图片.png

(2)通过官方App "App Store Connect" 查看

iOS端下载地址:apps.apple.com/cn/app/app-…
(如果你搜不到可能是你手机系统版本太低了。没有安卓端。)

图片.png App Store Connect App核心功能:
-- 销售与趋势监控(查看 App 的下载量、销售额)
-- 版本状态管理(跟踪审核状态,回复审核)
-- 用户评论处理(查看和回复评论)

App Store Connect内查看评分及评论入口:
图片.png

3、重置总评分

发布新版本到 App Store 时(必须更包),你可以重置 App 总评分。重置后,你的 App Store 产品页面将显示说明,提示顾客 App 的总评分最近已重置。此说明将一直显示,直到有足够多的顾客对新版本进行了评分且页面出现新的总评分。

请注意,重置总评分并不会重置顾客评论,App Store 仍将继续显示历史的顾客评论

图片.png

二、Google

Google开发者后台 play.google.com/console/dev…,需要 用户反馈 权限。
“用户反馈”权限

1、如何监控评分和评论

Google官方支持收到新的评论后邮件提醒开发者,并支持按应用、评分星级设置不同的提醒开关。注意:邮件提醒默认是关闭的,需要手动开启。请按下列步骤操作。

Google开发者后台 - 设置 - 个人邮件通知(这个只会改你个人的通知设置,不会改整个团队的) 图片.png

按需将邮件提醒开关打开,修改后记得保存。
图片.png

如果你的账号拥有开发者账号下多个App的权限,默认是所有应用都给你发邮件,点击下图位置,可以选择哪些应用接收邮件。 图片.png

收到新的评论后,Google会给你推邮件,模板样式如下,包含了应用名称、评分星级、评论内容,不用打开Google后台就能看到评论内容,很方便。
注意:如果你接收了多个应用的邮件,请留意邮件标题里App的名字。

图片.png

2、查看和回复用户评论

(1)网页端

Google后台 - 应用 - 监控与改进 - 评分与评价。

Google后台的评论,Google会默认帮你翻译成你的语言,很贴心。如果你想看原始评论,点击“显示原评论”查看。你也可以在这里回复用户的评论。
图片.png

(2)官方 Google Play Console App

Google也像苹果一样,提供了官方的供开发者维护自己App的应用,Google Play Console App。你可以通过它在移动端方便的看评分和回复评论。

iOS端:apps.apple.com/cn/app/goog…
安卓端:play.google.com/store/apps/…

Google Play Console App

Google Play Console App 核心功能:

  • 查看数据指标:监控安装量、卸载量、更新量以及应用的崩溃率(ANR/Crash)。
  • 回复用户评论:及时查看并回复用户的评价,这对于维护 App 评分至关重要。
  • 订单管理:查看应用内购买和订阅的订单详情,甚至可以进行简单的退款操作。
  • 发布状态监控:跟踪应用版本的审核进度和发布状态。

3、Google不支持重置评分评论

Google不像苹果那样可以主动重置评分。虽然你不能手动重置,但 Google Play 的评分系统是动态权重的,更加偏重于近期(Recent)的用户评分权重会更高

这意味着:
(1)如果你的应用过去因为有 Bug 而评分很低,只要你在新版本中修复了问题,随着新用户和老用户在近期的好评增多,你的平均分会逐渐回升。
(2)时间是最好的解药:只要新版本的体验确实提升了,评分曲线会自动向好的方向修正。

三、结束语

其实维护应用商店的评论,并不需要多么复杂的流程或高深的技巧,但你做了和没做,用户感受是不一样的,每个人都希望被尊重,用真诚打动你的用户吧!

希望这篇文章能给你一点帮助。如果你有更好的监控方法,欢迎留言交流。

参考文档
【苹果官方文档】查看评分和评论

02-研究优秀开源框架@图层处理@iOS | Kingfisher 框架:从使用到原理解析

📋 目录


一、Kingfisher 概述与历史演进

1. 框架简介

Kingfisher 是一款面向 Apple 平台(iOS / macOS / tvOS / watchOS)的纯 Swift 异步图片下载与缓存库,由 onevcat(王巍)维护。其「图层处理」相关能力以 ImageProcessor 为核心:在「从数据到图像」以及「从图像到图像」的管线中,完成解码、缩放、圆角、模糊、着色等处理,并与 ImageCache(内存 + 磁盘)、ImageDownloader 协同,形成「请求 → 缓存查询 → 下载 → 处理 → 缓存 → 展示」的完整流程 [1][2]。

与 SDWebImage(Objective-C 为主)相比,Kingfisher 采用协议导向Options 模式,图层处理通过统一的 ImageProcessor 协议和 ImageProcessItem 双态输入抽象,便于扩展与组合。

2. 技术演进与版本脉络

Kingfisher 的图层处理能力随版本逐步增强,并与缓存、下载模块解耦清晰。

阶段 版本/时期 图层处理与相关能力
早期 3.x 基础下载与缓存,简单图片处理
缓存与处理器 3.10 带 ImageProcessor 的缓存策略:先查已处理图,若无再查原图,避免重复下载 [3]
架构升级 5.0 MemoryStorage / DiskStorage 分离,可缓存原始 Data,完善 KingfisherError,处理管线与缓存键绑定 [4]
下采样修复 5.3 下采样 scale 与内存表现修复:从原图加载下采样结果时的 scale 与内存问题 [5]
动图与序列化 7.8 磁盘缓存取回动图时正确使用请求中的 processor [6]
渐进式 JPEG 8.3 SwiftUI KFImage 支持 progressiveJPEG 修饰符 [7]

5.0 是重要分水岭:处理管线与缓存键(含 processorIdentifier)深度结合,使「同一 URL + 不同 Processor」对应不同缓存条目,原图与处理后图可并存。

3. 图层处理在整体架构中的位置

下图概括从「资源(URL / ImageDataProvider)」到「显示到视图」的流程,并标出 ImageProcessor 所在阶段。

flowchart LR
    subgraph 输入
        A[URL / ImageDataProvider]
    end
    subgraph 获取数据
        B[ImageDownloader / Provider.data]
    end
    subgraph 处理层
        C[Data]
        D[ImageProcessor 管线]
        E[KFCrossPlatformImage]
    end
    subgraph 缓存与输出
        F[ImageCache]
        G[ImageView / KFImage]
    end
    A --> B --> C --> D --> E --> F --> G

要点

  • ImageProcessor 的输入可以是 Data(未解码)或 Image(已解码);输出为 Image。因此它同时覆盖「Data → Image」(如 DefaultImageProcessor、DownsamplingImageProcessor)和「Image → Image」(如 RoundCorner、Blur、Resizing)两类操作。
  • 处理在 KingfisherManager 协调下、通常在后台队列执行,避免阻塞主线程,符合 Apple 图像最佳实践 [8]。

二、图像处理管线(ImageProcessor Pipeline)

1. ImageProcessItem 与双态输入

Kingfisher 用 ImageProcessItem 表示处理器的输入,有两种情况 [9]:

public enum ImageProcessItem: Sendable {
    /// 已解码的图像,处理器在其上做几何/像素变换
    case image(KFCrossPlatformImage)
    /// 原始数据,处理器需负责解码(或解码+变换)
    case data(Data)
}

设计意图

  • 统一接口:同一套管线既可处理「仅解码」(Data → Image),也可处理「仅变换」(Image → Image),或「解码 + 变换」(Data 经多个 Processor 最终得到 Image)。
  • 避免重复解码:当管线中第一个 Processor 已将 Data 转为 Image 后,后续 Processor 收到 .image(...),只需做几何/滤镜等操作,无需再次解码。

数据流概念

flowchart LR
    subgraph 管线输入
        I[Data]
    end
    subgraph P1[Processor 1]
        I --> D1[解码/下采样]
        D1 --> O1[Image]
    end
    subgraph P2[Processor 2]
        O1 --> D2[圆角/缩放等]
        D2 --> O2[Image]
    end
    O2 --> Out[输出]

2. ImageProcessor 协议与标识符

ImageProcessor 协议是 Kingfisher 图层处理的核心抽象 [9][10]:

协议 ImageProcessor:
    属性 identifier: String   // 唯一标识,参与缓存键
    方法 process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
  • identifier:相同功能/参数的 Processor 应返回相同字符串,用于缓存键。官方建议使用反向域名(如 com.onevcat.Kingfisher.RoundCornerImageProcessor(20)),且不要与 DefaultImageProcessor"" 冲突。
  • process:返回 nil 表示处理失败,管线会报错并中止;若输入已是 .image 且当前步骤可透传,可返回原图以继续后续 Processor。

伪代码:管线执行

函数 runPipeline(item: ImageProcessItem, processors: [ImageProcessor], options) -> Image?:
    current = item
    对每个 p in processors:
        若 current 为 .data 且 p 只支持 .image:
            current = .image(DefaultImageProcessor.default.process(current, options))
        若 current 为 nil: 返回 nil
        next = p.process(current, options)
        若 next 为 nil: 返回 nil
        current = .image(next)
    返回 current

许多内置 Processor(如 RoundCorner、Blur)在收到 .data 时,会先通过 DefaultImageProcessor.default |> self 将 Data 解码为 Image,再对 Image 做自身变换,从而复用同一套协议。

3. 下采样(Downsampling)与 Resizing 的区分

Kingfisher 明确区分两种「变小」的方式,对应不同的内存与 CPU 成本 [10][11]。

3.1 DownsamplingImageProcessor

  • 输入:仅 Data(压缩数据)。在解码阶段直接生成小尺寸位图,而不是先解码全图再缩放。
  • 实现:基于 ImageIO 的 CGImageSourceCreateThumbnailAtIndex,通过 kCGImageSourceThumbnailMaxPixelSize 等选项限制最大边长,在解码器内部只生成缩略图级像素缓冲。
  • 优势:内存占用与目标尺寸相关,避免「先全图解码」的峰值;大图列表、头像等场景推荐使用。

下采样算法步骤(与 Kingfisher / ImageIO 语义一致)

函数 Downsample(data: Data, size: CGSize) -> Image?:
    1. maxDimensionInPixels = max(size.width, size.height) * scale
    2. source = CGImageSourceCreateWithData(data, nil)
    3. options = {
         kCGImageSourceCreateThumbnailFromImageAlways: true,
         kCGImageSourceCreateThumbnailWithTransform: true,
         kCGImageSourceShouldCacheImmediately: true,
         kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
       }
    4. cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options)
    5. 由 cgImage 构造 UIImage/NSImage 并返回

注意size 不能为 (0, 0),否则会触发 "Processing image failed. Processor: DownsamplingImageProcessor" [11];在列表 cell 中应使用 cell 或目标视图的 bounds 计算合理 size。

3.2 ResizingImageProcessor

  • 输入:一般为 Image(或通过 DefaultImageProcessor 先解码的 Data)。对已解码的位图做缩放,支持 ContentMode(如 aspectFit、aspectFill)。
  • 实现:在像素缓冲上做几何变换(绘制到目标尺寸),会先占用全图解码的内存,再产生缩放后的新缓冲。
  • 适用:已解码图、或必须对 Image 做精确尺寸/比例控制时使用;若从 Data 缩小,应优先 DownsamplingImageProcessor

对比小结

维度 DownsamplingImageProcessor ResizingImageProcessor
输入 Data Image(或 Data 经 Default 解码)
时机 解码时直接出小图 先解码全图再缩放
内存 与目标尺寸相关 先有全图峰值再缩放
典型场景 列表缩略图、头像 已解码图的尺寸/比例调整

4. 多处理器链式组合

Kingfisher 支持将多个 ImageProcessor 串联成一条管线,按顺序执行:前一个的输出作为后一个的输入(.image(...))[10]。

组合方式:通过 append(another:)|> 运算符(Kingfisher 在 ImageProcessor 扩展中定义 |> 为调用 append(another:)):

// 先模糊,再圆角
let processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [.processor(processor)])

组合后的 identifier"\(p1.identifier)|>\(p2.identifier)",用于缓存键,保证「同一 URL + 同一处理器链」唯一对应一条缓存。

链式执行语义(伪代码)

函数 GeneralProcessor.process(item, options):
    image1 = self.process(item, options)
    若 image1 为 nil: 返回 nil
    返回 another.process(.image(image1), options)

因此,若链中第一个 Processor 能处理 .data(如 DefaultImageProcessor 或 DownsamplingImageProcessor),后续 Processor 将始终收到 .image(...)


三、解码、缓存与处理器的协同

1. 检索流程与缓存键

Kingfisher 的检索顺序可概括为 [2][3]:

  1. 使用 cacheKey + processorIdentifier内存缓存
  2. 若未命中,查磁盘缓存(同样 key + processorIdentifier);
  3. 若仍未命中,通过 ImageDownloaderImageDataProvider 获取 Data;
  4. 对 Data 执行 ImageProcessor 管线,得到 Image;
  5. 将结果写入内存与磁盘缓存,并交给视图或完成回调。

缓存键:缓存的唯一标识是 cacheKey + processorIdentifier(DefaultImageProcessor 的 identifier 为空字符串)。因此:

  • 同一 URL,不同 Processor(或不同链)会得到不同缓存条目
  • 原图(DefaultImageProcessor)与下采样/圆角等版本可并存
  • 判断或读取缓存时若请求中指定了非 Default 的 Processor,需传入相同 processorIdentifier,例如:cache.isCached(forKey: cacheKey, processorIdentifier: processor.identifier)cache.retrieveImage(forKey: cacheKey, options: [.processor(processor)], ...)
flowchart TD
    A[请求: URL + Processor] --> B[构造 cacheKey + processorIdentifier]
    B --> C{内存缓存?}
    C -->|命中| D[返回 Image]
    C -->|未命中| E{磁盘缓存?}
    E -->|命中| F[解码/反序列化]
    F --> D
    E -->|未命中| G[下载 / Provider]
    G --> H[Processor 管线]
    H --> I[写内存+磁盘]
    I --> D

2. CacheSerializer 与磁盘格式

CacheSerializer 负责「Image ↔ Data」在磁盘缓存中的序列化与反序列化 [10]:

  • 存储data(with:original:),将当前要缓存的 Image 转为 Data(可结合 original Data 决定格式);
  • 读取image(with:options:),将磁盘上的 Data 转回 Image。

调用时机(便于理解与扩展):

  • Processor.process:① 网络下载成功或 ImageDataProvider 返回 Data 后,将 Data 加工为 Image;② 从磁盘读取到原始 Data 后,先经 CacheSerializer 反序列化为 Image,再经 Processor 处理(若请求中指定了 Processor)。因此磁盘命中「已处理图」时直接返回,命中「原图」时会再走一次 Processor。
  • CacheSerializer.image:从磁盘读取到 Data 后,用于将 Data 反序列化为 Image。
  • CacheSerializer.data:需要写入磁盘时,将 Image 序列化为 Data 再落盘。

默认行为:尽量保持原始数据格式(如 JPEG 仍存为 JPEG)。但当使用 RoundCornerImageProcessor 等会引入透明通道的处理器时,若原图是 JPEG(无透明通道),直接按 JPEG 存会丢失圆角透明区域。此时可指定 FormatIndicatedCacheSerializer.png,强制以 PNG 缓存处理后的图像:

imageView.kf.setImage(with: url,
    options: [.processor(RoundCornerImageProcessor(cornerRadius: 20)),
              .cacheSerializer(FormatIndicatedCacheSerializer.png)])

3. 内置 Processor 一览

Processor 输入偏好 功能
DefaultImageProcessor Data / Image Data→Image 解码,或 Image 按 scale 缩放
DownsamplingImageProcessor Data 解码时下采样,限制最大尺寸
ResizingImageProcessor Image 按 referenceSize + ContentMode 缩放
RoundCornerImageProcessor Image 圆角(可指定角、背景色、目标尺寸)
CroppingImageProcessor Image 按 size + anchor 裁剪
BlurImageProcessor Image 高斯模糊(Accelerate)
TintImageProcessor / OverlayImageProcessor Image 着色 / 叠色
ColorControlsProcessor / BlackWhiteProcessor Image 亮度对比度饱和度 / 黑白
BorderImageProcessor Image 加边框
BlendImageProcessor (iOS) / CompositingImageProcessor (macOS) Image 混合模式

4. 应用场景与选型

场景 推荐 Processor 说明
列表/表格缩略图 DownsamplingImageProcessor(size:) 从 Data 直接下采样,控制内存;size 取 cell 或目标尺寸
头像/圆角 RoundCornerImageProcessor 可配合 .png serializer 保留透明圆角
占位/毛玻璃 BlurImageProcessor 基于 Accelerate 的高斯模糊
统一尺寸且需等比 ResizingImageProcessor(referenceSize, mode: .aspectFit) 对已解码图做缩放
多步效果 链式:e.g. Blur |> RoundCorner 顺序决定最终效果与缓存键

RoundCornerImageProcessor 指定圆角:除四角统一圆角外,可指定部分角,如仅左上与右下:RoundCornerImageProcessor(cornerRadius: 20, roundingCorners: [.topLeft, .bottomRight])


四、类结构图分析

1. 核心类总览

Kingfisher 的类可按职责分为:入口与协调加载缓存处理管线视图扩展 五类。下表给出核心类/协议及其职责。

模块 核心类 / 协议 职责简述
协调 KingfisherManager 统一入口:协调 ImageDownloader、ImageCache、ImageProcessor 管线,执行「查缓存 → 下载/Provider → 处理 → 写缓存 → 回调」
加载 ImageDataProvider (协议) 定义数据来源接口:根据 URL 或资源标识返回 Data(如 Base64ImageDataProviderLocalFileImageDataProvider
ImageDownloader 默认网络加载:基于 URLSession 下载,支持并发、取消、RequestModifier、SessionDelegate
ImageDownloaderOperation 单次下载任务,封装 URLSessionTask
缓存 ImageCache 内存 + 磁盘二级缓存,提供 retrieve/store/remove,key 含 cacheKey + processorIdentifier
MemoryStorage / DiskStorage 5.0+ 内存层、磁盘层具体实现,可配置 count/cost 限制与过期策略
处理管线 ImageProcessor (协议) 定义 process(item:options:) -> KFCrossPlatformImage?,输入为 ImageProcessItem(.data / .image)
ImageProcessItem (枚举) 双态输入:.data(Data).image(KFCrossPlatformImage),统一「仅解码」「仅变换」「解码+变换」
DefaultImageProcessor / DownsamplingImageProcessor / RoundCornerImageProcessor 内置 Processor 实现,支持 ` >` 链式组合
CacheSerializer (协议) 磁盘格式:Image ↔ Data 序列化/反序列化,如 FormatIndicatedCacheSerializer.png
视图 KingfisherWrapper + ImageView.kf 为 UIImageView/NSImageView 等提供 kf.setImage(with:options:...)kf.cancelDownloadTask()
KFImage (SwiftUI) SwiftUI 图片组件,支持 URL、Processor、progressiveJPEG 等
ImagePrefetcher 预取多张图片,可配合 UICollectionView 的 prefetch

2. 模块划分与依赖关系

下图从「模块」维度表示各层之间的依赖方向:视图扩展与 Prefetcher 依赖 KingfisherManager,Manager 依赖 Downloader/Cache,处理管线在 Manager 内执行(Processor 链与 CacheSerializer 参与缓存键与磁盘格式)。

flowchart TB
    subgraph 视图层
        V1[ImageView.kf / KFImage]
        V2[ImagePrefetcher]
    end
    subgraph 协调层
        M[KingfisherManager]
    end
    subgraph 加载层
        L[ImageDownloader]
        P[ImageDataProvider 实现]
    end
    subgraph 缓存层
        C[ImageCache]
    end
    subgraph 处理管线层
        IP[ImageProcessor 实现]
        CS[CacheSerializer]
    end
    V1 --> M
    V2 --> M
    M --> L
    M --> P
    M --> C
    M --> IP
    M --> CS

3. 加载与缓存类结构

ImageDownloader 负责从网络获取 Data;ImageDataProvider 可提供本地或自定义 Data;ImageCache 负责内存与磁盘的读写。KingfisherManager 持有 cache 与 downloader,在单次请求中先查缓存(key = cacheKey + processorIdentifier),未命中再通过 downloader 或 provider 取数据,经 Processor 管线后写回缓存。

classDiagram
    class KingfisherManager {
        -cache: ImageCache
        -downloader: ImageDownloader
        +retrieveImage(with:options:progressBlock:completionHandler:)
        -loadAndCacheImage(source:options:completionHandler:)
    }
    class ImageCache {
        -memoryStorage: MemoryStorage
        -diskStorage: DiskStorage
        +retrieveImage(forKey:options:callbackQueue:completionHandler:)
        +store(_:forKey:options:toDisk:completionHandler:)
        +removeImage(forKey:fromMemory:fromDisk:completionHandler:)
    }
    class ImageDownloader {
        -session: URLSession
        -downloadQueue: OperationQueue
        +downloadImage(with:options:completionHandler:)
    }
    class ImageDataProvider {
        <<protocol>>
        +data(handler:)
        +cacheKey
    }
    KingfisherManager --> ImageCache : 使用
    KingfisherManager --> ImageDownloader : 使用
    KingfisherManager ..> ImageDataProvider : 支持 Source.provider
  • KingfisherManager:对外通过 retrieveImage(with:...) 接收 Source(.network(URL) 或 .provider(ImageDataProvider)),先查 ImageCache(key 含 processorIdentifier),未命中则调 downloader 或 provider 取 Data,再跑 Processor 管线并写回缓存。
  • ImageCache:5.0+ 将内存与磁盘拆为 MemoryStorage / DiskStorage,可配置 count/cost、过期时间;存储时由 CacheSerializer 决定 Image → Data 的格式(如 PNG 保留圆角透明)。
  • ImageDownloader:基于 URLSession,单次下载封装为 ImageDownloaderOperation,支持并发数、超时、RequestModifier;与 Provider 一起构成「数据来源」的两种方式。

4. 处理管线与 Processor 类结构

ImageProcessor 协议是图层处理的核心:输入为 ImageProcessItem(.data 或 .image),输出为 KFCrossPlatformImage。Manager 在「取得 Data 后」按 options 中的 processor(或链)依次执行;链的 identifier 拼接后参与缓存键,实现「同一 URL + 不同 Processor」对应不同缓存条目。

classDiagram
    class KingfisherManager {
        -runProcessors(_:data:options:)
    }
    class ImageProcessItem {
        <<enumeration>>
        +image(KFCrossPlatformImage)
        +data(Data)
    }
    class ImageProcessor {
        <<protocol>>
        +identifier: String
        +process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo): KFCrossPlatformImage?
    }
    class DefaultImageProcessor {
        +process(item:options:)
    }
    class DownsamplingImageProcessor {
        +size: CGSize
        +process(item:options:)
    }
    class RoundCornerImageProcessor {
        +cornerRadius: CGFloat
        +process(item:options:)
    }
    class ImageProcessorGroup {
        -processors: [ImageProcessor]
        +append(another:)
        +identifier
    }
    class CacheSerializer {
        <<protocol>>
        +data(with:original:)
        +image(with:options:)
    }
    ImageProcessItem --> ImageProcessor : 输入
    ImageProcessor <|.. DefaultImageProcessor : 实现
    ImageProcessor <|.. DownsamplingImageProcessor : 实现
    ImageProcessor <|.. RoundCornerImageProcessor : 实现
    ImageProcessor <|.. ImageProcessorGroup : 链式组合
    KingfisherManager ..> ImageProcessor : 执行管线
    KingfisherManager ..> CacheSerializer : 磁盘序列化
  • ImageProcessItem:双态设计使同一管线既可处理「Data → Image」(解码/下采样),也可处理「Image → Image」(圆角、模糊、缩放),或混合链式处理;收到 .data 的 Processor 可先通过 DefaultImageProcessor.default |> self 解码再变换。
  • ImageProcessor 链:通过 append(another:)|> 组合,链的 identifier 为各子 Processor identifier 用 "|>" 拼接,参与缓存键;执行时前一个输出作为后一个的 .image(...) 输入。
  • CacheSerializer:磁盘存储时由 data(with:original:) 将 Image 转为 Data,读取时由 image(with:options:) 反序列化;圆角等带透明通道的结果可选用 FormatIndicatedCacheSerializer.png 避免 JPEG 丢失透明。

5. View 扩展与调用链

视图扩展(如 ImageView.kf、SwiftUI 的 KFImage)是业务最常接触的入口:内部将 Resource(URL 或 ImageDataProvider)、placeholder、options 交给 KingfisherManager,并把返回的 DownloadTask 与 view 关联,以便在复用时取消。

sequenceDiagram
    participant V as ImageView
    participant KF as ImageView.kf
    participant M as KingfisherManager
    participant C as ImageCache
    participant D as ImageDownloader

    V->>KF: setImage(with: url, options: [.processor(...)])
    KF->>KF: cancelDownloadTask()
    KF->>M: retrieveImage(with: .network(url), options: ...)
    M->>C: retrieveImage(forKey: cacheKey+processorIdentifier)
    alt 缓存命中
        C-->>M: image (memory/disk)
        M-->>KF: completion(image, .memory/.disk)
    else 未命中
        M->>D: downloadImage(with: url, ...)
        D-->>M: data
        M->>M: Processor 管线处理
        M->>C: store(image, forKey: ...)
        M-->>KF: completion(image, .none)
    end
    KF->>V: imageView.image = image
  • kf.setImage(with: placeholder: options: progressBlock: completionHandler:):先对当前 view 取消未完成的 DownloadTask,再调 KingfisherManager.shared.retrieveImage(with: source, options: options, ...);在 completion 中把得到的 image 赋给 imageView.image(并可选执行 transition)。
  • kf.cancelDownloadTask():取消与该 view 绑定的任务,避免 cell 复用时旧请求覆盖新图。
  • KFImage (SwiftUI):通过 KFImage 传入 url、processor、placeholder 等,内部同样走 KingfisherManager,支持 progressiveJPEG(8.3+)等选项。

将上述「核心类总览」「模块依赖」「加载与缓存类图」「Processor 与管线类图」「View 调用链」串联起来,即可形成对 Kingfisher 类结构图 的完整分析:入口在视图扩展(kf / KFImage),核心协调在 KingfisherManager,加载(Downloader/Provider)、缓存(ImageCache + CacheSerializer)、处理(ImageProcessor + ImageProcessItem)均为协议导向的可插拔设计,便于扩展与测试。


五、与系统及业界实践的衔接

1. Apple 图像与图形最佳实践

Apple 在 WWDC 2018「Image and Graphics Best Practices」[8] 中强调:

  • 在后台线程解码与下采样,避免主线程卡顿;
  • 解码时即做下采样,使解码后缓冲与显示尺寸匹配,降低内存峰值;
  • 预取:在列表等场景提前准备即将显示的图像。

Kingfisher 的 DownsamplingImageProcessor 直接对应「解码时下采样」;处理管线在 KingfisherManager 的队列中执行,满足「后台处理」;配合 ImagePrefetcherUICollectionViewDataSourcePrefetching 等可实现预取 [10]。与 SDWebImage 类似,其设计与此类最佳实践一致。

2. 与 SDWebImage 的对比

维度 Kingfisher SDWebImage
语言 纯 Swift Objective-C 为主,Swift 接口
处理抽象 ImageProcessor + ImageProcessItem SDImageTransformer
输入类型 .image / .data 双态 一般为已解码 Image
下采样 DownsamplingImageProcessor(Data→Image) 解码管线内缩略图/limitBytes
链式组合 append / |>,identifier 拼接 SDImagePipelineTransformer 数组
缓存键 cacheKey + processorIdentifier 含 transformer 信息
渐进式 8.3+ KFImage progressiveJPEG Progressive Coder 体系

二者都遵循「解码/下采样 + 变换 + 缓存」的管线思想,Kingfisher 通过 ImageProcessItem 将「解码」与「变换」统一进同一协议,便于从 Data 直接到最终 Image 的一体化处理。

3. 动图加载(GIF)与 AnimatedImageView

Kingfisher 加载 GIF 的两种方式:UIImageViewAnimatedImageView(继承自 UIImageView),调用方式相同,内部行为不同 [12]。

  • UIImageViewshouldPreloadAllAnimation() 扩展返回 true,即 preloadAllAnimationData 被设为 true,GIF 会先解码为所有帧的 UIImage 数组,再通过 UIImage.animatedImage(with:duration:) 展示。适合帧数少的动图。
  • AnimatedImageView:重写 shouldPreloadAllAnimation() 返回 false,不预加载全部帧;通过关联的 CGImageSourceAnimator 按需解码(默认仅预加载前若干帧),用 CADisplayLink 在每帧刷新时更新 layer.contents(重写 display(_ layer:))。更省内存,CPU 略高。

AnimatedImageView 独有runLoopModebackgroundDecodeframePreloadCountautoPlayAnimatedImagerepeatCount 等。若需在列表或详情中播放 GIF 且控制内存,建议使用 AnimatedImageView


六、设计模式与编程思想

1. 设计模式应用

Kingfisher 在架构上大量运用经典设计模式,与纯 Swift、协议导向的风格结合,使扩展与维护成本可控。

模式 在 Kingfisher 中的体现 作用
外观 / 门面(Facade) KingfisherManager 对外提供 retrieveImage(with:options:progressBlock:completionHandler:),内部协调 ImageDownloader、ImageCache、ImageProcessor 管线,调用方无需关心多级缓存与处理顺序 简化使用、隐藏复杂度
策略(Strategy) ImageProcessorCacheSerializerImageDataProvider 均为协议,多种实现可替换(RoundCorner、Downsampling、FormatIndicatedCacheSerializer 等),通过 KingfisherOptionsInfo 传入 算法/行为可插拔,易扩展新处理与存储格式
责任链 / 管道(Chain of Responsibility / Pipeline) ImageProcessor 通过 append(another:)|> 串联成管线;ImageProcessItem 双态(.data / .image)使「解码 → 变换」在同一链中顺序执行 多步处理顺序清晰,便于组合与复用
单例 + 共享依赖(Singleton) KingfisherManager.sharedImageCache.defaultImageDownloader.default 提供默认实例,同时 retrieveImage 等 API 支持传入自定义 cache、downloader,打破单例绑定 全局统一入口,又保留可测试性与多实例能力
观察者 / 回调(Observer / Callback) 通过 progressBlockcompletionHandler 闭包通知进度与结果;Swift 并发下也可用 async/await 异步结果与 UI 解耦
组合 / 装饰(Composite) 多个 ImageProcessor 通过 |> 组合成新 Processor,其 identifier 为子 Processor 的 identifier 拼接,对外仍满足同一 ImageProcessor 协议 链式处理器可当作单一策略使用,参与缓存键一致

类图关系(概念层)

classDiagram
    class KingfisherManager {
        -cache: ImageCache
        -downloader: ImageDownloader
        +retrieveImage(with:options:progressBlock:completionHandler:)
    }
    class ImageCache {
        +retrieveImage(forKey:options:callbackQueue:completionHandler:)
        +store(_:forKey:options:toDisk:completionHandler:)
    }
    class ImageDownloader {
        +downloadImage(with:options:completionHandler:)
    }
    class ImageProcessor {
        <<protocol>>
        +identifier: String
        +process(item: ImageProcessItem, options:): KFCrossPlatformImage?
    }
    class ImageDataProvider {
        <<protocol>>
        +data(handler:)
        +cacheKey
    }
    KingfisherManager --> ImageCache : 使用
    KingfisherManager --> ImageDownloader : 使用
    KingfisherManager ..> ImageProcessor : 处理时选用
    KingfisherManager ..> ImageDataProvider : Source.provider

2. 编程思想精华

Kingfisher 的编程思想可提炼为以下几点,对理解与模仿其设计很有帮助。

2.1 协议导向与「可替换实现」

  • ImageProcessorCacheSerializerImageDataProvider 均以协议呈现,具体实现可替换、可组合。
  • 新增一种图像处理或一种磁盘格式,只需实现对应协议并通过 options 传入(如 .processor(...).cacheSerializer(...)),无需改动 KingfisherManager 核心流程。这体现了开闭原则:对扩展开放,对修改关闭。

2.2 管线化与单一职责

  • 把「从 Source 到屏幕」拆成:获取数据(Downloader/Provider)→ Processor 管线(解码/下采样 + 变换)→ 缓存 → 展示,每一步只做一件事。
  • Processor 只关心 ImageProcessItem → Image,Cache 只关心存储与查找,Downloader 只关心网络 Data。单一职责使每块可独立测试、优化和扩展;管线化则使数据流清晰,便于加日志与监控。

2.3 双态输入与「解码+变换」统一

  • ImageProcessItem.data / .image 双态设计,使同一 ImageProcessor 协议既能表达「Data → Image」(如 Default、Downsampling),也能表达「Image → Image」(如 RoundCorner、Blur),还能通过链式组合在一次管线中完成解码与多步变换。
  • 避免「解码器」与「变换器」两套抽象,降低概念数量,便于链式组合与缓存键一致(整条链一个 identifier 串)。

2.4 缓存键与「同一资源多形态」

  • 通过 cacheKey + processorIdentifier 的设计,同一 URL 可以对应「原图」「下采样图」「圆角图」等多条缓存,避免重复下载,又满足不同场景对尺寸/形态的需求。这体现了用键设计表达业务差异的思想。

2.5 后台处理与主线程回调

  • 下载、Processor 管线、磁盘 I/O 均在后台队列执行,completionHandler 通过 CallbackQueue.mainAsync 等派发到主线程,兼顾性能与 UI 安全。这是移动端异步加载库的通用范式:重活放后台,结果回主线程

2.6 取消与生命周期绑定

  • 视图扩展(如 ImageView.kf)会把「当前正在进行的 DownloadTask」与 view 绑定,当对同一 view 发起新请求时先取消旧任务,避免错位和浪费。这体现了生命周期与请求绑定的思想,在列表 cell 复用时尤为重要。

2.7 配置通过 Options 透传

  • 不通过全局单例属性堆砌配置,而是通过 KingfisherOptionsInfo(如 .processor.cacheSerializer.callbackQueue)在单次请求中传入,使「同一 App 内不同页面/模块」可使用不同 Processor 与缓存策略,且易于单元测试时注入 mock。

Kingfisher 编程思想精华一览

思想 在框架中的体现
协议导向、可替换 ImageProcessor / CacheSerializer / ImageDataProvider 协议化,新处理、新格式仅需实现协议并通过 options 传入
管线化、单一职责 获取数据 → Processor 管线 → 缓存 → 展示,每步职责单一,便于扩展与测试
双态输入、解码+变换统一 ImageProcessItem(.data / .image) + 链式 Processor,一条管线完成解码与多步变换,identifier 参与缓存键
键设计表达多形态 同一 URL 通过 cacheKey + processorIdentifier 支持原图、下采样图、圆角图等多条缓存
后台处理、主线程回调 重 CPU/IO 在后台队列,completion 回主线程(CallbackQueue),兼顾性能与 UI 安全
生命周期绑定取消 View 与 DownloadTask 绑定,新请求自动取消旧请求,避免列表错位
Options 透传配置 单次请求级 options(processor、cacheSerializer、callbackQueue 等),避免全局状态,利于多策略并存与测试注入

七、使用示例与最佳实践

1. 基础加载与圆角

let processor = RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [.processor(processor)])

2. 列表缩略图(下采样)

let size = imageView.bounds.size
let processor = DownsamplingImageProcessor(size: size)
imageView.kf.setImage(with: url, options: [.processor(processor)])
// 注意:size 不可为 .zero

3. 多处理器链与强制 PNG 缓存

let processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [
    .processor(processor),
    .cacheSerializer(FormatIndicatedCacheSerializer.png)
])

4. 自定义 Processor(仅做示意)

struct MyProcessor: ImageProcessor {
    let identifier = "com.example.myprocessor"
    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        switch item {
        case .image(let image): return image // 或对 image 做变换
        case .data(let data): return DefaultImageProcessor.default.process(item: item, options: options)
        }
    }
}

5. 预取与列表

// 配合 UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { URL(string: model(at: $0).imageURL) }
    ImagePrefetcher(urls: urls).start()
}

6. Cell 完整示例(复用、下采样、进度与完成回调)

列表 Cell 中需在 prepareForReuse 中取消任务并清空,在 configure 中按目标尺寸下采样并可选显示进度。

class PhotoCell: UITableViewCell {
    static let reuseId = "PhotoCell"
    @IBOutlet weak var photoImageView: UIImageView!
    @IBOutlet weak var progressView: UIProgressView!

    override func prepareForReuse() {
        super.prepareForReuse()
        photoImageView.kf.cancelDownloadTask()
        photoImageView.image = nil
        progressView.progress = 0
        progressView.isHidden = true
    }

    func configure(with url: URL) {
        let size = photoImageView.bounds.size
        let processor = DownsamplingImageProcessor(size: size.isEmpty ? CGSize(width: 120, height: 120) : size)
        photoImageView.kf.setImage(
            with: url,
            placeholder: UIImage(named: "placeholder"),
            options: [.processor(processor), .scaleFactor(UIScreen.main.scale)],
            progressBlock: { [weak self] received, total in
                guard let self = self, total > 0 else { return }
                DispatchQueue.main.async {
                    self.progressView.isHidden = false
                    self.progressView.progress = Float(received) / Float(total)
                }
            },
            completionHandler: { [weak self] result in
                DispatchQueue.main.async {
                    self?.progressView.isHidden = true
                    if case .failure = result { /* 可设置失败占位图 */ }
                }
            }
        )
    }
}

7. UIButton 设置网络图片

为 UIButton 的不同 state 设置网络图片,可配合 Processor 与完成回调。

// 设置 normal / highlighted 等状态的图片
button.kf.setImage(with: url, for: .normal, placeholder: UIImage(named: "btn_placeholder"))
button.kf.setImage(with: highlightedURL, for: .highlighted)
button.kf.setBackgroundImage(with: backgroundURL, for: .normal)

// 带圆角与完成回调
let processor = RoundCornerImageProcessor(cornerRadius: 8)
button.kf.setImage(
    with: url,
    for: .normal,
    placeholder: nil,
    options: [.processor(processor), .cacheSerializer(FormatIndicatedCacheSerializer.png)],
    completionHandler: { result in
        if case .failure = result { print("加载失败") }
    }
)

8. 占位图、进度与过渡动画

使用占位图、下载进度条,并在图片加载完成后执行淡入等过渡动画。

imageView.kf.setImage(
    with: url,
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(ImageTransition.fade(0.3)),
        .retryFailed
    ],
    progressBlock: { [weak progressView] received, total in
        guard let pv = progressView, total > 0 else { return }
        DispatchQueue.main.async {
            pv.progress = Float(received) / Float(total)
            pv.isHidden = false
        }
    },
    completionHandler: { [weak progressView] result in
        DispatchQueue.main.async {
            progressView?.isHidden = true
            if case .failure = result { /* 可显示失败占位或提示 */ }
        }
    }
)

9. 自定义缓存键与请求修饰(RequestModifier)

同一 URL 在不同业务下需要不同缓存键时,可通过 KingfisherOptionsInfo 传入自定义 cacheKey;需要鉴权或自定义 Header 时使用 ImageDownloadRequestModifier

// 自定义缓存键:列表用 thumb key、详情用原图 key
let listResource = ImageResource(downloadURL: url, cacheKey: "list_\(url.absoluteString)")
let detailResource = ImageResource(downloadURL: url, cacheKey: "detail_\(url.absoluteString)")
listImageView.kf.setImage(with: listResource, options: [.processor(DownsamplingImageProcessor(size: thumbSize))])
detailImageView.kf.setImage(with: detailResource)

// 请求修饰:Header、Token、超时
struct AuthModifier: ImageDownloadRequestModifier {
    let token: String
    func modified(for request: URLRequest) -> URLRequest? {
        var r = request
        r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        r.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
        return r
    }
}
imageView.kf.setImage(with: url, options: [.requestModifier(AuthModifier(token: userToken))])

10. 缓存查询与手动存储

不经过视图加载流程,直接使用 ImageCache 查询、存储或移除缓存。

let cache = ImageCache.default
let key = url.absoluteString  // 或自定义 cacheKey(与 Processor 组合时由框架自动拼接 processorIdentifier)

// 查询是否已缓存
cache.imageCachedType(forKey: key) { result in
    switch result {
    case .success(let cached):
        switch cached {
        case .none:   print("未缓存")
        case .memory: print("在内存")
        case .disk:   print("在磁盘")
        }
    case .failure: break
    }
}

// 从缓存读取(不触发下载)
cache.retrieveImage(forKey: key, options: nil) { result in
    switch result {
    case .success(let value):
        if let image = value.image { imageView.image = image }
    case .failure: break
    }
}

// 手动写入缓存(如本地生成或从相册来的图)
cache.store(image, forKey: key, options: nil, toDisk: true) { _ in }

11. 自定义 Processor 完整示例(加边框)

实现 ImageProcessor 协议,对已解码图像做自定义绘制(如加灰色边框)。

struct GrayBorderProcessor: ImageProcessor {
    let identifier = "com.example.grayborder(\(borderWidth))"
    let borderWidth: CGFloat

    init(borderWidth: CGFloat = 2) { self.borderWidth = borderWidth }

    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        switch item {
        case .image(let image):
            let size = image.size
            let renderer = UIGraphicsImageRenderer(size: size)
            return renderer.image { ctx in
                image.draw(at: .zero)
                UIColor.gray.setStroke()
                let rect = CGRect(origin: .zero, size: size).insetBy(dx: borderWidth/2, dy: borderWidth/2)
                ctx.stroke(rect, with: .color(.gray), lineWidth: borderWidth)
            }
        case .data:
            return DefaultImageProcessor.default.process(item: item, options: options)
        }
    }
}

// 使用
imageView.kf.setImage(with: url, options: [.processor(GrayBorderProcessor(borderWidth: 3))])

12. SwiftUI KFImage 与 async/await

在 SwiftUI 中使用 KFImage,并配合渐进式 JPEG、占位与异步加载。

// 基础用法
KFImage(url)
    .placeholder { ProgressView() }
    .fade(duration: 0.25)
    .resizable()
    .aspectRatio(contentMode: .fit)

// 带 Processor 与圆角
KFImage(url)
    .setProcessor(RoundCornerImageProcessor(cornerRadius: 12))
    .placeholder { Color.gray.opacity(0.2) }
    .cacheSerializer(FormatIndicatedCacheSerializer.png)

// 8.3+ 渐进式 JPEG
KFImage(url)
    .progressiveJPEG(ImageProgressive(isBlur: true, isFastestScan: true, scanInterval: 0.1))

// 使用 async/await(Kingfisher 提供的异步 API)
Task {
    let result = await KingfisherManager.shared.retrieveImage(with: url)
    if case .success(let value) = result {
        await MainActor.run { imageView.image = value.image }
    }
}

13. ImageDataProvider(本地与 Base64)用法

不依赖网络 URL 时,可用 ImageDataProvider 从本地文件或 Base64 字符串加载,并同样走缓存与 Processor 管线。

// 本地文件
let fileURL = Bundle.main.url(forResource: "avatar", withExtension: "jpg")!
let provider = LocalFileImageDataProvider(fileURL: fileURL)
imageView.kf.setImage(with: provider)

// Base64 数据(如接口返回的 data URL)
let base64String = "data:image/png;base64,iVBORw0KGgo..."
if let provider = Base64ImageDataProvider(base64String: base64String, cacheKey: "custom_key") {
    imageView.kf.setImage(with: provider, options: [.processor(RoundCornerImageProcessor(cornerRadius: 10))])
}

// 自定义 Provider:从相册、加密存储等获取 Data
struct MyImageDataProvider: ImageDataProvider {
    var cacheKey: String { "my_\(id)" }
    let id: String
    func data(handler: @escaping (Result<Data, Error>) -> Void) {
        // 异步获取 Data 后调用 handler(.success(data)) 或 handler(.failure(...))
    }
}
imageView.kf.setImage(with: MyImageDataProvider(id: "123"))

14. 其他常用选项速览

选项 含义
.forceRefresh 跳过缓存,强制重新下载
.retryFailed 对之前失败的 URL 重试
.onlyFromCache 仅从缓存读取,不发起网络请求
.backgroundDecode 在后台队列解码,减少主线程压力
.callbackQueue(.mainAsync) 指定完成回调的派发队列
.downloadPriority(1.0) 下载任务优先级(iOS)
.scaleFactor(UIScreen.main.scale) 与 @2x/@3x 匹配,避免模糊
.cacheMemoryOnly 仅写内存缓存,不写磁盘
.loadDiskFileSynchronously 从磁盘加载时是否同步(默认异步)
imageView.kf.setImage(with: url, options: [.forceRefresh, .retryFailed, .callbackQueue(.mainAsync)])

15. Options 详解(延伸)

  • targetCache / originalCache:默认为 nil 时使用 ImageCache(name: "default")targetCache 为最终展示图的缓存(含 Processor 处理后的图),originalCache 为原始数据的缓存,可用于「列表用处理图、详情用原图」等分离策略。
  • transition:图片加载完成后的展示动画;forceTransition 为 true 时即使命中缓存也执行 transition,为 false 时仅在不使用缓存(新下载)时执行。
  • callbackQueue / processingQueuecallbackQueue 可选 .mainAsync.mainCurrentOrAsync(当前线程为主线程则直接执行,否则主线程异步)、.untouch.dispatch(DispatchQueue),默认多为 .mainCurrentOrAsyncprocessingQueue 为 Processor 执行所在队列,默认串行子队列。
  • memoryCacheAccessExtendingExpiration:从内存/磁盘取图时是否延长过期时间,可选 .none(不延长)、.cacheTime(当前时间 + 原过期时长)、.expirationTime(StorageExpiration)(延长到指定时长)。

16. 指示器、Placeholder 与 Transition 类型

  • 指示器(Indicator)imageView.kf.indicatorType 可选 .none.activity(UIActivityIndicatorView)、.image(imageData: Data)(GIF 等)、.custom(indicator: Indicator),自定义需实现 Indicator 协议(startAnimatingView / stopAnimatingView)。
  • Placeholder:除 UIImage 外,可实现 Placeholder 协议的自定义 View(如 class MyPlaceholder: UIView, Placeholder {}),设置 imageView.kf.setImage(with: url, placeholder: myPlaceholderView)
  • ImageTransitionnonefade(TimeInterval)flipFromLeft/Right/Top/Bottom(TimeInterval)custom(duration:options:animations:completion:)

17. 缓存配置与清除

内存缓存cache.memoryStorage.config):totalCostLimit(默认约物理内存 1/4)、countLimitexpiration(默认 300 秒)、cleanInterval(清除过期缓存的时间间隔,仅初始化可设)。单张可设 .memoryCacheExpiration(.never);访问时延长策略用 .memoryCacheAccessExtendingExpiration(.cacheTime)

磁盘缓存cache.diskStorage.config):sizeLimitexpiration(默认 7 天)、pathExtensionusesHashedFileName(文件名是否用 key 的 MD5)。超出容量时按最后访问时间排序,删除最旧文件直至低于 sizeLimit 的一半。

清除cache.clearMemoryCache() / cache.cleanExpiredMemoryCache()cache.clearDiskCache() / cache.cleanExpiredDiskCache();删除指定 key 可用 cache.removeImage(forKey:processorIdentifier:fromMemory:fromDisk:completionHandler:)。获取磁盘占用:cache.calculateDiskStorageSize { result in ... }

18. ImagePrefetcher 与请求修饰、重定向

ImagePrefetcher:除 start() 外,提供 completionHandler(参数为 [Resource] 的 skipped/failed/completed)与 completionSourceHandler(参数为 [Source]),分别对应用 URL/Resource 初始化与用 Source 初始化的场景;progressBlock / progressSourceBlock 同理。maxConcurrentDownloads 控制并发数。stop() 会取消当前未完成的下载任务,并将剩余未加载项计入「完成回调」的 skipped;若调用 stop 时已全部完成,则不会再次触发完成回调。

请求修饰:通过 AnyModifier 或实现 ImageDownloadRequestModifier 在请求前添加 Header、Token 等,例如 let modifier = AnyModifier { var r = $0; r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization"); return r },options 中加 .requestModifier(modifier)超时ImageDownloader.default.downloadTimeout = 60重定向:通过 .redirectHandler(AnyRedirectHandler { ... }) 自定义 302 等重定向后的请求。

19. 扩展 WebP 支持(Processor + CacheSerializer)

Kingfisher 默认不包含 WebP 编解码,可借助 ProcessorCacheSerializer 扩展 [13]。依赖 libwebp 实现 Data ↔ Image 后,定义 WebPProcessor(在 process 中若为 .data 则用 WebP 解码为 Image,若为 .image 则透传)与 WebPCacheSerializerdata(with:original:) 返回 WebP 编码、image(with:options:) 返回 WebP 解码),使用时设置 options: [.processor(WebPProcessor.default), .cacheSerializer(WebPCacheSerializer.default)] 即可对 .webp URL 加载并缓存。


延伸阅读(掘金系列)

以下为同一作者的 Kingfisher 源码解析系列文章,可按需跳转深入阅读(链接与标题保持一致):

主题 链接 内容概要
使用 Kingfisher源码解析之使用 Resource/ImageDataProvider、Placeholder、GIF、Indicator、Transition、Processor 概览、缓存与下载配置、预加载、常用 options
Options 解释 Kingfisher源码解析之Options解释 targetCache/originalCache、downloader、transition/forceTransition、preloadAllAnimationData、callbackQueue/processingQueue、memoryCacheAccessExtendingExpiration
加载流程 Kingfisher源码解析之加载流程 setImage 之后发生了什么、图片加载与缓存查找流程
ImageCache Kingfisher源码解析之ImageCache MemoryStorage(NSCache、StorageObject、Config)、DiskStorage(FileMeta、removeExpiredValues、removeSizeExceededValues)、缓存读写与清理
加载动图 Kingfisher源码解析之加载动图 UIImageView 与 AnimatedImageView 加载 GIF 的差异、preloadAllAnimationData、CGImageSource、Animator、CADisplayLink、display(_ layer:)
Processor 和 CacheSerializer Kingfisher源码解析之Processor和CacheSerializer Processor/ImageProcessItem 定义与调用时机、CacheSerializer 调用时机、使用 Processor+CacheSerializer 扩展 WebP
ImagePrefetcher Kingfisher源码解析之ImagePrefetcher 预加载功能、completionHandler/completionSourceHandler、progressBlock/progressSourceBlock、stop() 行为、Resource 与 Source 两套回调

参考文献

[1] Kingfisher. Cheat Sheet. GitHub Wiki.
[2] Kingfisher. Image Manager Structure. studyraid.com / Agent Docs.
[3] Kingfisher. CHANGELOG / Releases — 3.10.0 cache retrieval with ImageProcessor.
[4] Kingfisher. Release 5.0.0. GitHub.
[5] Kingfisher. Release 5.3.0 — Downsampling scale/memory fix.
[6] Kingfisher. Release 7.8.1 — Animated image from disk cache with processor.
[7] Kingfisher. Release 8.3.0 — Progressive JPEG for KFImage.
[8] Apple. Image and Graphics Best Practices. WWDC 2018, Session 219.
[9] Kingfisher. ImageProcessor.swift. GitHub (onevcat/Kingfisher).
[10] Kingfisher. Cheat Sheet — Processor, Cache, Downloader. GitHub Wiki.
[11] Stack Overflow / Kingfisher Issues. DownsamplingImageProcessor size (0,0) and processing failure.

01-研究优秀开源框架@图层处理@iOS | SDWebImage 框架:从使用到原理解析

📋 目录


一、SDWebImage 概述与历史演进

0. 框架结构概览与功能简介

SDWebImage 的框架结构

SDWebImage的框架结构

SDWebImage 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。

功能简介

  1. 一个添加了 web 图片加载和缓存管理的 UIImageView 分类
  2. 一个异步图片下载器
  3. 一个异步的内存加磁盘综合存储图片并且自动处理过期图片
  4. 支持动态 gif 图
    • 4.0 之前的动图效果并不是太好
    • 4.0 以后基于 FLAnimatedImage 加载动图
  5. 支持 webP 格式的图片
  6. 后台图片解压处理
  7. 确保同样的图片 url 不会下载多次
  8. 确保伪造的图片 url 不会重复尝试下载
  9. 确保主线程不会阻塞

1. 框架简介

SDWebImage 是 Apple 平台(iOS / macOS / watchOS / visionOS)上广泛使用的异步图片下载与缓存库,提供从网络(或自定义 Loader)加载图片、解码、变换、缓存到展示的完整管线。其「图层处理」相关能力主要体现在:解码管线(将压缩数据解码为可渲染的位图)与变换管线(在解码后对位图做缩放、裁剪、滤镜等处理),二者共同构成「从数据到屏幕」的中间处理层。

2. 技术演进与版本脉络

SDWebImage 的图层处理能力并非一蹴而就,而是随版本逐步完善,与系统 API 和业界实践同步演进。

阶段 版本/时期 解码与图层处理相关能力
早期 3.x 及以前 以网络下载 + 简单缓存为主,解码依赖系统默认行为
规范化 4.0 引入 Custom Download Operation、更清晰的职责划分
编解码扩展 4.2 Custom Coder:支持注册自定义编解码器(如 WebP、渐进 JPEG)
统一管线 5.0 Image TransformerAnimated Image 全栈方案(GIF/WebP/APNG)、解码与变换在 Manager 内统一调度
精细化 5.x 后续 缩略图解码、HDR、强制解码策略(Force Decode Policy)、解码尺度与字节限制等

5.0 是重要分水岭:解码(Coder)、变换(Transformer)、缓存(Cache)、加载(Loader)在 SDWebImageManager 中形成一条清晰流水线,便于理解「图层处理」在整体中的位置。

3. 图层处理在整体架构中的位置

下图概括了从「URL 请求」到「显示到视图」的流程,并标出解码与变换所在阶段。

flowchart LR
    subgraph 输入
        A[URL / 自定义 Loader]
    end
    subgraph 加载
        B[SDImageLoader]
    end
    subgraph 解码层
        C[Data]
        D[SDImageCoder 解码]
        E[UIImage/NSImage]
    end
    subgraph 变换层
        F[SDImageTransformer]
        G[变换后图像]
    end
    subgraph 缓存与输出
        H[SDImageCache]
        I[UIImageView 等]
    end
    A --> B --> C --> D --> E --> F --> G --> H --> I

要点

  • 解码(Decoder):将压缩格式数据(JPEG/PNG/WebP/HEIC/AVIF 等)转为内存中的位图(如 UIImage/NSImage),是「数据 → 图层」的第一步。
  • 变换(Transformer):在「已解码的位图」上做几何或像素级处理(缩放、裁剪、圆角、滤镜等),输出仍是位图,再写入缓存或交给视图。
  • 二者均可在后台线程执行,避免阻塞主线程,符合 Apple 在 WWDC 等场合强调的「Image and Graphics Best Practices」[1]。

二、图像解码管线(Decoder Pipeline)

1. 解码的基础概念与双缓冲模型

在操作系统与图形栈中,图像通常以两种形式存在:

  1. 数据缓冲(Data Buffer)
    即磁盘或网络中的压缩编码数据(如 JPEG、PNG 的二进制)。体积小,但不能直接用于渲染。

  2. 图像缓冲(Image Buffer)
    解码后的像素矩阵(如 RGBA 位图),可被 GPU/CPU 渲染。其大小与分辨率(宽×高×通道数)成正比,与压缩格式无关。

因此,解码(Decoding) 的含义是:将 Data Buffer 转换为 Image Buffer。该过程是 CPU 密集型,且解码后的图像缓冲往往远大于原始数据(例如一张 4K 图片可解码为上百 MB 像素数据)。系统会在首次渲染时触发解码,若在主线程进行,易造成卡顿;若不经控制,大图会带来内存峰值与 OOM 风险。

双缓冲概念可归纳为

┌─────────────────┐     decode      ┌─────────────────┐
│  Data Buffer    │  ────────────►  │  Image Buffer   │
│ (JPEG/PNG/…)    │   (CPU 密集)     │  (像素矩阵)      │
└─────────────────┘                 └─────────────────┘
      体积较小                          体积 ∝ 宽×高×4

Apple 在 WWDC 2018「Image and Graphics Best Practices」[1] 中明确指出:解码后的缓冲区大小由图像尺寸决定,而非显示尺寸;因此在解码阶段就做下采样(Downsampling),避免先解码全尺寸再缩放的巨大内存与 CPU 开销。

2. 缩略图与下采样(Downsampling)

下采样指在解码时直接生成较小尺寸的位图,而不是先解码全图再缩放。这样既能减少内存占用,也能减少解码与后续绘制的计算量。

2.1 系统 API:ImageIO 与缩略图

在 iOS/macOS 上,推荐使用 ImageIOCGImageSourceCreateThumbnailAtIndex 在解码阶段就限制最大尺寸,从而在内存中只生成缩略图级别的像素缓冲 [2][3]。

算法思路(伪代码)

函数 DownsampleImage(数据 data, 最大边长 maxPixelSize):
    1. 使用 data 创建 CGImageSourceRef source
    2. 设置选项 options:
       - kCGImageSourceCreateThumbnailFromImageAlways: true
       - kCGImageSourceCreateThumbnailWithTransform: true
       - kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
    3. thumbnail = CGImageSourceCreateThumbnailAtIndex(source, 0, options)
    4. 由 thumbnail 创建 UIImage/NSImage 并返回

这样,解码器内部可以在部分解码低分辨率解码路径上生成缩略图,避免全图解码。SDWebImage 在 5.x 中通过 SDImageCoderDecodeScaleDownLimitBytes 等能力,将「按目标尺寸或字节限制做缩略图解码」纳入其解码管线,与上述思路一致。

2.2 下采样算法与内存估算

算法步骤(与 ImageIO 语义一致)

  1. Data 创建 CGImageSourceRef,不立即解码全图。
  2. 从 source 读取图像属性(宽、高),计算缩放比例,使长边不超过 maxPixelSize
  3. 设置 kCGImageSourceThumbnailMaxPixelSizekCGImageSourceCreateThumbnailFromImageAlwayskCGImageSourceCreateThumbnailWithTransform 等选项。
  4. 调用 CGImageSourceCreateThumbnailAtIndex(source, 0, options) 得到缩略图 CGImage
  5. CGImage 创建 UIImage/NSImage 并返回。

这样解码器在内部只生成目标尺寸的像素缓冲,避免「先全图解码再缩放」的双倍内存与 CPU 开销。

2.3 内存与性能关系

下采样带来的内存节省可近似表示为:

  • 全图解码:memory ≈ width × height × 4(假设 RGBA)。
  • 限制最大边长 L 后:若等比缩放,则 memory ≈ L² × 4,与原始分辨率无关。

因此,在列表、缩略图等场景下,在解码阶段就限制最大尺寸是业界公认的最佳实践,也是 SDWebImage 解码管线优化的核心之一。

3. 渐进式解码(Progressive Decoding)

渐进式编码(如 Progressive JPEG)允许数据分块到达时逐步呈现:先看到模糊全图,再随数据增多逐步变清晰。渐进式解码即在未完整接收数据时,对当前已有数据做解码并显示,以提升感知性能(尤其在弱网环境)[4]。

流程概念

sequenceDiagram
    participant N as 网络
    participant D as 渐进式解码器
    participant V as 视图

    N->>D: 数据块 1
    D->>D: 解码当前数据
    D->>V: 显示低分辨率帧 1
    N->>D: 数据块 2
    D->>D: 更新解码状态,解码
    D->>V: 显示更清晰帧 2
    N->>D: 数据块 n(完成)
    D->>D: 最终解码
    D->>V: 显示最终图像

SDWebImage 通过 SDWebImageProgressiveCoder 协议扩展解码器:支持「增量数据」输入,每次 updateIncrementalData:finished: 时更新内部解码状态并输出当前可用的图像,供上层展示。对动图(如 GIF),还可配合 SDAnimatedImageCoder 在渐进加载时逐帧解码并驱动 SDAnimatedImageView 的渐进动画。

渐进式解码流程(伪代码)

函数 ProgressiveDecode:
    状态: 已接收数据 buffer, 解码器内部状态 decoderState
    当 收到新数据块 chunk:
        append(buffer, chunk)
        decodedFrame = decoder.decodeIncremental(buffer, decoderState)
        若 decodedFrame 非空:
            回调 onPartialImage(decodedFrame)
    当 数据接收完成:
        finalImage = decoder.finalize(buffer, decoderState)
        回调 onComplete(finalImage)

注意:渐进式解码比单次完整解码的 CPU 开销更高 [4],适合「先显示再细化」的体验需求,需在流畅度与电量之间做权衡。

4. 编解码器扩展与多格式支持

SDWebImage 将「解码 / 编码」抽象为 SDImageCoder 协议,通过 SDImageCodersManager 注册多个 Coder,按数据格式(或 MIME 类型)选择对应实现。这样可在不修改核心管线的前提下支持新格式。

解码器选择与解码流程(高层)

flowchart TD
    A[Image Data] --> B{SDImageCodersManager}
    B --> C[遍历已注册 Coder]
    C --> D{canDecodeFromData?}
    D -->|是| E[该 Coder 解码]
    D -->|否| C
    E --> F[UIImage/NSImage]
    F --> G[可选: 缩略图/字节限制]
    G --> H[解码结果]

典型 Coder 职责

方法/能力 含义
decodedImageWithData:options: 将 Data 解码为 UIImage/NSImage
encodedDataWithImage:format:options: 将图像编码为指定格式 Data
canDecodeFromData: / canEncodeToFormat: 是否支持某格式的解码/编码

动图则通过 SDAnimatedImageCoder 扩展:提供按帧解码、帧时长、循环次数等,供 SDAnimatedImage + SDAnimatedImageView 使用。内置支持 GIF、WebP、APNG、HEIC 动图等;用户也可实现自定义 Coder 并注册,从而纳入统一的加载与缓存流程。


三、图像变换管线(Transformer Pipeline)

1. 变换器的设计思想与协议

图像变换在 SDWebImage 中定义为:输入与输出均为图像对象(如 UIImage/NSImage)的运算。与 Coder(Data ↔ Image)不同,Transformer 只做 Image → Image,例如缩放、裁剪、圆角、滤镜等,对应「数字图像处理」中的几何变换与像素操作 [5]。

协议设计(概念)

协议 SDImageTransformer:
    方法 transform(image, key) -> Image?:
        输入: 原始图像、缓存 key(可选,用于生成变换后的 cache key)
        输出: 变换后的图像;失败可返回 nil

这样设计便于:

  • SDWebImageManager 中,在「解码完成」之后、「写入缓存」之前插入变换步骤;
  • 对同一 URL 可因不同变换参数得到不同 cache key,从而分别缓存原图与变换结果。

2. 内置变换器与组合管线

SDWebImage 提供多种内置 Transformer,覆盖常见 UI 需求:

变换器 功能说明
SDImageResizingTransformer 缩放到指定尺寸,支持 scaleMode(fill/aspectFit 等)
SDImageCroppingTransformer 按矩形裁剪
SDImageRoundCornerTransformer 圆角(可带边框)
SDImageRotationTransformer 按角度旋转,可选 fitSize
SDImageFlippingTransformer 水平/垂直翻转
SDImageBlurTransformer 高斯模糊
SDImageTintTransformer 颜色 tint
SDImageFilterTransformer 基于 CIFilter 的滤镜(除 watchOS 外)

组合管线:通过 SDImagePipelineTransformer 将多个 Transformer 按顺序组合,形成链式处理:

图像 → Transformer1 → Transformer2 → … → TransformerN → 最终图像

例如先裁剪再圆角再缩放,只需将三个 Transformer 放入一个 Pipeline 即可。对应伪代码:

pipeline = SDImagePipelineTransformer([CropTransformer(rect), RoundCornerTransformer(radius), ResizingTransformer(size)])
resultImage = pipeline.transform(originalImage, key)

在 Swift/Objective-C 中的用法可参见官方 Advanced Usage - Image Transformer [6]。

3. 变换与缓存的协同

变换发生在 Manager 层:
先由 Loader 得到 Data,由 Coder 解码得到 Image,再经 Transformer 得到最终 Image,最后再写入 Cache 并交给 UI。因此:

  • 原始图变换后的图可以分别缓存:
    • 原图可用 SDWebImageContextOriginalImageCache 指定单独缓存实例;
    • 变换后的图使用默认(或指定)的 Cache,其 cache key 会包含变换信息,避免不同变换结果互相覆盖。
  • 若只关心「下载 + 变换」而不写缓存,可通过 .fromLoaderOnlystoreCacheType = .none 实现,仅走 Loader → 解码 → 变换 → 回调,不读/写缓存。

变换与缓存的整体管线(含解码)

flowchart LR
    subgraph 请求
        U[URL + Context]
    end
    subgraph 缓存查询
        C1{查 Cache}
    end
    subgraph 加载与解码
        L[Loader]
        D[Coder 解码]
    end
    subgraph 变换
        T[Transformer]
    end
    subgraph 写回与展示
        C2[写 Cache]
        V[View]
    end
    U --> C1
    C1 -->|命中| V
    C1 -->|未命中| L --> D --> T --> C2 --> V

4. 应用场景简述

场景 解码侧 变换侧
列表缩略图 使用 scaleDown/limitBytes 做缩略图解码,降低内存 可选 ResizingTransformer 统一尺寸
头像/圆角 常规解码即可 RoundCornerTransformer
弱网/大图 Progressive Coder 渐进显示 可配合 Resizing 限制最终尺寸
相册/大图预览 原图或高分辨率解码 少用或仅做旋转/裁剪
动图(GIF/WebP) SDAnimatedImageCoder + 帧缓冲 一般不做几何变换,或仅对首帧做

四、类结构图分析

1. 核心类总览

SDWebImage 的类可按职责分为:入口与协调加载缓存解码变换视图扩展 六类。下表给出核心类及其职责(名称以 5.x 为主,OC/Swift 可能略有差异)。

模块 核心类 / 协议 职责简述
协调 SDWebImageManager 统一入口:协调 Loader、Cache、Coder、Transformer,执行「查缓存 → 下载 → 解码 → 变换 → 写缓存」
加载 SDImageLoader (协议) 定义加载接口:根据 URL 返回 Data 或 Image
SDWebImageDownloader 默认 Loader 实现:基于 URLSession 下载,支持并发、取消、RequestModifier
SDWebImageDownloaderOperation 单次下载任务,实现 SDWebImageDownloaderOperation 协议
缓存 SDImageCache 内存 + 磁盘二级缓存,提供 query/store/remove,支持自定义 key、过期策略
SDMemoryCache / SDDiskCache 内存层、磁盘层具体实现(5.x 可拆分)
解码 SDImageCoder (协议) 定义 Data ↔ Image 编解码,如 decodedImageWithData:options:
SDImageCodersManager 管理多个 Coder,按数据格式选择可用 Coder
SDWebImageImageIOCoder 内置 Coder 实现(JPEG/PNG/HEIC/…)
变换 SDImageTransformer (协议) 定义 Image → Image 变换,如 transformedImageWithImage:forKey:
SDImagePipelineTransformer 将多个 Transformer 串联为一条管线
SDImageResizingTransformer 内置 Transformer 实现
视图 UIImageView+WebCache 为 UIImageView 提供 sd_setImage(with:...)sd_cancelCurrentImageLoad
SDAnimatedImageView 动图展示,配合 SDAnimatedImage
UIButton+WebCache 其他控件的扩展

2. 模块划分与依赖关系

下图从「模块」维度表示各层之间的依赖方向:视图扩展依赖 Manager,Manager 依赖 Loader/Cache,解码与变换在 Manager 内被调用,Loader 只产出 Data,Cache 只做存取。

flowchart TB
    subgraph 视图层
        V1[UIImageView+WebCache]
        V2[UIButton+WebCache]
        V3[SDAnimatedImageView]
    end
    subgraph 协调层
        M[SDWebImageManager]
    end
    subgraph 加载层
        L[SDWebImageDownloader]
    end
    subgraph 缓存层
        C[SDImageCache]
    end
    subgraph 编解码层
        CM[SDImageCodersManager]
        CO[SDImageCoder 实现]
    end
    subgraph 变换层
        T[SDImageTransformer 实现]
    end
    V1 --> M
    V2 --> M
    V3 --> M
    M --> L
    M --> C
    M --> CM
    M --> T
    CM --> CO

3. 加载与缓存类结构

Loader 负责从网络(或自定义来源)获取数据;Cache 负责内存与磁盘的读写。Manager 持有两者引用,在单次请求中先问 Cache,未命中再调 Loader。

classDiagram
    class SDWebImageManager {
        -imageLoader: SDImageLoader
        -imageCache: SDImageCache
        +loadImage(with:options:context:progress:completed:)
        -callLoadImage(with:options:context:progress:completed:)
    }
    class SDImageLoader {
        <<protocol>>
        +requestImageWithURL:options:context:progress:completed()
        +canRequestImageForURL()
    }
    class SDWebImageDownloader {
        -session: URLSession
        -downloadQueue: NSOperationQueue
        +downloadImageWithURL:options:progress:completed()
    }
    class SDImageCache {
        -memoryCache: SDMemoryCache
        -diskCache: SDDiskCache
        +queryImageForKey:options:context:callback()
        +storeImage:imageData:forKey:completion()
        +removeImageForKey:withCompletion()
    }
    SDWebImageManager --> SDImageLoader : 使用
    SDWebImageManager --> SDImageCache : 使用
    SDWebImageDownloader ..|> SDImageLoader : 实现
  • SDWebImageManager:对外提供 loadImage(with:...),内部先查 imageCache,再根据需要调用 imageLoader,最后根据 context 决定是否解码、变换并写回缓存。
  • SDWebImageDownloader:实现 SDImageLoader 协议,通过 URLSession 下载,支持并发数、超时、RequestModifier;单次下载封装为 SDWebImageDownloaderOperation
  • SDImageCache:内存缓存通常用 NSCache 或自研 LRU,磁盘缓存为文件系统;query/store 的 key 由 Manager 根据 URL + context(含 transformer 等)生成。

4. 解码与变换类结构

Coder 将 Data 转为 Image(或反向);Transformer 将 Image 转为另一 Image。Manager 在「Loader 返回 Data 后」先选 Coder 解码,再按 context 中的 Transformer 做变换,得到最终 Image 再写入 Cache。

classDiagram
    class SDWebImageManager {
        -loadImage(with:...)
    }
    class SDImageCoder {
        <<protocol>>
        +decodedImageWithData:options()
        +encodedDataWithImage:format:options()
        +canDecodeFromData()
        +canEncodeToFormat()
    }
    class SDImageCodersManager {
        -coders: [SDImageCoder]
        +addCoder()
        +removeCoder()
        +canDecodeFromData()
        +decodedImageWithData:options()
    }
    class SDImageTransformer {
        <<protocol>>
        +transformerKey
        +transformedImageWithImage:forKey()
    }
    class SDImagePipelineTransformer {
        -transformers: [SDImageTransformer]
        +transformerKey
        +transformedImageWithImage:forKey()
    }
    SDWebImageManager ..> SDImageCodersManager : 解码时使用
    SDWebImageManager ..> SDImageTransformer : 变换时使用
    SDImageCodersManager --> SDImageCoder : 委托具体 Coder
    SDImagePipelineTransformer ..|> SDImageTransformer : 实现
  • SDImageCodersManager:持有一组 SDImageCoder,按 canDecodeFromData: 选出第一个能处理当前 Data 的 Coder 执行解码;编码同理。
  • SDImagePipelineTransformer:持有一组 SDImageTransformer,按顺序对 Image 依次变换;其 transformerKey 通常由各子 Transformer 的 key 拼接而成,参与缓存 key 生成。

5. View 扩展与调用链

视图扩展(如 UIImageView+WebCache)是业务最常接触的入口:内部将「当前 URL、placeholder、options、context」交给 SDWebImageManager,并把返回的加载任务与 view 关联,以便在复用时取消。

sequenceDiagram
    participant V as UIImageView
    participant Ext as UIImageView+WebCache
    participant M as SDWebImageManager
    participant C as SDImageCache
    participant L as SDWebImageDownloader

    V->>Ext: sd_setImage(with: url, ...)
    Ext->>Ext: sd_cancelCurrentImageLoad()
    Ext->>M: loadImage(with: url, context: [...])
    M->>C: queryImage(forKey:)
    alt 缓存命中
        C-->>M: image
        M-->>Ext: completed(image, .memory/.disk)
    else 未命中
        M->>L: requestImageWithURL:...
        L-->>M: data
        M->>M: 解码 + 变换
        M->>C: storeImage(forKey:)
        M-->>Ext: completed(image, .none)
    end
    Ext->>V: imageView.image = image
  • sd_setImage(with: placeholder: options: context: completed:):先对当前 view 取消未完成任务,再调 SDWebImageManager.shared.loadImage(with: url, options: options, context: context, progress: progress, completed: completed);在 completed 中把得到的 image 赋给 imageView.image(并可选执行 transition 动画)。
  • sd_cancelCurrentImageLoad():取消与该 view 绑定的 load 任务,避免 cell 复用时旧请求覆盖新图片。

将上述「核心类总览」「模块依赖」「Loader/Cache 类图」「Coder/Transformer 类图」「View 调用链」串联起来,即可形成对 SDWebImage 类结构图 的完整分析:入口在视图扩展,核心协调在 Manager,加载与缓存、解码与变换均为可插拔的协议实现,便于扩展与测试。


五、与系统及业界实践的衔接

1. Apple 图像与图形最佳实践

Apple 在 WWDC 2018「Image and Graphics Best Practices」[1] 中强调:

  • 在后台线程进行解码与下采样,避免在主线程做重 CPU 工作导致的卡顿。
  • 解码时即做下采样,使解码后的图像缓冲与显示尺寸匹配,降低内存与 CPU。
  • 预取(Prefetch):在列表等场景提前准备即将显示的图像,避免在滚动时才开始解码。

SDWebImage 的解码与变换均在后台队列执行,且支持按尺寸/字节限制的缩略图解码,与上述建议一致。其 Prefetch 能力(如 UITableView 的 prefetch 结合 sd_setImageWithURL:)可在业务层配合使用,实现「提前解码、避免滚动时卡顿」。

Force Decode 策略(5.17+):SDWebImage 引入 SDImageForceDecodePolicy,用于控制是否在加载管线中强制解码(将延迟解码的图片提前转为位图)。在部分场景下可避免在渲染阶段才触发 CA 的帧缓冲拷贝,从而降低主线程峰值与内存抖动;具体策略可根据「是否使用自定义渲染」「是否配合 Transformer」等选择,详见官方文档与 CHANGELOG。

2. 移动端图像管线研究简述

在移动端部署图像管线(含解码、缩放、轻量级「变换」)方面,业界与学界有大量工作:

  • FlexiViT [7] 等通过可变的 patch 尺寸在训练与推理时平衡精度与速度;
  • NanoFLUX [8]、SnapGen [9] 等关注在移动设备上的高效图像生成与压缩。
    这些工作与「在端侧做高效解码与分辨率控制」的目标一致:在有限算力与内存下,通过解码阶段控制(如缩略图、渐进解码)和管线化处理(解码 → 变换 → 缓存)提升体验。SDWebImage 的 Decoder + Transformer 双管线正是这一思路在「图片加载库」中的具体实现。

六、使用案例与原理分析

0. 框架结构速览

0.1 实现原理

  1. 架构图(UML 类图)

架构图(UML 类图)

  1. 流程图(方法调用顺序图)

1559217862563-364c0d60-3f2a-4db9-b5c5-e81f01cd125e.png

0.2 目录结构

  • Downloader\
    • SDWebImageDownloader\
    • SDWebImageDownloaderOperation
  • Cache\
    • SDImageCache
  • Utils\
    • SDWebImageManager\
    • SDWebImageDecoder\
    • SDWebImagePrefetcher
  • Categories\
    • UIView+WebCacheOperation\
    • UIImageView+WebCache\
    • UIImageView+HighlightedWebCache\
    • UIButton+WebCache\
    • MKAnnotationView+WebCache\
    • NSData+ImageContentType\
    • UIImage+GIF\
    • UIImage+MultiFormat\
    • UIImage+WebP
  • Other\
    • SDWebImageOperation(协议)\
    • SDWebImageCompat(宏定义、常量、通用函数)

0.3 相关类名与功能描述

  • SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系
  • SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的
  • SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程
  • SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来
  • SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码
  • SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由 SDWebImageManager 来处理图片下载和缓存
  • UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
  • UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用
  • UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片
  • UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用
  • MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似
  • NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG 等)
  • UIImage+GIF:用于加载 GIF 动图
  • UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象
  • UIImage+WebP:用于解码并加载 WebP 图片

0.4 工作流程

工作流程

  • 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
  • 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo: 交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:。
  • 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
  • SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
  • 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
  • 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
  • 如果从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 进而回调展示图片。
  • 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
  • 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
  • 图片下载由 NSURLConnection(3.8.0 之后使用了 NSURLSession),实现相关 delegate 来判断图片下载中、下载完成和下载失败。
  • connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
  • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
  • imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
  • 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
  • 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
  • SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
  • SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

1. 典型使用案例

1.1 列表 Cell 中加载缩略图(防错位 + 下采样)

UITableView / UICollectionView 的 cell 中,若不限制图片尺寸,大图会带来内存峰值与卡顿;且 cell 复用时需避免「先显示旧图再被新图覆盖」的错位。SDWebImage 通过 URL 绑定取消机制 解决错位,通过 Transformer 限制尺寸 控制内存。

// Cell 内
func configure(with url: URL) {
    imageView.sd_cancelCurrentImageLoad()
    let transformer = SDImageResizingTransformer(
        size: CGSize(width: 120, height: 120),
        scaleMode: .aspectFill
    )
    imageView.sd_setImage(
        with: url,
        placeholderImage: UIImage(named: "placeholder"),
        context: [.imageTransformer: transformer]
    )
}

要点sd_cancelCurrentImageLoad() 会取消该 view 上未完成的请求,新 URL 加载完成后才设置,避免复用时显示错误图片。

1.2 预取(Prefetch)提前解码

利用系统预取 API 在 cell 尚未显示时就开始加载,滚动时直接从缓存读取,减少卡顿。

// 实现 UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { model(at: $0).imageURL }
    urls.forEach { url in
        SDWebImagePrefetcher.shared.prefetchURLs([url])
    }
}

// 可选:取消不再需要的预取
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { model(at: $0).imageURL }
    SDWebImagePrefetcher.shared.cancelPrefetching(for: urls)
}

1.3 多设备/多 Channel 下的加载

在多设备场景(如同一 URL 在不同 Channel 下需要不同尺寸)中,通过 context 传入不同的 Transformer 或 Cache,使同一 URL 对应多条缓存条目。

// 列表用小图
imageView.sd_setImage(with: url, context: [
    .imageTransformer: SDImageResizingTransformer(size: CGSize(width: 80, height: 80), scaleMode: .aspectFill)
])

// 详情用原图或大图
detailImageView.sd_setImage(with: url)  // 不传 transformer,用原图

1.4 占位图 + 加载完成过渡动画

通过 sd_imageTransition 在图片从网络加载完成后做淡入等过渡,提升观感。

imageView.sd_imageTransition = .fade(0.25)
imageView.sd_setImage(with: url, placeholderImage: placeholder)

1.5 仅下载不展示(后台缓存)

希望提前把图片下载并写入缓存,供后续使用,而不绑定到某个 view。

SDWebImageManager.shared.loadImage(
    with: url,
    options: [],
    progress: nil
) { image, data, error, cacheType, finished, url in
    if let image = image, finished {
        // 已缓存,可做后续逻辑
    }
}

1.6 完成回调与错误处理

通过 completed 区分来源(内存/磁盘/网络)并处理失败与取消。

imageView.sd_setImage(with: url, placeholderImage: placeholder) { image, error, cacheType, url in
    if let error = error {
        // 可根据 error 类型提示用户或降级
        return
    }
    switch cacheType {
    case .none:   break // 本次从网络加载
    case .memory: break // 从内存缓存
    case .disk:   break // 从磁盘缓存
    @unknown default: break
    }
}

2. 更多使用案例与代码

2.1 UITableViewCell 完整示例(含复用与尺寸)

class PhotoCell: UITableViewCell {
    static let reuseId = "PhotoCell"
    @IBOutlet weak var photoImageView: UIImageView!
    @IBOutlet weak var progressView: UIProgressView!

    override func prepareForReuse() {
        super.prepareForReuse()
        photoImageView.sd_cancelCurrentImageLoad()
        photoImageView.image = nil
        progressView.progress = 0
    }

    func configure(with url: URL) {
        let size = photoImageView.bounds.size
        let transformer = SDImageResizingTransformer(
            size: size.isEmpty ? CGSize(width: 120, height: 120) : size,
            scaleMode: .aspectFill
        )
        photoImageView.sd_setImage(
            with: url,
            placeholderImage: UIImage(named: "placeholder"),
            context: [.imageTransformer: transformer],
            progress: { [weak self] received, total, _ in
                guard let self = self, total > 0 else { return }
                DispatchQueue.main.async {
                    self.progressView.progress = Float(received) / Float(total)
                }
            },
            completed: { [weak self] image, error, _, _ in
                DispatchQueue.main.async {
                    self?.progressView.isHidden = (image != nil)
                }
            }
        )
    }
}

2.2 UIButton 设置网络图片

// 设置不同 state 的图片
button.sd_setImage(with: url, for: .normal, placeholderImage: UIImage(named: "btn_placeholder"))
button.sd_setImage(with: highlightedURL, for: .highlighted)
button.sd_setBackgroundImage(with: backgroundURL, for: .normal)

// 带圆角与完成回调
let transformer = SDImageRoundCornerTransformer(radius: 8, corners: .allCorners, borderWidth: 0, borderColor: nil)
button.sd_setImage(with: url, for: .normal, placeholderImage: nil, context: [.imageTransformer: transformer]) { _, error, _, _ in
    if error != nil { print("加载失败") }
}

2.3 自定义缓存键(同一 URL 多用途)

当同一 URL 在不同业务下需要不同缓存(例如列表用缩略图、详情用原图)时,可用 cacheKeyFilter 或自定义 key。

// 方式一:通过 context 的 cacheKeyFilter 生成不同 key
let listKeyFilter: SDWebImageCacheKeyFilter = { url in
    return "list_\(url?.absoluteString ?? "")" as NSString
}
imageView.sd_setImage(with: url, context: [.cacheKeyFilter: listKeyFilter])

let detailKeyFilter: SDWebImageCacheKeyFilter = { url in
    return "detail_\(url?.absoluteString ?? "")" as NSString
}
detailImageView.sd_setImage(with: url, context: [.cacheKeyFilter: detailKeyFilter])

// 方式二:在业务层用不同 URL 或 query 区分(如服务端支持 ?size=thumb)
let listURL = url.appendingPathComponent("?size=thumb")
let detailURL = url
imageView.sd_setImage(with: listURL, context: [.cacheKeyFilter: listKeyFilter])
detailImageView.sd_setImage(with: detailURL, context: [.cacheKeyFilter: detailKeyFilter])

2.4 请求修饰(Header、Token、超时)

需要带鉴权或自定义 Header 时,使用 requestModifier

let modifier = SDWebImageDownloaderRequestModifier { request in
    var r = request
    r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    r.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
    r.timeoutInterval = 30
    return r
}
imageView.sd_setImage(
    with: url,
    context: [.requestModifier: modifier]
)

2.5 下载进度条 + 占位 + 过渡动画

imageView.sd_imageTransition = .fade(0.3)
imageView.sd_setImage(
    with: url,
    placeholderImage: UIImage(named: "placeholder"),
    options: [.retryFailed],
    progress: { [weak progressView] received, total, _ in
        guard let pv = progressView, total > 0 else { return }
        DispatchQueue.main.async {
            pv.progress = Float(received) / Float(total)
            pv.isHidden = false
        }
    },
    completed: { [weak progressView] image, _, cacheType, _ in
        DispatchQueue.main.async {
            progressView?.isHidden = true
            if image == nil { /* 显示失败占位 */ }
        }
    }
)

2.6 动图 GIF(SDAnimatedImageView)

// 使用 SDAnimatedImageView 播放 GIF/WebP/APNG
let animatedImageView = SDAnimatedImageView()
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil)

// 仅加载动图第一帧作为封面(节省内存)
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil, options: [.decodeFirstFrameOnly])

// 渐进式加载动图(边下边播)
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil, options: [.progressiveLoad])

2.7 缓存查询与手动存储

let cache = SDImageCache.shared
let key = url.absoluteString

// 查询是否已缓存
cache.containsImage(forKey: key) { cacheType in
    switch cacheType {
    case .none:   print("未缓存")
    case .memory: print("在内存")
    case .disk:   print("在磁盘")
    @unknown default: break
    }
}

// 从缓存读取(不触发下载)
cache.queryImage(forKey: key, options: nil, context: nil) { image, data, cacheType in
    if let image = image {
        imageView.image = image
    }
}

// 手动写入缓存(例如本地生成或从相册来的图)
cache.store(image, forKey: key, completion: nil)

2.8 自定义 Transformer 示例

实现 SDImageTransformer 协议,对已解码图像做自定义绘制或滤镜(以下方法名以 SDWebImage 5.x 协议为准,实际请参照当前版本头文件)。

// 实现协议:为图片加灰色边框
class GrayBorderTransformer: NSObject, SDImageTransformer {
    var transformerKey: String { "GrayBorder(\(borderWidth))" }
    let borderWidth: CGFloat

    init(borderWidth: CGFloat = 2) { self.borderWidth = borderWidth }

    func transformedImage(with image: UIImage, forKey key: String) -> UIImage? {
        let size = image.size
        UIGraphicsBeginImageContextWithOptions(size, false, image.scale)
        defer { UIGraphicsEndImageContext() }
        image.draw(at: .zero)
        UIColor.gray.setStroke()
        let path = UIBezierPath(rect: CGRect(origin: .zero, size: size).insetBy(dx: borderWidth/2, dy: borderWidth/2))
        path.lineWidth = borderWidth
        path.stroke()
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

// 使用
let transformer = GrayBorderTransformer(borderWidth: 3)
imageView.sd_setImage(with: url, context: [.imageTransformer: transformer])

2.9 强制刷新与仅从缓存读取

// 忽略缓存,强制重新下载(适用于需要刷新内容的场景)
imageView.sd_setImage(with: url, options: [.forceRefresh])

// 仅从缓存读取,没有则显示占位或报错(离线/省流量场景)
imageView.sd_setImage(with: url, options: [.onlyFromCache]) { image, error, _, _ in
    if image == nil { print("缓存中无此图") }
}

2.10 Objective-C 常用写法

// 基础加载
[imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder"]];

// 带 context 的 Transformer
id<SDImageTransformer> transformer = [SDImagePipelineTransformer transformerWithTransformers:@[
    [SDImageResizingTransformer transformerWithSize:CGSizeMake(100, 100) scaleMode:SDImageScaleModeFill],
    [SDImageRoundCornerTransformer transformerWithRadius:10 corners:SDRectCornerAllCorners borderWidth:0 borderColor:nil]
]];
[imageView sd_setImageWithURL:url placeholderImage:nil context:@{SDWebImageContextImageTransformer: transformer}];

// 取消当前加载
[imageView sd_cancelCurrentImageLoad];

// 完成回调
[imageView sd_setImageWithURL:url completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
    if (error) { NSLog(@"加载失败: %@", error); }
}];

3. 核心流程原理分析

3.1 Manager 协调的完整链路

SDWebImageManager 是整条「加载 → 解码 → 变换 → 缓存」的协调者,其内部逻辑可概括为:

flowchart TD
    A[loadImageWithURL] --> B{查缓存 key}
    B --> C[先查内存]
    C --> D{命中?}
    D -->|是| E[回调 .memory]
    D -->|否| F[再查磁盘]
    F --> G{命中?}
    G -->|是| H[解码/反序列化]
    H --> I[写回内存]
    I --> E
    G -->|否| J[构造 Loader 任务]
    J --> K[Loader 返回 Data]
    K --> L[Coder 解码]
    L --> M{有 Transformer?}
    M -->|是| N[Transformer 变换]
    M -->|否| O[得到 Image]
    N --> O
    O --> P[写内存+磁盘缓存]
    P --> Q[回调 .none 或 .disk]

要点

  • 缓存 key 由 URL(或自定义 key)与 context(如 transformer、cacheKeyFilter)共同决定,同一 URL 不同 transformer 会得到不同 key。
  • 解码与变换均在 Manager 持有的串行/并发队列 中执行,回调通过 dispatch_async(main_queue) 回到主线程,便于更新 UI。

3.2 回调与线程模型

  • progress:在下载进度回调所在线程(多为 URLSession 回调线程),若需更新 UI 需自行切主线程。
  • completed:SDWebImage 内部会派发到主线程再调用,因此 completed 里可直接操作 UI。
  • 取消:当再次对同一 view 调用 sd_setImage(with: newURL) 时,会取消该 view 上此前由 SDWebImage 发起的任务,completed 仍可能被调用一次(cancel 语义),可通过 SDWebImageOption 或检查 finished 区分。

3.3 缓存键与 Transformer 的关系

变换后的图片会以「新 key」写入缓存:通常为 key + transformerIdentifier 或等价组合。因此:

  • 原图:key = url.absoluteString(或自定义)。
  • 变换图:key = f(url, transformer),例如 url.absoluteString + "_" + transformer.identifier

这样同一 URL 可同时存在「原图」与「缩放版」「圆角版」等多份缓存,互不覆盖;原图也可通过 SDWebImageContextOriginalImageCache 写入单独缓存实例,供大图页等使用。


七、设计模式与编程思想

1. 设计模式应用

SDWebImage 在架构上大量运用经典设计模式,使扩展与维护成本可控。

模式 在 SDWebImage 中的体现 作用
外观 / 门面(Facade) SDWebImageManager 对外提供 loadImage(with:options:progress:completed:),内部协调 Loader、Cache、Coder、Transformer,调用方无需关心多级缓存与管线顺序 简化使用、隐藏复杂度
策略(Strategy) SDImageTransformerSDImageCoder 均为协议,多种实现可替换(Resizing、RoundCorner、WebP Coder 等),通过 context 或注册表注入 算法/行为可插拔,易扩展新格式与新变换
责任链 / 管道(Chain of Responsibility / Pipeline) SDImagePipelineTransformer 将多个 Transformer 串联;解码管线中 Coder 的选取也可视为「按责任链匹配 canDecodeFromData」 多步处理顺序清晰,便于组合与复用
单例 + 共享依赖(Singleton) SDWebImageManager.sharedSDImageCache.sharedSDWebImageDownloader.shared 提供默认实例,同时支持传入自定义 Cache/Loader 以打破单例 全局统一入口,又保留可测试性与多实例能力
观察者 / 回调(Observer / Callback) 通过 progresscompleted 闭包通知进度与结果;部分能力通过 delegate 扩展 异步结果与 UI 解耦
工厂思想(Factory) SDImageCodersManager 根据 Data 格式选择 Coder;Loader 根据 URL 或 scheme 选择具体 Loader 实现 创建逻辑集中,便于支持新协议与新格式

类图关系(概念层)

classDiagram
    class SDWebImageManager {
        -cache: SDImageCache
        -loader: SDImageLoader
        +loadImage(with:options:progress:completed:)
    }
    class SDImageCache {
        +store(_:forKey:)
        +queryImage(forKey:options:callback:)
    }
    class SDImageLoader {
        +loadImage(with:options:progress:completed:)
    }
    class SDImageCoder {
        <<protocol>>
        +decodedImageWithData:options:
        +canDecodeFromData:
    }
    class SDImageTransformer {
        <<protocol>>
        +transform(image:key:)
    }
    SDWebImageManager --> SDImageCache : 使用
    SDWebImageManager --> SDImageLoader : 使用
    SDWebImageManager ..> SDImageCoder : 解码时选用
    SDWebImageManager ..> SDImageTransformer : 变换时选用

2. 编程思想精华

SDWebImage 的编程思想可提炼为以下几点,对理解与模仿其设计很有帮助。

2.1 协议导向与「可替换实现」

  • CoderTransformerLoaderCache 均以协议或抽象接口呈现,具体实现可替换、可组合。
  • 新增一种图片格式或一种变换,只需实现对应协议并注册,无需改动 Manager 核心流程。这体现了开闭原则:对扩展开放,对修改关闭。

2.2 管线化与单一职责

  • 把「从 URL 到屏幕」拆成:加载 → 解码 → 变换 → 缓存 → 展示,每一步只做一件事。
  • 解码只关心 Data → Image,变换只关心 Image → Image,缓存只关心存储与查找。单一职责使每块可独立测试、优化和扩展;管线化则使数据流清晰,便于加日志、监控和插桩。

2.3 缓存键与「同一资源多形态」

  • 通过 key = f(URL, context) 的设计,同一 URL 可以对应「原图」「缩略图」「圆角图」等多条缓存,避免重复下载,又满足不同场景对尺寸/形态的需求。这体现了用键设计表达业务差异的思想。

2.4 后台处理与主线程回调

  • 解码、变换、磁盘 I/O 均在后台队列执行,completed 回调派发到主线程,兼顾性能与 UI 安全。这是移动端异步加载库的通用范式:重活放后台,结果回主线程

2.5 取消与生命周期绑定

  • View 扩展(如 UIImageView+WebCache)会把「当前正在进行的任务」与 view 绑定,当对同一 view 发起新请求时自动取消旧请求,避免错位和浪费。这体现了生命周期与请求绑定的思想,在列表场景中尤为重要。

2.6 配置通过 Context 透传

  • 不通过全局单例属性堆砌配置,而是通过 SDWebImageContext 在单次请求中传入 Cache、Transformer、Loader、CacheKeyFilter 等,使「同一 App 内不同页面/模块」可使用不同策略,且易于单元测试时注入 mock。

SDWebImage 编程思想精华一览

思想 在框架中的体现
协议导向、可替换 Coder / Transformer / Loader 协议化,新格式、新变换仅需实现协议并注册
管线化、单一职责 加载 → 解码 → 变换 → 缓存 → 展示,每步职责单一,便于扩展与测试
键设计表达多形态 同一 URL 通过 key = f(URL, context) 支持原图、缩略图、圆角图等多条缓存
后台处理、主线程回调 重 CPU/IO 在后台队列,completed 回主线程,兼顾性能与 UI 安全
生命周期绑定取消 View 与当前任务绑定,新请求自动取消旧请求,避免列表错位
Context 透传配置 单次请求级配置,避免全局状态,利于多策略并存与测试注入

八、使用示例与最佳实践

1. 使用内置变换器(缩放 + 圆角)

let transformer = SDImagePipelineTransformer(transformers: [
    SDImageResizingTransformer(size: CGSize(width: 300, height: 300), scaleMode: .fill),
    SDImageRoundCornerTransformer(radius: 20, corners: .allCorners, borderWidth: 0, borderColor: nil)
])
imageView.sd_setImage(with: url, placeholderImage: nil, context: [.imageTransformer: transformer])

2. 仅下载并变换、不写缓存

SDWebImageManager.shared.loadImage(
    with: url,
    options: [.fromLoaderOnly],
    context: [.storeCacheType: SDImageCacheType.none.rawValue, .imageTransformer: transformer],
    progress: nil
) { image, _, _, _, _, _ in
    // 使用变换后的 image
}

3. 渐进式加载(渐进解码)

imageView.sd_setImage(with: url, placeholderImage: nil, options: [.progressiveLoad])

4. 自定义 Coder 注册(以 WebP 为例)

SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)

5. 最佳实践小结

  • 列表/网格:cell 内先 sd_cancelCurrentImageLoad(),再 sd_setImage,并配合 Transformer 限制尺寸或使用下采样选项,减少内存与错位。
  • 预取:用 SDWebImagePrefetcher 或系统 prefetch API 提前加载即将出现的图片,滚动时优先命中缓存。
  • 大图/详情:列表用缩小版 Transformer,详情页用原图或单独 OriginalImageCache,避免重复下载。
  • 动图:使用 SDAnimatedImageView + SDAnimatedImage,并视情况注册 GIF/WebP/APNG 等 Coder。
  • 扩展与测试:自定义 Coder/Transformer 通过协议实现并注册;通过 context 注入自定义 Cache/Loader 便于单测与多策略并存。

九、常见面试题

1. 图片文件缓存的时间有多长?

一周。_maxCacheAge = kDefaultCacheMaxCacheAge

2. SDWebImage 的内存缓存是用什么实现的?

NSCache

3. SDWebImage 的最大并发数是多少?

maxConcurrentDownloads = 6(程序固定,可通过属性调整)

4. SDWebImage 支持动图吗?GIF

支持。示例:

#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];

5. SDWebImage 是如何区分不同格式的图像的?

  • 根据图像数据第一个字节来判断
  • PNG:压缩比没有 JPG 高,但无损压缩,解压缩性能高,苹果推荐的图像格式
  • JPG:压缩比最高的一种图片格式,有损压缩,最多使用的场景如照相机,解压缩性能不好
  • GIF:序列帧动图,只支持 256 种颜色,曾流行于 1998~1999,有专利

6. SDWebImage 缓存图片的名称是怎么确定的?

  • 使用 md5 对完整 URL 做散列,得到 32 位字符串作为文件名;若单纯用文件名保存,重名几率高

7. SDWebImage 的内存警告是如何处理的?

  • 利用通知中心观察:
    • UIApplicationDidReceiveMemoryWarningNotification:接收到内存警告后执行 clearMemory,清理内存缓存
    • UIApplicationWillTerminateNotification:接收到应用将要终止后执行 cleanDisk,清理磁盘缓存
    • UIApplicationDidEnterBackgroundNotification:接收到应用进入后台后执行 backgroundCleanDisk,后台清理磁盘
  • 通过以上通知监听,保证缓存文件大小在控制范围内;clearDisk 可清空磁盘缓存,删除缓存目录中全部文件

参考文献

[1] Apple. Image and Graphics Best Practices. WWDC 2018, Session 219.
[2] Stack Overflow / Apple. Creating a thumbnail from UIImage using CGImageSourceCreateThumbnailAtIndex.
[3] Apple. Image decompression strategies for performance. developer.apple.com/forums/thread/653738.
[4] Ctrl.blog. Progressive JPEG loading; Google 研究:渐进解码约 3 倍于 baseline 的 CPU 开销.
[5] Wikipedia. Digital image processing.
[6] SDWebImage. Advanced Usage - Image Transformer, Custom Coder. GitHub Wiki.
[7] Beyer et al. FlexiViT: One Model for All Patch Sizes. CVPR 2023.
[8] NanoFLUX. Distillation-Driven Compression of Large Text-to-Image Generation Models for Mobile Devices. arXiv.
[9] SnapGen. Taming High-Resolution Text-to-Image Models for Mobile Devices. arXiv 2024.

04-研究优秀开源框架@响应式编程@iOS | RxSwift框架:从使用到源码解析

📋 目录


一、RxSwift框架使用详解

1. RxSwift框架概述

RxSwift 是 ReactiveX(Reactive Extensions)的 Swift 实现,是一个用于处理异步事件流的函数式响应式编程框架。

1.1 什么是RxSwift

RxSwift 基于观察者模式,允许你通过组合不同的操作符来处理异步事件序列。它提供了声明式的 API 来处理时间序列数据。

核心特点:

  • 响应式编程:基于观察者模式的事件驱动编程
  • 函数式编程:使用高阶函数和操作符组合
  • 类型安全:充分利用 Swift 的类型系统
  • 跨平台:基于 ReactiveX 标准,与其他平台一致
  • 丰富的操作符:提供大量操作符处理各种场景

1.2 RxSwift vs Combine

特性 RxSwift Combine
平台 跨平台(iOS、macOS、watchOS、tvOS) Apple 生态(iOS 13+)
语言 Swift Swift
官方支持 ❌ 第三方(ReactiveX) ✅ Apple 官方
最低版本 iOS 8.0+ iOS 13.0+
API风格 ReactiveX 标准 Apple 风格
学习曲线 陡峭 中等
生态 丰富(RxCocoa、RxDataSources等) 官方集成(SwiftUI)

1.3 RxSwift生态系统

  • RxSwift:核心框架
  • RxCocoa:UIKit/AppKit 集成
  • RxDataSources:TableView/CollectionView 数据源
  • RxTest:测试工具
  • RxBlocking:阻塞操作符(用于测试)

1.4 安装方式

CocoaPods:

pod 'RxSwift', '~> 6.0'
pod 'RxCocoa', '~> 6.0'

SPM:

dependencies: [
    .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0")
]

1.5 编程思想(背后的范式与理念)

为什么要先谈编程思想?
会用 RxSwift 的 API(ObservablesubscribemapflatMap 等)不等于能写好响应式架构。很多「看起来能跑」的代码其实仍是用响应式语法写命令式逻辑(例如在 subscribe 里写满 if-else、嵌套请求),难以测试、难以复用。先理解背后的范式与理念,再写代码,才能做到「用对场景、写对抽象、边界清晰」。RxSwift 与 Combine 同属 ReactiveX 一脉,背后的编程思想高度一致;理解这些思想有助于写出更清晰、可维护的响应式代码。

范式定位:FRP(函数式响应式编程)
RxSwift 是 FRP(Functional Reactive Programming) 的一种实现:用函数式组合与不可变方式,处理响应式事件流。不是「要么函数式要么响应式」,而是两者结合——流用操作符做纯变换(函数式),用订阅对事件做出反应(响应式)。了解这一点,就不会把 Rx 单纯当成「另一种回调封装」,而是从「流 + 变换 + 订阅」的视角设计数据与 UI 的边界。


(1)响应式编程(Reactive Programming)

  • 核心:将「数据与事件」视为随时间发生的事件序列,通过订阅对序列中的每一项做出反应,而不是主动轮询或层层回调。
  • 在 RxSwift 中Observable 表示一条事件流,Observer 通过 subscribe 订阅后,在 onNext / onError / onCompleted 中响应;按钮点击、网络返回、定时器都可统一为 Observable,用同一套操作符处理。
  • 思维转变:从「先调 A,等回调再调 B」变为「当流里出现某类事件时,执行 B」,逻辑由数据/事件驱动。

(2)声明式 vs 命令式

维度 命令式(Imperative) 声明式(Declarative)
关注点 「怎么做」:显式控制顺序与分支 「做什么」:描述结果与数据变换关系
典型写法 for 循环、if-else、嵌套回调 链式操作符:map / filter / flatMap / combineLatest
在 RxSwift 中 手写「请求 → 回调里解析 → 再请求」 observable.map(...).flatMap(...).subscribe(...) 描述整条流水线

声明式让「数据从哪来、怎么变、到哪去」一目了然,便于阅读和单元测试。

从 OOP/命令式到响应式的思维转变:传统写法习惯「谁持有谁、谁调谁」——对象持有状态,方法里 if-else 控制流程,异步靠回调或 delegate。响应式则把「谁在什么时候产生什么」抽象成流,把「对数据的处理」抽象成操作符链,把「最终消费」放在订阅里。习惯后,你会先想「有哪些事件源」「它们如何组合、变换」,再写具体订阅逻辑,而不是一上来就写一堆属性和回调。

同一需求的两种写法对比(搜索框防抖 + 请求 + 只取非空):
命令式常见写法是:在文本回调里设 Timer、取消上一次请求、判断非空再发请求、在回调里更新 UI,逻辑分散在多处。用 RxSwift 可以写成一条「流」:

// 响应式:一条链描述「输入 → 防抖 → 非空过滤 → 请求 → 主线程更新」
searchTextField.rx.text.orEmpty
    .debounce(.milliseconds(300), scheduler: MainScheduler.instance)
    .filter { !$0.isEmpty }
    .flatMapLatest { query in api.search(query) }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { results in self.updateUI(results) })
    .disposed(by: disposeBag)

这样,「防抖」「过滤空串」「只保留最后一次请求」「切回主线程」都体现在操作符上,阅读时一眼能看出数据流;单元测试时可以对 Observable 链单独测,而不必依赖 UI。

(3)函数式思想(组合与不可变)

  • 组合(Composition):每个操作符只做一件事,通过 .map().filter().distinctUntilChanged() 等组合成完整逻辑,而不是在一个闭包里写尽所有逻辑。
  • 不可变(Immutability):操作符不修改原 Observable,而是返回新的 Observable;原流不变,便于复用和推理。
  • 副作用边界:纯变换放在操作符链中,副作用(UI 更新、写库、弹窗)集中在 subscribe 的闭包里,便于测试和并发安全。

(4)流与时间(Streams & Time)

  • 把所有「会随时间产生的事件」都视为时间序列:next、next、…、completed/error。
  • 时间相关操作符:debounce(静默一段时间后取最新)、throttle(间隔内只取第一个/最后一个)、delay(延后发射),统一表达「何时」而不只是「何值」。

(5)观察者与发布-订阅

  • 观察者模式:Observer 订阅 Observable,在事件发生时被通知。RxSwift 的 subscribe(onNext:onError:onCompleted:) 就是在注册观察者。
  • 发布-订阅:生产端(Observable)与消费端(Observer)解耦,通过 Disposable 表示一次订阅的生命周期;Rx 的「热/冷」流、背压(部分算子)都是在这一模型上的扩展。

(6)设计原则在 Rx 中的体现

原则 在 RxSwift 中的体现
单一职责 每个操作符只做一种变换(map 只做映射,filter 只做过滤),复杂逻辑由链式组合完成。
关注点分离 数据获取与变换在 Observable 链中,线程切换用 subscribeOn/observeOn,副作用集中在 subscribe
依赖倒置 业务依赖「Observable 流」的抽象,而不依赖具体如何产生事件(网络、本地、Mock 都可替换)。
开闭原则 通过新操作符或新 Observable 扩展行为,而不必修改已有链;原流不可变,易于复用。

小结:RxSwift 用声明式事件流(Observable)和可组合操作符,在观察者/发布-订阅模型下做响应式的异步与事件处理,并用 Scheduler 控制线程与时机。掌握这些思想后,再写「为什么用 map 而不是在 subscribe 里写一大段」「为什么需要 observeOn/subscribeOn」会更自然。


2. 核心概念

2.1 Observable(可观察序列)

Observable 是 RxSwift 的核心,表示可以观察的事件序列。

protocol ObservableType {
    associatedtype Element
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

特点:

  • 可以发出零个或多个事件
  • 可能以完成或错误结束
  • 是值类型(struct)
  • 不可变(每次操作返回新的 Observable)

事件类型:

enum Event<Element> {
    case next(Element)      // 下一个元素
    case error(Swift.Error) // 错误
    case completed          // 完成
}

示例:

// 创建一个简单的 Observable
let observable = Observable<String>.just("Hello, RxSwift!")

observable.subscribe(onNext: { value in
    print(value)  // 输出: Hello, RxSwift!
}, onError: { error in
    print("错误: \(error)")
}, onCompleted: {
    print("完成")
})
.disposed(by: disposeBag)

// 使用数组创建 Observable
let arrayObservable = Observable.from([1, 2, 3, 4, 5])

arrayObservable.subscribe(onNext: { value in
    print(value)  // 依次输出: 1, 2, 3, 4, 5
})
.disposed(by: disposeBag)

2.2 Observer(观察者)

Observer 是接收 Observable 事件的协议。

protocol ObserverType {
    associatedtype Element
    func on(_ event: Event<Element>)
}

内置 Observer:

  • onNext:接收下一个元素
  • onError:接收错误
  • onCompleted:接收完成事件

示例:

let observable = Observable.from([1, 2, 3])

observable.subscribe(
    onNext: { value in
        print("收到值: \(value)")
    },
    onError: { error in
        print("错误: \(error)")
    },
    onCompleted: {
        print("完成")
    }
)
.disposed(by: disposeBag)

2.3 Disposable(可释放资源)

Disposable 表示订阅关系,用于取消订阅和释放资源。

protocol Disposable {
    func dispose()
}

DisposeBag:

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.just("Hello")
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)  // 自动管理生命周期
    }
}

3. Observable与Observer

3.1 创建Observable

just

创建只发出一个元素的 Observable。

let observable = Observable.just("Hello")
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
from

从数组或序列创建 Observable。

let observable = Observable.from([1, 2, 3])
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
of

从多个元素创建 Observable。

let observable = Observable.of(1, 2, 3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
create

自定义创建 Observable。

let observable = Observable<String>.create { observer in
    observer.onNext("A")
    observer.onNext("B")
    observer.onCompleted()
    return Disposables.create()
}

observable.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
empty

创建不发出任何元素的 Observable。

let observable = Observable<Int>.empty()
    .subscribe(
        onNext: { print($0) },
        onCompleted: { print("完成") }
    )
    .disposed(by: disposeBag)
// 输出: 完成
never

创建永不发出事件也永不完成的 Observable。

let observable = Observable<Int>.never()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 无输出
error

创建立即发出错误的 Observable。

enum MyError: Error {
    case customError
}

let observable = Observable<Int>.error(MyError.customError)
    .subscribe(
        onNext: { print($0) },
        onError: { print("错误: \($0)") }
    )
    .disposed(by: disposeBag)
// 输出: 错误: customError
range

创建发出指定范围内整数的 Observable。

let observable = Observable.range(start: 1, count: 5)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3, 4, 5
repeatElement

重复发出指定元素。

let observable = Observable.repeatElement("Hello")
    .take(3)  // 只取前3个
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: Hello, Hello, Hello
interval

按指定时间间隔发出整数。

let observable = Observable<Int>.interval(
    .seconds(1),
    scheduler: MainScheduler.instance
)
.take(5)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 每秒输出: 0, 1, 2, 3, 4
timer

延迟指定时间后发出元素。

let observable = Observable<Int>.timer(
    .seconds(2),
    scheduler: MainScheduler.instance
)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 2秒后输出: 0

3.2 自定义Observable

struct CustomObservable<Element>: ObservableType {
    typealias Element = Element
    
    private let _subscribe: (AnyObserver<Element>) -> Disposable
    
    init(_ subscribe: @escaping (AnyObserver<Element>) -> Disposable) {
        self._subscribe = subscribe
    }
    
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        let anyObserver = AnyObserver(observer)
        return _subscribe(anyObserver)
    }
}

// 使用
let custom = CustomObservable<Int> { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onCompleted()
    return Disposables.create()
}

custom.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

4. Operators操作符

4.1 转换操作符

map

转换每个元素。

Observable.from([1, 2, 3])
    .map { $0 * 2 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 2, 4, 6
flatMap

将 Observable 发出的元素转换为 Observable,然后合并。

Observable.from(["A", "B", "C"])
    .flatMap { letter in
        Observable.from([1, 2]).map { "\(letter)\($0)" }
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: A1, A2, B1, B2, C1, C2
flatMapLatest

只保留最新的内部 Observable。

Observable.from(["A", "B", "C"])
    .flatMapLatest { letter in
        Observable.just(letter).delay(.seconds(1), scheduler: MainScheduler.instance)
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 只输出: C(A和B被取消)
scan

累积值。

Observable.from([1, 2, 3, 4, 5])
    .scan(0, accumulator: +)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 3, 6, 10, 15
buffer

缓冲元素。

Observable.from([1, 2, 3, 4, 5, 6, 7, 8])
    .buffer(timeSpan: .seconds(1), count: 3, scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: [1, 2, 3], [4, 5, 6], [7, 8]
window

将 Observable 分割为多个 Observable。

Observable.from([1, 2, 3, 4, 5, 6])
    .window(timeSpan: .seconds(1), count: 2, scheduler: MainScheduler.instance)
    .flatMap { $0 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

4.2 过滤操作符

filter

过滤元素。

Observable.from([1, 2, 3, 4, 5])
    .filter { $0 % 2 == 0 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 2, 4
distinctUntilChanged

移除连续重复的元素。

Observable.from([1, 1, 2, 2, 3, 3])
    .distinctUntilChanged()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3
elementAt

获取指定索引的元素。

Observable.from([1, 2, 3, 4, 5])
    .elementAt(2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3
first / last

获取第一个或最后一个元素。

Observable.from([1, 2, 3, 4, 5])
    .first()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1

Observable.from([1, 2, 3, 4, 5])
    .last()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 5
take / takeLast

获取前几个或后几个元素。

Observable.from([1, 2, 3, 4, 5])
    .take(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3

Observable.from([1, 2, 3, 4, 5])
    .takeLast(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3, 4, 5
skip / skipLast

跳过前几个或后几个元素。

Observable.from([1, 2, 3, 4, 5])
    .skip(2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3, 4, 5
debounce

防抖,等待指定时间后发出最新值。

let subject = PublishSubject<String>()

subject
    .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("H")     // 不输出
subject.onNext("He")    // 不输出
subject.onNext("Hel")   // 不输出
subject.onNext("Hell")  // 不输出
subject.onNext("Hello") // 0.5秒后输出: Hello
throttle

节流,在指定时间间隔内只发出第一个值。

let subject = PublishSubject<String>()

subject
    .throttle(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("A")  // 立即输出: A
subject.onNext("B")  // 不输出(1秒内)
subject.onNext("C")  // 不输出(1秒内)
// 1秒后
subject.onNext("D")  // 输出: D

4.3 组合操作符

startWith

在序列开始前插入元素。

Observable.from([1, 2, 3])
    .startWith(0)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 0, 1, 2, 3
merge

合并多个 Observable。

let subject1 = PublishSubject<Int>()
let subject2 = PublishSubject<Int>()

Observable.merge(subject1, subject2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject1.onNext(1)  // 输出: 1
subject2.onNext(2)  // 输出: 2
subject1.onNext(3)  // 输出: 3
combineLatest

组合多个 Observable 的最新值。

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<Int>()

Observable.combineLatest(subject1, subject2)
    .subscribe(onNext: { value1, value2 in
        print("\(value1): \(value2)")
    })
    .disposed(by: disposeBag)

subject1.onNext("A")  // 无输出(等待 subject2)
subject2.onNext(1)    // 输出: A: 1
subject1.onNext("B")  // 输出: B: 1
subject2.onNext(2)    // 输出: B: 2
zip

按顺序组合多个 Observable。

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<Int>()

Observable.zip(subject1, subject2)
    .subscribe(onNext: { value1, value2 in
        print("\(value1): \(value2)")
    })
    .disposed(by: disposeBag)

subject1.onNext("A")  // 等待 subject2
subject1.onNext("B")  // 等待 subject2
subject2.onNext(1)    // 输出: A: 1
subject2.onNext(2)    // 输出: B: 2
withLatestFrom

当源 Observable 发出元素时,使用另一个 Observable 的最新值。

let button = PublishSubject<Void>()
let textField = PublishSubject<String>()

button
    .withLatestFrom(textField)
    .subscribe(onNext: { text in
        print("按钮点击,文本: \(text)")
    })
    .disposed(by: disposeBag)

textField.onNext("Hello")  // 无输出
textField.onNext("World")  // 无输出
button.onNext(())          // 输出: 按钮点击,文本: World
switchLatest

切换到最新的内部 Observable。

let subject1 = PublishSubject<Int>()
let subject2 = PublishSubject<Int>()
let source = PublishSubject<Observable<Int>>()

source
    .switchLatest()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

source.onNext(subject1)
subject1.onNext(1)  // 输出: 1
subject1.onNext(2)  // 输出: 2

source.onNext(subject2)
subject1.onNext(3)  // 不输出(已切换)
subject2.onNext(4)  // 输出: 4

4.4 错误处理操作符

catchError

捕获错误并返回备用 Observable。

enum MyError: Error {
    case failure
}

let observable = Observable<String>.error(MyError.failure)
    .catchError { error -> Observable<String> in
        print("捕获错误: \(error)")
        return Observable.just("备用值")
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 捕获错误: failure, 备用值
catchErrorJustReturn

用默认值替换错误。

let observable = Observable<String>.error(MyError.failure)
    .catchErrorJustReturn("默认值")
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 默认值
retry

重试失败的 Observable。

var attempts = 0

let observable = Observable<String>.create { observer in
    attempts += 1
    if attempts < 3 {
        observer.onError(MyError.failure)
    } else {
        observer.onNext("成功")
        observer.onCompleted()
    }
    return Disposables.create()
}
.retry(2)  // 最多重试 2 次
.subscribe(
    onNext: { print($0) },
    onError: { print("错误: \($0)") }
)
.disposed(by: disposeBag)
// 输出: 成功
retryWhen

根据条件重试。

let observable = Observable<String>.error(MyError.failure)
    .retryWhen { errors in
        errors.enumerated().flatMap { index, error -> Observable<Int> in
            if index < 2 {
                return Observable<Int>.timer(.seconds(index + 1), scheduler: MainScheduler.instance)
            } else {
                return Observable.error(error)
            }
        }
    }
    .subscribe(
        onNext: { print($0) },
        onError: { print("最终错误: \($0)") }
    )
    .disposed(by: disposeBag)

4.5 工具操作符

do

执行副作用操作。

Observable.from([1, 2, 3])
    .do(onNext: { print("即将发出: \($0)") },
        onError: { print("错误: \($0)") },
        onCompleted: { print("完成") },
        onSubscribe: { print("订阅") },
        onDispose: { print("释放") })
    .subscribe(onNext: { print("收到: \($0)") })
    .disposed(by: disposeBag)
delay

延迟发出元素。

Observable.from([1, 2, 3])
    .delay(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 1秒后依次输出: 1, 2, 3
delaySubscription

延迟订阅。

Observable.from([1, 2, 3])
    .delaySubscription(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 1秒后开始输出: 1, 2, 3
materialize / dematerialize

将事件序列化/反序列化。

Observable.from([1, 2, 3])
    .materialize()
    .subscribe(onNext: { event in
        print(event)  // 输出: next(1), next(2), next(3), completed
    })
    .disposed(by: disposeBag)
timeout

超时处理。

Observable<Int>.never()
    .timeout(.seconds(2), scheduler: MainScheduler.instance)
    .subscribe(
        onNext: { print($0) },
        onError: { print("超时: \($0)") }
    )
    .disposed(by: disposeBag)
// 2秒后输出: 超时: RxError.timeout

5. Subjects

Subjects 既是 Observable 又是 Observer,可以手动发送事件。

5.1 PublishSubject

不保存当前值,只向订阅者发送订阅后的事件。

let subject = PublishSubject<String>()

// 订阅1
subject.subscribe(onNext: { print("订阅1: \($0)") })
    .disposed(by: disposeBag)

subject.onNext("A")  // 输出: 订阅1: A

// 订阅2
subject.subscribe(onNext: { print("订阅2: \($0)") })
    .disposed(by: disposeBag)

subject.onNext("B")  // 输出: 订阅1: B, 订阅2: B
subject.onCompleted()

5.2 BehaviorSubject

保存当前值,新订阅者会立即收到当前值。

let subject = BehaviorSubject<String>(value: "初始值")

// 订阅1
subject.subscribe(onNext: { print("订阅1: \($0)") })
    .disposed(by: disposeBag)
// 输出: 订阅1: 初始值

subject.onNext("新值")  // 输出: 订阅1: 新值

// 订阅2
subject.subscribe(onNext: { print("订阅2: \($0)") })
    .disposed(by: disposeBag)
// 输出: 订阅2: 新值(立即收到当前值)

5.3 ReplaySubject

保存指定数量的最近值,新订阅者会收到这些值。

let subject = ReplaySubject<String>.create(bufferSize: 2)

subject.onNext("A")
subject.onNext("B")
subject.onNext("C")

// 订阅
subject.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: B, C(最近2个值)

5.4 AsyncSubject

只发出最后一个值(在完成时)。

let subject = AsyncSubject<String>()

subject.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("A")  // 不输出
subject.onNext("B")  // 不输出
subject.onNext("C")  // 不输出
subject.onCompleted()  // 输出: C

6. Schedulers调度器

Schedulers 决定操作在哪个线程执行。

6.1 内置Scheduler

MainScheduler

主线程调度器。

Observable.just(1)
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { value in
        // 在主线程执行
        print(Thread.isMainThread)  // true
    })
    .disposed(by: disposeBag)
SerialDispatchQueueScheduler

串行队列调度器。

let scheduler = SerialDispatchQueueScheduler(
    qos: .userInitiated,
    internalSerialQueueName: "custom.queue"
)

Observable.just(1)
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在后台线程执行
    })
    .disposed(by: disposeBag)
ConcurrentDispatchQueueScheduler

并发队列调度器。

let scheduler = ConcurrentDispatchQueueScheduler(
    qos: .background
)

Observable.from([1, 2, 3])
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在后台线程执行
    })
    .disposed(by: disposeBag)
OperationQueueScheduler

操作队列调度器。

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

let scheduler = OperationQueueScheduler(operationQueue: queue)

Observable.from([1, 2, 3, 4, 5])
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在操作队列执行
    })
    .disposed(by: disposeBag)

6.2 subscribeOn vs observeOn

  • subscribeOn:指定订阅在哪个线程执行
  • observeOn:指定后续操作在哪个线程执行
Observable.create { observer in
    print("订阅线程: \(Thread.current)")
    observer.onNext(1)
    observer.onCompleted()
    return Disposables.create()
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { value in
    print("接收线程: \(Thread.current)")
})
.disposed(by: disposeBag)

7. 错误处理

7.1 错误类型

enum RxError: Swift.Error {
    case unknown
    case disposed
    case timeout
    case noElements
    case moreThanOneElement
}

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

7.2 错误处理策略

func fetchData() -> Observable<String> {
    return Observable.create { observer in
        // 模拟网络请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            observer.onError(NetworkError.noData)
        }
        return Disposables.create()
    }
}

fetchData()
    .catchError { error -> Observable<String> in
        // 捕获错误,返回备用 Observable
        return Observable.just("默认数据")
    }
    .retry(3)  // 重试 3 次
    .subscribe(
        onNext: { print($0) },
        onError: { print("最终错误: \($0)") }
    )
    .disposed(by: disposeBag)

8. 内存管理

8.1 DisposeBag

自动管理订阅的生命周期。

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.just("Hello")
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)  // 自动管理
    }
    
    // viewController 释放时,disposeBag 会自动释放所有订阅
}

8.2 避免循环引用

class ViewModel {
    private let disposeBag = DisposeBag()
    
    func setup() {
        Observable.just("Data")
            .subscribe(onNext: { [weak self] value in
                // 使用 weak self 避免循环引用
                self?.process(value)
            })
            .disposed(by: disposeBag)
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
}

8.3 takeUntil

在指定条件满足时自动取消订阅。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.interval(.seconds(1), scheduler: MainScheduler.instance)
            .takeUntil(self.rx.deallocated)  // viewController 释放时自动取消
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)
    }
}

9. 与UIKit集成

9.1 RxCocoa基础

RxCocoa 提供了 UIKit 的 Rx 扩展。

import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var label: UILabel!
    
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 文本输入绑定
        textField.rx.text
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
        
        // 按钮点击
        button.rx.tap
            .subscribe(onNext: { [weak self] in
                self?.handleButtonTap()
            })
            .disposed(by: disposeBag)
    }
}

9.2 常用绑定

// UILabel
label.rx.text.onNext("Hello")
label.rx.attributedText.onNext(attributedString)

// UITextField
textField.rx.text
    .subscribe(onNext: { text in
        print("文本: \(text ?? "")")
    })
    .disposed(by: disposeBag)

// UIButton
button.rx.tap
    .subscribe(onNext: {
        print("按钮点击")
    })
    .disposed(by: disposeBag)

// UISwitch
switch.rx.isOn
    .subscribe(onNext: { isOn in
        print("开关: \(isOn)")
    })
    .disposed(by: disposeBag)

// UISlider
slider.rx.value
    .subscribe(onNext: { value in
        print("值: \(value)")
    })
    .disposed(by: disposeBag)

9.3 TableView绑定

import RxDataSources

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    
    private let disposeBag = DisposeBag()
    private let items = BehaviorSubject<[String]>(value: ["Item 1", "Item 2", "Item 3"])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let dataSource = RxTableViewSectionedReloadDataSource<String> { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item
            return cell
        }
        
        items
            .map { [SectionModel(model: "", items: $0)] }
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
}

10. 实际应用场景

10.1 网络请求

struct API {
    static func fetchUser(id: Int) -> Observable<User> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.rx.data(request: URLRequest(url: url))
            .map { data in
                try JSONDecoder().decode(User.self, from: data)
            }
            .observeOn(MainScheduler.instance)
    }
}

API.fetchUser(id: 1)
    .subscribe(
        onNext: { user in
            print("用户: \(user)")
        },
        onError: { error in
            print("错误: \(error)")
        }
    )
    .disposed(by: disposeBag)

10.2 用户输入处理

class SearchViewModel {
    private let disposeBag = DisposeBag()
    let searchText = BehaviorSubject<String>(value: "")
    let results = BehaviorSubject<[String]>(value: [])
    
    init() {
        searchText
            .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .filter { !$0.isEmpty }
            .flatMapLatest { query -> Observable<[String]> in
                return self.search(query: query)
                    .catchErrorJustReturn([])
            }
            .bind(to: results)
            .disposed(by: disposeBag)
    }
    
    private func search(query: String) -> Observable<[String]> {
        // 实现搜索逻辑
        return Observable.just(["结果1", "结果2"])
    }
}

10.3 组合多个数据源

class DashboardViewModel {
    private let disposeBag = DisposeBag()
    let user = BehaviorSubject<User?>(value: nil)
    let posts = BehaviorSubject<[Post]>(value: [])
    let isLoading = BehaviorSubject<Bool>(value: false)
    
    func loadData() {
        isLoading.onNext(true)
        
        let userObservable = API.fetchUser(id: 1)
        let postsObservable = API.fetchPosts()
        
        Observable.zip(userObservable, postsObservable)
            .observeOn(MainScheduler.instance)
            .subscribe(
                onNext: { [weak self] user, posts in
                    self?.user.onNext(user)
                    self?.posts.onNext(posts)
                    self?.isLoading.onNext(false)
                },
                onError: { [weak self] error in
                    self?.isLoading.onNext(false)
                    print("错误: \(error)")
                }
            )
            .disposed(by: disposeBag)
    }
}

10.4 表单验证(多字段实时校验)

多字段表单:用户名、密码、确认密码实时校验,用 combineLatest 聚合多流,用 map 产出错误文案或是否可提交。

class FormViewModel {
    private let disposeBag = DisposeBag()

    let username = BehaviorRelay<String>(value: "")
    let password = BehaviorRelay<String>(value: "")
    let confirmPassword = BehaviorRelay<String>(value: "")

    let usernameError = BehaviorRelay<String?>(value: nil)
    let isFormValid = BehaviorRelay<Bool>(value: false)

    init() {
        // 用户名:非空 + 长度
        username
            .map { name in
                if name.isEmpty { return "请输入用户名" }
                if name.count < 3 { return "至少 3 个字符" }
                return nil
            }
            .bind(to: usernameError)
            .disposed(by: disposeBag)

        // 三字段 combineLatest,任一变化都重新计算表单是否有效
        Observable.combineLatest(username, password, confirmPassword)
            .map { name, pwd, confirm in
                if name.isEmpty || pwd.isEmpty { return false }
                if pwd != confirm { return false }
                if pwd.count < 6 { return false }
                return true
            }
            .bind(to: isFormValid)
            .disposed(by: disposeBag)
    }
}

// VC 中绑定
viewModel.isFormValid
    .bind(to: submitButton.rx.isEnabled)
    .disposed(by: disposeBag)
viewModel.usernameError
    .bind(to: usernameErrorLabel.rx.text)
    .disposed(by: disposeBag)

10.5 NotificationCenter 转 Observable

系统通知或自定义通知转为 Observable,便于在链中 mapfilterobserveOn

// 键盘即将显示:取键盘 frame
let keyboardWillShow = NotificationCenter.default.rx
    .notification(UIResponder.keyboardWillShowNotification)
    .map { notification -> CGRect in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero
    }
    .observeOn(MainScheduler.instance)

keyboardWillShow
    .subscribe(onNext: { frame in
        print("键盘高度: \(frame.height)")
    })
    .disposed(by: disposeBag)

// 自定义通知
extension Notification.Name {
    static let myCustomEvent = Notification.Name("MyCustomEvent")
}
let customObservable = NotificationCenter.default.rx.notification(.myCustomEvent)

10.6 Timer 与周期任务

Observable.interval 做定时轮询,或用 Observable.timer 做延迟/单次任务。

// 每 1 秒发一个递增整数,主线程接收
let timerObservable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .take(10)  // 只取 10 次
    .subscribe(onNext: { tick in
        print("tick: \(tick)")
    })
    .disposed(by: disposeBag)

// 延迟 2 秒后执行一次
Observable<Int>.timer(.seconds(2), scheduler: MainScheduler.instance)
    .subscribe(onNext: { _ in
        print("2 秒后执行")
    })
    .disposed(by: disposeBag)

// 轮询接口:每 5 秒请求一次,直到满足条件
Observable<Int>.interval(.seconds(5), scheduler: MainScheduler.instance)
    .flatMapLatest { _ in API.pollStatus() }
    .takeWhile { !$0.isDone }
    .subscribe(onNext: { status in
        print("状态: \(status)")
    })
    .disposed(by: disposeBag)

10.7 请求重试与超时

retry 在失败时重新订阅上游;timeout 超时未完成则发 error;配合 catchError 做兜底。

URLSession.shared.rx.data(request: request)
    .timeout(.seconds(10), scheduler: MainScheduler.instance)
    .retry(3)
    .map { data in try JSONDecoder().decode(User.self, from: data) }
    .catchError { _ in Observable.just(User.placeholder) }
    .observeOn(MainScheduler.instance)
    .subscribe(
        onNext: { user in
            // 更新 UI
        },
        onError: { error in
            print("错误: \(error)")
        }
    )
    .disposed(by: disposeBag)

10.8 多源竞速(主备 / race)

主接口失败时切到备用接口,用 catchError 切流;或 merge + take(1) 实现「谁先完成用谁」。

// 主接口失败时用备用接口
func loadFromPrimaryOrFallback() -> Observable<Data> {
    let primary = URLSession.shared.rx.data(request: primaryRequest)
    let fallback = URLSession.shared.rx.data(request: fallbackRequest)
    return primary.catchError { _ in fallback }
}

// 显式 race:两个请求谁先完成用谁
func race<Element>(_ a: Observable<Element>, _ b: Observable<Element>) -> Observable<Element> {
    Observable.merge(a, b).take(1)
}

10.9 节流与防抖组合(搜索 + 按钮防重复点击)

搜索框用 debounce 减少请求频率;提交按钮用 throttle 防止连续点击重复提交。

// 搜索:防抖 + 去重 + 非空 + flatMapLatest 只保留最后一次请求
searchBar.rx.text.orEmpty
    .debounce(.milliseconds(400), scheduler: MainScheduler.instance)
    .distinctUntilChanged()
    .filter { !$0.isEmpty }
    .flatMapLatest { query in
        API.search(query: query).catchErrorJustReturn([])
    }
    .observeOn(MainScheduler.instance)
    .bind(to: results)
    .disposed(by: disposeBag)

// 提交按钮:节流 1 秒内只响应一次
submitButton.rx.tap
    .throttle(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { [weak self] in
        self?.submit()
    })
    .disposed(by: disposeBag)

10.10 RxCocoa 进阶:UISearchBar、RefreshControl、DelegateProxy

UISearchBarrx.textrx.searchButtonClicked 组合做「点击搜索」或「实时搜索」。

// 点击搜索按钮时用当前文本请求
searchBar.rx.searchButtonClicked
    .withLatestFrom(searchBar.rx.text.orEmpty)
    .filter { !$0.isEmpty }
    .flatMapLatest { API.search(query: $0) }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] results in
        self?.updateResults(results)
    })
    .disposed(by: disposeBag)

UIRefreshControl:下拉刷新与 isRefreshing 绑定。

refreshControl.rx.controlEvent(.valueChanged)
    .flatMapLatest { [weak self] _ in
        self?.loadData() ?? Observable.never()
    }
    .observeOn(MainScheduler.instance)
    .subscribe(
        onNext: { [weak self] _ in
            self?.refreshControl.endRefreshing()
        },
        onError: { [weak self] _ in
            self?.refreshControl.endRefreshing()
        }
    )
    .disposed(by: disposeBag)

DelegateProxy 示例(UITableView 点击):RxCocoa 已为常用控件提供 rx 扩展,如需自定义可继承 DelegateProxy

// 使用 RxCocoa 内置的 itemSelected
tableView.rx.itemSelected
    .subscribe(onNext: { indexPath in
        print("选中: \(indexPath)")
    })
    .disposed(by: disposeBag)

tableView.rx.modelSelected(Item.self)
    .subscribe(onNext: { item in
        print("选中项: \(item)")
    })
    .disposed(by: disposeBag)

10.11 页面生命周期与 takeUntil

在 VC 中让订阅随页面消失而自动取消:用 rx.deallocatingtakeUntil(self.rx.deallocated),避免重复订阅和泄漏。

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 方式一:统一丢进 disposeBag,VC 释放时一起 dispose
        someObservable
            .subscribe(onNext: { })
            .disposed(by: disposeBag)

        // 方式二:显式「直到某事件发生就结束」(如直到页面即将消失)
        someObservable
            .takeUntil(rx.deallocated)
            .subscribe(onNext: { })
            .disposed(by: disposeBag)
    }
}

10.12 CollectionView 与 RxDataSources

使用 RxDataSources 的 Section 模型驱动 UICollectionView,与 TableView 用法类似(Item 为业务模型类型,需与 Cell 一致)。

import RxDataSources

typealias Section = SectionModel<String, Item>  // Item 为业务模型
let dataSource = RxCollectionViewSectionedReloadDataSource<Section> { dataSource, collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ItemCell
    cell.configure(with: item)
    return cell
}

items
    .map { [Section(model: "列表", items: $0)] }
    .bind(to: collectionView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

10.13 双向绑定与 ControlProperty

RxCocoa 的 ControlProperty 支持双向绑定:一方是「用户输入」,一方是「模型/ViewModel」。

// 将 TextField 与 BehaviorRelay 双向绑定(需自己写绑定逻辑,或使用 RxCocoa 的 bind)
// 单向:ViewModel -> UI
viewModel.username
    .bind(to: textField.rx.text)
    .disposed(by: disposeBag)

// 单向:UI -> ViewModel
textField.rx.text.orEmpty
    .bind(to: viewModel.username)
    .disposed(by: disposeBag)

// 若需「初始值 + 用户修改都同步」,两行都写即可(Relay 与控件类型匹配时)

10.14 错误流与用户提示

将网络/业务错误统一转为「可展示的提示」,用 materialize()catchError 转成另一种元素类型,再在 UI 层订阅。

API.fetchUser(id: 1)
    .materialize()
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] event in
        switch event {
        case .next(let user):
            self?.showUser(user)
        case .error(let error):
            self?.showToast("加载失败: \(error.localizedDescription)")
        case .completed:
            break
        }
    })
    .disposed(by: disposeBag)

二、RxSwift框架源码解析

1. 架构设计

1.1 整体架构

RxSwift 采用协议导向的设计,核心是三个协议:

ObservableType (可观察类型)
    ↓
ObserverType (观察者类型)
    ↓
Disposable (可释放资源)

数据流:

Observable → Observer
     ↑          ↓
     └── 反馈 ──┘

1.2 核心协议层次

// 第一层:ObservableType 协议
protocol ObservableType {
    associatedtype Element
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

// 第二层:ObserverType 协议
protocol ObserverType {
    associatedtype Element
    func on(_ event: Event<Element>)
}

// 第三层:Disposable 协议
protocol Disposable {
    func dispose()
}

1.3 事件类型

enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}

2. Observable协议实现

2.1 ObservableType协议定义

public protocol ObservableType {
    associatedtype Element
    
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

2.2 Observable实现

public class Observable<Element>: ObservableType {
    public typealias Element = Element
    
    internal init() {}
    
    public func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        rxAbstractMethod()
    }
    
    public func asObservable() -> Observable<Element> {
        return self
    }
}

关键点:

  • Observable 是抽象类
  • subscribe 方法需要子类实现
  • 使用 rxAbstractMethod() 防止直接实例化

2.3 Just实现分析

final private class Just<Element>: Producer<Element> {
    private let element: Element
    
    init(element: Element) {
        self.element = element
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = JustSink(parent: self, observer: observer, cancel: cancel)
        let subscription = sink.run()
        return (sink: sink, subscription: subscription)
    }
}

final private class JustSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Parent = Just<Element>
    
    private let parent: Parent
    
    init(parent: Parent, observer: Observer, cancel: Cancelable) {
        self.parent = parent
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next:
            forwardOn(.next(parent.element))
            forwardOn(.completed)
            self.dispose()
        case .error, .completed:
            forwardOn(event)
            self.dispose()
        }
    }
    
    func run() -> Disposable {
        forwardOn(.next(parent.element))
        forwardOn(.completed)
        return Disposables.create()
    }
}

关键点:

  • Just 继承自 Producer
  • 使用 JustSink 处理订阅逻辑
  • 立即发出元素并完成

2.4 Create实现分析

final private class AnonymousObservable<Element>: Producer<Element> {
    typealias SubscribeHandler = (AnyObserver<Element>) -> Disposable
    
    private let subscribeHandler: SubscribeHandler
    
    init(_ subscribeHandler: @escaping SubscribeHandler) {
        self.subscribeHandler = subscribeHandler
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = AnonymousObservableSink(observer: observer, cancel: cancel)
        let subscription = sink.run(self)
        return (sink: sink, subscription: subscription)
    }
}

final private class AnonymousObservableSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Parent = AnonymousObservable<Element>
    
    private let parent: Parent
    
    init(observer: Observer, cancel: Cancelable) {
        self.parent = AnonymousObservable(subscribeHandler: { observer in
            // 包装观察者
            return Disposables.create()
        })
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next:
            forwardOn(event)
        case .error, .completed:
            forwardOn(event)
            self.dispose()
        }
    }
    
    func run(_ parent: Parent) -> Disposable {
        return parent.subscribeHandler(AnyObserver(self))
    }
}

关键点:

  • AnonymousObservable 使用闭包创建
  • AnyObserver 包装观察者
  • 支持自定义订阅逻辑

3. Observer协议实现

3.1 ObserverType协议定义

public protocol ObserverType {
    associatedtype Element
    
    func on(_ event: Event<Element>)
}

3.2 AnyObserver实现

public struct AnyObserver<Element>: ObserverType {
    public typealias Element = Element
    
    private let observer: AnyObserverBase<Element>
    
    public init<Observer: ObserverType>(_ observer: Observer)
        where Observer.Element == Element {
        self.observer = ObserverBox(observer)
    }
    
    public func on(_ event: Event<Element>) {
        observer.on(event)
    }
}

private class AnyObserverBase<Element>: ObserverType {
    func on(_ event: Event<Element>) {
        rxAbstractMethod()
    }
}

private final class ObserverBox<Observer: ObserverType>: AnyObserverBase<Observer.Element> {
    private let observer: Observer
    
    init(_ observer: Observer) {
        self.observer = observer
    }
    
    override func on(_ event: Event<Observer.Element>) {
        observer.on(event)
    }
}

关键点:

  • AnyObserver 是类型擦除包装器
  • 使用 ObserverBox 存储具体观察者
  • 实现观察者的多态

3.3 Sink实现

class Sink<Observer: ObserverType>: Disposable {
    typealias Element = Observer.Element
    
    private let observer: Observer
    private let cancel: Cancelable
    private var disposed = false
    
    init(observer: Observer, cancel: Cancelable) {
        self.observer = observer
        self.cancel = cancel
    }
    
    final func forwardOn(_ event: Event<Element>) {
        if isDisposed {
            return
        }
        observer.on(event)
    }
    
    final func forwardOn(_ event: Event<Element>, _ disposeHandler: @escaping () -> Void) {
        if isDisposed {
            return
        }
        observer.on(event)
        disposeHandler()
    }
    
    func dispose() {
        if !disposed {
            disposed = true
            cancel.dispose()
        }
    }
    
    var isDisposed: Bool {
        return disposed
    }
}

关键点:

  • Sink 是观察者的基类
  • 提供 forwardOn 方法转发事件
  • 管理订阅的生命周期

4. Operators实现原理

4.1 Map操作符实现

extension ObservableType {
    public func map<Result>(_ transform: @escaping (Element) -> Result) -> Observable<Result> {
        return Map(source: self.asObservable(), transform: transform)
    }
}

final private class Map<SourceType, ResultType>: Producer<ResultType> {
    typealias Transform = (SourceType) -> ResultType
    
    private let source: Observable<SourceType>
    private let transform: Transform
    
    init(source: Observable<SourceType>, transform: @escaping Transform) {
        self.source = source
        self.transform = transform
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == ResultType {
        let sink = MapSink(transform: transform, observer: observer, cancel: cancel)
        let subscription = source.subscribe(sink)
        return (sink: sink, subscription: subscription)
    }
}

final private class MapSink<SourceType, Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias ResultType = Observer.Element
    typealias Transform = (SourceType) -> ResultType
    
    private let transform: Transform
    
    init(transform: @escaping Transform, observer: Observer, cancel: Cancelable) {
        self.transform = transform
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<SourceType>) {
        switch event {
        case .next(let element):
            do {
                let mappedElement = try transform(element)
                forwardOn(.next(mappedElement))
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error(let error):
            forwardOn(.error(error))
            dispose()
        case .completed:
            forwardOn(.completed)
            dispose()
        }
    }
}

关键点:

  • Map 是新的 Observable,包装源 Observable
  • 创建 MapSink 进行转换
  • 错误处理:转换失败时发出错误

4.2 Filter操作符实现

extension ObservableType {
    public func filter(_ predicate: @escaping (Element) -> Bool) -> Observable<Element> {
        return Filter(source: self.asObservable(), predicate: predicate)
    }
}

final private class Filter<Element>: Producer<Element> {
    typealias Predicate = (Element) -> Bool
    
    private let source: Observable<Element>
    private let predicate: Predicate
    
    init(source: Observable<Element>, predicate: @escaping Predicate) {
        self.source = source
        self.predicate = predicate
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = FilterSink(predicate: predicate, observer: observer, cancel: cancel)
        let subscription = source.subscribe(sink)
        return (sink: sink, subscription: subscription)
    }
}

final private class FilterSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Predicate = (Element) -> Bool
    
    private let predicate: Predicate
    
    init(predicate: @escaping Predicate, observer: Observer, cancel: Cancelable) {
        self.predicate = predicate
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next(let element):
            do {
                let satisfies = try predicate(element)
                if satisfies {
                    forwardOn(.next(element))
                }
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error, .completed:
            forwardOn(event)
            dispose()
        }
    }
}

关键点:

  • 不满足条件时不转发事件
  • 满足条件时才传递给下游

4.3 FlatMap操作符实现

extension ObservableType {
    public func flatMap<Source: ObservableConvertibleType>(
        _ selector: @escaping (Element) -> Source
    ) -> Observable<Source.Element> {
        return FlatMap(source: self.asObservable(), selector: selector)
    }
}

final private class FlatMap<SourceElement, SourceSequence: ObservableConvertibleType>: Producer<SourceSequence.Element> {
    typealias Selector = (SourceElement) -> SourceSequence
    
    private let source: Observable<SourceElement>
    private let selector: Selector
    
    init(source: Observable<SourceElement>, selector: @escaping Selector) {
        self.source = source
        self.selector = selector
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == SourceSequence.Element {
        let sink = FlatMapSink(selector: selector, observer: observer, cancel: cancel)
        let subscription = sink.run(source)
        return (sink: sink, subscription: subscription)
    }
}

final private class FlatMapSink<SourceElement, SourceSequence: ObservableConvertibleType, Observer: ObserverType>: MergeSink<SourceSequence, Observer>
    where Observer.Element == SourceSequence.Element {
    
    typealias Selector = (SourceElement) -> SourceSequence
    
    private let selector: Selector
    
    init(selector: @escaping Selector, observer: Observer, cancel: Cancelable) {
        self.selector = selector
        super.init(observer: observer, cancel: cancel)
    }
    
    override func on(_ event: Event<SourceElement>) {
        switch event {
        case .next(let element):
            do {
                let innerObservable = try selector(element).asObservable()
                subscribeInner(innerObservable, group: group)
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error(let error):
            forwardOn(.error(error))
            dispose()
        case .completed:
            groupCompleted()
        }
    }
}

关键点:

  • 管理多个内部 Observable 订阅
  • 使用 MergeSink 合并结果
  • 需要复杂的生命周期管理

5. Subjects实现原理

5.1 PublishSubject实现

public final class PublishSubject<Element>: Observable<Element>, SubjectType, Cancelable, ObserverType, SynchronizedUnsubscribeType {
    public typealias SubjectObserverType = PublishSubject<Element>
    
    typealias Observers = AnyObserver<Element>.s
    typealias DisposeKey = Observers.KeyType
    
    private let lock = RecursiveLock()
    private var observers: Observers = Observers()
    private var isDisposed = false
    private var stopped = false
    private var stoppedEvent: Event<Element>?
    
    public override init() {
        super.init()
    }
    
    public func on(_ event: Event<Element>) {
        dispatch(synchronized_on(event), event)
    }
    
    func synchronized_on(_ event: Event<Element>) -> Observers {
        lock.lock()
        defer { lock.unlock() }
        
        switch event {
        case .next:
            if isDisposed || stopped {
                return Observers()
            }
            return observers
        case .completed, .error:
            if stoppedEvent == nil {
                stoppedEvent = event
                stopped = true
                let observers = self.observers
                self.observers.removeAll()
                return observers
            }
            return Observers()
        }
    }
    
    public override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        lock.lock()
        defer { lock.unlock() }
        
        if let stoppedEvent = stoppedEvent {
            observer.on(stoppedEvent)
            return Disposables.create()
        }
        
        if isDisposed {
            observer.on(.error(RxError.disposed))
            return Disposables.create()
        }
        
        let key = observers.insert(observer.on)
        return SubscriptionDisposable(owner: self, key: key)
    }
    
    func synchronizedUnsubscribe(_ disposeKey: DisposeKey) {
        lock.lock()
        defer { lock.unlock() }
        observers.removeKey(disposeKey)
    }
}

关键点:

  • 使用锁保护 observers 集合
  • 不保存当前值,新订阅者不会收到历史值
  • 使用 SubscriptionDisposable 管理订阅

5.2 BehaviorSubject实现

public final class BehaviorSubject<Element>: Observable<Element>, SubjectType, ObserverType, SynchronizedUnsubscribeType {
    public typealias SubjectObserverType = BehaviorSubject<Element>
    
    typealias Observers = AnyObserver<Element>.s
    typealias DisposeKey = Observers.KeyType
    
    private let lock = RecursiveLock()
    private var observers: Observers = Observers()
    private var isDisposed = false
    private var stoppedEvent: Event<Element>?
    private var element: Element
    
    public init(value: Element) {
        self.element = value
        super.init()
    }
    
    public var value: Element {
        lock.lock()
        defer { lock.unlock() }
        return element
    }
    
    public func on(_ event: Event<Element>) {
        dispatch(synchronized_on(event), event)
    }
    
    func synchronized_on(_ event: Event<Element>) -> Observers {
        lock.lock()
        defer { lock.unlock() }
        
        if stoppedEvent != nil || isDisposed {
            return Observers()
        }
        
        switch event {
        case .next(let element):
            self.element = element
        case .error, .completed:
            stoppedEvent = event
        }
        
        return observers
    }
    
    public override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        lock.lock()
        defer { lock.unlock() }
        
        if let stoppedEvent = stoppedEvent {
            observer.on(stoppedEvent)
            return Disposables.create()
        }
        
        if isDisposed {
            observer.on(.error(RxError.disposed))
            return Disposables.create()
        }
        
        let key = observers.insert(observer.on)
        observer.on(.next(element))  // 立即发送当前值
        
        return SubscriptionDisposable(owner: self, key: key)
    }
}

关键点:

  • 保存当前值 element
  • 新订阅者立即收到当前值
  • 使用锁保护状态

6. Schedulers实现原理

6.1 SchedulerType协议

public protocol SchedulerType {
    var now: RxTime { get }
    
    func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable
    
    func scheduleRelative<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable
    
    func schedulePeriodic<StateType>(_ state: StateType, startAfter: RxTimeInterval, period: RxTimeInterval, action: @escaping (StateType) -> StateType) -> Disposable
}

6.2 MainScheduler实现

public final class MainScheduler: SerialDispatchQueueScheduler {
    private let mainQueue: DispatchQueue
    
    public static let instance = MainScheduler()
    
    public static let asyncInstance = SerialDispatchQueueScheduler(
        serialQueue: DispatchQueue.main
    )
    
    private init() {
        mainQueue = DispatchQueue.main
        super.init(serialQueue: mainQueue)
    }
    
    public static func ensureExecutingOnScheduler(errorMessage: String? = nil) {
        if !DispatchQueue.isMain {
            rxFatalError(errorMessage ?? "Executing on background thread. Please use `MainScheduler.instance.schedule` to schedule work on main thread.")
        }
    }
}

关键点:

  • 使用 DispatchQueue.main
  • 提供单例实例
  • 提供线程检查方法

6.3 SerialDispatchQueueScheduler实现

public class SerialDispatchQueueScheduler: SchedulerType {
    public typealias TimeInterval = Foundation.TimeInterval
    public typealias Time = Date
    
    private let configuration: DispatchQueueConfiguration
    private let serialQueue: DispatchQueue
    
    public var now: RxTime {
        return Date()
    }
    
    public init(serialQueue: DispatchQueue, leeway: DispatchTimeInterval = DispatchTimeInterval.nanoseconds(0)) {
        self.serialQueue = serialQueue
        self.configuration = DispatchQueueConfiguration(
            queue: serialQueue,
            leeway: leeway
        )
    }
    
    public final func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
        return self.scheduleInternal(state, action: action)
    }
    
    func scheduleInternal<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
        let cancel = SingleAssignmentDisposable()
        
        serialQueue.async {
            if cancel.isDisposed {
                return
            }
            cancel.setDisposable(action(state))
        }
        
        return cancel
    }
    
    public final func scheduleRelative<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable {
        return scheduleRelativeInternal(state, dueTime: dueTime, action: action)
    }
    
    func scheduleRelativeInternal<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable {
        let deadline = now.addingTimeInterval(dueTime)
        
        let cancel = SingleAssignmentDisposable()
        
        serialQueue.asyncAfter(deadline: deadline) {
            if cancel.isDisposed {
                return
            }
            cancel.setDisposable(action(state))
        }
        
        return cancel
    }
}

关键点:

  • 使用 DispatchQueue 执行任务
  • 支持立即和延迟调度
  • 使用 SingleAssignmentDisposable 管理取消

7. 背压处理机制

7.1 背压问题

当生产者产生数据的速度快于消费者处理数据的速度时,会产生背压问题。

7.2 背压处理策略

RxSwift 主要通过以下方式处理背压:

  1. 请求机制:Observer 可以控制请求的数据量
  2. 缓冲:使用 buffer 操作符缓冲数据
  3. 节流:使用 throttledebounce 控制数据流速度
  4. 采样:使用 sample 采样数据

7.3 背压处理示例

class BackpressureObserver: ObserverType {
    typealias Element = Int
    
    private var buffer: [Int] = []
    private let bufferSize: Int
    private var subscription: Subscription?
    
    init(bufferSize: Int = 10) {
        self.bufferSize = bufferSize
    }
    
    func on(_ event: Event<Int>) {
        switch event {
        case .next(let element):
            buffer.append(element)
            
            // 处理缓冲区
            processBuffer()
            
            // 如果缓冲区未满,可以继续接收
            if buffer.count < bufferSize {
                // 继续接收
            }
        case .error, .completed:
            // 处理完成
            processRemaining()
        }
    }
    
    private func processBuffer() {
        while !buffer.isEmpty {
            let value = buffer.removeFirst()
            print("处理: \(value)")
        }
    }
    
    private func processRemaining() {
        processBuffer()
    }
}

8. 性能优化策略

8.1 值类型优化

RxSwift 大量使用值类型(struct),避免堆分配:

// 值类型,零成本抽象
struct Just<Element>: ObservableType { }
struct Map<SourceType, ResultType>: ObservableType { }
struct Filter<Element>: ObservableType { }

8.2 类型擦除

使用 asObservable() 隐藏具体类型:

extension ObservableType {
    public func asObservable() -> Observable<Element> {
        return Observable.create { observer in
            return self.subscribe(observer)
        }
    }
}

8.3 延迟执行

使用 deferred 延迟创建 Observable:

let deferred = Observable.deferred {
    // 只在订阅时执行
    return expensiveOperation()
}

8.4 共享订阅

使用 share() 共享 Observable:

let shared = expensiveObservable()
    .share()  // 多个订阅者共享同一个 Observable

shared.subscribe(onNext: { })  // 订阅1
shared.subscribe(onNext: { })  // 订阅2(共享执行)

8.5 内存优化

  • 使用 DisposeBag 自动管理订阅
  • 使用 weak self 避免循环引用
  • 及时取消不需要的订阅

📚 总结

RxSwift 框架的核心优势

  1. 跨平台标准:基于 ReactiveX 标准,与其他平台一致
  2. 丰富的操作符:提供大量操作符处理各种场景
  3. 类型安全:充分利用 Swift 类型系统
  4. 性能优化:值类型、零成本抽象
  5. 生态丰富:RxCocoa、RxDataSources 等扩展

学习建议

  1. 从基础开始:理解 Observable、Observer、Disposable
  2. 实践操作符:熟悉常用操作符的使用
  3. 理解调度器:掌握 subscribeOnobserveOn
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 RxSwift

RxSwift vs Combine

  • RxSwift:适合需要支持 iOS 8+ 的项目,API 更丰富
  • Combine:适合 iOS 13+ 项目,与系统深度集成

文档版本:v1.0
最后更新:2026年1月15日
参考文献:RxSwift GitHub Repository, ReactiveX Documentation

03-研究优秀开源框架@响应式编程@iOS | ReactiveCocoa框架:从使用到源码解析

📋 目录


一、ReactiveCocoa框架使用详解

1. ReactiveCocoa框架概述

ReactiveCocoa(简称 RAC)是一个基于 ReactiveSwift 的响应式编程框架,用于处理异步事件流和状态管理。它是 GitHub 开源的项目,提供了声明式的 API 来处理时间序列数据。

1.1 什么是ReactiveCocoa

ReactiveCocoa 是一个函数式响应式编程(FRP)框架,允许你通过组合不同的操作符来处理异步事件序列。它提供了声明式的 API 来处理时间序列数据。

核心特点:

  • 函数式响应式编程:基于函数式编程和响应式编程的结合
  • 类型安全:充分利用 Swift 的类型系统
  • 状态管理:提供 Property 和 MutableProperty 管理状态
  • Action模式:提供 Action 处理用户交互
  • UIKit集成:深度集成 UIKit 控件

1.2 ReactiveCocoa vs RxSwift vs Combine

特性 ReactiveCocoa RxSwift Combine
平台 iOS、macOS 跨平台 Apple 生态(iOS 13+)
语言 Swift Swift Swift
官方支持 ❌ GitHub 开源 ❌ 第三方 ✅ Apple 官方
核心类型 Signal、SignalProducer Observable Publisher
状态管理 Property、MutableProperty BehaviorSubject @Published
Action模式 ✅ Action
学习曲线 陡峭 陡峭 中等
生态 ReactiveSwift、ReactiveObjC RxCocoa SwiftUI

1.3 ReactiveCocoa生态系统

  • ReactiveSwift:核心框架,提供 Signal、SignalProducer 等
  • ReactiveCocoa:UIKit/AppKit 集成,提供控件绑定
  • ReactiveObjC:Objective-C 版本

1.4 安装方式

CocoaPods:

pod 'ReactiveSwift', '~> 7.0'
pod 'ReactiveCocoa', '~> 12.0'

SPM:

dependencies: [
    .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "7.0.0"),
    .package(url: "https://github.com/ReactiveCocoa/ReactiveCocoa.git", from: "12.0.0")
]

1.5 编程思想(背后的范式与理念)

ReactiveCocoa 明确标榜函数式响应式编程(FRP),将函数式与响应式结合;理解其背后的编程思想,能更好地区分 Signal / SignalProducer、Property、Action 的适用场景。

(1)函数式响应式编程(FRP)

  • 核心:在「响应式」的事件流之上,用函数式的方式组合与变换——把「随时间发生的事件」视为可映射、可过滤、可合并的值,通过纯函数组合成新流,而不是在观察者闭包里写满副作用。
  • 在 RAC 中Signal / SignalProducer 表示事件流,mapfilterflatMap 等操作符对流做纯变换,observestart 才真正消费并产生副作用;流与副作用边界清晰。
  • 与「仅响应式」的对比:FRP 强调「流即数据」,用转换与组合表达业务逻辑,观察者只做「最后一步」的响应,便于测试和复用。

(2)声明式 vs 命令式

维度 命令式(Imperative) 声明式(Declarative)
关注点 「怎么做」:显式顺序与分支 「做什么」:描述数据/事件如何变换与约束
典型写法 回调嵌套、状态变量、if-else 链式操作符:map / filter / combineLatest
在 RAC 中 手写「请求 → 回调里判断 → 再请求」 signal.map(...).flatMap(...).observeValues(...) 描述整条流水线

声明式让「事件从哪来、如何变换、到哪去」一目了然,便于阅读和单元测试。

(3)函数式思想(组合与不可变)

  • 组合(Composition):每个操作符只做一件事,通过 .map().filter().flatMap(.latest) 等组合成完整逻辑;小能力组合成大能力,避免巨型闭包。
  • 不可变(Immutability):操作符不修改原 Signal/SignalProducer,而是返回新的;原流不变,便于复用和推理。
  • 副作用边界:纯变换放在操作符链中,副作用(UI 更新、写库、弹窗)集中在 observeValues / startWithValuesAction 的 execution 中,便于测试和并发安全。

(4)流与时间(Streams & Time)

  • 把所有「会随时间产生的事件」都视为时间序列:value、value、…、completed/failed/interrupted。
  • RAC 区分热信号(Signal)冷信号(SignalProducer):热信号有订阅即开始发送、多订阅者共享同一时间线;冷信号每次 start 才执行、每次订阅独立。时间相关操作符如 debouncethrottle 表达「何时」而不只是「何值」。

(5)观察者与「推」「拉」

  • 观察者模式:Observer 通过 observe 订阅 Signal,或通过 start 启动 SignalProducer,在事件发生时被通知。
  • 推模型:Signal 是「推」——事件由发送端推动,观察者被动接收;SignalProducer 是「按需拉」——只有 start 时才创建并执行,适合表示「一次异步操作」或「延迟计算」。

(6)Action 与「意图-执行」分离

  • 思想:用户操作(点击、下拉)是意图,网络请求、校验、弹窗是执行;将「意图」与「执行」分离,便于禁用、重试、统一错误处理。
  • 在 RAC 中Action 接收输入(如按钮 tap 或输入值),内部用 SignalProducer 描述一次执行,输出与错误统一由 Action 暴露;UI 只绑定「能否执行」与「执行结果」,不写一堆 isLoadingerror 状态。

小结:ReactiveCocoa 用声明式事件流(Signal/SignalProducer)和可组合操作符,在函数式响应式的范式下做异步与事件处理;用 Property 管理可变状态、用 Action 封装「意图-执行」,并用 Scheduler 控制线程。掌握这些思想后,再区分「用 Signal 还是 SignalProducer」「何时用 Property、何时用 Action」会更自然。


2. 核心概念

2.1 Signal(信号)

Signal 是 ReactiveCocoa 的核心类型,表示一个可以观察的事件流。

protocol SignalProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func observe(_ observer: Observer<Value, Error>) -> Disposable?
}

特点:

  • 可以发出零个或多个值
  • 可能以完成或错误结束
  • 是引用类型(class)
  • 热信号(Hot Signal):有订阅者时立即开始发送事件

事件类型:

enum Event<Value, Error: Swift.Error> {
    case value(Value)      // 值事件
    case failed(Error)    // 错误事件
    case completed        // 完成事件
    case interrupted      // 中断事件
}

示例:

let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { value in
    print("收到值: \(value)")
}

observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()

2.2 SignalProducer(信号生产者)

SignalProducer 是延迟创建 Signal 的类型,类似于 RxSwift 的 Observable。

struct SignalProducer<Value, Error: Swift.Error> {
    private let startHandler: (Observer<Value, Error>, Lifetime) -> Void
    
    func start(_ observer: Observer<Value, Error>) -> Disposable
}

特点:

  • 冷信号(Cold Signal):只有在被订阅时才开始发送事件
  • 每次订阅都会创建新的 Signal
  • 适合表示异步操作

示例:

let producer = SignalProducer<String, Never> { observer, lifetime in
    observer.send(value: "Hello")
    observer.send(value: "World")
    observer.sendCompleted()
}

producer.startWithValues { value in
    print("收到值: \(value)")
}

2.3 Observer(观察者)

Observer 是接收 Signal 事件的类型。

final class Observer<Value, Error: Swift.Error> {
    func send(value: Value)
    func send(error: Error)
    func sendCompleted()
    func sendInterrupted()
}

示例:

let (signal, observer) = Signal<Int, Never>.pipe()

signal.observe { event in
    switch event {
    case .value(let value):
        print("值: \(value)")
    case .completed:
        print("完成")
    case .failed(let error):
        print("错误: \(error)")
    case .interrupted:
        print("中断")
    }
}

observer.send(value: 1)
observer.send(value: 2)
observer.sendCompleted()

2.4 Disposable(可释放资源)

Disposable 表示订阅关系,用于取消订阅和释放资源。

protocol Disposable {
    func dispose()
}

CompositeDisposable:

let disposable = CompositeDisposable()

disposable += signal.observeValues { value in
    print(value)
}

disposable += anotherSignal.observeValues { value in
    print(value)
}

// 释放所有订阅
disposable.dispose()

3. Signal与SignalProducer

3.1 Signal创建方式

pipe

创建 Signal 和 Observer。

let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { print($0) }
observer.send(value: "Hello")
never

创建永不发出事件的 Signal。

let signal = Signal<Int, Never>.never()
signal.observeValues { print($0) }  // 永远不会执行
empty

创建立即完成的 Signal。

let signal = Signal<Int, Never>.empty()
signal.observeCompleted { print("完成") }
failed

创建立即失败的 Signal。

enum MyError: Error {
    case failure
}

let signal = Signal<Int, MyError>.failed(.failure)
signal.observeFailed { print("错误: \($0)") }

3.2 SignalProducer创建方式

init

使用闭包创建 SignalProducer。

let producer = SignalProducer<String, Never> { observer, lifetime in
    observer.send(value: "A")
    observer.send(value: "B")
    observer.sendCompleted()
}

producer.startWithValues { print($0) }
value

创建发出单个值的 SignalProducer。

let producer = SignalProducer<String, Never>(value: "Hello")
producer.startWithValues { print($0) }
values

从序列创建 SignalProducer。

let producer = SignalProducer<String, Never>(values: ["A", "B", "C"])
producer.startWithValues { print($0) }
error

创建立即失败的 SignalProducer。

let producer = SignalProducer<Int, MyError>(error: .failure)
producer.startWithFailed { print("错误: \($0)") }
empty

创建立即完成的 SignalProducer。

let producer = SignalProducer<Int, Never>.empty
producer.startWithCompleted { print("完成") }
never

创建永不发出事件的 SignalProducer。

let producer = SignalProducer<Int, Never>.never
producer.startWithValues { print($0) }  // 永远不会执行

3.3 Signal vs SignalProducer

Signal(热信号):

  • 立即开始发送事件
  • 多个观察者共享同一个事件流
  • 适合表示已经发生的事件

SignalProducer(冷信号):

  • 延迟创建,只有在订阅时才开始
  • 每个观察者获得独立的事件流
  • 适合表示异步操作

转换:

// SignalProducer -> Signal
let producer = SignalProducer<String, Never>(value: "Hello")
let signal = producer.promoteToSignal()

// Signal -> SignalProducer
let (signal, observer) = Signal<String, Never>.pipe()
let producer = SignalProducer(signal)

4. Property与MutableProperty

4.1 Property

Property 是不可变的状态容器,表示一个随时间变化的值。

protocol PropertyProtocol {
    associatedtype Value
    
    var value: Value { get }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
}

特点:

  • 只读属性
  • 提供当前值
  • 提供 Signal 和 SignalProducer 观察变化

示例:

let property = Property(value: "初始值")

// 获取当前值
print(property.value)  // 输出: 初始值

// 观察变化
property.signal.observeValues { value in
    print("值变化: \(value)")
}

4.2 MutableProperty

MutableProperty 是可变的状态容器。

final class MutableProperty<Value>: MutablePropertyProtocol {
    var value: Value { get set }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
    
    init(_ value: Value)
}

特点:

  • 可读写属性
  • 修改值时会发出事件
  • 新观察者会立即收到当前值

示例:

let property = MutableProperty("初始值")

// 观察变化
property.signal.observeValues { value in
    print("值变化: \(value)")
}
// 立即输出: 值变化: 初始值

// 修改值
property.value = "新值"  // 输出: 值变化: 新值
property.value = "另一个值"  // 输出: 值变化: 另一个值

4.3 Property绑定

双向绑定:

let property1 = MutableProperty("")
let property2 = MutableProperty("")

// 双向绑定
property1 <~ property2
property2 <~ property1

property1.value = "Hello"  // property2.value 也变为 "Hello"
property2.value = "World"  // property1.value 也变为 "World"

单向绑定:

let source = MutableProperty("源")
let target = MutableProperty("目标")

// 单向绑定:source -> target
target <~ source

source.value = "新值"  // target.value 也变为 "新值"
target.value = "修改"  // source.value 不变

5. Action

Action 是 ReactiveCocoa 特有的类型,用于处理用户交互和异步操作。

5.1 Action基本使用

let action = Action<String, String, Never> { input in
    return SignalProducer { observer, lifetime in
        // 执行异步操作
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            observer.send(value: "结果: \(input)")
            observer.sendCompleted()
        }
    }
}

// 执行 Action
action.apply("输入").startWithValues { result in
    print(result)  // 输出: 结果: 输入
}

5.2 Action状态

Action 提供多个状态 Signal:

let action = Action<String, String, Never> { input in
    return SignalProducer(value: "结果: \(input)")
}

// 观察执行状态
action.isExecuting.signal.observeValues { isExecuting in
    print("执行中: \(isExecuting)")
}

// 观察值
action.values.observeValues { value in
    print("值: \(value)")
}

// 观察错误
action.errors.observeValues { error in
    print("错误: \(error)")
}

// 执行
action.apply("输入").start()

5.3 Action与UIButton绑定

let action = Action<Void, String, Never> {
    return SignalProducer(value: "按钮点击")
}

// 绑定到按钮
button.reactive.pressed = CocoaAction(action) { _ in }

// 观察结果
action.values.observeValues { result in
    print(result)
}

6. Operators操作符

6.1 转换操作符

map

转换每个值。

SignalProducer(values: [1, 2, 3])
    .map { $0 * 2 }
    .startWithValues { print($0) }
// 输出: 2, 4, 6
flatMap

将 Signal 发出的值转换为 SignalProducer,然后合并。

SignalProducer(values: ["A", "B", "C"])
    .flatMap(.latest) { letter in
        SignalProducer(values: [1, 2]).map { "\(letter)\($0)" }
    }
    .startWithValues { print($0) }
// 输出: A1, A2, B1, B2, C1, C2
scan

累积值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .scan(0, +)
    .startWithValues { print($0) }
// 输出: 1, 3, 6, 10, 15

6.2 过滤操作符

filter

过滤值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .filter { $0 % 2 == 0 }
    .startWithValues { print($0) }
// 输出: 2, 4
skip

跳过前几个值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .skip(first: 2)
    .startWithValues { print($0) }
// 输出: 3, 4, 5
take

获取前几个值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .take(first: 3)
    .startWithValues { print($0) }
// 输出: 1, 2, 3
distinctUntilChanged

移除连续重复的值。

SignalProducer(values: [1, 1, 2, 2, 3, 3])
    .distinctUntilChanged()
    .startWithValues { print($0) }
// 输出: 1, 2, 3

6.3 组合操作符

combineLatest

组合多个 Signal 的最新值。

let (signal1, observer1) = Signal<String, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.combineLatest(with: signal2)
    .observeValues { value1, value2 in
        print("\(value1): \(value2)")
    }

observer1.send(value: "A")  // 无输出(等待 signal2)
observer2.send(value: 1)    // 输出: A: 1
observer1.send(value: "B")  // 输出: B: 1
observer2.send(value: 2)    // 输出: B: 2
merge

合并多个 Signal。

let (signal1, observer1) = Signal<Int, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.merge(with: signal2)
    .observeValues { print($0) }

observer1.send(value: 1)  // 输出: 1
observer2.send(value: 2)  // 输出: 2
observer1.send(value: 3)  // 输出: 3
zip

按顺序组合多个 Signal。

let (signal1, observer1) = Signal<String, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.zip(with: signal2)
    .observeValues { value1, value2 in
        print("\(value1): \(value2)")
    }

observer1.send(value: "A")  // 等待 signal2
observer1.send(value: "B")  // 等待 signal2
observer2.send(value: 1)    // 输出: A: 1
observer2.send(value: 2)    // 输出: B: 2

6.4 时间操作符

debounce

防抖,等待指定时间后发出最新值。

let (signal, observer) = Signal<String, Never>.pipe()

signal.debounce(0.5, on: QueueScheduler.main)
    .observeValues { print($0) }

observer.send(value: "H")     // 不输出
observer.send(value: "He")    // 不输出
observer.send(value: "Hel")   // 不输出
observer.send(value: "Hell")  // 不输出
observer.send(value: "Hello") // 0.5秒后输出: Hello
throttle

节流,在指定时间间隔内只发出第一个值。

let (signal, observer) = Signal<String, Never>.pipe()

signal.throttle(1.0, on: QueueScheduler.main)
    .observeValues { print($0) }

observer.send(value: "A")  // 立即输出: A
observer.send(value: "B")  // 不输出(1秒内)
observer.send(value: "C")  // 不输出(1秒内)
// 1秒后
observer.send(value: "D")  // 输出: D
delay

延迟发出值。

SignalProducer(values: [1, 2, 3])
    .delay(1.0, on: QueueScheduler.main)
    .startWithValues { print($0) }
// 1秒后依次输出: 1, 2, 3

7. Schedulers调度器

7.1 内置Scheduler

QueueScheduler

队列调度器。

// 主队列
let mainScheduler = QueueScheduler.main

// 后台队列
let backgroundScheduler = QueueScheduler(
    qos: .background,
    name: "background.queue"
)

SignalProducer(value: 1)
    .start(on: backgroundScheduler)
    .observe(on: mainScheduler)
    .startWithValues { value in
        print(Thread.isMainThread)  // true
    }
UIScheduler

UI 调度器(主线程)。

let uiScheduler = UIScheduler()

SignalProducer(value: 1)
    .observe(on: uiScheduler)
    .startWithValues { value in
        print(Thread.isMainThread)  // true
    }

7.2 start vs observe

  • start:指定 SignalProducer 在哪个调度器上开始执行
  • observe:指定观察者在哪个调度器上接收事件
SignalProducer { observer, lifetime in
    print("执行线程: \(Thread.current)")
    observer.send(value: 1)
    observer.sendCompleted())
}
.start(on: QueueScheduler(qos: .background))
.observe(on: UIScheduler())
.startWithValues { value in
    print("接收线程: \(Thread.current)")
}

8. 错误处理

8.1 错误类型

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

8.2 错误处理操作符

catch

捕获错误并返回备用 SignalProducer。

SignalProducer<String, NetworkError>(error: .noData)
    .catch { error -> SignalProducer<String, Never> in
        print("捕获错误: \(error)")
        return SignalProducer(value: "备用值")
    }
    .startWithValues { print($0) }
retry

重试失败的 SignalProducer。

var attempts = 0

SignalProducer<String, NetworkError> { observer, lifetime in
    attempts += 1
    if attempts < 3 {
        observer.send(error: .noData)
    } else {
        observer.send(value: "成功")
        observer.sendCompleted()
    }
}
.retry(upTo: 2)  // 最多重试 2 次
.start(
    value: { print($0) },
    failed: { print("错误: \($0)") }
)
flatMapError

将错误转换为值。

SignalProducer<String, NetworkError>(error: .noData)
    .flatMapError { error in
        SignalProducer(value: "错误: \(error)")
    }
    .startWithValues { print($0) }

9. 内存管理

9.1 Lifetime

Lifetime 用于管理 SignalProducer 的生命周期。

let producer = SignalProducer<String, Never> { observer, lifetime in
    let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        observer.send(value: "Tick")
    }
    
    lifetime.observeEnded {
        timer.invalidate()
    }
}

let disposable = producer.startWithValues { print($0) }

// 取消订阅时,timer 会自动失效
disposable.dispose()

9.2 避免循环引用

class ViewModel {
    private let property = MutableProperty("")
    
    func setup() {
        property.signal.observeValues { [weak self] value in
            self?.process(value)
        }
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
}

10. 与UIKit集成

10.1 Reactive扩展

ReactiveCocoa 为 UIKit 控件提供了 Reactive 扩展。

import ReactiveSwift
import ReactiveCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 文本输入绑定
        label.reactive.text <~ textField.reactive.continuousTextValues
        
        // 按钮点击
        button.reactive.pressed = CocoaAction(Action { [weak self] _ in
            return SignalProducer(value: "按钮点击")
        })
    }
}

10.2 常用绑定

// UILabel
label.reactive.text <~ property.producer.map { $0 }

// UITextField
textField.reactive.text <~ property.producer.map { $0 }
property <~ textField.reactive.continuousTextValues

// UIButton
button.reactive.pressed = CocoaAction(action)

// UISwitch
switch.reactive.isOn <~ property.producer.map { $0 }
property <~ switch.reactive.isOnValues

// UISlider
slider.reactive.value <~ property.producer.map { Float($0) }
property <~ slider.reactive.values.map { Int($0) }

11. 实际应用场景

11.1 网络请求

struct API {
    static func fetchUser(id: Int) -> SignalProducer<User, NetworkError> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.reactive.data(with: URLRequest(url: url))
            .attemptMap { data, _ in
                try JSONDecoder().decode(User.self, from: data)
            }
            .observe(on: UIScheduler())
    }
}

API.fetchUser(id: 1)
    .start(
        value: { user in
            print("用户: \(user)")
        },
        failed: { error in
            print("错误: \(error)")
        }
    )

11.2 用户输入处理

class SearchViewModel {
    let searchText = MutableProperty("")
    let results = MutableProperty<[String]>([])
    
    init() {
        results <~ searchText.producer
            .debounce(0.5, on: QueueScheduler.main)
            .skipRepeats()
            .filter { !$0.isEmpty }
            .flatMap(.latest) { query -> SignalProducer<[String], Never> in
                return self.search(query: query)
                    .flatMapError { _ in SignalProducer(value: []) }
            }
    }
    
    private func search(query: String) -> SignalProducer<[String], NetworkError> {
        // 实现搜索逻辑
        return SignalProducer(value: ["结果1", "结果2"])
    }
}

二、ReactiveCocoa框架源码解析

1. 架构设计

1.1 整体架构

ReactiveCocoa 采用协议导向的设计,核心是 Signal 和 SignalProducer。

Signal (热信号)
    ↓
Observer
    ↓
Event (value/failed/completed/interrupted)

SignalProducer (冷信号)
    ↓
Observer
    ↓
Signal

1.2 核心协议层次

// Signal 协议
protocol SignalProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func observe(_ observer: Observer<Value, Error>) -> Disposable?
}

// SignalProducer 协议
protocol SignalProducerProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func start(_ observer: Observer<Value, Error>) -> Disposable
}

2. Signal实现原理

2.1 Signal类实现

public final class Signal<Value, Error: Swift.Error>: SignalProtocol {
    private let generator: (Observer<Value, Error>) -> Disposable?
    private var observers: Bag<Observer<Value, Error>> = Bag()
    private let lock = NSRecursiveLock()
    
    public init(_ generator: @escaping (Observer<Value, Error>) -> Disposable?) {
        self.generator = generator
    }
    
    public func observe(_ observer: Observer<Value, Error>) -> Disposable? {
        lock.lock()
        defer { lock.unlock() }
        
        let token = observers.insert(observer)
        
        // 如果是第一个观察者,开始生成事件
        if observers.count == 1 {
            let disposable = generator(Observer { [weak self] event in
                self?.send(event)
            })
            
            return CompositeDisposable(
                disposable,
                Disposable { [weak self] in
                    self?.lock.lock()
                    self?.observers.remove(using: token)
                    self?.lock.unlock()
                }
            )
        }
        
        return Disposable { [weak self] in
            self?.lock.lock()
            self?.observers.remove(using: token)
            self?.lock.unlock()
        }
    }
    
    private func send(_ event: Event<Value, Error>) {
        lock.lock()
        let currentObservers = observers
        lock.unlock()
        
        for observer in currentObservers {
            observer.send(event)
        }
        
        // 如果是终止事件,清理观察者
        if event.isTerminating {
            lock.lock()
            observers.removeAll()
            lock.unlock()
        }
    }
}

关键点:

  • Signal 是引用类型(class)
  • 使用 Bag 存储多个观察者
  • 使用锁保护共享状态
  • 第一个观察者订阅时开始生成事件

2.2 pipe实现

extension Signal {
    public static func pipe() -> (Signal<Value, Error>, Observer<Value, Error>) {
        let observer = Observer<Value, Error>()
        let signal = Signal<Value, Error> { observer in
            // 将外部 observer 的事件转发给内部 observer
            return observer.observe { event in
                observer.send(event)
            }
        }
        
        return (signal, observer)
    }
}

关键点:

  • pipe 创建 Signal 和 Observer 对
  • Observer 可以手动发送事件
  • 适合将命令式代码转换为响应式代码

3. SignalProducer实现原理

3.1 SignalProducer结构

public struct SignalProducer<Value, Error: Swift.Error>: SignalProducerProtocol {
    private let startHandler: (Observer<Value, Error>, Lifetime) -> Void
    
    public init(_ startHandler: @escaping (Observer<Value, Error>, Lifetime) -> Void) {
        self.startHandler = startHandler
    }
    
    public func start(_ observer: Observer<Value, Error>) -> Disposable {
        let lifetime = Lifetime()
        let compositeDisposable = CompositeDisposable()
        
        lifetime.observeEnded {
            compositeDisposable.dispose()
        }
        
        startHandler(observer, lifetime)
        
        return compositeDisposable
    }
}

关键点:

  • SignalProducer 是值类型(struct)
  • 每次 start 都会创建新的 Signal
  • 使用 Lifetime 管理资源生命周期

3.2 SignalProducer转换

extension SignalProducer {
    public var signal: Signal<Value, Error> {
        return Signal { observer in
            return self.start(observer)
        }
    }
}

extension Signal {
    public var producer: SignalProducer<Value, Error> {
        return SignalProducer { observer, lifetime in
            let disposable = self.observe(observer)
            lifetime.observeEnded {
                disposable?.dispose()
            }
        }
    }
}

关键点:

  • SignalProducer 可以转换为 Signal
  • Signal 可以转换为 SignalProducer
  • 转换是延迟的,不会立即执行

4. Property实现原理

4.1 Property协议

public protocol PropertyProtocol {
    associatedtype Value
    
    var value: Value { get }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
}

4.2 MutableProperty实现

public final class MutableProperty<Value>: MutablePropertyProtocol {
    private let lock = NSRecursiveLock()
    private var _value: Value
    private let observer: Observer<Value, Never>
    private let signal: Signal<Value, Never>
    
    public var value: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _value
        }
        set {
            lock.lock()
            let oldValue = _value
            _value = newValue
            lock.unlock()
            
            if oldValue != newValue {
                observer.send(value: newValue)
            }
        }
    }
    
    public init(_ value: Value) {
        _value = value
        let (signal, observer) = Signal<Value, Never>.pipe()
        self.signal = signal
        self.observer = observer
        
        // 立即发送初始值
        observer.send(value: value)
    }
    
    public var producer: SignalProducer<Value, Never> {
        return SignalProducer { observer, lifetime in
            // 立即发送当前值
            observer.send(value: self.value)
            
            // 观察后续变化
            let disposable = self.signal.observe(observer)
            lifetime.observeEnded {
                disposable?.dispose()
            }
        }
    }
}

关键点:

  • MutableProperty 是引用类型
  • 使用锁保护 _value
  • 值变化时发出事件
  • producer 会立即发送当前值

4.3 绑定操作符实现

infix operator <~ : BindingPrecedence

public func <~ <Source: SignalProducerProtocol, Destination: BindingTargetProtocol>(
    destination: Destination,
    source: Source
) -> Disposable
where Source.Value == Destination.Value, Source.Error == Never {
    return source.startWithValues { value in
        destination.consume(value)
    }
}

public func <~ <Source: SignalProtocol, Destination: BindingTargetProtocol>(
    destination: Destination,
    source: Source
) -> Disposable?
where Source.Value == Destination.Value, Source.Error == Never {
    return source.observeValues { value in
        destination.consume(value)
    }
}

关键点:

  • <~ 操作符实现单向绑定
  • 自动管理订阅生命周期
  • 支持 Signal 和 SignalProducer

5. Action实现原理

5.1 Action结构

public final class Action<Input, Output, Error: Swift.Error> {
    private let executeClosure: (Input) -> SignalProducer<Output, Error>
    private let isEnabledProperty: MutableProperty<Bool>
    private let eventsObserver: Observer<Event<Output, Error>, Never>
    
    public let isEnabled: Property<Bool>
    public let isExecuting: Property<Bool>
    public let values: Signal<Output, Never>
    public let errors: Signal<Error, Never>
    public let events: Signal<Event<Output, Error>, Never>
    
    public init(enabledIf: Property<Bool> = Property(value: true),
                execute: @escaping (Input) -> SignalProducer<Output, Error>) {
        self.executeClosure = execute
        self.isEnabledProperty = MutableProperty(true)
        self.isEnabled = Property(capturing: isEnabledProperty)
        
        let (eventsSignal, eventsObserver) = Signal<Event<Output, Error>, Never>.pipe()
        self.events = eventsSignal
        self.eventsObserver = eventsObserver
        
        self.values = events.map { $0.value }.skipNil()
        self.errors = events.map { $0.error }.skipNil()
        
        let isExecutingProperty = MutableProperty(false)
        self.isExecuting = Property(capturing: isExecutingProperty)
        
        // 监听执行状态
        events.observeValues { event in
            switch event {
            case .value:
                isExecutingProperty.value = true
            case .completed, .failed, .interrupted:
                isExecutingProperty.value = false
            }
        }
    }
    
    public func apply(_ input: Input) -> SignalProducer<Output, Error> {
        return SignalProducer { observer, lifetime in
            guard self.isEnabled.value else {
                observer.sendInterrupted()
                return
            }
            
            let producer = self.executeClosure(input)
            let disposable = producer.start { event in
                self.eventsObserver.send(value: event)
                observer.send(event)
            }
            
            lifetime.observeEnded {
                disposable.dispose()
            }
        }
    }
}

关键点:

  • Action 封装异步操作
  • 提供执行状态(isEnabled、isExecuting)
  • 提供值、错误、事件流
  • 可以禁用 Action

6. Operators实现原理

6.1 map实现

extension SignalProducer {
    public func map<U>(_ transform: @escaping (Value) -> U) -> SignalProducer<U, Error> {
        return SignalProducer { observer, lifetime in
            self.start { event in
                switch event {
                case .value(let value):
                    observer.send(value: transform(value))
                case .failed(let error):
                    observer.send(error: error)
                case .completed:
                    observer.sendCompleted()
                case .interrupted:
                    observer.sendInterrupted()
                }
            }
        }
    }
}

关键点:

  • map 创建新的 SignalProducer
  • 转换每个值事件
  • 保持其他事件不变

6.2 filter实现

extension SignalProducer {
    public func filter(_ predicate: @escaping (Value) -> Bool) -> SignalProducer<Value, Error> {
        return SignalProducer { observer, lifetime in
            self.start { event in
                switch event {
                case .value(let value):
                    if predicate(value) {
                        observer.send(value: value)
                    }
                case .failed(let error):
                    observer.send(error: error)
                case .completed:
                    observer.sendCompleted()
                case .interrupted:
                    observer.sendInterrupted()
                }
            }
        }
    }
}

关键点:

  • filter 创建新的 SignalProducer
  • 只转发满足条件的值
  • 保持其他事件不变

6.3 flatMap实现

extension SignalProducer {
    public func flatMap<U>(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> SignalProducer<U, Error>) -> SignalProducer<U, Error> {
        return SignalProducer { observer, lifetime in
            let flattenProducer = self.map(transform).flatten(strategy)
            let disposable = flattenProducer.start(observer)
            lifetime.observeEnded {
                disposable.dispose()
            }
        }
    }
}

关键点:

  • flatMap 支持多种策略(.latest、.merge、.concat)
  • 管理多个内部 SignalProducer
  • 需要复杂的生命周期管理

7. Schedulers实现原理

7.1 Scheduler协议

public protocol Scheduler {
    func schedule(_ action: @escaping () -> Void) -> Disposable?
    func schedule(after date: Date, action: @escaping () -> Void) -> Disposable?
    func schedule(after date: Date, interval: TimeInterval, action: @escaping () -> Void) -> Disposable?
}

7.2 QueueScheduler实现

public final class QueueScheduler: Scheduler {
    public let queue: DispatchQueue
    
    public init(qos: DispatchQoS = .default, name: String = "org.reactivecocoa.ReactiveSwift.QueueScheduler") {
        self.queue = DispatchQueue(label: name, qos: qos)
    }
    
    public static let main = QueueScheduler(queue: .main, name: "org.reactivecocoa.ReactiveSwift.QueueScheduler.main")
    
    public func schedule(_ action: @escaping () -> Void) -> Disposable? {
        let disposable = SimpleDisposable()
        queue.async {
            if !disposable.isDisposed {
                action()
            }
        }
        return disposable
    }
    
    public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? {
        let disposable = SimpleDisposable()
        let timeInterval = date.timeIntervalSinceNow
        queue.asyncAfter(deadline: .now() + timeInterval) {
            if !disposable.isDisposed {
                action()
            }
        }
        return disposable
    }
}

关键点:

  • QueueScheduler 使用 DispatchQueue
  • 支持立即和延迟调度
  • 支持取消调度

8. 生命周期管理

8.1 Lifetime实现

public final class Lifetime {
    private let token: Token
    private var observers: Bag<() -> Void> = Bag()
    private let lock = NSRecursiveLock()
    
    public init() {
        token = Token()
    }
    
    public func observeEnded(_ action: @escaping () -> Void) {
        lock.lock()
        let isEnded = token.isEnded
        if !isEnded {
            observers.insert(action)
        }
        lock.unlock()
        
        if isEnded {
            action()
        }
    }
    
    deinit {
        token.markEnded()
        lock.lock()
        let currentObservers = observers
        observers.removeAll()
        lock.unlock()
        
        for observer in currentObservers {
            observer()
        }
    }
}

关键点:

  • Lifetime 管理资源生命周期
  • 对象释放时自动执行清理操作
  • 使用 observeEnded 注册清理回调

8.2 Disposable管理

public final class CompositeDisposable: Disposable {
    private var disposables: [Disposable] = []
    private let lock = NSRecursiveLock()
    private var isDisposed = false
    
    public init(_ disposables: Disposable...) {
        self.disposables = disposables
    }
    
    public func add(_ disposable: Disposable?) {
        guard let disposable = disposable else { return }
        
        lock.lock()
        if isDisposed {
            lock.unlock()
            disposable.dispose()
            return
        }
        
        disposables.append(disposable)
        lock.unlock()
    }
    
    public func dispose() {
        lock.lock()
        guard !isDisposed else {
            lock.unlock()
            return
        }
        
        isDisposed = true
        let currentDisposables = disposables
        disposables.removeAll()
        lock.unlock()
        
        for disposable in currentDisposables {
            disposable.dispose()
        }
    }
}

关键点:

  • CompositeDisposable 管理多个 Disposable
  • 线程安全
  • 一次性释放所有资源

9. 性能优化策略

9.1 值类型优化

SignalProducer 是值类型,避免堆分配:

// 值类型,零成本抽象
struct SignalProducer<Value, Error: Swift.Error> { }

9.2 延迟执行

SignalProducer 延迟创建 Signal:

let producer = SignalProducer<String, Never> { observer, lifetime in
    // 只在 start 时执行
    observer.send(value: "Hello")
}

9.3 共享执行

使用 share() 共享 SignalProducer:

let shared = expensiveProducer().share()

shared.startWithValues { }  // 订阅1
shared.startWithValues { }  // 订阅2(共享执行)

9.4 内存优化

  • 使用 weak 引用避免循环引用
  • 使用 Lifetime 自动管理资源
  • 及时释放不需要的订阅

📚 总结

ReactiveCocoa 的核心优势

  1. Property 状态管理:提供 Property 和 MutableProperty 管理状态
  2. Action 模式:提供 Action 处理用户交互和异步操作
  3. 类型安全:充分利用 Swift 类型系统
  4. 生命周期管理:使用 Lifetime 自动管理资源
  5. UIKit 集成:深度集成 UIKit 控件

学习建议

  1. 理解 Signal vs SignalProducer:掌握热信号和冷信号的区别
  2. 理解 Property:掌握状态管理
  3. 理解 Action:掌握用户交互处理
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 ReactiveCocoa

文档版本:v1.0
最后更新:2026年1月15日
参考文献:ReactiveCocoa GitHub Repository, ReactiveSwift Source Code

02-研究优秀开源框架@响应式编程@iOS | Combine框架:源码解析


二、Combine框架源码解析

1. 架构设计

1.1 整体架构

Combine 采用协议导向的设计,核心是三个协议:

Publisher (发布者)
    ↓
Subscription (订阅关系)
    ↓
Subscriber (订阅者)

数据流:

Publisher → Subscription → Subscriber
     ↑                          ↓
     └────────── 反馈 ──────────┘

1.2 核心协议层次

// 第一层:Publisher 协议
protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error
    func receive<S: Subscriber>(subscriber: S)
}

// 第二层:Subscription 协议
protocol Subscription: Cancellable {
    func request(_ demand: Subscribers.Demand)
}

// 第三层:Subscriber 协议
protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

1.3 内部架构分层(三层视图)

Combine 从内到外可以理解为协议层 → 实现层 → 调度层,三者共同决定「谁在何时、何地、以何种方式」传递事件。

架构分层示意:

┌─────────────────────────────────────────────────────────────────────────────┐
│ 调度层 (Scheduler)                                                           │
│  · 决定事件在哪个线程/队列执行                                                 │
│  · subscribe(on:) / receive(on:) / 时间类操作符(debounce, delay) 依赖调度器     │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 实现层 (Concrete Types)                                                       │
│  · Just / Future / PassthroughSubject / Publishers.Map / Sink / Assign ...   │
│  · 每个操作符 = 新 Publisher + 中间 Subscriber,形成链式实现                    │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 协议层 (Protocols)                                                            │
│  · Publisher:定义「可被订阅」的契约                                           │
│  · Subscription:定义「请求/取消」的契约                                       │
│  · Subscriber:定义「接收值/完成」的契约                                       │
└─────────────────────────────────────────────────────────────────────────────┘
  • 协议层:只规定接口(Output/Failure、receive(subscription/input/completion)、request(demand)),不关心具体类型。
  • 实现层:所有 JustMapFilterSink 等具体类型都遵循上述协议,并通过「包装上游 + 向下游转发」组成链条。
  • 调度层:由 Scheduler 协议抽象(如 DispatchQueueRunLoop),操作符在需要时把回调投递到指定调度器执行,从而控制线程与时机。

1.4 响应者链(订阅链)

一次 publisher.map(...).filter(...).sink(...) 会在内部形成一条从上游到下游的订阅链:每一环都是一个 Publisher,下游订阅上游,最末端是真正的 Subscriber(如 Sink)。值沿这条链自上而下传递,Demand 可自下而上反馈。

响应者链结构图:

  [上游]          [操作符]           [操作符]          [终端]
   Just    →    Map<Int,String>  →  Filter<String>  →   Sink
    │                 │                    │               │
    │  subscribe      │  subscribe         │  subscribe    │
    │ ◄───────────────┼────────────────────┼───────────────┤
    │                 │                    │               │
    │  receive(S)     │  receive(S)        │  receive(S)   │
    │  创建 Subscription                    │               │
    │  向下游传 subscription                │               │
    │                 │  request(demand)   │               │
    │                 │ ◄──────────────────┼───────────────┤
    │  receive(1)     │  receive("1")       │  receive("1") │
    │                 │  receive(2)        │  (若通过)     │
    │                 │  receive("2")      │  receive("2") │
    │                 │  ...               │  ...          │
    │  receive(.finished)                   │               │
    │                 │  receive(completion)                │
    │                 │                    │  receive(completion)
    │                 │                    │               │
    ▼                 ▼                    ▼               ▼

要点:

  • 谁是谁的上游/下游:例如 Just(1).map { "\($0)" } 中,Just 是上游,Publishers.Map<Just<Int>, String> 是下游;.sink 时,Sink 是整条链的最终下游。
  • 订阅方向:下游调用 upstream.receive(subscriber: self),即「下游作为 Subscriber 被上游接收」,从而建立订阅。
  • 值传递方向:上游通过 subscriber.receive(value) 把值交给下游;若下游是另一个操作符的包装 Subscriber,该 Subscriber 会做变换后再调用自己的下游的 receive,形成链式传递。
  • Demand 反馈receive(_ input:) 返回 Subscribers.Demand,上游(或中间层)根据该返回值决定是否继续发送、发送多少,实现背压。

1.5 信息流流转(从订阅到结束)

从调用 subscribe(如 .sink(...))到收到完成,整条链上的调用顺序是固定的,可归纳为建立订阅 → 请求 Demand → 多次下发值 → 下发完成

阶段一:建立订阅(自上而下)

  sink(...) 被调用
       │
       ▼
  Sink 作为 Subscriber 被传给最下游 Publisher(如 Filter)
       │
       ▼
  Filter.receive(subscriber: Sink)  →  创建 FilterSubscriber,包装 Sink
       │
       ▼
  FilterSubscriber 作为 Subscriber 被传给上游(Map)
       │
       ▼
  Map.receive(subscriber: FilterSubscriber)  →  创建 MapSubscriber,包装 FilterSubscriber
       │
       ▼
  MapSubscriber 作为 Subscriber 被传给上游(Just)
       │
       ▼
  Just.receive(subscriber: MapSubscriber)  →  创建 Subscription(如 SimpleSubscription)
       │
       ▼
  subscriber.receive(subscription:)  从 Just 一路向下传递到 Sink
       │
       ▼
  Sink 保存 subscription,并调用 subscription.request(.unlimited)  [或 .max(n)]

阶段二:请求与下发(上游 → 下游)

  Subscription.request(demand)  [由 Sink 发起]
       │
       ▼
  上游(如 Just)开始向 MapSubscriber 发送值:subscriber.receive(1)
       │
       ▼
  MapSubscriber.receive(1)  →  transform(1)  →  downstream.receive("1")
       │
       ▼
  FilterSubscriber.receive("1")  →  若通过,downstream.receive("1");否则 return .max(1)
       │
       ▼
  Sink.receive("1")  →  执行 sink 的 receiveValue 闭包;返回 .none 或新 Demand
       │
       ▼
  (可选)Demand 沿链返回,上游据此决定是否继续 send

阶段三:完成

  上游发送 subscriber.receive(completion: .finished) 或 .failure(e)
       │
       ▼
  沿链向下传递 completion,每一层收到后转发给 downstream
       │
       ▼
  Sink.receive(completion:)  →  执行 receiveCompletion 闭包;置空 subscription
       │
       ▼
  订阅结束,链上各层可释放资源

信息流总览图(时序):

  Subscriber (Sink)               中间层 (Map/Filter)              Publisher (Just)
        │                                  │                              │
        │  receive(subscriber:)            │                              │
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │  receive(subscriber:)        │
        │                                  │ ◄────────────────────────────┤
        │                                  │                              │
        │  receive(subscription:)         │  receive(subscription:)      │  create
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │                              │
        │  request(.unlimited)            │  request(...)                 │
        │ ─────────────────────────────────────────────────────────────► │
        │                                  │                              │
        │  receive(1)                      │  receive(1) → "1"            │  send 1
        │ ◄─────────────────────────────────────────────────────────────┤
        │  receive("1")  [若经 Map]        │                              │
        │ ◄─────────────────────────────────────────────────────────────┤
        │  receive(completion:)            │  receive(completion:)        │  send completion
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │                              │

1.6 核心协议关系小结

角色 职责 在链中的位置
Publisher 提供 receive(subscriber:),被订阅时创建 Subscription 并下发给 Subscriber 链中每一环(含操作符)都是 Publisher
Subscription 响应 request(_ demand) 向上游要数据;实现 cancel() 结束订阅 通常由最上游(如 Just)创建,引用传给下游
Subscriber 接收 receive(subscription:)receive(_ input:)receive(completion:);通过返回值反馈 Demand 链中每一环的「下游」都是 Subscriber;终端是 Sink/Assign

理解上述内部架构、响应者链、信息流后,再看任意操作符的源码,都可以套用「新 Publisher 包装上游 + 新 Subscriber 包装下游,在 receive(_ input:) 里做变换再转发」这一模式。

Mermaid 数据流图(可选渲染):

sequenceDiagram
    participant S as Sink(Subscriber)
    participant F as Filter
    participant M as Map
    participant J as Just(Publisher)

    S->>F: receive(subscriber: S)
    F->>M: receive(subscriber: FilterSub)
    M->>J: receive(subscriber: MapSub)
    J->>M: receive(subscription)
    M->>F: receive(subscription)
    F->>S: receive(subscription)
    S->>S: subscription.request(.unlimited)
    S->>F: request 向上传递
    F->>M: request
    M->>J: request
    J->>M: receive(1)
    M->>F: receive("1")
    F->>S: receive("1")
    J->>M: receive(completion)
    M->>F: receive(completion)
    F->>S: receive(completion)

2. Publisher协议实现

2.1 Publisher协议定义

public protocol Publisher {
    /// 发布的值类型
    associatedtype Output
    
    /// 错误类型
    associatedtype Failure: Error
    
    /// 接收订阅者
    func receive<S>(subscriber: S) 
        where S: Subscriber, 
              S.Input == Output, 
              S.Failure == Failure
}

2.2 Just实现分析

public struct Just<Output>: Publisher {
    public typealias Failure = Never
    
    public let output: Output
    
    public init(_ output: Output) {
        self.output = output
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Never {
        // 创建订阅
        let subscription = Subscriptions.SimpleSubscription(
            subscriber: subscriber,
            output: output
        )
        subscriber.receive(subscription: subscription)
    }
}

关键点:

  • Just 是值类型(struct)
  • 立即发布值并完成
  • 错误类型是 Never(不会失败)

2.3 Future实现分析

public struct Future<Output, Failure: Error>: Publisher {
    public typealias Output = Output
    public typealias Failure = Failure
    
    private let promise: (@escaping (Result<Output, Failure>) -> Void) -> Void
    
    public init(_ attemptToFulfill: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void) {
        self.promise = attemptToFulfill
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        let subscription = FutureSubscription(
            subscriber: subscriber,
            promise: promise
        )
        subscriber.receive(subscription: subscription)
    }
}

private final class FutureSubscription<Output, Failure: Error, S: Subscriber>: Subscription 
    where S.Input == Output, S.Failure == Failure {
    
    private var subscriber: S?
    private let promise: (@escaping (Result<Output, Failure>) -> Void) -> Void
    private var hasFulfilled = false
    
    init(subscriber: S, promise: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void) {
        self.subscriber = subscriber
        self.promise = promise
    }
    
    func request(_ demand: Subscribers.Demand) {
        guard !hasFulfilled else { return }
        hasFulfilled = true
        
        promise { [weak self] result in
            guard let self = self, let subscriber = self.subscriber else { return }
            
            switch result {
            case .success(let value):
                _ = subscriber.receive(value)
                subscriber.receive(completion: .finished)
            case .failure(let error):
                subscriber.receive(completion: .failure(error))
            }
            
            self.subscriber = nil
        }
    }
    
    func cancel() {
        subscriber = nil
    }
}

关键点:

  • Future 是值类型,但内部使用引用类型 FutureSubscription
  • 只执行一次 promise
  • 使用 hasFulfilled 防止重复执行

3. Subscriber协议实现

3.1 Subscriber协议定义

public protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

3.2 Sink实现分析

public struct Sink<Input, Failure: Error>: Subscriber, Cancellable {
    public typealias Input = Input
    public typealias Failure = Failure
    
    private let receiveValue: (Input) -> Void
    private let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
    private var subscription: Subscription?
    
    public init(
        receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
        receiveValue: @escaping (Input) -> Void
    ) {
        self.receiveCompletion = receiveCompletion
        self.receiveValue = receiveValue
    }
    
    public func receive(subscription: Subscription) {
        self.subscription = subscription
        subscription.request(.unlimited)  // 请求无限值
    }
    
    public func receive(_ input: Input) -> Subscribers.Demand {
        receiveValue(input)
        return .none  // 不再请求更多值(因为已经请求了 .unlimited)
    }
    
    public func receive(completion: Subscribers.Completion<Failure>) {
        receiveCompletion(completion)
        subscription = nil
    }
    
    public func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}

关键点:

  • Sink 是值类型,但内部持有 Subscription 引用
  • 默认请求 .unlimited
  • 完成或取消时清理 subscription

3.3 Assign实现分析

public struct Assign<Root, Input>: Subscriber, Cancellable {
    public typealias Input = Input
    public typealias Failure = Never
    
    public let object: Root
    public let keyPath: ReferenceWritableKeyPath<Root, Input>
    private var subscription: Subscription?
    
    public init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>) {
        self.object = object
        self.keyPath = keyPath
    }
    
    public func receive(subscription: Subscription) {
        self.subscription = subscription
        subscription.request(.unlimited)
    }
    
    public func receive(_ input: Input) -> Subscribers.Demand {
        object[keyPath: keyPath] = input
        return .none
    }
    
    public func receive(completion: Subscribers.Completion<Never>) {
        subscription = nil
    }
    
    public func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}

关键点:

  • 使用 ReferenceWritableKeyPath 修改对象属性
  • 错误类型是 Never(不会失败)

4. Operators实现原理

4.1 Map操作符实现

extension Publisher {
    public func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {
        return Publishers.Map(upstream: self, transform: transform)
    }
}

extension Publishers {
    public struct Map<Upstream: Publisher, Output>: Publisher {
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let transform: (Upstream.Output) -> Output
        
        public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output) {
            self.upstream = upstream
            self.transform = transform
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let mapSubscriber = MapSubscriber(
                downstream: subscriber,
                transform: transform
            )
            upstream.receive(subscriber: mapSubscriber)
        }
    }
    
    private struct MapSubscriber<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        let downstream: Downstream
        let transform: (Upstream.Output) -> Downstream.Input
        
        func receive(subscription: Subscription) {
            downstream.receive(subscription: subscription)
        }
        
        func receive(_ input: Upstream.Output) -> Subscribers.Demand {
            let transformed = transform(input)
            return downstream.receive(transformed)
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            downstream.receive(completion: completion)
        }
    }
}

关键点:

  • Map 是新的 Publisher,包装上游 Publisher
  • 创建中间 Subscriber 进行转换
  • 保持错误类型不变

4.2 Filter操作符实现

extension Publisher {
    public func filter(_ predicate: @escaping (Output) -> Bool) -> Publishers.Filter<Self> {
        return Publishers.Filter(upstream: self, predicate: predicate)
    }
}

extension Publishers {
    public struct Filter<Upstream: Publisher>: Publisher {
        public typealias Output = Upstream.Output
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let predicate: (Output) -> Bool
        
        public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
            self.upstream = upstream
            self.predicate = predicate
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let filterSubscriber = FilterSubscriber(
                downstream: subscriber,
                predicate: predicate
            )
            upstream.receive(subscriber: filterSubscriber)
        }
    }
    
    private struct FilterSubscriber<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        let downstream: Downstream
        let predicate: (Input) -> Bool
        
        func receive(subscription: Subscription) {
            downstream.receive(subscription: subscription)
        }
        
        func receive(_ input: Input) -> Subscribers.Demand {
            if predicate(input) {
                return downstream.receive(input)
            } else {
                return .max(1)  // 请求下一个值
            }
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            downstream.receive(completion: completion)
        }
    }
}

关键点:

  • 不满足条件时返回 .max(1) 继续请求
  • 满足条件时才传递给下游

4.3 FlatMap操作符实现

extension Publisher {
    public func flatMap<T, P: Publisher>(
        maxPublishers: Subscribers.Demand = .unlimited,
        _ transform: @escaping (Output) -> P
    ) -> Publishers.FlatMap<P, Self> 
        where P.Failure == Failure {
        return Publishers.FlatMap(
            upstream: self,
            maxPublishers: maxPublishers,
            transform: transform
        )
    }
}

extension Publishers {
    public struct FlatMap<NewPublisher: Publisher, Upstream: Publisher>: Publisher 
        where NewPublisher.Failure == Upstream.Failure {
        
        public typealias Output = NewPublisher.Output
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let maxPublishers: Subscribers.Demand
        public let transform: (Upstream.Output) -> NewPublisher
        
        public init(
            upstream: Upstream,
            maxPublishers: Subscribers.Demand,
            transform: @escaping (Upstream.Output) -> NewPublisher
        ) {
            self.upstream = upstream
            self.maxPublishers = maxPublishers
            self.transform = transform
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let flatMapSubscriber = FlatMapSubscriber(
                downstream: subscriber,
                maxPublishers: maxPublishers,
                transform: transform
            )
            upstream.receive(subscriber: flatMapSubscriber)
        }
    }
    
    private final class FlatMapSubscriber<Upstream: Publisher, NewPublisher: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        private let downstream: Downstream
        private let maxPublishers: Subscribers.Demand
        private let transform: (Input) -> NewPublisher
        private var activeSubscriptions: [AnyCancellable] = []
        private var subscription: Subscription?
        private var demand: Subscribers.Demand = .none
        
        init(
            downstream: Downstream,
            maxPublishers: Subscribers.Demand,
            transform: @escaping (Input) -> NewPublisher
        ) {
            self.downstream = downstream
            self.maxPublishers = maxPublishers
            self.transform = transform
        }
        
        func receive(subscription: Subscription) {
            self.subscription = subscription
            downstream.receive(subscription: InnerSubscription(parent: self))
        }
        
        func receive(_ input: Input) -> Subscribers.Demand {
            let newPublisher = transform(input)
            let cancellable = newPublisher.sink(
                receiveCompletion: { [weak self] completion in
                    self?.handleCompletion(completion)
                },
                receiveValue: { [weak self] value in
                    _ = self?.downstream.receive(value)
                }
            )
            activeSubscriptions.append(cancellable)
            return .none
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            // 处理完成
        }
        
        private func handleCompletion(_ completion: Subscribers.Completion<Failure>) {
            // 处理内部 Publisher 完成
        }
    }
}

关键点:

  • 管理多个内部 Publisher 订阅
  • 使用 maxPublishers 限制并发数
  • 需要复杂的生命周期管理

5. Subjects实现原理

5.1 PassthroughSubject实现

public final class PassthroughSubject<Output, Failure: Error>: Subject {
    private var subscribers: [AnySubscriber<Output, Failure>] = []
    private let lock = NSRecursiveLock()
    
    public func send(_ value: Output) {
        lock.lock()
        defer { lock.unlock() }
        
        let currentSubscribers = subscribers
        for subscriber in currentSubscribers {
            _ = subscriber.receive(value)
        }
    }
    
    public func send(completion: Subscribers.Completion<Failure>) {
        lock.lock()
        defer { lock.unlock() }
        
        let currentSubscribers = subscribers
        subscribers.removeAll()
        
        for subscriber in currentSubscribers {
            subscriber.receive(completion: completion)
        }
    }
    
    public func send(subscription: Subscription) {
        // 实现 Subject 协议
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        lock.lock()
        defer { lock.unlock() }
        
        let anySubscriber = AnySubscriber(subscriber)
        subscribers.append(anySubscriber)
        
        subscriber.receive(subscription: PassthroughSubscription(
            subject: self,
            subscriber: anySubscriber
        ))
    }
    
    private func removeSubscriber(_ subscriber: AnySubscriber<Output, Failure>) {
        lock.lock()
        defer { lock.unlock() }
        
        subscribers.removeAll { $0 === subscriber }
    }
}

private final class PassthroughSubscription<Output, Failure: Error>: Subscription {
    weak var subject: PassthroughSubject<Output, Failure>?
    let subscriber: AnySubscriber<Output, Failure>
    var demand: Subscribers.Demand = .none
    
    init(
        subject: PassthroughSubject<Output, Failure>,
        subscriber: AnySubscriber<Output, Failure>
    ) {
        self.subject = subject
        self.subscriber = subscriber
    }
    
    func request(_ demand: Subscribers.Demand) {
        self.demand += demand
    }
    
    func cancel() {
        subject?.removeSubscriber(subscriber)
        subject = nil
    }
}

关键点:

  • 使用锁保护 subscribers 数组
  • 不保存当前值,新订阅者不会收到历史值
  • 使用 weak 引用避免循环引用

5.2 CurrentValueSubject实现

public final class CurrentValueSubject<Output, Failure: Error>: Subject {
    private var subscribers: [AnySubscriber<Output, Failure>] = []
    private let lock = NSRecursiveLock()
    private var _value: Output
    
    public var value: Output {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _value
        }
        set {
            send(newValue)
        }
    }
    
    public init(_ value: Output) {
        self._value = value
    }
    
    public func send(_ value: Output) {
        lock.lock()
        defer { lock.unlock() }
        
        _value = value
        let currentSubscribers = subscribers
        for subscriber in currentSubscribers {
            _ = subscriber.receive(value)
        }
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        lock.lock()
        defer { lock.unlock() }
        
        let anySubscriber = AnySubscriber(subscriber)
        subscribers.append(anySubscriber)
        
        subscriber.receive(subscription: CurrentValueSubscription(
            subject: self,
            subscriber: anySubscriber
        ))
        
        // 立即发送当前值
        _ = subscriber.receive(_value)
    }
}

关键点:

  • 保存当前值 _value
  • 新订阅者立即收到当前值
  • 使用锁保护状态

6. Schedulers实现原理

6.1 Scheduler协议

public protocol Scheduler {
    associatedtype SchedulerTimeType: Strideable where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
    associatedtype SchedulerOptions
    
    var now: SchedulerTimeType { get }
    var minimumTolerance: SchedulerTimeType.Stride { get }
    
    func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
    func schedule(
        after date: SchedulerTimeType,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    )
    func schedule(
        after date: SchedulerTimeType,
        interval: SchedulerTimeType.Stride,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    ) -> Cancellable
}

6.2 DispatchQueue Scheduler实现

extension DispatchQueue: Scheduler {
    public struct SchedulerOptions {
        public var qos: DispatchQoS
        public var flags: DispatchWorkItemFlags
        public var group: DispatchGroup?
    }
    
    public struct SchedulerTimeType: Strideable {
        public let dispatchTime: DispatchTime
        
        public func distance(to other: SchedulerTimeType) -> Stride {
            return Stride(dispatchTime.uptimeNanoseconds - other.dispatchTime.uptimeNanoseconds)
        }
        
        public func advanced(by n: Stride) -> SchedulerTimeType {
            return SchedulerTimeType(
                dispatchTime: DispatchTime(uptimeNanoseconds: dispatchTime.uptimeNanoseconds + n.magnitude)
            )
        }
    }
    
    public var now: SchedulerTimeType {
        return SchedulerTimeType(dispatchTime: .now())
    }
    
    public var minimumTolerance: SchedulerTimeType.Stride {
        return SchedulerTimeType.Stride(0)
    }
    
    public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
        if let options = options {
            async(group: options.group, qos: options.qos, flags: options.flags, execute: action)
        } else {
            async(execute: action)
        }
    }
    
    public func schedule(
        after date: SchedulerTimeType,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    ) {
        let deadline = date.dispatchTime
        if let options = options {
            asyncAfter(deadline: deadline, qos: options.qos, flags: options.flags, execute: action)
        } else {
            asyncAfter(deadline: deadline, execute: action)
        }
    }
}

关键点:

  • DispatchQueue 适配为 Scheduler
  • 使用 DispatchTime 作为时间类型
  • 支持 QoS 和 DispatchGroup

7. 背压处理机制

7.1 Demand系统

extension Subscribers {
    public struct Demand: Equatable, Hashable {
        public static let unlimited: Demand
        public static let max: (Int) -> Demand
        public static let none: Demand
        
        public static func + (lhs: Demand, rhs: Demand) -> Demand
        public static func - (lhs: Demand, rhs: Demand) -> Demand
        public static func += (lhs: inout Demand, rhs: Demand)
        public static func -= (lhs: inout Demand, rhs: Demand)
    }
}

Demand 的作用:

  • 控制 Publisher 发送值的速度
  • 实现背压(backpressure)
  • 防止内存溢出

7.2 背压处理示例

class BackpressureSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    
    private var subscription: Subscription?
    private let bufferSize: Int
    private var buffer: [Int] = []
    
    init(bufferSize: Int = 10) {
        self.bufferSize = bufferSize
    }
    
    func receive(subscription: Subscription) {
        self.subscription = subscription
        // 初始请求 bufferSize 个值
        subscription.request(.max(bufferSize))
    }
    
    func receive(_ input: Int) -> Subscribers.Demand {
        buffer.append(input)
        
        // 处理缓冲区
        processBuffer()
        
        // 如果缓冲区未满,请求更多值
        if buffer.count < bufferSize {
            return .max(1)
        } else {
            return .none  // 暂停请求
        }
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        // 处理完成
    }
    
    private func processBuffer() {
        // 处理缓冲区中的数据
        while !buffer.isEmpty {
            let value = buffer.removeFirst()
            print("处理: \(value)")
        }
        
        // 处理完后请求更多值
        subscription?.request(.max(bufferSize - buffer.count))
    }
}

8. 性能优化策略

8.1 值类型优化

Combine 大量使用值类型(struct),避免堆分配:

// 值类型,零成本抽象
struct Just<Output>: Publisher { }
struct Map<Upstream, Output>: Publisher { }
struct Filter<Upstream>: Publisher { }

8.2 类型擦除(eraseToAnyPublisher)

eraseToAnyPublisher() 是 Combine 中非常重要的方法,用于隐藏 Publisher 的具体类型,只暴露 OutputFailure 类型信息。这在需要统一返回类型、简化接口、避免类型泄露等场景中非常有用。

8.2.1 为什么需要类型擦除

问题:类型泄露(Type Leakage)

Combine 的操作符链式调用会产生复杂的嵌套类型,这些类型信息会"泄露"到函数签名中:

// ❌ 问题:类型过于复杂,难以维护
func fetchUserData() -> Publishers.Map<
    Publishers.FlatMap<
        Publishers.Catch<
            Publishers.Map<
                URLSession.DataTaskPublisher,
                User
            >,
            Just<User>
        >,
        Publishers.Map<
            Publishers.Debounce<
                PassthroughSubject<String, Never>,
                RunLoop
            >,
            URLSession.DataTaskPublisher
        >
    >,
    String
> {
    // 实现...
}

// ✅ 解决:使用 eraseToAnyPublisher() 简化类型
func fetchUserData() -> AnyPublisher<String, Never> {
    // 实现...
    return publisher.eraseToAnyPublisher()
}

类型擦除的优势:

  1. 简化接口:隐藏内部实现细节,只暴露必要的类型信息(Output 和 Failure)
  2. 统一返回类型:不同分支可以返回不同的具体 Publisher,但统一为 AnyPublisher
  3. 避免类型泄露:防止复杂的嵌套类型污染 API
  4. 提高可维护性:修改内部实现不影响外部接口
8.2.2 eraseToAnyPublisher 的基本用法

基本语法:

extension Publisher {
    /// 将 Publisher 转换为 AnyPublisher,隐藏具体类型
    public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
        return AnyPublisher(self)
    }
}

使用示例:

// 示例1:函数返回类型简化
func loadData() -> AnyPublisher<String, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .compactMap { String(data: $0, encoding: .utf8) }
        .mapError { $0 as Error }
        .eraseToAnyPublisher()  // 隐藏 URLSession.DataTaskPublisher 等具体类型
}

// 示例2:条件分支统一返回类型
func fetchData(useCache: Bool) -> AnyPublisher<Data, Error> {
    if useCache {
        return loadFromCache()
            .eraseToAnyPublisher()  // Just<Data, Error> -> AnyPublisher
    } else {
        return loadFromNetwork()
            .eraseToAnyPublisher()  // URLSession.DataTaskPublisher -> AnyPublisher
    }
}

func loadFromCache() -> Just<Data> {
    return Just(Data())
}

func loadFromNetwork() -> URLSession.DataTaskPublisher {
    return URLSession.shared.dataTaskPublisher(for: url)
}
8.2.3 AnyPublisher 的内部实现

AnyPublisher 使用类型擦除模式(Type Erasure Pattern),通过包装具体 Publisher 来隐藏类型信息:

public struct AnyPublisher<Output, Failure: Error>: Publisher {
    // 使用内部 Box 类型来存储具体的 Publisher
    private let box: _AnyPublisherBox<Output, Failure>
    
    /// 初始化:接受任何符合 Publisher 协议的类型
    public init<P: Publisher>(_ publisher: P) 
        where P.Output == Output, P.Failure == Failure {
        // 将具体 Publisher 包装到 Box 中
        self.box = _AnyPublisherBox(publisher)
    }
    
    /// 实现 Publisher 协议:转发给内部 Box
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        box.receive(subscriber: subscriber)
    }
}

// 内部 Box 类(简化版实现)
private class _AnyPublisherBox<Output, Failure: Error> {
    private let _receive: (AnySubscriber<Output, Failure>) -> Void
    
    init<P: Publisher>(_ publisher: P) 
        where P.Output == Output, P.Failure == Failure {
        // 保存 publisher 的 receive 方法
        self._receive = { subscriber in
            publisher.receive(subscriber: subscriber)
        }
    }
    
    func receive<S: Subscriber>(_ subscriber: S) 
        where S.Input == Output, S.Failure == Failure {
        let anySubscriber = AnySubscriber(subscriber)
        _receive(anySubscriber)
    }
}

实现原理:

  • AnyPublisher 是值类型(struct),但内部持有引用类型的 Box
  • Box 存储具体 Publisher 的 receive 方法
  • 通过闭包捕获和转发,实现类型擦除
8.2.4 常见使用场景

场景1:函数返回类型统一

class DataService {
    // 不同方法返回不同的具体 Publisher,但统一为 AnyPublisher
    func fetchUser() -> AnyPublisher<User, Error> {
        return URLSession.shared.dataTaskPublisher(for: userURL)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
    
    func fetchPosts() -> AnyPublisher<[Post], Error> {
        return URLSession.shared.dataTaskPublisher(for: postsURL)
            .map(\.data)
            .decode(type: [Post].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
    
    func fetchComments() -> AnyPublisher<[Comment], Never> {
        return Just([])  // 示例:返回 Just
            .eraseToAnyPublisher()
    }
}

场景2:条件分支统一类型

func loadData(source: DataSource) -> AnyPublisher<Data, Error> {
    switch source {
    case .network:
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
            
    case .cache:
        return loadFromCache()
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
            
    case .mock:
        return Just(mockData)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

enum DataSource {
    case network
    case cache
    case mock
}

场景3:操作符链中的类型擦除

class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    
    var searchResults: AnyPublisher<[String], Never> {
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
            .flatMap { query -> AnyPublisher<[String], Never> in
                if query.isEmpty {
                    return Just([])
                        .eraseToAnyPublisher()
                } else {
                    return self.performSearch(query: query)
                        .catch { _ in Just([]) }
                        .eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()  // 最终统一类型
    }
    
    private func performSearch(query: String) -> AnyPublisher<[String], Error> {
        // 搜索实现
        return URLSession.shared.dataTaskPublisher(for: searchURL)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

场景4:协议中的类型擦除

protocol DataRepository {
    func fetchData() -> AnyPublisher<Data, Error>
}

class NetworkRepository: DataRepository {
    func fetchData() -> AnyPublisher<Data, Error> {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
    }
}

class MockRepository: DataRepository {
    func fetchData() -> AnyPublisher<Data, Error> {
        return Just(mockData)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}
8.2.5 何时使用 eraseToAnyPublisher

应该使用的情况:

  1. 函数返回类型:公开 API 需要返回 Publisher 时
  2. 协议要求:协议方法需要返回 Publisher 时
  3. 条件分支:不同分支返回不同类型,需要统一时
  4. 存储属性:需要存储 Publisher 但不想暴露具体类型时
  5. 简化接口:避免类型泄露到外部时

不应该使用的情况:

  1. 内部实现:只在内部使用的 Publisher,不需要擦除
  2. 性能敏感:类型擦除有轻微性能开销(包装和转发)
  3. 需要具体类型:需要访问具体 Publisher 的特殊方法时

示例对比:

// ✅ 正确:公开 API 使用类型擦除
class API {
    static func fetchUser(id: Int) -> AnyPublisher<User, Error> {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

// ❌ 不必要:内部实现不需要类型擦除
class ViewModel {
    private func setupBinding() {
        // 不需要 eraseToAnyPublisher,因为只在内部使用
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }
}
8.2.6 类型擦除的性能考虑

性能开销:

  1. 内存开销AnyPublisher 需要额外的 Box 包装,增加一个间接层
  2. 调用开销:方法调用需要通过 Box 转发,有轻微的性能损失
  3. 优化机会:编译器无法对擦除后的类型进行特殊优化

性能对比:

// 直接使用具体类型(性能更好)
let publisher: Publishers.Map<URLSession.DataTaskPublisher, Data> = ...

// 使用类型擦除(有轻微开销)
let publisher: AnyPublisher<Data, Error> = ...
    .eraseToAnyPublisher()

建议:

  • 在公开 API 中使用类型擦除,简化接口
  • 在内部实现中尽量保持具体类型,获得更好的性能
  • 性能敏感的场景谨慎使用
8.2.7 与其他类型擦除方法对比

Combine 提供了多种类型擦除方法:

方法 用途 示例
eraseToAnyPublisher() 擦除 Publisher 类型 publisher.eraseToAnyPublisher()
AnySubscriber 擦除 Subscriber 类型 AnySubscriber(subscriber)
AnyCancellable 擦除 Cancellable 类型 AnyCancellable(cancellable)

统一使用模式:

// Publisher 类型擦除
let anyPublisher: AnyPublisher<String, Error> = publisher
    .eraseToAnyPublisher()

// Subscriber 类型擦除(内部使用)
let anySubscriber = AnySubscriber(subscriber)

// Cancellable 类型擦除(存储订阅)
let cancellable = AnyCancellable(subscription)
8.2.8 常见错误与注意事项

错误1:忘记类型擦除导致编译错误

// ❌ 错误:类型不匹配
func fetchData() -> AnyPublisher<Data, Error> {
    if condition {
        return Just(data)  // 类型是 Just<Data, Never>,不匹配
    } else {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)  // 类型是 Publishers.Map<...>,不匹配
    }
}

// ✅ 正确:使用 eraseToAnyPublisher 统一类型
func fetchData() -> AnyPublisher<Data, Error> {
    if condition {
        return Just(data)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    } else {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
    }
}

错误2:过度使用类型擦除

// ❌ 不必要:每个操作符都擦除
let publisher = [1, 2, 3].publisher
    .map { $0 * 2 }
    .eraseToAnyPublisher()  // 不必要
    .filter { $0 > 2 }
    .eraseToAnyPublisher()  // 不必要
    .sink { print($0) }

// ✅ 正确:只在最后需要时擦除
let publisher = [1, 2, 3].publisher
    .map { $0 * 2 }
    .filter { $0 > 2 }
    .eraseToAnyPublisher()  // 只在需要统一类型时使用

错误3:类型擦除后无法访问具体方法

// ❌ 错误:AnyPublisher 没有具体 Publisher 的特殊方法
let publisher: AnyPublisher<String, Error> = ...
publisher.someSpecificMethod()  // 编译错误:AnyPublisher 没有此方法

// ✅ 正确:在擦除前使用具体方法
let publisher = specificPublisher
    .someSpecificMethod()  // 先使用具体方法
    .eraseToAnyPublisher()  // 再擦除类型
8.2.9 最佳实践总结
  1. 公开 API 使用类型擦除:简化接口,隐藏实现细节
  2. 内部实现保持具体类型:获得更好的性能和类型信息
  3. 条件分支统一类型:使用 eraseToAnyPublisher() 统一返回类型
  4. 避免过度使用:只在必要时使用,不要每个操作符都擦除
  5. 注意性能影响:性能敏感场景谨慎使用

代码示例:

// 最佳实践示例
class DataManager {
    // ✅ 公开方法:使用类型擦除
    func fetchData() -> AnyPublisher<Data, Error> {
        return internalFetchData()
            .eraseToAnyPublisher()
    }
    
    // ✅ 内部方法:保持具体类型
    private func internalFetchData() -> URLSession.DataTaskPublisher {
        return URLSession.shared.dataTaskPublisher(for: url)
    }
    
    // ✅ 条件分支:统一返回类型
    func loadData(from source: DataSource) -> AnyPublisher<Data, Error> {
        switch source {
        case .network:
            return networkFetch()
                .eraseToAnyPublisher()
        case .cache:
            return cacheFetch()
                .eraseToAnyPublisher()
        }
    }
}

通过 eraseToAnyPublisher(),我们可以在保持类型安全的同时,简化 API 接口,提高代码的可维护性和可读性。

8.3 延迟执行

使用 Deferred 延迟创建 Publisher:

let deferred = Deferred {
    // 只在订阅时执行
    return expensiveOperation()
}

8.4 共享订阅

使用 share() 共享 Publisher:

let shared = expensivePublisher()
    .share()  // 多个订阅者共享同一个 Publisher

shared.sink { }  // 订阅1
shared.sink { }  // 订阅2(共享执行)

📚 总结

Combine 框架的核心优势

  1. 类型安全:充分利用 Swift 类型系统
  2. 性能优化:值类型、零成本抽象
  3. 声明式编程:代码更简洁、易读
  4. 异步处理:优雅处理异步操作
  5. 系统集成:与 SwiftUI、Foundation 深度集成

学习建议

  1. 从基础开始:理解 Publisher、Subscriber、Subscription
  2. 实践操作符:熟悉常用操作符的使用
  3. 理解背压:掌握 Demand 系统
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 Combine
❌