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
相关推荐
风中飘爻12 分钟前
JavaScript:BOM编程
开发语言·javascript·ecmascript
恋猫de小郭14 分钟前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter
清岚_lxn5 小时前
原生SSE实现AI智能问答+Vue3前端打字机流效果
前端·javascript·人工智能·vue·ai问答
ZoeLandia5 小时前
Element UI 设置 el-table-column 宽度 width 为百分比无效
前端·ui·element-ui
橘子味的冰淇淋~6 小时前
解决 vite.config.ts 引入scss 预处理报错
前端·vue·scss
萌萌哒草头将军7 小时前
💎这么做,cursor 生成的代码更懂你!💎
javascript·visual studio code·cursor
小小小小宇7 小时前
V8 引擎垃圾回收机制详解
前端
lauo8 小时前
智体知识库:ai-docs对分布式智体编程语言Poplang和javascript的语法的比较(知识库问答)
开发语言·前端·javascript·分布式·机器人·开源
拉不动的猪8 小时前
设计模式之------单例模式
前端·javascript·面试
一袋米扛几楼988 小时前
【React框架】什么是 Vite?如何使用vite自动生成react的目录?
前端·react.js·前端框架