编程 CSS 锚点定位 + Signals 响应式范式:2026 前端开发范式革命

2026-05-28 20:36:00 +0800 CST views 7

CSS 锚点定位 + Signals 响应式范式:2026 前端开发范式革命

一、引言:前端正在经历一场静默的革命

如果问你 2026 年前端领域最大的变化是什么,大多数人可能会说 "AI 生成代码" 或者 "WebAssembly 普及"。这些答案没错,但它们都只是表层的工具革新。而真正触及前端开发本质——如何组织状态、如何管理 UI 布局——的两场革命,却很少被深入讨论。

这两场革命分别是:

  • CSS 锚点定位(Anchor Positioning):一个完全不需要 JavaScript 的布局机制,用来替代过去十年里统治前端生态的 Popper.js 类定位库
  • Signals 响应式范式:一种跳过虚拟 DOM、直接追踪数据依赖并精确更新 DOM 节点的新一代响应式机制,正在席卷 SolidJS、Vue、Angular、Preact 等所有主流框架

这两个话题,一个属于 CSS,一个属于 JavaScript 框架层,看起来风马牛不相及。但它们有一个共同的核心驱动力:消灭不必要的计算和重绘

在 2026 年的前端语境下,我们已经拥有足够快的 JavaScript 引擎、足够强大的浏览器 API,但我们的应用中仍然充斥着大量无效的计算——重复的 diff、冗余的 reflow、不必要的重新渲染。这两场革命,正是从不同方向对同一个问题的回应。

本文将深入解析这两项技术的底层原理、核心 API、生产级实践,以及它们对前端未来格局的深远影响。


二、CSS 锚点定位:Popper.js 时代的终结者

2.1 为什么我们需要一个替代方案

在 Web 开发中,"将一个浮层元素(tooltip、dropdown、popover)精准地定位在触发元素旁边" 这个需求,大概是前端历史上被解决次数最多的 "小问题" 了。

让我们回顾一下这个问题的演进史:

1. 固定定位(Fixed Positioning)时代
最早的做法是根据触发元素的 getBoundingClientRect() 计算坐标,直接用 position: fixed 加上 top/left 来定位。这在简单场景下工作良好,但一旦触发元素滚动出视口、或者窗口尺寸变化,浮层就会 "漂移"。

2. Popper.js 革命
2016 年,popper.js(后来演变为 @popperjs/core)问世,带来了 "智能定位" 的概念。它解决的核心问题是:根据视口边界动态翻转定位方向(上面放不下就放下面,左边放不下就放右边),同时提供 flippreventOverflowarrow 等修饰器(modifiers)。这套方案统治了前端生态近十年,Element UI、Ant Design、MUI、Headless UI 等几乎所有 UI 组件库都依赖它。

但 Popper.js 方案有三个根本性缺陷:

缺陷一:JavaScript 全权负责空间计算
翻转载南、边界检测、箭头偏移量计算——所有这些本应是浏览器职责范围的布局计算,全部由 JavaScript 承担。每次窗口 resize、滚动、或者触发元素位置变化,都需要触发 Popper 实例的 update() 方法,在下一帧重新计算。这意味着你必须订阅 window.resizescroll 事件,还要处理防抖。这是巨大的运行时开销。

缺陷二:跨 iframe 和 Shadow DOM 的噩梦
当弹出层需要放在 Shadow DOM 中,或者跨越 iframe 边界时,Popper 的坐标计算会完全失效。getBoundingClientRect() 只能告诉你相对于最近一个有定位上下文的祖先元素的坐标,跨边界时根本无法工作。

缺陷三:响应式布局的困境
在响应式设计中,浮层的位置需要根据容器宽度动态调整。但这要求在每次布局变化时都重新运行 Popper 的计算逻辑,这在复杂页面中是性能瓶颈。

2.2 锚点定位的原生方案

CSS 锚点定位(Anchor Positioning)是 CSS Position Layout Level 3 规范中定义的一组 CSS 特性,允许你用纯 CSS 将一个元素 "拴" 在另一个元素(称为锚点)上,由浏览器负责所有空间检测和位置调整。

这个概念非常简单:你声明一个元素是锚点,然后声明另一个元素通过某种关系定位到锚点上,浏览器自动处理碰撞检测和方向翻转。

2.3 核心 API 详解

2.3.1 声明锚点

/* 方式一:给元素起一个锚点名 */
.anchor-button {
  anchor-name: --my-anchor; /* 用 -- 开头是 CSS 变量的命名习惯,但不是强制的 */
}

/* 方式二:多个元素共享同一个锚点名(第一个生效) */
.trigger {
  anchor-name: --shared-anchor;
}

2.3.2 声明被锚定的浮层

.popover-content {
  position-anchor: --my-anchor; /* 引用锚点名称 */
  
  /* 使用 inset 物理属性定位 */
  top: anchor(bottom); /* popover 的顶部 = 锚点的底部 */
  left: anchor(center); /* 或者 anchor(left) / anchor(right) */
  
  /* 锚点相对定位(默认) */
  position: absolute;
}

这里最关键的是 anchor() 函数。它接受一个锚点的逻辑边(top/bottom/left/right/center)作为参数,返回该边相对于浮动层的偏移量。

2.3.3 position-anchor:核心绑定属性

/* 被定位元素的顶层容器必须声明锚点引用 */
.floating-panel {
  position-anchor: --my-anchor;
  position: absolute;
  
  /* 相对于锚点的位置 */
  top: anchor(bottom);           /* 紧贴在锚点下方 */
  left: anchor(left);           /* 左对齐 */
}

2.3.4 逻辑边与物理边的对齐

anchor() 函数支持逻辑边(start/end/self-start/self-end),这些逻辑边会根据书写方向(writing mode)自动翻转:

.floating-tooltip {
  position-anchor: --tooltip-anchor;
  position: absolute;
  
  /* 使用逻辑边 */
  inset-inline-start: anchor(anchor-end); /* 锚点的 end 侧 → 浮层的 start 侧 */
  /* 在 LTR 模式下:锚点右边缘 → 浮层左边缘 */
  /* 在 RTL 模式下:锚点左边缘 → 浮层右边缘 */
}

2.3.5 使用 anchor-size() 自适应大小

锚点定位还支持根据锚点大小动态设置浮层尺寸:

.floating-card {
  position-anchor: --card-anchor;
  position: absolute;
  
  /* 浮层最小宽度与锚点一致 */
  min-width: anchor-size(width);
  
  /* 最大宽度为锚点宽度的两倍 */
  max-width: calc(anchor-size(width) * 2);
}

2.4 碰撞检测与方向翻转

这是锚点定位最强大的部分——浏览器原生支持 flip 行为,不需要任何 JavaScript:

.floating-tooltip {
  position-anchor: --tooltip-anchor;
  position: absolute;
  
  /* 优先上方,其次下方 */
  top: anchor(bottom);
  bottom: anchor(top);  /* 如果上方放不下,自动切换到下方 */
  
  /* 翻转策略 */
  position-anchor-default: --tooltip-anchor;
}

/* 翻转修饰器(flip-inline / flip-block) */
.floating-dropdown {
  position-anchor: --dropdown-anchor;
  position: absolute;
  
  /* 内联方向翻转(相当于水平翻转) */
  inset: anchor(bottom) auto auto anchor(left);
  flip-inline: allow; /* 左右方向自动翻转 */
  flip-block: allow;  /* 上下方向自动翻转 */
}

关键属性解释:

  • flip-inline: allow:如果浮层在锚点右侧超出视口,则自动翻到左侧
  • flip-block: allow:如果浮层在锚点下方超出视口,则自动翻到上方
  • anchor-margin:设置浮层和锚点之间的间距(相当于 Popper.js 的 offset
/* 设置浮层与锚点之间 8px 间距 */
.floating-tooltip {
  position-anchor: --my-anchor;
  position: absolute;
  top: calc(anchor(bottom) + 8px); /* 手动加间距 */
  left: anchor(left);
}

/* 或者使用专门的 margin 属性(取决于浏览器实现) */
/* 预期语法:*/
position-anchor-margin: 8px;

2.5 完整实战:重构一个 Tooltip 组件

让我们对比一下传统 Popper.js 实现和一个纯 CSS 锚点定位的实现:

传统 Popper.js 实现(React):

import { useFloating, offset, flip, shift, arrow } from '@floating-ui/react';
import { useState, useRef } from 'react';

function Tooltip({ children, content }) {
  const [isOpen, setIsOpen] = useState(false);
  const arrowRef = useRef(null);
  
  const { refs, x, y, middlewareData: { arrow: { x: arrowX, y: arrowY } = {} } } = 
    useFloating({
      middleware: [
        offset(8),         // 间距
        flip(),           // 翻转
        shift(),          // 边界约束
        arrow({ element: arrowRef }), // 箭头
      ],
    });
  
  return (
    <>
      {/* 触发器,需要 ref 绑定 */}
      <button 
        ref={refs.setReference} 
        onMouseEnter={() => setIsOpen(true)}
        onMouseLeave={() => setIsOpen(false)}
      >
        {children}
      </button>
      
      {/* 浮层,渲染后才知道位置 */}
      {isOpen && (
        <div 
          ref={refs.setFloating}
          style={{ 
            position: 'absolute', 
            left: x, 
            top: y,
            transform: `translate(${arrowX ?? 0}px, ${arrowY ?? 0}px)`,
          }}
        >
          {content}
          <div ref={arrowRef} className="tooltip-arrow" />
        </div>
      )}
    </>
  );
}

CSS 锚点定位实现(原生 HTML + CSS):

<!-- HTML 结构:简单清晰,没有 ref,没有 JS 状态管理 -->
<style>
  .tooltip-trigger {
    anchor-name: --tooltip-anchor;
    /* 触发器本身的样式 */
  }
  
  .tooltip-content {
    position: anchor(--tooltip-anchor);
    position: absolute;
    
    /* 定位:紧贴在锚点下方 */
    top: calc(anchor(bottom) + 8px);
    left: anchor(left);
    
    /* 默认隐藏 */
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease, visibility 0.15s ease;
    
    /* 翻转策略 */
    inset: anchor(bottom) auto auto anchor(left);
    flip-block: allow;    /* 上下翻转 */
    flip-inline: allow;   /* 左右翻转 */
    /* inset-area 控制优先位置区域 */
  }
  
  /* hover 触发(无需 JavaScript) */
  .tooltip-trigger:hover ~ .tooltip-content,
  .tooltip-trigger:focus + .tooltip-content {
    opacity: 1;
    visibility: visible;
  }
  
  /* 箭头用伪元素实现 */
  .tooltip-content::before {
    content: '';
    position: absolute;
    /* 箭头位于锚点上方 */
    bottom: calc(100% + 8px); /* 如果 flip 后位置变化,这里也需要调整 */
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-bottom-color: #333;
  }
</style>

<button class="tooltip-trigger">Hover me</button>
<div class="tooltip-content">提示内容</div>

CSS 版本的优点:

  • 零 JavaScript:没有状态、没有 ref、没有事件监听
  • 零性能开销:浏览器在合成线程上处理碰撞检测,不触发 JavaScript
  • 响应式原生:resize、scroll、zoom 等场景下自动重新计算,无需手动订阅
  • SSR 友好:HTML 结构完全静态,服务端渲染无差别

2.6 浏览器支持现状(2026年5月)

根据 caniuse 数据,截至 2026 年 5 月:

特性ChromeFirefoxSafariEdge
anchor-name✅ 125+✅ 129+✅ 18.2+✅ 125+
position-anchor✅ 125+✅ 129+✅ 18.2+✅ 125+
anchor() 函数✅ 125+✅ 129+✅ 18.2+✅ 125+
anchor-size()✅ 125+✅ 129+✅ 18.2+✅ 125+
flip-*✅ 125+✅ 129+🔜 即将✅ 125+

Firefox 从 129 版本开始支持,已经覆盖主流版本。Safari 的支持稍晚,但 18.2 版本已经实现了核心 API。整体来看,2026 年已经是生产级可用的状态。

2.7 从 Popper.js 迁移:实战策略

对于已有项目,最安全的迁移策略是 渐进增强(Progressive Enhancement)

/* 第一层:基础定位(所有浏览器都支持) */
.floating-element {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

/* 第二层:CSS 锚点定位(增强层) */
@supports (position-anchor: --anchor) {
  .floating-element {
    position: anchor(--anchor-btn);
    inset: anchor(bottom) auto auto anchor(left);
    transform: none; /* 不再需要 transform 居中 */
  }
}
// 第三层:JavaScript 降级(针对旧版浏览器)
function initFloating() {
  const el = document.querySelector('.floating-element');
  if (CSS.supports('position-anchor', '--anchor')) {
    // 浏览器原生支持,无需初始化
    return;
  }
  // 降级到 Floating UI
  import('@floating-ui/dom').then(({ computePosition, autoUpdate }) => {
    autoUpdate(el, document.querySelector('.anchor-btn'), () => {
      computePosition(document.querySelector('.anchor-btn'), el, {
        middleware: [flip(), shift()],
      }).then(({ x, y }) => {
        Object.assign(el.style, { left: `${x}px`, top: `${y}px` });
      });
    });
  });
}

这种策略确保:

  • 旧浏览器:降级到 Floating UI,仍可正常工作
  • 新浏览器:完全由 CSS 处理,性能最优
  • 迁移过程中:不需要改动任何 JSX / HTML 结构,只需要改 CSS

三、Signals 响应式范式:绕过虚拟 DOM 的性能革命

3.1 虚拟 DOM 的功与过

React 团队在 2013 年引入虚拟 DOM(Virtual DOM)时,解决了当时前端开发的一个核心痛点:如何高效地更新页面状态

在虚拟 DOM 出现之前,jQuery 时代的做法是:

状态变化 → 手动 DOM 操作 → 新的状态变化 → 更多手动 DOM 操作

这导致代码库中充斥着 $('#container').find('.item-' + id).text('new value') 这样的选择器查询,状态管理和 DOM 操作深度耦合,极难维护。

虚拟 DOM 的思路是:用 JavaScript 对象描述 UI,然后用"比对"算法(diff)计算出实际 DOM 需要变更的部分。这个思路优雅地实现了 UI = f(state) 的函数式映射,让开发者不再关心 DOM 操作。

但是,虚拟 DOM 有两个被低估的成本:

成本一:重新执行组件函数
当状态变化时,React 会触发相关组件的重新渲染。这不仅仅是虚拟 DOM diff 的开销——整个组件函数会从头到尾重新执行一遍。这意味着:

function ProductList({ category }) {
  // ⚠️ 每次 category 变化,这个函数会完全重新执行
  // 即使我们只是过滤了一个列表,也需要重新创建所有 ProductCard 元素
  const products = getProducts(category); // 重新计算
  const filtered = products.filter(p => p.inStock); // 重新过滤
  
  return (
    <div>
      {filtered.map(p => (
        <ProductCard key={p.id} {...p} /> // 创建新的虚拟 DOM 节点
      ))}
    </div>
  );
}

成本二:diff 算法的局限性
React 的 diff 算法基于两个假设:

  1. 两个不同类型的元素会产生不同的树
  2. 可以通过 key 来暗示子元素的稳定性

但这些假设在复杂场景下会失效。例如,一个表格组件中某一行数据变化时,React 可能会 diff 整个表格而不是只 diff 变化的那一行。

3.2 Signals 的核心思想

Signals 的灵感来自响应式编程(Reactive Programming),核心概念很简单:不要重新执行函数,只更新需要更新的值

Signals 是一种细粒度的响应式原语,由三部分组成:

// 1. Signal(信号):一个可追踪其读取的值
const count = signal(0);

// 2. Computed(计算):基于信号派生的值,自动追踪依赖
const doubled = computed(() => count.get() * 2);

// 3. Effect(副作用):当信号变化时自动执行的逻辑
effect(() => {
  console.log(`Count changed to: ${count.get()}`);
});

// 触发变化
count.set(1); // doubled 自动更新,effect 自动执行
// 输出: "Count changed to: 1"

关键洞察:当你读取 count.get() 时,Signals 会自动记录这个依赖关系。当你调用 count.set(1) 时,Signals 会:

  1. 找到所有直接依赖 count 的 Computed 和 Effect
  2. 只更新它们,而不是重新执行整个组件树
  3. 精确到单个 DOM 节点更新

这与 React 的粗粒度 "重新渲染整个组件" 形成了鲜明对比。

3.3 Signals 的实现原理

为了深入理解 Signals,我们需要看一下它的依赖追踪机制。不同的 Signals 实现(SolidJS、Preact Signals、Vue signals 等)在细节上有所不同,但核心机制是一致的。

3.3.1 全局依赖追踪器

// 简化版的全局依赖追踪实现
let currentTracker: Tracker | null = null;

class Tracker {
  private deps = new Set<Signal<any>>();
  private update: () => void;
  
  constructor(update: () => void) {
    this.update = update;
  }
  
  // 添加一个依赖信号
  addDep(signal: Signal<any>) {
    this.deps.add(signal);
    signal.subscribers.add(this);
  }
  
  // 信号变化时,通知所有订阅者
  notify() {
    this.update();
  }
}

// 读取信号时注册依赖
function get<T>(this: Signal<T>): T {
  if (currentTracker) {
    currentTracker.addDep(this);
  }
  return this.value;
}

// 写入信号时触发通知
function set<T>(this: Signal<T>, value: T): void {
  if (this.value !== value) {
    this.value = value;
    // 通知所有订阅的 Tracker
    this.subscribers.forEach(t => t.notify());
  }
}

3.3.2 Computed 的惰性求值

function computed<T>(fn: () => T): Signal<T> {
  let cachedValue: T;
  let cached = false;
  
  const signal = new Signal<T>(undefined as T, {
    get() {
      if (!cached) {
        // 惰性计算:只在首次读取或依赖变化时重新计算
        const previousTracker = currentTracker;
        currentTracker = null;
        
        cachedValue = fn();
        cached = true;
        
        currentTracker = previousTracker;
      }
      return cachedValue;
    }
  });
  
  // 注意:这里的依赖追踪是在 fn() 执行时完成的
  // 如果 fn() 中读取了某个 signal,currentTracker 会记录这个依赖
  return signal;
}

3.3.3 Effect 与批量更新

function effect(fn: () => void): () => void {
  const tracker = new Tracker(fn);
  
  // 立即执行一次以建立依赖
  const previousTracker = currentTracker;
  currentTracker = tracker;
  fn();  // 读取信号时建立依赖
  currentTracker = previousTracker;
  
  // 返回清理函数
  return () => {
    tracker.deps.forEach(dep => dep.subscribers.delete(tracker));
  };
}

3.4 SolidJS:Signals 范式的最佳代言人

SolidJS 是 Signals 范式最成熟的生产级实现。它的性能 benchmark 在几乎所有测试场景中都显著领先于 React。

让我们看一下 SolidJS 的核心 API:

import { createSignal, createEffect, createMemo, batch } from 'solid-js';

// 基础信号
const [count, setCount] = createSignal(0);

// 派生计算
const doubled = createMemo(() => count() * 2);

// 副作用
createEffect(() => {
  console.log(`Count: ${count()}, Doubled: ${doubled()}`);
});

// 更新
setCount(1);  // 自动触发 effect
// → "Count: 1, Doubled: 2"

3.4.1 JSX 编译的秘密

SolidJS 的 JSX 编译策略与 React 有本质区别。React 的 JSX 会编译成 React.createElement,创建虚拟 DOM 节点。而 SolidJS 的 JSX 编译后会生成直接操作 DOM 的代码:

// 源代码
function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <div>
      <p>Count: {count()}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

SolidJS 编译后的代码(概念上的等价表示):

function Counter() {
  const [count, setCount] = createSignal(0);
  
  // 创建 DOM 节点(只执行一次!)
  const _div = document.createElement('div');
  const _p = document.createElement('p');
  const _text = document.createTextNode(''); // 文本节点
  const _button = document.createElement('button');
  
  // 按钮点击处理
  _button.addEventListener('click', () => {
    setCount(c => c + 1); // 只更新 count,不重新渲染任何组件
  });
  
  // 副作用:count 变化时更新文本节点内容(精确更新!)
  createEffect(() => {
    _text.data = `Count: ${count()}`;
  });
  
  return _div;
}

关键区别:组件函数只执行一次,用于创建 DOM 结构。后续的状态变化只触发相关的 Effects,直接更新对应的 DOM 节点,完全跳过了虚拟 DOM 机制。

3.4.2 SolidJS 与 React 的性能对比

让我们通过一个具体场景来对比:

// React 场景:大型列表中某一项的状态变化
function ProductList({ products }) {
  const [selectedId, setSelectedId] = useState(null);
  
  return (
    <div>
      {products.map(p => (
        <ProductRow 
          key={p.id} 
          product={p} 
          isSelected={p.id === selectedId}
          onClick={() => setSelectedId(p.id)}
        />
      ))}
    </div>
  );
}
// 性能问题:
// - selectedId 变化 → ProductList 重新渲染
// - products.map 重新执行 → 创建 1000 个新的 ProductRow 虚拟 DOM 节点
// - React diff 对比新旧虚拟 DOM 树
// - 找出实际需要更新的 DOM(可能只需要更新一个 className)
// SolidJS 场景
function ProductList(props) {
  const [selectedId, setSelectedId] = createSignal(null);
  
  return (
    <div>
      {/* For 组件:只对 items 的增删执行 DOM 操作 */}
      <For each={props.products}>
        {(product) => (
          <ProductRow
            product={product}
            isSelected={() => product.id === selectedId()}
            onClick={() => setSelectedId(product.id)}
          />
        )}
      </For>
    </div>
  );
}
// 性能优势:
// - selectedId 变化 → 精确更新对应行 ProductRow 的 isSelected
// - 其他 999 行的 DOM 完全不受影响
// - 没有虚拟 DOM diff,直接操作 DOM

3.5 Vue 的 Signals 拥抱:渐进式迁移

Vue 3.4 引入了 refreactive,从某种意义上说已经进入了 Signals 家族。但 Vue 的 Signals 拥抱更具战略意义——它提供了 渐进式迁移路径,让现有 Vue 项目可以逐步采用 Signals 模式。

3.5.1 Vue Signals 的响应式系统

import { ref, computed, watch, reactive } from 'vue';

// 响应式引用
const count = ref(0);

// Computed(自动追踪依赖)
const doubled = computed(() => count.value * 2);

// Watch(副作用)
watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`);
});

// 批量更新
import { batch } from 'vue';

batch(() => {
  count.value = 1;
  count.value = 2; // 如果同步有多次更新,可以合并
});

3.5.2 组合式 API 中的 Signals 模式

Vue 3 的 Composition API 设计上已经与 Signals 高度兼容:

import { ref, computed, watchEffect } from 'vue';

export function useProductList(initialProducts) {
  const products = ref(initialProducts);
  const filter = ref('all');
  const sortBy = ref('name');
  
  // 派生状态(自动追踪依赖)
  const filteredProducts = computed(() => {
    let result = products.value;
    
    if (filter.value !== 'all') {
      result = result.filter(p => p.category === filter.value);
    }
    
    return result.sort((a, b) => {
      const aVal = a[sortBy.value];
      const bVal = b[sortBy.value];
      return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
    });
  });
  
  // 自动追踪:sortBy 变化时自动重新计算
  watchEffect(() => {
    analytics.track('filter_applied', {
      filter: filter.value,
      sortBy: sortBy.value,
      resultCount: filteredProducts.value.length,
    });
  });
  
  return {
    products,
    filter,
    sortBy,
    filteredProducts,
  };
}

这与 SolidJS 的模式非常相似——派生值(computed)会自动追踪依赖,状态变化时只更新需要更新的部分。

3.6 Preact 和 Angular 的 Signals 集成

Preact Signals(2023年发布)是 Preact 的官方 Signals 状态管理方案:

import { signal, computed, effect } from '@preact/signals';

// 全局信号(跨组件共享)
export const theme = signal<'light' | 'dark'>('light');
export const user = signal<User | null>(null);

// 计算派生
export const isAuthenticated = computed(() => user.value !== null);

// 副作用
effect(() => {
  document.body.className = theme.value;
});

在 Preact 组件中使用:

import { useSignal } from '@preact/signals';
import { theme } from './store';

function Header() {
  // 组件级别的响应式状态
  const searchQuery = useSignal('');
  
  return (
    <header>
      <div class={theme.value}>...</div>
      <input 
        value={searchQuery.value}
        onInput={(e) => searchQuery.value = e.target.value}
      />
    </header>
  );
}

Angular 17+ 也在 Signals 上大步前进:

import { signal, computed, effect } from '@angular/core';

export class CartComponent {
  // 基础信号
  items = signal<CartItem[]>([]);
  
  // Computed
  total = computed(() => 
    this.items().reduce((sum, item) => sum + item.price, 0)
  );
  
  // Effect
  constructor() {
    effect(() => {
      if (this.items().length > 0) {
        this.saveToStorage();
      }
    });
  }
}

3.7 Signals 与状态管理:告别 Redux 时代

Signals 的出现,正在重新定义前端状态管理的格局。

在 Redux 时代,状态管理是一个独立的技术栈:

UI Component → dispatch(action) → Reducer → New State → Selector → UI Update

在 Signals 时代,状态管理被内化到了框架层:

Signal.set(value) → 自动通知所有订阅者 → 精确 DOM 更新

这并不意味着 Redux 等全局状态管理器完全消亡——在跨多个不相关组件共享复杂状态时,集中式存储仍有价值。但对于 组件内状态局部共享状态,Signals 提供了一种更自然、更高效的编程模型。

让我们看一个从 Redux 迁移到 Signals 的案例对比:

Redux 版本的购物车:

// store/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CartItem {
  id: string;
  name: string;
  quantity: number;
  price: number;
}

interface CartState {
  items: CartItem[];
  coupon: string | null;
}

const initialState: CartState = { items: [], coupon: null };

const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addItem(state, action: PayloadAction<CartItem>) {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        existing.quantity += action.payload.quantity;
      } else {
        state.items.push(action.payload);
      }
    },
    removeItem(state, action: PayloadAction<string>) {
      state.items = state.items.filter(i => i.id !== action.payload);
    },
    applyCoupon(state, action: PayloadAction<string>) {
      state.coupon = action.payload;
    },
  },
});

// 选择器
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = (state: RootState) => 
  state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
export const selectDiscount = createSelector(
  [selectCartItems, (state: RootState) => state.cart.coupon],
  (items, coupon) => coupon ? items.length * 10 : 0 // 每件商品优惠10元
);

Signals 版本的购物车:

// store/cart.ts
import { signal, computed } from '@solidjs/signals';

export interface CartItem {
  id: string;
  name: string;
  quantity: number;
  price: number;
}

// 响应式状态
export const cartItems = signal<CartItem[]>([]);
export const couponCode = signal<string | null>(null);

// 派生状态(自动追踪依赖,Redux 需要 createSelector,这里只需要 computed)
export const cartTotal = computed(() => 
  cartItems().reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const discount = computed(() => {
  if (!couponCode()) return 0;
  return cartItems().length * 10;
});

export const finalTotal = computed(() => cartTotal() - discount());

// 操作函数(直接调用,不需要 dispatch)
export function addItem(item: CartItem) {
  const existing = cartItems().find(i => i.id === item.id);
  if (existing) {
    cartItems()[cartItems().findIndex(i => i.id === item.id)].quantity += item.quantity;
    cartItems([...cartItems()]); // 触发更新
  } else {
    cartItems([...cartItems(), item]);
  }
}

export function removeItem(id: string) {
  cartItems(cartItems().filter(i => i.id !== id));
}

export function applyCoupon(code: string) {
  couponCode(code);
}

两者的对比:

维度ReduxSignals
样板代码大量 action/reducer/selector 模板极简,信号 + 派生
状态更新dispatch + reducer + immutable 更新直接赋值
依赖追踪手动选择器优化(createSelector)自动追踪
异步处理thunk/saga middleware直接 async 函数
性能依赖 redux 优化(reselect)原生粒度追踪
学习曲线陡峭(actions、reducers、store、selectors)平缓

3.8 Signals 的挑战与最佳实践

虽然 Signals 带来了显著的编程效率提升,但它也有一些需要特别注意的陷阱。

陷阱一:避免循环依赖

// ❌ 错误:循环依赖导致无限递归
const a = signal(1);
const b = computed(() => a() + 1);
const c = computed(() => b() + 1);

effect(() => {
  a.set(c()); // a 变化触发 c 变化,c 变化触发 a 变化 → 无限循环
});

// ✅ 正确:在 effect 内部使用 batch 或临时断开追踪
effect(() => {
  const computedValue = c(); // 先读取
  batch(() => {
    a.set(computedValue);
  });
});

陷阱二:衍生计算的正确使用

// ❌ 错误:在 computed 内部触发副作用
const doubled = computed(() => {
  if (count() > 100) {
    analytics.track('high_count'); // 副作用不应该在 computed 中
  }
  return count() * 2;
});

// ✅ 正确:副作用应该使用 effect
const doubled = computed(() => count() * 2);

effect(() => {
  if (count() > 100) {
    analytics.track('high_count');
  }
});

陷阱三:避免过长的依赖链

// ❌ 不推荐:过长的派生链
const a = signal(1);
const b = computed(() => a() + 1);     // 依赖 a
const c = computed(() => b() + 1);     // 依赖 b(间接依赖 a)
const d = computed(() => c() + 1);     // 依赖 c(间接依赖 a、b)
const e = computed(() => d() + 1);     // 依赖 d

// 推荐:扁平化派生
const a = signal(1);
const b = computed(() => a() + 1);
const d = computed(() => a() * 2 + 1); // 直接依赖 a,减少中间层

四、融合实战:构建现代浮层系统

现在,让我们把 CSS 锚点定位和 Signals 结合起来,构建一个生产级的浮层系统。这个系统将展示如何将两者融合以获得最佳开发体验和运行时性能。

4.1 整体架构

用户交互(Click / Hover)
    ↓
Signals 状态管理
    ↓
浮层显示/隐藏(通过 class toggle)
    ↓
CSS 锚点定位(处理位置、翻转、间距)
    ↓
浏览器原生合成线程处理
    ↓
精确渲染

4.2 核心实现

// floating.ts - 基于 SolidJS + CSS 锚点定位的浮层系统
import { createSignal, Show, JSX } from 'solid-js';

type Placement = 'top' | 'bottom' | 'left' | 'right';
type FlipStrategy = 'allow' | 'none';

interface FloatingConfig {
  placement: Placement;
  flipInline?: FlipStrategy;
  flipBlock?: FlipStrategy;
  gap?: number;
  anchorMargin?: number;
}

const [floatingState, setFloatingState] = createSignal<{
  visible: boolean;
  anchor: HTMLElement | null;
  config: FloatingConfig;
}>({
  visible: false,
  anchor: null,
  config: {
    placement: 'bottom',
    flipInline: 'allow',
    flipBlock: 'allow',
    gap: 8,
  },
});

// 显示浮层
export function showFloating(
  anchor: HTMLElement,
  config: Partial<FloatingConfig> = {}
) {
  setFloatingState({
    visible: true,
    anchor,
    config: { ...floatingState().config, ...config },
  });
}

// 隐藏浮层
export function hideFloating() {
  setFloatingState({
    ...floatingState(),
    visible: false,
  });
}

// 生成 CSS 类名(基于配置动态构建)
function getFloatingStyles(config: FloatingConfig): string {
  const placementMap: Record<Placement, JSX.CSSProperties> = {
    bottom: {
      top: `calc(anchor(bottom) + ${config.gap ?? 8}px)`,
      left: 'anchor(left)',
    },
    top: {
      bottom: `calc(anchor(top) - ${config.gap ?? 8}px)`,
      left: 'anchor(left)',
    },
    left: {
      right: `calc(anchor(left) - ${config.gap ?? 8}px)`,
      top: 'anchor(top)',
    },
    right: {
      left: `calc(anchor(right) + ${config.gap ?? 8}px)`,
      top: 'anchor(top)',
    },
  };
  
  return placementMap[config.placement];
}
// FloatingContainer.tsx - 浮层容器组件
import { Show, createMemo } from 'solid-js';
import { floatingState } from './floating';

export function FloatingContainer(props: {
  children: JSX.Element;
}) {
  const styles = createMemo(() => {
    const { visible, anchor, config } = floatingState();
    
    if (!visible) {
      return {
        opacity: '0',
        visibility: 'hidden',
      };
    }
    
    return {
      opacity: '1',
      visibility: 'visible',
      position: 'anchor(--floating-anchor)',
      ...getFloatingStyles(config),
    };
  });
  
  return (
    <div 
      class="floating-container"
      style={styles()}
    >
      {props.children}
    </div>
  );
}
// useFloating.ts - Hook:让任意元素成为浮层触发器
import { onMount, onCleanup } from 'solid-js';

export function useFloating(
  triggerRef: HTMLElement,
  config: Partial<FloatingConfig> = {}
) {
  onMount(() => {
    // 注册锚点名
    const anchorName = `floating-${Math.random().toString(36).slice(2)}`;
    triggerRef.style.anchorName = `--${anchorName}`;
    
    // 触发器的锚点名称(用于 CSS 选择器)
    triggerRef.dataset.anchorName = anchorName;
    
    const handleClick = (e: MouseEvent) => {
      e.stopPropagation();
      showFloating(triggerRef, config);
    };
    
    triggerRef.addEventListener('click', handleClick);
    
    onCleanup(() => {
      triggerRef.removeEventListener('click', handleClick);
    });
  });
}

// 在浮层容器中引用锚点
export function attachToFloating(containerRef: HTMLElement) {
  // 容器通过 CSS anchor() 引用触发器
}
/* floating.css - 纯 CSS 处理定位逻辑 */

.floating-trigger {
  anchor-name: --floating-anchor;
}

.floating-container {
  position: absolute;
  
  /* 定位基准 */
  top: calc(anchor(bottom) + 8px);
  left: anchor(left);
  
  /* 翻转策略 */
  flip-block: allow;
  flip-inline: allow;
  
  /* 动画 */
  transition: opacity 0.15s ease, transform 0.15s ease;
}

/* 翻转后的位置调整(通过伪元素箭头或 inset-area) */
.floating-container[style*="bottom"]::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-bottom-color: inherit;
}

4.3 性能分析

对比三个方案的渲染性能(基于 js-framework-benchmark 数据):

方案1000行表格更新一行浮层定位初次加载
React 18 + Floating UI42ms3.2ms180ms
SolidJS + CSS 锚点8ms0.8ms95ms
Preact + Signals15ms1.1ms85ms

SolidJS + CSS 锚点方案在所有场景下都显著领先:

  • 表格更新:Signals 精确更新一行 DOM,React 重新渲染整个列表
  • 浮层定位:CSS 锚点由浏览器合成线程处理,零 JavaScript 开销
  • 初次加载:SolidJS 的编译时优化(无虚拟 DOM),bundle 更小

五、2026 前端架构的演进方向

5.1 从命令式到声明式,再到"精确式"

前端架构演进可以划分为三个阶段:

第一阶段:命令式(Imperative)

// 直接操作 DOM
document.getElementById('btn').addEventListener('click', () => {
  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.innerHTML = '<p>Hello</p>';
  modal.style.top = '50%';
  modal.style.left = '50%';
  document.body.appendChild(modal);
});

优点:完全可控。缺点:状态和 UI 深度耦合。

第二阶段:声明式(Declarative)

// React 的函数式组件
function App() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open</button>
      <Modal open={isOpen} onClose={() => setIsOpen(false)} />
    </div>
  );
}

优点:UI = f(state),状态管理清晰。缺点:粗粒度更新,虚拟 DOM diff 有开销。

第三阶段:精确式(Precise)

// SolidJS + CSS 锚点
function App() {
  const [isOpen, setIsOpen] = createSignal(false);
  // 精确更新:只有 Modal 的 display 改变,没有重新渲染
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open</button>
      <Show when={isOpen()}>
        <Modal 
          position="center"
          onClose={() => setIsOpen(false)} 
        />
      </Show>
    </div>
  );
}

优点:精确到节点级别的更新,零虚拟 DOM 开销。缺点:需要新的编程心智模型。

5.2 技术选型决策树

根据不同场景,给出 2026 年的技术选型建议:

项目类型
│
├─ 全新项目(2026+)
│  ├─ 性能敏感型(如 SaaS 仪表盘、协作工具)
│  │  └─ 推荐:SolidJS + CSS 锚点 + Nano Stores
│  │
│  ├─ 内容型(如博客、文档站点)
│  │  └─ 推荐:Astro + Islands 架构
│  │
│  └─ 企业级大型应用
│     └─ 推荐:Angular 17+ Signals 或 Vue 3.4+ Composition API
│
├─ 现有 React 项目(渐进迁移)
│  ├─ 浮层组件 → 用 CSS 锚点替代 @floating-ui/react
│  ├─ 状态密集组件 → 用 Signals 逐步替换 useState/useContext
│  └─ 全局状态 → 保留 Redux Toolkit 作为数据源,Signals 作为视图层
│
└─ 新人学习路径
   ├─ 先学 HTML + CSS(锚点定位!)
   ├─ 再学 SolidJS(Signals 基础)
   └─ 最后学 React(理解虚拟 DOM 历史)

5.3 框架预测:2027 年的前端格局

基于当前趋势,我们可以对 2027 年的前端技术格局做出一些有根据的预测:

预测一:React 19 将集成 Signals
React 19 的 Compiler(Previously React Forget)已经展示了自动细粒度追踪的能力。虽然它仍然基于虚拟 DOM,但通过 Compiler 的优化,已经在结果上接近 Signals 的精确更新能力。2027 年的 React 可能会正式拥抱 Signals API。

预测二:CSS 锚点定位成为 UI 组件库标配
到 2027 年,所有主流 UI 组件库(Material UI、Ant Design、Chakra UI 等)的浮层组件都将默认使用 CSS 锚点定位,Floating UI 等 JavaScript 定位库将退居降级路径。

预测三:状态管理框架式微
随着 Signals 的普及,独立的状态管理框架(如 Redux、MobX)的使用率将显著下降。状态管理将从独立的技术栈回归到框架内置能力。Redux 可能会转型为中间件平台(数据持久化、同步等),而不是状态管理核心。

预测四:Web Components + Signals 的结合
Web Components 的自定义元素与 Signals 的响应式能力有着天然的契合点。Signals 的 effect() API 可以完美地与 Custom Elements 的生命周期钩子结合,生产出一种框架无关的高性能组件模型。这可能是解决 "跨框架组件复用" 问题的最终方案。


六、总结:前端范式转移的逻辑

回顾全文,我们讨论了两个看似独立、实则同源的技术革新:

CSS 锚点定位解决的是 "布局计算应该由谁负责" 的问题。传统方案让 JavaScript 承担了所有空间检测和位置计算的工作,而锚点定位将这份职责还给了 CSS——以及最终的执行者:浏览器。浏览器拥有完整布局信息的访问权限,在合成线程上运行,不需要跨线程通信,也没有 JavaScript 的调用开销。这是浏览器平台能力的回归。

Signals 响应式解决的是 "状态变化应该触发什么" 的问题。传统方案让状态变化触发组件树的重新渲染,然后通过 diff 找到需要变更的节点。Signals 废除了中间层(虚拟 DOM diff),让状态变化直接映射到精确的 DOM 节点更新。这是响应式编程理念在 UI 领域的终极形态。

两者有一个共同的设计哲学:让对的执行者做对的事

  • 布局计算 → 浏览器(CSS 引擎)
  • 依赖追踪 → 运行时(Signals)
  • DOM 更新 → 浏览器(渲染引擎)

2026 年的前端,正在从 "JavaScript 全能" 的时代,走向 "各司其职" 的协作时代。这不是技术的退步,而是架构思想的成熟。

理解这一点,比记住任何一个 API 都重要。

复制全文 生成海报 CSS 前端 响应式 Signals Web开发

推荐文章

微信小程序热更新
2024-11-18 15:08:49 +0800 CST
用 Rust 玩转 Google Sheets API
2024-11-19 02:36:20 +0800 CST
XSS攻击是什么?
2024-11-19 02:10:07 +0800 CST
GROMACS:一个美轮美奂的C++库
2024-11-18 19:43:29 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
回到上次阅读位置技术实践
2025-04-19 09:47:31 +0800 CST
Golang Select 的使用及基本实现
2024-11-18 13:48:21 +0800 CST
网络数据抓取神器 Pipet
2024-11-19 05:43:20 +0800 CST
程序员茄子在线接单