最近刚刚整明白点Svelte
感觉整个世界都清净了,但是昨天,有人给我介绍了SolidJS
,
当时我心想:这又是啥玩意啊!
经过一番深入交流才知道,居然又是个前端框架。
“还有完没完了,一个接一个的框架啥时候是个头啊!”
不过本着给大家踩坑避雷的精神,我又秉烛夜读,通宵达旦研究了一番。
🚀模仿?超越?
💎写法
先上代码
import { render } from "solid-js/web";
import { createSignal, createMemo, createEffect } from "solid-js";
function Counter() {
// 定义变量
const [count, setCount] = createSignal(0);
// 缓存中间值
const fib = createMemo(() => {
console.log('Calculating Fibonacci');
return (count() * 2 + 10);
});
// 执行副作用
createEffect(() => { console.log("The count is now", count()); });
return (
<div onClick={() => setCount(() => count() + 1)}>
Count: {count()}
fib Count: {fib()}
</div>
);
}
render(() => <Counter />, document.getElementById('app'));
是不是很熟悉,这不就是React
吗?
难道这是React
被抄袭的最惨的一次吗?
是的,官网明确告诉你,它会让你感觉既熟悉又现代。
和React
类似的hook
写法,一样的Jsx
模板语法,熟悉吧?
不过,当你揭开它神秘的面纱,你会发现里面居然是你曾经的神——Vue
!
💎响应式原理
因为它的响应式官方称为primitive
,是基于Proxy
的发布订阅模式的API
,
primitive
的响应式主要包括Signal
、Memo
和 Effect
,对应的接口如下
// 定义变量
const [count, setCount] = createSignal(0);
// 缓存中间值
const fib = createMemo(() => (count() * 2 + 10));
// 执行副作用
createEffect(() => { console.log("The count is now", count()); });
来看看createSignal
的大致逻辑
function createSignal(value) {
const subscribers = new Set();
const read = () => {
const listener = getCurrentListener();
if (listener) subscribers.add(listener);
return value;
};
const write = nextValue => {
value = nextValue;
for (const sub of subscribers) sub.run();
};
return [read, write];
}
在每次read()
的地方收集listener
,做为订阅者,每次write()
的时候作为发布者,通知每个listener
更新数据。
SolidJS的发布订阅模式也是基于
Proxy
的。下篇文章会做详细的对比。
和React
不同的是,reead
是个方法,这也是前面模板使用count()
,而不是count
的原因。
createMemo
和createEffect
会自动收集依赖项,每次触发依赖项listener
的更新时,都会重新执行。
到这,是不是觉得,这太简单了吧,这不就是React
和Vue
的结合体嘛!
欢欣之后,你又想和它谈心,可当你走近它的心,又发现了你最近心心念念的Svelte
的影子!
💎模板编译原理
上述例子的编译结果如下: (编译结果可以在官网的演练场Output
查看)
import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
const _tmpl$ = /*#__PURE__*/_$template(`<div>Count: <!>fib Count: </div>`, 3);
import { render } from "solid-js/web";
import { createSignal, createMemo, createEffect } from "solid-js";
function Counter() {
// 定义变量
const [count, setCount] = createSignal(0); // 缓存中间值
const fib = createMemo(() => {
console.log('Calculating Fibonacci');
return count() * 2 + 10;
}); // 执行副作用
createEffect(() => {
console.log("The count is now", count());
});
return (() => {
const _el$ = _tmpl$.cloneNode(true),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_el$.$$click = () => setCount(() => count() + 1);
_$insert(_el$, count, _el$4);
_$insert(_el$, fib, null);
return _el$;
})();
}
render(() => _$createComponent(Counter, {}), document.getElementById('app'));
_$delegateEvents(["click"]);
简单分析之后可以得出结论如下:
- 🚗首先,使用
_$template
创建纯静态的jsx
模板, - 🚗接着,通过
cloneNode
方法,以及firstChild
等属性获取动态元素, - 🚗紧接着,为每个元素绑定对应的方法
- 🚗再接着,将动态的片段使用
_$insert
方法插入模板中,注意到count
和fib
都是未执行的函数。 - 🚗接着使用
$createComponent
包裹组件。 - 🚗最后组装
render
方法,将组件包装成函数,和根节点一起作为render
方法的参数。
这和Svelte
的编译结果有两个十分类似的地方:
- 💎将每动态片段的更新范围,精确到了原子级别。
- 💎它们的返回值都没有
虚拟DOM
_$insert(_el$, count, _el$4);
_$insert(_el$, fib, null);
// Svelte编译之后create_fragment返回的p方法,也就是update方法
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0]);
},
💎运行时原理
在运行时阶段,会执行render
方法,render
方法如下
function render(code, element, init, options = {}) {
let disposer;
createRoot(dispose => {
disposer = dispose;
element === document
? code()
: insert(
element,
code(),
element.firstChild ? null : undefined,
init
);
}, options.owner);
return () => {
disposer();
element.textContent = "";
};
}
代码都会将编译的() => _$createComponent(Counter, {})
执行,并挂载到document.getElementById('app')
由于在编译阶段还没有建立变量的响应式机制,执行render
方法后,才会通过发布订阅模式创建响应式变量,每次调用write()
、或者触发事件时,导致变量更新,以及对应的元素节点
使用_$insert
更新DOM
。
看着SolidJS
朴素的运行时原理,
你才回过神来,发现你曾经邂逅过的一切,它早已拥有,
你爱慕着的,也为你准备完毕,
最后你不禁感叹,SolidJS
才是你那个:
『众里寻他千百度,慕然回首,那人却在,灯火阑珊处』
的框架啊!
你刚想抓住它,它却早已隐入了那灯影里!!!
好了好了,不做梦了,今天的分享就这些了,
下篇文章会介绍下SolidJS
别的用法以及响应式原理。
敬请期待!