reactivity
SolidJS源码解读 - 用函数闭包实现高性能发布订阅模式 - 掘金
令人唏嘘,细粒度的 DOM 更新在十年前被认为是古老而缓慢的技术,可如今 SolidJS 竟然靠它夺得性能冠军,很大一部分功劳都来自于它成功解决了内存消耗问题。
虽然 SolidJS 的语法与 React 几乎一致,但它响应式的实现是和 Vue 一样的发布订阅模式,接下来我们就来看看它是如何实现的。
发布者 Dep
的实现
createSignal
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
const s: SignalState<T> = {
value,
observers: null,
observerSlots: null,
comparator: options.equals || undefined
};
const setter: Setter<T | undefined> = (value?: unknown) => {
if (typeof value === "function") {
if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue);
else value = value(s.value);
}
return writeSignal(s, value);
};
return [readSignal.bind(s), setter];
}
SolidJS 不像 Vue 那样使用 Object.defineProperty
或 Proxy
数据劫持实现依赖收集,而是用函数闭包的形式保存依赖,函数内返回 getter
和 setter
方法,非常的巧妙,不仅减少了这些 API 的大量内存消耗,也解决了 Proxy
的目标必须是对象的问题 (没错,吐槽的就是 Vue3 的 ref.value
)。尤其在响应式数据是大对象或大数组时,由于 Object.defineProperty
对所有属性的递归监听,会造成严重的性能问题。
SolidJS 则将对象整体作为一个 Signal
,更新 Signal
需要像 React 一样调用 setXXX
方法整体更新,虽然避免了性能问题,但并不完美。
在以下示例中,我们在一个 Signal
中存放待办事项列表。为了将待办事项标记为完成,我们需要用克隆对象替换旧的待办事项。尽管在 JSX 中使用 For
语句会进行差异对比,但仍然造成了不必要的性能浪费
const [todos, setTodos] = createSignal([])
setTodos(pre => pre.map((todo) => (todo.id !== id ? todo : { ...todo, completed: !todo.completed }))
但这并不代表 SolidJS 不能像 Svelte 一样细粒度的更新对象内的属性,否则怎么对得起细粒度响应式框架的称号,在 SolidJS 中,我们可以使用嵌套的 Signal
初始化数据
const initTodos = () => {
const res = []
for (let i = 0; i < 10; i++) {
const [completed, setCompleted] = createSignal(false);
res.push({id: i, completed, setCompleted})
}
setTodos(res)
}
const toggleTodo = (index) => {
todos()[index].setCompleted(!todo.completed())
}
之后我们可以通过调用 setCompleted
来更新,而无需任何额外的差异对比。因为我们确切地知道数据要如何变化, 所以我们可以将复杂性转移到数据而不是视图。
const toggleTodo = (index) => {
todos()[index].setCompleted(!todo.completed())
}
这正是 SolidJS 的魅力所在,基于函数实现的响应式 API 非常自由,既可以避免 Object.defineProperty
的全量监听带来的性能浪费,同时支持自定义的细粒度响应式数据控制,将性能压榨到了极致。
readSignal
export function readSignal(this: SignalState<any> | Memo<any>) {
const runningTransition = Transition && Transition.running;
if (
(this as Memo<any>).sources &&
((!runningTransition && (this as Memo<any>).state) ||
(runningTransition && (this as Memo<any>).tState))
) {
if (
(!runningTransition && (this as Memo<any>).state === STALE) ||
(runningTransition && (this as Memo<any>).tState === STALE)
)
updateComputation(this as Memo<any>);
else {
const updates = Updates;
Updates = null;
runUpdates(() => lookUpstream(this as Memo<any>), false);
Updates = updates;
}
}
if (Listener) {
const sSlot = this.observers ? this.observers.length : 0;
if (!Listener.sources) {
Listener.sources = [this];
Listener.sourceSlots = [sSlot];
} else {
Listener.sources.push(this);
Listener.sourceSlots!.push(sSlot);
}
if (!this.observers) {
this.observers = [Listener];
this.observerSlots = [Listener.sources.length - 1];
} else {
this.observers.push(Listener);
this.observerSlots!.push(Listener.sources.length - 1);
}
}
if (runningTransition && Transition!.sources.has(this)) return this.tValue;
return this.value;
}
Transition
的逻辑非主分支逻辑可跳过不看 (此后的这部分逻辑都可跳过),重点看 Listener
分支内的逻辑即可。
此处的 this
是 createSignal
中创建的响应式数据 s
,可以看出,this.observers
就是 Dep
,正如节点级更新框架之名,一个数据对应一个 Dep
。Dep
中存放复数的观察者 Wathcer
(也就是此处的 Listener
),那么 Listener
从何而来,那么就要从看接下来的观察者 Wathcer
的实现了
观察者 Wathcer
的实现
创建 Wathcer
是在 effect
函数内实现的,effect
函数是 createRenderEffect
函数的别名
createRenderEffect
export function createRenderEffect<Next, Init>(
fn: EffectFunction<Init | Next, Next>,
value?: Init,
options?: EffectOptions
): void {
const c = createComputation(fn, value!, false, STALE, "_SOLID_DEV_" ? options : undefined);
if (Scheduler && Transition && Transition.running) Updates!.push(c);
else updateComputation(c);
}
createComputation
createComputation
看似很长,但剔除无关逻辑后只走了这一行
if (!Owner.owned) Owner.owned = [c];`
根据 fn
创建出的 Computation
赋值给 Owner.owned
,让 c
和 Owner
形成相互引用的关系。随后返回 c
进入 updateComputation(c)
函数
updateComputation
updateComputation
函数中将 node
,也就是 createComputation
返回的 c
赋给了 Listener
,我们仿佛看见了胜利的曙光
runComputation
重点关注这一行
nextValue = node.fn(value);
node.fn
则是 () => _el$.value = text()
,在访问 text()
时调用 readSignal
,至此完成了整个依赖收集的过程。
可以看出 SolidJS 作者的 JavaScript 基本功非常扎实,对 【闭包】【函数是 JavaScript 中的一等公民】 两项特性运用得淋漓尽致,优雅的实现了高性能发布订阅模式框架。