Solidjs 响应式 & 编译原理初探

简介

Solidjs 是一个类似于 React & Vue 的前端框架。它实现了细粒度的自动依赖追踪,当状态变化时,只会触发直接依赖它的视图的更新。它没有虚拟 DOM,而是在编译阶段将 JSX 模版转换为原生 DOM 操作指令,运行时直接操作真实 DOM 节点。

核心思想:核心设计理念是"声明式编程",类似于React,但更接近于Vue的模板语法。借助Signal实现状态到UI的极精细化同步,这种同步的最小单位是DOM元素,框架能够通过最小化的修改去满足程序员的要求。

实现原理

响应式原理:参考S.js,核心是将state和dom或者Component做关联。当调用createSignal时产生一个响应式数据signalstate等待收集依赖;当insert时将dom操作封装成一个函数存放到signalstate的observers中;当setCount触发signalstate中的observers执行视图更新。

编译:提供了支持多种源到产物的编译方式,如将jsx编译为产物、将tag template转成产物、将HyperScript转成产物。

运行时:提供运行时api支持,例如render、insert、createComponent、template等功能。

优势

高性能:接近原生的性能,在js - framework - benchmark排名中名列前茅,通过直接操作DOM而非虚拟DOM实现高性能,减少额外渲染层开销,且状态改变时只更新受影响部分,而非整个组件树。

极小的打包体积:编译为直接的DOM操作,无虚拟DOM,极小的运行时,适合打为独立的webComponent在其它应用中嵌入。

易于使用:近似React的使用体验,便于快速上手,API设计直观且易于理解,从其他React或Vue背景的开发者能够轻松上手。

强大的响应式系统:不仅限于状态,还可用于跟踪计算属性,使代码更整洁。

劣势:配套的生态不成熟。

原理

响应式 & Signal

createSignal

signalsolid中基本的响应单元,createSignal类似react中的useState,传递给createSignal调用的参数是初始值,createSignal返回[getter, setter],需要注意:第一个返回的值是一个getter而不是一个值,使用的时候需要调用。

框架拦截读取值的任何位置来进行自动跟踪,从而响应式更新,所以调用getter的位置很重要。

react不同的是,例如setState触发更新,react会生成Fiber树,进行diff算法,最后执行dom操作。solid则是直接调用编译好的dom操作方法,没有虚拟dom比较。

用法

TS 定义

js 复制代码
function createSignal<T>(
    initialValue: T,
    options?: { equals?: false | ((prev: T, next: T) => boolean) }
): [get: () => T, set: (v: T) => T];

Solid JS的一个特点:"你可以定义变量是否为响应式,甚至可以定义响应式的时机。"

  • 仅提供initialValue时,(默认)是响应式的。
  • options设置equalsfalse时不管何时都是响应式。
  • equals设置为函数,根据新值和旧值的关系来设置何时为响应式。

使用

js 复制代码
import { createSignal, createEffect } from "solid-js";

const EqualityExample = () => {
  // 使用对象类型并自定义比较函数
  const [user, setUser] = createSignal(
    { id: 1, name: "Alice" },
    { equals: (prev, next) => prev.id === next.id }
  );

  createEffect(() => {
    console.log("User changed:", user().name);
  });

  // 测试用例:
  // 1. 相同ID不触发更新
  setUser({ id: 1, name: "Alice Smith" }); // 不会触发 effect
  // 2. 不同ID触发更新
  setUser({ id: 2, name: "Bob" }); // 会触发 effect
};
实现
  • 在下文createEffect中会涉及响应式逻辑,createSignal仅关注数据存取。
js 复制代码
function createSignal<T>(
  value?: T,
  options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
  // 处理外部 equals 逻辑
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;

  const s: SignalState<T | undefined> = {
    value,
    comparator: options.equals || undefined
  };

  const setter: Setter<T | undefined> = (value?: unknown) => {
    if (typeof value === "function") {
      return value = value(s.value);
    }
    return writeSignal(s, value);
  };

  return [readSignal.bind(s), setter];
}

function writeSignal(node: SignalState<any> | Memo<any>, value: any) {
  // 核心比较逻辑
  if (!node.comparator || !node.comparator(node.value, value)) {
    node.value = value; // 直接更新值
  }
  return value;
}

function readSignal(this: SignalState<any> | Memo<any>) {
  return this.value;
}

createEffect

createEffect开始,我们将正式开始了解响应式的逻辑。

用法:createEffect接收一个函数,监听其执行情况,createEffect会自动订阅在执行期间读取的所有Signal,并在Signal值之一发生改变的时候,重新运行此函数。

用法

TS定义

js 复制代码
function createEffect<Next, Init>(
  fn: EffectFunction<Init | Next, Next>,
  value?: Init,
  options?: EffectOptions & { render?: boolean }
): void

用法

js 复制代码
import { createSignal, createEffect } from "solid-js";

function CounterDemo() {
  const [count, setCount] = createSignal(0);
  const [unrelated, setUnrelated] = createSignal(1);

  // 自动追踪依赖:当count()变化时触发
  createEffect(() => {
    console.log("计数器变化:", count());
  });

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        计数 {count()}
      </button>
      <button onClick={() => setUnrelated(c => c + 1)}>
        无关项 {unrelated()}(不会触发effect)
      </button>
    </div>
  );
}
原理
  • 依赖收集动态性:Effect执行时通过全局Listener 指针建立依赖关系,实现运行时动态追踪。每次执行都会重建依赖,确保精确性。
  • 更新标记策略:Signal更新时通过writeSignal 将关联Effect标记为STALE(陈旧状态),而非立即执行。采用位标记(STALE=1)实现轻量级状态管理。
  • 批量更新机制:通过微任务队列(queueMicrotask)实现异步批量更新,在runUpdates 中统一处理所有待更新Effect,避免重复计算。
  • 值比较优化:通过SignalState中的comparator 实现自定义值比较,避免不必要的更新传播。
  • 分层调度控制:通过EffectOptions中的render 标记区分渲染阶段/非渲染阶段Effect,实现不同优先级的更新调度。
  • 状态位运算:使用数字状态位(STALE=1, PENDING=2)代替布尔值,支持多状态组合判断,在updateComputation 中实现高效状态流转。
  • 内存安全机制:每次执行Effect前调用cleanNode 清除旧依赖,防止内存泄漏。采用双向链表(observers/sources)实现快速依赖解绑。

Solidjs 中有一个 Listener,这个东西相当于全局的以及最新的 Effect

实现
js 复制代码
// 全局状态
let Listener: EffectFn | null = null;
const Updates = new Set<EffectFn>();

// Signal 结构
interface Signal<T> {
  value: T;
  observers: Set<EffectFn>;
  comparator?: (a: T, b: T) => boolean;
}

// Effect 结构
interface EffectFn {
  (): void;
  state: number;
  sources: Signal<any>[];
}

function createSignal<T>(
  value?: T,
  options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
  // 处理外部 equals 逻辑
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;

  const s: SignalState<T | undefined> = {
    value,
    observers: new Set(),
    comparator: options.equals || undefined
  };

  const setter: Setter<T | undefined> = (value?: unknown) => {
    if (typeof value === "function") {
      else value = value(s.value);
    }
    return writeSignal(s, value);
  };

  return [readSignal.bind(s), setter];
}

function readSignal<T>(signal: Signal<T>) {
  if (Listener) {
    signal.observers.add(Listener);
    Listener.sources.push(signal);
  }
  return signal.value;
}

function writeSignal<T>(signal: Signal<T>, value: T) {
  if (!signal.comparator!(signal.value, value)) {
    signal.value = value;
    signal.observers.forEach(effect => {
      effect.state = STALE;
      Updates.add(effect);
    });
    scheduleUpdate();
  }
  return value;
}

function createEffect(fn: () => void) {
  const effect: EffectFn = () => {
    cleanNode(effect);  // 清理旧依赖
    Listener = effect;
    fn();
    Listener = null;
  };
  effect.state = STALE;
  effect.sources = [];
  effect();
}

function cleanNode(effect: EffectFn) {
  effect.sources.forEach(signal => {
    signal.observers.delete(effect);
  });
  effect.sources.length = 0;
}

// 调度更新
const STALE = 1;
function scheduleUpdate() {
  if (!updateScheduled) {
    queueMicrotask(() => {
      Updates.forEach(effect => {
        if (effect.state === STALE) {
          effect();
        }
      });
      Updates.clear();
    });
    updateScheduled = true;
  }
}
let updateScheduled = false;

batch

用法

Solid 的 batch 工具函数允许将多个更改推入队列,然后在通知观察者之前同时使用它们。在批处理中更新的信号值直到批处理完成才会提交。

js 复制代码
import { render } from "solid-js/web";
import { createSignal, batch } from "solid-js";

const App = () => {
  const [firstName, setFirstName] = createSignal("John");
  const [lastName, setLastName] = createSignal("Smith");
  const fullName = () => {
    console.log("Running FullName");
    return `${firstName()} ${lastName()}`
  } 
 const updateNames = () => {
    console.log("Button Clicked");
    batch(() => {
      setFirstName(firstName() + "n");
      setLastName(lastName() + "!");
    })
  }
  
  return <button onClick={updateNames}>My name is {fullName()}</button>
};

render(App, document.getElementById("app"));
实现
js 复制代码
export function batch<T>(fn: Accessor<T>): T {
  return runUpdates(fn, false) as T; // ① 入口封装
}

function runUpdates<T>(fn: () => T, init: boolean) {
  if (Updates) return fn();          // ② 嵌套批处理优化
  if (!init) Updates = [];           // ③ 初始化更新队列
  ExecCount++;                       // ④ 事务计数器
  const res = fn();
  completeUpdates(wait);            // ⑤ 完成批处理
}

function completeUpdates(wait: boolean) {
  if (Updates) runQueue(Updates);    // ⑥ 执行计算更新
  const e = Effects!;
  if (e.length) runUpdates(() => runEffects(e), false); // ⑦ 延迟副作用
}

untrack

在 solidjs 中,Listener 代表的是核心依赖追踪器,在 createEffect 的时候进行赋值,在 readSignal 的时候进行取用。

在 插件 中,evalContext 代表的是核心依赖追踪器,在 createEffect 的时候进行赋值,在 readSignal 的时候进行取用。

两者的执行非常类似,只是封装逻辑不一样。

用法
js 复制代码
import { render } from "solid-js/web";
import { createSignal, createEffect, untrack } from "solid-js";

const App = () => {
  const [a, setA] = createSignal(1);
  const [b, setB] = createSignal(1);

  createEffect(() => {
    console.log(a(), untrack(b));
  });

  return <>
    <button onClick={() => setA(a() + 1)}>Increment A</button>
    <button onClick={() => setB(b() + 1)}>Increment B</button>
  </>
};

render(App, document.getElementById("app"));
原理

不收集依赖地执行方法。

实现
js 复制代码
export function untrack<T>(fn: Accessor<T>): T {
  const listener = Listener;
  Listener = null;
  try {
    return fn();
  } finally {
    Listener = listener;
  }
}

on

Solid 提供一个 on 工具函数,可以为我们的计算设置显式依赖。这主要用来更明确地简洁地声明跟踪哪些信号。然而,它也允许计算不立即执行而只在第一次更改时运行。可以使用defer 选项启用此功能。

用法
js 复制代码
import { render } from "solid-js/web";
import { createSignal, createEffect, on } from "solid-js";

const App = () => {
  const [a, setA] = createSignal(1);
  const [b, setB] = createSignal(1);

  createEffect(on(a, (a) => {
    console.log(a, b());
  }, { defer: true }));

  return <>
    <button onClick={() => setA(a() + 1)}>Increment A</button>
    <button onClick={() => setB(b() + 1)}>Increment B</button>
  </>
};

render(App, document.getElementById("app"));
原理
  • 依赖快照 :在effect执行瞬间捕获依赖值,避免后续依赖变更影响当前计算
  • 双缓存策略 :通过 prevInput 保留前次输入,实现差分比较优化
  • 批处理更新 :与 batch 协同实现批量更新
  • 惰性求值 : defer 选项延迟计算到下一个微任务周期
实现
js 复制代码
export function on<S, Next extends Prev, Prev = Next>(
  deps: AccessorArray<S> | Accessor<S>,
  fn: OnEffectFunction<S, undefined | NoInfer<Prev>, Next>,
  options?: OnOptions
): EffectFunction<undefined | NoInfer<Next>> {
  // 1. 判断依赖类型(数组/单个)
  const isArray = Array.isArray(deps);
  let prevInput: S;
  let defer = options && options.defer;

  // 2. 返回可被createEffect使用的effect函数
  return prevValue => {
    let input: S;
    
    // 3. 收集依赖的最新值
    if (isArray) {
      input = Array(deps.length) as unknown as S;
      for (let i = 0; i < deps.length; i++) 
        (input as unknown as TODO[])[i] = deps[i]();
    } else {
      input = deps();
    }

    // 4. 处理延迟执行逻辑
    if (defer) {
      defer = false;
      return prevValue;
    }

    // 5. 在untrack上下文中执行用户函数
    const result = untrack(() => fn(input, prevInput, prevValue));
    
    // 6. 保存当前输入作为下次的prevInput
    prevInput = input;
    return result;
  };
}
插件的实现
js 复制代码
export function on<S, Next extends Prev, Prev = Next>(
  deps: SignalGetter<S>[] | SignalGetter<S>,
  fn: OnEffectFunction<S, undefined | NoInfer<Prev>, Next>,
  options?: OnOptions,
): EffectFunction<undefined | NoInfer<Next>> {
  let prevInput: S;
  let defer = options?.defer;

  return (prevValue) => {
    let input: S;
    if (Array.isArray(deps)) {
      input = (Array(deps.length) as unknown) as S;
      for (let i = 0; i < deps.length; i++) {
        ((input as unknown) as any[])[i] = deps[i]();
      }
    } else {
      input = deps();
    }
    if (defer) {
      defer = false;
      return undefined;
    }
    const result = untrack(() => fn(input, prevInput, prevValue));
    prevInput = input;
    return result;
  };
}

渲染 & 编译

模版编译

源代码

js 复制代码
import { render } from 'solid-js/web';

function HelloWorld() {
  const name = 'world';
  return <div>Hello {name}!</div>;
}

render(() => <HelloWorld />, document.getElementById('app'))

编译后代码

js 复制代码
import { template as _$template } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";

const _tmpl$ = /*#__PURE__*/_$template(`<div>Hello world!</div>`, 2);

import { render } from 'solid-js/web';

function HelloWorld() {
  const name = 'world';
  return _tmpl$.cloneNode(true);
}

render(() => _$createComponent(HelloWorld, {}), document.getElementById('app'));

编译时优化 :

  • 将 JSX 模板编译为真实的 DOM 创建指令
  • 静态内容预先生成为克隆模板
  • 动态部分通过 insert() 等指令进行标记

看看 template 实现

js 复制代码
export function template(html, check, isSVG) {
  const t = document.createElement("template");
  t.innerHTML = html;
  // ...一些代码
  let node = t.content.firstChild;
  if (isSVG) node = node.firstChild;
  return node;
}

可以看到,name编译时已经被插入了。

看看 createComponent 实现

js 复制代码
export function createComponent<T>(Comp: Component<T>, props: T): JSX.Element {
  // ...一些代码
  return untrack(() => Comp(props || ({} as T)));
}

响应式对 DOM 更新的影响

  • 每个 DOM 绑定对应独立的 effect
  • 状态变更时通过响应式系统直接定位到需要更新的 DOM 节点
  • 无需虚拟 DOM diff,直接操作真实 DOM
相关推荐
编程猪猪侠25 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞29 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架