Svelteのコンパイルに関してつらつら
参考
Svelteの概要掴むのはこれ読むだけでよさそう。(こんなに簡潔に誰にもわかり易くかけるのすごい)
一般的なCompilerのながれ
Lexer(ソースをトークンに) -> Parse(トークンをAST) -> Generator(ASTを別言語等(機械語等)に)
Svelteのコンパイルのながれ
下記は上の記事に記載のあるもの(ざっくり)
const template = 'svelte code'
// ast つくる
const ast = parse(template)
const component = new Component(ast)
component.generate()
自分でざっくり理解した点(parse)
ざっくりながれ(関数のコールスタックもどき)
- まずファイルの文頭にくるかもな
<--
とかを歩きながら刈り上げていく。 - その後tagかmustache内でコードの中を歩いていってAST組み上げてる...ですよね?
compile -> parse -> new Parse -> fragment -> Parse.match(tag or mustache) -> tag() or mustache
- JS/CSSのパースはacornとcss-treeにおまかせ
- こちらで頑張ってパースしなければいけないのはsvelteとくゆうのもの(svelte:〇〇)とか{if}/{each}とかとか)
ASTは下記の形式
- うえでも書いたが、下のhtml部分を頑張る
- cssはcss_treeの結果をいれる
- instance(JS)もacornの結果をいれる
Component
正直TanLiHauさんの記事読まないとよく理解できなかった。(今でもいまいちこの全体の処理が詳細までは理解できていないとおもう。)
Componentインスタンスの例
Component {
ignore_stack: [],
vars: [
{
name: 'text',
initialised: true,
writable: true,
imported: false,
referenced: true,
hoistable: true
}
],
var_lookup: Map(1) {
'text' => {
name: 'text',
initialised: true,
writable: true,
imported: false,
referenced: true,
hoistable: true
}
},
imports: [],
exports_from: [],
instance_exports_from: [],
hoistable_nodes: Set(1) {
Node {
type: 'VariableDeclaration',
start: 12,
end: 30,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
}
},
node_for_declaration: Map(1) {
'text' => Node {
type: 'VariableDeclaration',
start: 12,
end: 30,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
}
},
partly_hoisted: [],
fully_hoisted: [
Node {
type: 'VariableDeclaration',
start: 12,
end: 30,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
}
],
reactive_declarations: [],
reactive_declaration_nodes: Set(0) {},
has_reactive_assignments: false,
injected_reactive_declaration_vars: Set(0) {},
helpers: Map(0) {},
globals: Map(0) {},
indirect_dependencies: Map(0) {},
elements: [
Element {
start: 42,
end: 80,
type: 'Element',
attributes: [Array],
actions: [],
bindings: [],
classes: [],
styles: [],
handlers: [],
lets: [],
intro: null,
outro: null,
animation: null,
name: 'h1',
namespace: null,
scope: [TemplateScope],
children: [Array],
prev: [Text],
next: [Text]
}
],
aliases: Map(0) {},
used_names: Set(1) { 'Component' },
globally_used_names: Set(0) {},
slots: Map(0) {},
slot_outlets: Set(0) {},
name: { type: 'Identifier', name: 'Component' },
stats: Stats {
start_time: 331363032.89668,
stack: [],
timings: [ [Object], [Object] ],
current_children: [ [Object], [Object] ],
current_timing: undefined
},
warnings: [],
ast: {
html: { start: 1, end: 80, type: 'Fragment', children: [Array] },
css: {
type: 'Style',
start: 82,
end: 132,
attributes: [],
children: [Array],
content: [Object]
},
instance: {
type: 'Script',
start: 1,
end: 40,
context: 'default',
content: [Node]
},
module: undefined
},
source: '\n' +
'<script>\n' +
' let text = "word";\n' +
'</script>\n' +
'\n' +
'<h1 class="greeting">Hello {text}</h1>\n' +
'\n' +
'<style>\n' +
' .greeting {\n' +
' color: red;\n' +
' }\n' +
'</style>\n',
compile_options: { generate: 'dom', dev: false, enableSourcemap: true },
original_ast: {
html: { start: 1, end: 80, type: 'Fragment', children: [Array] },
css: {
type: 'Style',
start: 82,
end: 132,
attributes: [],
children: [Array],
content: [Object]
},
instance: {
type: 'Script',
start: 1,
end: 40,
context: 'default',
content: [Object]
},
module: undefined
},
file: undefined,
locate: [Function: locate],
stylesheet: Stylesheet {
children: [ [Rule] ],
keyframes: Map(0) {},
nodes_with_css_class: Set(1) { [Element] },
source: '\n' +
'<script>\n' +
' let text = "word";\n' +
'</script>\n' +
'\n' +
'<h1 class="greeting">Hello {text}</h1>\n' +
'\n' +
'<style>\n' +
' .greeting {\n' +
' color: red;\n' +
' }\n' +
'</style>\n',
ast: {
html: [Object],
css: [Object],
instance: [Object],
module: undefined
},
filename: undefined,
dev: false,
id: 'svelte-9led3h',
has_styles: true
},
component_options: {
immutable: false,
accessors: false,
preserveWhitespace: false,
namespace: undefined
},
namespace: undefined,
tag: 'Component',
ignores: undefined,
instance_scope: Scope {
parent: null,
block: false,
declarations: Map(1) { 'text' => [Node] },
initialised_declarations: Set(1) { 'text' },
references: Set(1) { 'text' }
},
instance_scope_map: WeakMap { <items unknown> },
fragment: Fragment {
start: 1,
end: 80,
type: 'Fragment',
scope: TemplateScope {
owners: Map(0) {},
parent: undefined,
names: Set(0) {},
dependencies_for_name: Map(0) {}
},
children: [ [Text], [Text], [Element], [Text] ]
}
}
- ast.instance(=js)を走査して変数を(reactive(dependencies))をしっておく
- template(ast.html)を走査していってFragment木にする
- もう一回ast.instance(=js)を走査する。hoist(変数一番上にもってきたり)したりする。
- style tagをいれていく?(中身はあまり読んでいない)
メモメモ(Expressionのとこにちゅうもく)
<ref *1> MustacheTag {
start: 119,
end: 125,
type: 'MustacheTag',
expression: Expression {
type: 'Expression',
references: Set(1) { 'text' },
dependencies: Set(1) { 'text' },
contextual_dependencies: Set(0) {},
declarations: [],
uses_context: false,
node: Node {
type: 'Identifier',
start: 120,
end: 124,
loc: [SourceLocation],
name: 'text'
},
template_scope: TemplateScope {
owners: Map(0) {},
parent: undefined,
names: Set(0) {},
dependencies_for_name: Map(0) {}
},
owner: [Circular *1],
scope: Scope {
parent: null,
block: false,
declarations: Map(0) {},
initialised_declarations: Set(0) {},
references: [Set]
},
scope_map: WeakMap { <items unknown> }
},
コードよんでて迷子になった........ので他のソースからもっとインプットする(音声が...)
コンパイルてよりもコンポーネントが作られてから、どうやってDOMが組み立てられて、その後更新されてるかの説明てかんじだと思う
以下上のビデオから引用
<html>: a fragment which has
- c(): create elements
- m(): to mount the elements
- d(): to detach the elsements
- p(): to update elements with set_** function
and call init to create the svelteComponent
<style>: do both of
- add a css class, output css
- attach class to DOM element with setAttrirbute
<script>: either do
- verbatim(copied) pasting the js code
- passing it in an instance with acess to $$self, $$props, $$invalidate
- transform all assignments with $$invalidate
- return all top scope variables for $$.cts
v2のドキュメント(v1はdocないらしいhttps://github.com/sveltejs/v2.svelte.dev/issues/281)
概要はわかった。コード(AST -> Componentの辺から)はいまいち理解できていない。
とりあえず考古学で最初のコミットからよんでみている。
ざっくり形ができてきてそうな下記のこみっとあたりから(v0.0.2あたり読んでてもコード量多くて大変だったから)下記のRepoでいろいろためしてみる(localではnode linkを使用)
e620fbbd69ec395e758e6f5fb76a64725785394b
5コミット目はさすがに色々シンプル
ここまでだと、Runtime周りのコード(ライフサイクルとか)は存在せず、ビルドでコンパイルしたJS吐き出してあとは読み込んでひたすらDOM組み上げていくだけ。
- component内でDOM組み上げてる(V3のFragmentのC<create>()の元)
- Componet.teardownがv3でonUnmountの感じ
- propsはdataとよばれている(v2でもそうっぽい)
- component.setでdataセットできる
5コミット目のコンパイル結果
export default function createComponent ( options ) {
var component = {};
var state = {};
var observers = {
immediate: Object.create( null ),
deferred: Object.create( null )
};
// universal methods
function dispatchObservers ( group, state, oldState ) {
for ( const key in group ) {
const newValue = state[ key ];
const oldValue = oldState[ key ];
if ( newValue === oldValue && typeof newValue !== 'object' ) continue;
const callbacks = group[ key ];
if ( !callbacks ) continue;
for ( let i = 0; i < callbacks.length; i += 1 ) {
callbacks[i].call( component, newValue, oldValue );
}
}
}
component.get = function get ( key ) {
return state[ key ];
};
component.set = function set ( newState ) {
const oldState = state;
state = Object.assign( {}, oldState, newState );
if ( state.name !== oldState.name ) {
text_0.data = state.name;
}
};
component.observe = function ( key, callback, options = {} ) {
const group = options.defer ? observers.deferred : observers.immediate;
( group[ key ] || ( group[ key ] = [] ) ).push( callback );
if ( options.init !== false ) callback( state[ key ] );
return {
cancel () {
const index = group[ key ].indexOf( callback );
if ( ~index ) group[ key ].splice( index, 1 );
}
};
};
component.teardown = function teardown () {
element_0.parentNode.removeChild( element_0 );
state = {};
};
var element_0 = document.createElement( 'h1' );
options.target.appendChild( element_0 );
element_0.appendChild( document.createTextNode( "hello " ) );
var text_0 = document.createTextNode( '' );
element_0.appendChild( text_0 );
element_0.appendChild( document.createTextNode( "!" ) );
component.set( options.data );
return component;
}
コミット履歴おっていたらv0.0.2(git tag v0.0.2)からTODOリストが作れるとあったので、そっちを使ってみる。
- component.get / set 周りのは大きく変わっていない。
- 基本的なParseの流れはv3と大きくかわらない(fragmentで<か{{で)
- createComponentがリファクタされていて,DOMの組み立てはrenderMainFragmentでさせている(いまのdom_rendererの超シンプルばんぽい)
- updateがある
- testも整っていて使いやすい
v0.0.2のコンパイル結果
function renderMainFragment ( component, target ) {
var h1 = document.createElement( 'h1' );
h1.appendChild( document.createTextNode( "hello " ) );
var text = document.createTextNode( '' );
var text_value = '';
h1.appendChild( text );
h1.appendChild( document.createTextNode( "!" ) );
target.appendChild( h1 );
target.appendChild( document.createTextNode( "\n" ) );
var h11 = document.createElement( 'h1' );
h11.appendChild( document.createTextNode( "This is written by " ) );
var text1 = document.createTextNode( '' );
var text1_value = '';
h11.appendChild( text1 );
h11.appendChild( document.createTextNode( "!" ) );
target.appendChild( h11 );
return {
update: function ( root ) {
if ( root.name !== text_value ) {
text_value = root.name;
text.data = text_value;
}
if ( root.author !== text1_value ) {
text1_value = root.author;
text1.data = text1_value;
}
},
teardown: function () {
h1.parentNode.removeChild( h1 );
h11.parentNode.removeChild( h11 );
}
};
}
export default function createComponent ( options ) {
var component = {};
var state = {};
var observers = {
immediate: Object.create( null ),
deferred: Object.create( null )
};
function dispatchObservers ( group, newState, oldState ) {
for ( const key in group ) {
if ( !( key in newState ) ) continue;
const newValue = newState[ key ];
const oldValue = oldState[ key ];
if ( newValue === oldValue && typeof newValue !== 'object' ) continue;
const callbacks = group[ key ];
if ( !callbacks ) continue;
for ( let i = 0; i < callbacks.length; i += 1 ) {
callbacks[i].call( component, newValue, oldValue );
}
}
}
component.get = function get ( key ) {
return state[ key ];
};
component.set = function set ( newState ) {
const oldState = state;
state = Object.assign( {}, oldState, newState );
dispatchObservers( observers.immediate, newState, oldState );
mainFragment.update( state );
dispatchObservers( observers.deferred, newState, oldState );
};
component.observe = function ( key, callback, options = {} ) {
const group = options.defer ? observers.deferred : observers.immediate;
( group[ key ] || ( group[ key ] = [] ) ).push( callback );
if ( options.init !== false ) callback( state[ key ] );
return {
cancel () {
const index = group[ key ].indexOf( callback );
if ( ~index ) group[ key ].splice( index, 1 );
}
};
};
component.teardown = function teardown () {
mainFragment.teardown();
mainFragment = null;
state = {};
};
let mainFragment = renderMainFragment( component, options.target );
component.set( options.data );
return component;
}
v0.0.2のparseのながれ(v3と大枠はおなじ)
- templateをひたすら歩いていきます。に
-
<
に当たったら- HTMLエレメントだったらAttributes(on, bind)とかもろもろの処理をしてElementを組み立てる
- <script>に当たったらacornを召喚する。
-
{{
(今の{
)に当たったら - この時点で(if, each, 変数のパターンしか存在しない)- それぞれの処理を行う。
parseの結果
{
html: {
start: 1,
end: 64,
type: 'Fragment',
children: [ [Object], [Object], [Object] ]
},
css: null,
js: {
start: 66,
end: 221,
attributes: [],
content: Node {
type: 'Program',
start: 74,
end: 211,
body: [Array],
sourceType: 'module'
}
}
}
scriptタグ配下もとはこんな感じに書かないといけなかったのかな?これはすごく大変そう。
<script>
export default {
data: () => ({
x: 0,
y: 0
}),
events: {
tap ( node, callback ) {
function clickHandler ( event ) {
callback({
x: event.clientX,
y: event.clientY
});
}
node.addEventListener( 'click', clickHandler, false );
return {
teardown () {
node.addEventListener( 'click', clickHandler, false );
}
};
}
}
};
</script>
v0.0.2は非常にシンプルであとは上のParse結果(AST)を使ってGenerateしてあげるだけ
generateのやっていること
- sourcemapを作成する
- astからDOM Elementを作成できるJS(CreateComponent)を組み立てる
generateの流れ
- astのhtmlを歩いていく
- nodeがコメントだったら
- スルー
- elementだったら
- まずcreateElementでElementを作成する
- attributesを回して正しい形式で上でつくったElementに設定する(Text./IF/Eachそれぞれ正しくジェネレートする)
Rich Harrisさんが作ったこれが使われている
ここでやってみている