跳至主要內容

学不完的框架,🐔啄不完的米,SolidJS,你到底爱谁?😘

萌萌哒草头将军大约 5 分钟前端JavaScriptSolidJS

10分钟快速了解SolidJS原理

最近刚刚整明白点Svelte感觉整个世界都清净了,但是昨天,有人给我介绍了SolidJS

上篇:Svelte原理和进阶看这篇就够了open in new window

当时我心想:这又是啥玩意啊!

image.png

经过一番深入交流才知道,居然又是个前端框架。

image.png

“还有完没完了,一个接一个的框架啥时候是个头啊!”

image.png

不过本着给大家踩坑避雷的精神,我又秉烛夜读,通宵达旦研究了一番。

image.png

🚀模仿?超越?

💎写法

先上代码

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

image.png

💎响应式原理

因为它的响应式官方称为primitive,是基于Proxy的发布订阅模式的API

primitive的响应式主要包括SignalMemoEffect,对应的接口如下

 // 定义变量
  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的原因。

createMemocreateEffect会自动收集依赖项,每次触发依赖项listener的更新时,都会重新执行。

到这,是不是觉得,这太简单了吧,这不就是ReactVue的结合体嘛!

欢欣之后,你又想和它谈心,可当你走近它的心,又发现了你最近心心念念的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方法插入模板中,注意到countfib都是未执行的函数
  • 🚗接着使用$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才是你那个:

『众里寻他千百度,慕然回首,那人却在,灯火阑珊处』

的框架啊!

你刚想抓住它,它却早已隐入了那灯影里!!!

好了好了,不做梦了,今天的分享就这些了,

image.png

下篇文章会介绍下SolidJS别的用法以及响应式原理。

敬请期待!欢迎关注我

image.png