在最新的Benchmark中,能够看到SolidJS以近乎原生的1.05夺得功能冠军,号称无Virtual DOM的细粒度呼应式结构SolidJS终究为什么这么快,今天我就来解读一下。
1. Virtual DOM真的高效吗
1.1 react和vue的Virtual DOM
- React对数据的处理是不可变(
immutable
):详细表现是整树更新,更新时,不关注是详细哪个状况改变了,只需有状况改动,直接整树 Diff 找出差异进行对应更新。 - Vue对数据的处理是呼应式、可变的(
mutable
):更新时,能够准确知道是哪些状况产生了改动,能够完结准确到组件级的更新。
Rich Harris 在设计 Svelte 的时分没有选用 Virtual DOM 是由于觉得 Virtual DOM Diff 的进程是十分低效的。人们觉得 Virtual DOM 高效的一个理由,便是它不会直接操作原生的 DOM 节点。在浏览器傍边,JavaScript的运算在现代的引擎中十分快,但DOM自身是十分缓慢的东西。当你调用原生 DOM API 的时分,浏览器需求在JavaScript引擎的语境下去触摸原生的DOM的完结,这个进程有适当的功能损耗。
比如说,下面的比如中,React 为了更新 message
对应的DOM节点,需求做n屡次遍历,才干找到详细要更新哪些节点。
为了处理这个问题,React 提供 pureComponent
, shouldComponentUpdate
, useMemo
, useCallback
让开发者来操心哪些 subtree
是需求从头烘托的,哪些是不需求从头烘托的。究其实质,是由于 React 选用的 JSX 语法过于灵敏,难以了解开发者写出代码所代表的意义,没有办法做出优化。
而 Vue2 尽管完结了准确到组件等级的呼应式更新,但关于组件内部的DOM节点还是需求进行n屡次遍历的 Diff 运算。直到Vue3中学习Svelte的思路运用静态提升对烘托函数做 AOT (ahead-of-time,能够了解为预编译) 优化后,才使得功能进一步提升。
1.2 更新粒度对比
-
运用级:有状况改动,就更新整个运用,生成新的虚拟DOM树,与旧树进行
Diff
(代表作:React,当然了,现在它的虚拟DOM已晋级为了Fiber
)。 - 组件级:与上方相似,只不过粒度小了一个等级(代表作:Vue2 及之后的版别)。
- 节点级:状况更新直接与详细的更新节点的操作绑定(代表作 vue1.x 、Svelte、SolidJS)。
Vue1.x 年代,关于数据是每个生成一个对应的 Wather
,更新颗粒度为节点等级,但这样创立许多的 Wather
会造成极大的功能开支,因此在 Vue2.x 年代,经过引进虚拟DOM优化呼应,改为了组件级颗粒度的更新。
而关于 React 来说,虚拟 DOM 便是至关重要的部分,乃至是核心,React是归于运用等级的更新,因此整个DOM树的更新开支是极大的,所以这儿关于 Virtual DOM Diff 的运用便是极其必要的。
1.3 是否选用虚拟DOM
这个挑选是与上边选用何种粒度的更新设计紧密相关的:
-
是:对运用级的这种更新粒度,虚拟DOM简直是必需品,由于在 Diff 前它并不能得到此次更新的详细节点信息,必需要经过 Virtual DOM Diff 筛选出最小差异,不然整树
append
对功能是灾难(代表结构:React、vue)。 - 否:对节点级更新粒度的结构来说,没有必要选用虚拟DOM(代表作:Svelte、SolidJS)
2.预编译 (AOT) 阶段的代码优化
- JSX阵营 :React/SolidJs
- Template阵营 :Vue/Svelte
2.1 JSX 优缺点
JSX 具有 JavaScript 的完好表现力,十分具有表现力,能够构建十分复杂的组件。
但是灵敏的语法,也意味着引擎难以了解,无法预判开发者的用户目的,然后难以优化功能。你很可能会写出下面的代码:
在运用 JavaScript 的时分,编译器不可能hold住一切可能产生的工作,由于 JavaScript 太过于动态化使得很难做目的剖析。也有人对这块做了许多尝试,但从实质上来说很难提供安全的优化。
2.2 Template优缺点
Template模板是一种十分有束缚的语言,你只能以某种方式去编写模板。
例如,当你写出这样的代码的时分,编译器能够马上明白: ”哦!这些 p 标签的次序是不会变的,这个 id 是不会变的,这些 class 也不会变的,唯一会变的便是这个“ 。
在编译时,编译器对你的目的能够做更多的预判,然后给它更多的空间去做履行优化。左边 template
中,其他一切内容都是静态的,只要 name
可能会产生改动。
2.3 SolidJS的优化
SolidJS选用的计划是:在JSX的基础上做了一层规范,中文译名为操控流,防止了编译器难以了解的代码。这样即学习了 template
更容易做编译阶段优化的优势,又保留了JSX的灵敏性,以 For
为例
假如不运用引荐的操控流句子
能够看到,不运用引荐的操控流句子时,SolidJS 将流大括号内的流程句子编译为了一整个变量,每次 list
改动时会导致整个列表从头烘托,功能低下
那么For
内部做了什么呢,咱们打开源码看看
export function For<T, U extends JSX.Element>(props: {
each: readonly T[] | undefined | null | false;
fallback?: JSX.Element;
children: (item: T, index: Accessor<number>) => U;
}) {
const fallback = "fallback" in props && { fallback: () => props.fallback };
return createMemo(
mapArray<T, U>(() => props.each, props.children, fallback ? fallback : undefined)
);
}
能够看到 For
的回来是 createMemo
包裹的 mapArray
办法,createMemo
相似 React 的 useMemo
,mapArray
中做的工作便是会比较新老数组,仅更新改变的节点,并尽可能的从旧节点中复用,和 Diff 的思路很像。
3. 呼应式的完结
令人唏嘘,细粒度的DOM更新在十年前被认为是陈旧而缓慢的技术,可如今 SolidJS 居然靠它夺得功能冠军,很大一部分功劳都来自于它成功处理了内存耗费问题。
尽管 SolidJS 的语法与 React 简直一致,但它呼应式的完结是和 Vue 相同的发布订阅形式,接下来咱们就来看看它是如何完结的。
3.1 发布者 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
};
if ("_SOLID_DEV_" && !options.internal)
s.name = registerGraph(options.name || hashValue(value), s as { value: unknown });
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
的完结了
3.2 观察者 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
function createComputation<Next, Init = unknown>(
fn: EffectFunction<Init | Next, Next>,
init: Init,
pure: boolean,
state: number = STALE,
options?: EffectOptions
): Computation<Init | Next, Next> {
const c: Computation<Init | Next, Next> = {
fn,
state: state,
updatedAt: null,
owned: null,
sources: null,
sourceSlots: null,
cleanups: null,
value: init,
owner: Owner,
context: null,
pure
};
if (Transition && Transition.running) {
c.state = 0;
c.tState = state;
}
if (Owner === null)
"_SOLID_DEV_" &&
console.warn(
"computations created outside a `createRoot` or `render` will never be disposed"
);
else if (Owner !== UNOWNED) {
if (Transition && Transition.running && (Owner as Memo<Init, Next>).pure) {
if (!(Owner as Memo<Init, Next>).tOwned) (Owner as Memo<Init, Next>).tOwned = [c];
else (Owner as Memo<Init, Next>).tOwned!.push(c);
} else {
if (!Owner.owned) Owner.owned = [c];
else Owner.owned.push(c);
}
if ("_SOLID_DEV_")
c.name =
(options && options.name) ||
`${(Owner as Computation<any>).name || "c"}-${
(Owner.owned || (Owner as Memo<Init, Next>).tOwned!).length
}`;
}
if (ExternalSourceFactory) {
const [track, trigger] = createSignal<void>(undefined, { equals: false });
const ordinary = ExternalSourceFactory(c.fn, trigger);
onCleanup(() => ordinary.dispose());
const triggerInTransition: () => void = () =>
startTransition(trigger).then(() => inTransition.dispose());
const inTransition = ExternalSourceFactory(c.fn, triggerInTransition);
c.fn = x => {
track();
return Transition && Transition.running ? inTransition.track(x) : ordinary.track(x);
};
}
return c;
}
createComputation
看似很长,但剔除无关逻辑后只走了这一行
if (!Owner.owned) Owner.owned = [c];
依据 fn
创立出的Computation
赋值给 Owner.owned
,让c
和Owner
构成彼此引证的关系。随后回来 c
进入updateComputation(c)
函数
updateComputation
function updateComputation(node: Computation<any>) {
if (!node.fn) return;
cleanNode(node);
const owner = Owner,
listener = Listener,
time = ExecCount;
Listener = Owner = node;
runComputation(
node,
Transition && Transition.running && Transition.sources.has(node as Memo<any>)
? (node as Memo<any>).tValue
: node.value,
time
);
if (Transition && !Transition.running && Transition.sources.has(node as Memo<any>)) {
queueMicrotask(() => {
runUpdates(() => {
Transition && (Transition.running = true);
runComputation(node, (node as Memo<any>).tValue, time);
}, false);
});
}
Listener = listener;
Owner = owner;
}
updateComputation
函数中将 node
,也便是createComputation
回来的 c
赋给了 Listener
,咱们似乎看见了成功的曙光
runComputation
function runComputation(node: Computation<any>, value: any, time: number) {
let nextValue;
try {
nextValue = node.fn(value);
} catch (err) {
if (node.pure) Transition && Transition.running ? (node.tState = STALE) : (node.state = STALE);
handleError(err);
}
if (!node.updatedAt || node.updatedAt <= time) {
if (node.updatedAt != null && "observers" in (node as Memo<any>)) {
writeSignal(node as Memo<any>, nextValue, true);
} else if (Transition && Transition.running && node.pure) {
Transition.sources.add(node as Memo<any>);
(node as Memo<any>).tValue = nextValue;
} else node.value = nextValue;
node.updatedAt = time;
}
}
重点关注这一行
nextValue = node.fn(value);
node.fn
则是 () => _el$.value = text()
,在访问 text()
时调用readSignal
,至此完结了整个依靠收集的进程。
能够看出SolidJS作者的JavaScript基本功十分扎实,对 【闭包】【函数是JavaScript中的一等公民】 两项特性运用得淋漓尽致,优雅的完结了高功能发布订阅形式结构。
DEMO
最终附上我用 SolidJS 写的DEMO,一款三消小游戏,体会感触是 SolidJS 功能瓶颈的确高 gitee.com/huang-guanz…
参考文献
- SolidJS 反应式 JavaScript 库
- 新式前端结构 Svelte 从入门到原理 – 字节前端的文章 – 知乎
- 功能爆表的SolidJS – 小帅不太帅的文章 – 稀土
- 又一个前端结构 Solid ?功能直逼原生 JS ?- peen的文章 – 稀土