reactiveフレームワークの構築
pyqt元々のapiが気に食わないので自作reactive fameworkでラップすることになった。これはその情報収集。
言語は英語中国語日本語混在する。
自分は現存フレームワークの構造や設計をしているものの、細かいコードの実現方法がわかってないのでまずそこから。
まずモデル駆動で一番人気のあるreact
そしてfine-grainedのVue
コアは同じくfine-graindedですが、APIデザインにおいてreact哲学を準じたsolidjs
テストに簡単なreactive-systemを書く
参考:
current_subscriber = None
class Signal:
def __init__(self, init_value):
super().__init__()
self._value = init_value
self._subscribers = set()
def get(self):
if current_subscriber:
self._subscribers.add(current_subscriber)
return self._value
def set(self, next_value):
if callable(next_value):
raise NotImplementedError("Callable not supported yet")
if self._value == next_value:
return
self._value = next_value
for subscriber in self._subscribers:
subscriber()
def subscribe(self, cb):
self._subscribers.add(cb)
def create_effect(cb):
global current_subscriber
prev_subscriber = current_subscriber
current_subscriber = cb
cb()
current_subscriber = prev_subscriber
def create_signal(value):
s = Signal(value)
return (s.get, s.set)
if __name__ == "__main__":
import threading
def set_timeout(func, sec):
timer = threading.Timer(sec, func)
timer.start()
name, set_name = create_signal("Hello")
create_effect(lambda: print(name()))
set_timeout(lambda: set_name("World"), 3)
solidjsのレンダリングプロセス
SolidJS
solid/src/render/components.ts - line24
Component
=> A function that take props and return a JSX.Element
CreateComponent()
function createComponent() {
// ...
return untrack(() => Comp(...))
}
untrack()
Listener
is a global variable
一旦cuurent_listenerのlinkを解除して、fn()の実行結果を返してからまたlisnterを登録する
export function untrack<T>(fn: Accessor<T>): T {
const listener = Listener;
Listener = null
try {
return fn()
} finnaly {
Listenr = listener
}
}
なぜnullにするのか、こちらを参照してわかるように、solidjsは今実行すべきlistnerをグローバル変数として保存してるので、listenerを実行したくないときは一旦外すようにしている、だから関数名もuntrack
onMount
APIがまさにこれに基づいて実現している
export function onMount(fn: () => void) {
createEffect(() => untrack(fn));
}
そしてこのlibのrender関数をcallして、domを更新する
この記事はレンダリングプロセスを細かく分解してくれている
中国語ですが30記事でsolidjsのソースコードを手取り足取りで一緒に見てくれる記事があった(後ほど日本語に訳すかもしれない)
solidjsのチョコとしたcompiler部分
dom-expresssions & solid & rxcore
dom-expressions は solid の一番コアになっているlibの一つである、が、solidjsのrepoに入っていない。
作者のryan氏の野望でもあると思うが、まさにdom-expressionsのrepoのreadmeの一番下に書いてあるように
My goal here is to better understand and generalize this approach to provide non Virtual DOM alternatives to developing web applications.
未来のバーチャルDOMのないweb開発のための基盤になれたらと思います。
だからsolidjsと別に単独なlibとして出しているかもしれません。(あくまで筆者個人の憶測)
で本題に入るですが、solidjsのコードを読んでると、rxcoreという訳のわからないimportが出てくると思いますが、これはbebelのimport nameを書き換えるpluginと一緒に使うことが前提になっているからです。
solidjs-repo/packages/solid/babel.config.js
を覗いてみると
[
"babel-plugin-transform-rename-import",
{
replacements: [
{
original: "rxcore",
replacement: path.join(__dirname, "../../packages/solid/web/src/core")
},
{
original: "^solid-js$",
replacement: path.join(__dirname, "src"),
}
]
}
],
とあります。
そしてsolidjsのソースコードがdom-expressionsをimportしており
solidjs-repo/packages/solid/web/src/client.ts
export * from "dom-expressions/src/client.js";
そのdom-expressionsの中もこう書いているので
import {
root,
effect,
memo,
getOwner,
createComponent,
sharedConfig,
untrack,
mergeProps
} from "rxcore";
結果から言うと、このsolidjs構築においては、dom-expressionsのこのimportは、solidjs-repo/packages/solid/web/src/core.ts
からimportすることになっている。つまりビルド時は以下にようイメージです
import {
root,
effect,
memo,
getOwner,
createComponent,
sharedConfig,
untrack,
mergeProps
} from "solidjs-repo/packages/solid/web/src/core.ts";
この分かりづらいが巧妙なやり方で、dom-expressions libが"未来のパッケージ"をimportすることができるようになっている。つまり他の人でも
{
root,
effect,
memo,
getOwner,
createComponent,
sharedConfig,
untrack,
mergeProps
}
これらの方法が実装されたライブラリーであれば、誰でもこのdom-expressions libが利用できると言うことになる。
まだsolidjsの構築に戻ると、dom-expressionsは実際solidの中に実装されているこれらのmethodを利用してreactivite性を提供している。
solidjs -> reactive apiの提供
dom-expressions -> これらのapiを実際のdom操作に変換
だからsolidjsを理解するには、dom-expressionsを理解しないといけない。
dom-expressions: insert()
ここで一番重要なinsert functionのロジックを簡単にしていきます
元のコード
export function insert(parent, accessor, marker, initial) {
if (marker !== undefined && !initial) initial = [];
if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
effect(current => insertExpression(parent, accessor(), current, marker), initial);
}
上で知っている通り、solidjsにおいては、effect = createRenderEffect
、なので色々省いて、この関数はこう書ける
function insert() {
createRenderEffect(...)
}
dom-expressions: render()
solidjsや反応式のライブライーの入り口となる関数
元の関数
export function render(code, element, init, options = {}) {
if ("_DX_DEV_" && !element) {
throw new Error(
"The `element` passed to `render(..., element)` doesn't exist. Make sure `element` exists in the document."
);
}
let disposer;
root(dispose => {
disposer = dispose;
element === document
? code()
: insert(element, code(), element.firstChild ? null : undefined, init);
}, options.owner);
return () => {
disposer();
element.textContent = "";
};
}
省いて置き換えて
export 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 = "";
};
}
solidjsがexport * from "..."
を多用しているので、どっからこの関数が来ているのか探すのがガチで苦労する
ここのcreateRootはsolidjs-repo/packages/solid/src/reactive/signal.ts
の中にある
export function createRoot<T>(fn: RootFunction<T>, detachedOwner?: typeof Owner): T {}
これから三項演算子をできるだけ書かないようにしよう。。。特に繋がっているやつ(見てて目が回るw
次回からさらにレンダリングロジックを紐解いいく
https://github.com/ZacharyL2/mini-react/blob/master/src/mini-react.ts)
これに基づいてreactのrender-processを理解してみた
window.requestIdleCallback(workLoop)でworkLoopを起動させる
↓
React.render(document.getElementById("root")!, <App />);
↓
build container fiber node(id=rootのあれ)
↓
nextUnitOfWork = 先buildしたやつ
↓
workLoopが常にremainTimeがある時動いているの、nextUnitOfWorkがnullでなくなったことを検知
↓
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)をやる
↓
performUnitOfWorkのreturnがfiber-treeの次のものになっているので、workLoopとnextUnitOfWorkで再帰的なことを再帰callせずにやっている(実際は違うのですが、一旦この理解で良い)
performUnitOfWorkを展開していく
// 実際このようにcallしているので
performUnitOfWork(nextUnitOfWork)
だから実際の処理を見ていく時はこのように理解できる
const performUnitOfWork = (nextUnitOfWork: FiberNode) => {
switch {
// FiberNodeの種類によって違う前処理をしてから
reconcileChildren(nextUnitOfWork, fiberNode.props.children)
// をcallする
}
// reconcileChildren(nextUnitOfWork, fiberNode.props.children)を完了
if (fiberNode.child) {
return fiberNode.child; // reactのfiber-treeの中では, child = children[0], sibling = children[1,2,3...]
}
// paramaterを変更しないようにlocal変数を作ってparamaterを一旦保存しとく
let nextFiberNode: FiberNode | undefined = nextUnitOfWork;
while (typeof nextFiberNode !== "undefined") {
if (nextFiberNode.sibling) {
return nextFiberNode.sibling; // childrenがまだ残っているのなら関数ごとreturnしてworkLoopに返す
}
nextFiberNode = nextFiberNode.return; // returnがfiber-treeの中のparentの意味
// つまりこのwhileはparent == undefinedまでloopする
// つまりもしこのnodeの全childrenの処理が終わったら何もせずにwhileを抜ける
}
return null; // nullをworkLoopに返す(詳細は次のworkLoopで)
}
workLoop
const workLoop: IdleRequestCallback = (deadline) => {
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
// remain timeがあればperformUnitOfWorkを実行(前回)
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
// performUnitOfWorkはchildrenがなくなると最終的にnullを返してnextUnitOfWork = nullになるので
if (!nextUnitOfWork && wipRoot) {
// その場合これが呼ばれる
commitRoot();
}
window.requestIdleCallback(workLoop);
};
次回がさらにperformUnitOfWorkを深く入る
またsolidjsに戻って、solidjsがどうやってlistをマネジメントしているのかが気になって
これが見つかった、ForとIndexはこれに基づいてやっている。