🐡
Svelte と Vue Vapor Mode の出力を比較する
Vue Vapor Mode をやる可能性があるので、調べることにした。
Svelteの知識があるので、自分の為のキャッチアップとして、Vue Vapor と Svelte 出力の比較を行う。SSR時の処理などは追ってない。
試した場所
静的コンポーネント
まず何もしない Static なコンポーネントで比較する。
Svelte
<h1>Hello</h1>
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
var root = $.template(`<h1>Hello</h1>`);
export default function App($$anchor) {
var h1 = root();
$.append($$anchor, h1);
}
静的なテンプレートがあり、これをそのまま注入する。
差分計算等は svelte/internal/*
側にある。誤解している人もいるが、Svelte もコンパイル時に全部を消し去るのではなく最低限のランタイムはある。
Vue Vapor
<template>
<h1>Hello</h1>
</template>
const __sfc__ = {};
import { template as _template } from 'vue/vapor';
const t0 = _template("<h1>Hello</h1>")
function render(_ctx) {
const n0 = t0()
return n0
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__
出力内容としては同等
部分的な動的更新
Svelte
<script lang=ts>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
出力
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
var root = $.template(`<h1></h1>`);
export default function App($$anchor) {
let name = 'world';
var h1 = root();
h1.textContent = `Hello ${name ?? ""}!`;
$.append($$anchor, h1);
}
テンプレート展開後に、 h1 の textContent を注入している。ここの組み立てが動的。
Vue Vapor
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('World')
</script>
<template>
<h1>Hello, {{ name }}</h1>
</template>
/* Analyzed bindings: {
"ref": "setup-const",
"name": "setup-ref"
} */
import { defineComponent as _defineComponent } from 'vue/vapor'
import { ref } from 'vue'
const __sfc__ = /*@__PURE__*/_defineComponent({
vapor: true,
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const name = ref('World')
const __returned__ = { name }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
});
import { renderEffect as _renderEffect, setText as _setText, template as _template } from 'vue/vapor';
const t0 = _template("<h1></h1>")
function render(_ctx) {
const n0 = t0()
_renderEffect(() => _setText(n0, "Hello, ", _ctx.name))
return n0
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__
ほとんど同じ。 _renderEffect
の部分に副作用が押し込まれている。
実装はこのあたり
const effect = new ReactiveEffect(() =>
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
)
// ...
effect.run()
// https://github.com/vuejs/vue-vapor/blob/main/packages/reactivity/src/effect.ts#L150
run(): T {
// TODO cleanupEffect
if (!(this.flags & EffectFlags.ACTIVE)) {
// stopped during cleanup
return this.fn()
}
this.flags |= EffectFlags.RUNNING
cleanupEffect(this)
prepareDeps(this)
const prevEffect = activeSub
const prevShouldTrack = shouldTrack
activeSub = this
shouldTrack = true
try {
return this.fn()
} finally {
if (__DEV__ && activeSub !== this) {
warn(
'Active effect was not restored correctly - ' +
'this is likely a Vue internal bug.',
)
}
cleanupDeps(this)
activeSub = prevEffect
shouldTrack = prevShouldTrack
this.flags &= ~EffectFlags.RUNNING
}
}
バッチ化してそう。
というのが、ここに解説があった。
DOMとの双方向通信
時分が試した時点では v-model が動かなかったので、簡単なバインディングを自分で書いた。
<script setup lang="ts">
import { ref, computed } from 'vue'
const input = ref('text')
const onInput = (ev: InputEvent) => {
input.value = (ev.target as HTMLInputElement).value;
}
const mes = computed(() => `Hello, ${input.value}`)
</script>
<template>
<input
:value="input"
@input="onInput"
/>
<p>{{mes}}</p>
</template>
/* Analyzed bindings: {
"ref": "setup-const",
"computed": "setup-const",
"input": "setup-ref",
"onInput": "setup-const",
"mes": "setup-ref"
} */
import { defineComponent as _defineComponent } from 'vue/vapor'
import { ref, computed } from 'vue'
const __sfc__ = /*@__PURE__*/_defineComponent({
vapor: true,
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const input = ref('text')
const onInput = (ev) => {
input.value = (ev.target ).value;
}
const mes = computed(() => `Hello, ${input.value}`)
const __returned__ = { input, onInput, mes }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
});
import { delegate as _delegate, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp, setText as _setText, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
const t0 = _template("<input>")
const t1 = _template("<p></p>")
_delegateEvents("input")
function render(_ctx) {
const n0 = t0()
const n1 = t1()
_delegate(n0, "input", () => _ctx.onInput)
_renderEffect(() => _setDynamicProp(n0, "value", _ctx.input))
_renderEffect(() => _setText(n1, _ctx.mes))
return [n0, n1]
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__
_delegateEvents("input")
このイベントが再レンダーをキックするという目印。
n0(=t0=template(...))
にハンドラ登録し、input と t1 に値の再計算処理がある。
感想
コンパイル時に値を確定させるまでのツリーを作って、再計算部分を払い出しておくというのはVue VaporもSvelteも同じ。内部イベントモデルは違いそうだが、ユーザーが気にする部分は多分はない。
Vue Vapor で細かいのを色々呼ぶと未実装で落ちた。ここの互換性を上げてる最中という認識。
Discussion