最近刚刚整明白点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
别的用法以及响应式原理。
敬请期待!
如果这篇文章对你有帮助,可以帮我点个赞。十分感谢!