脑海中来编译一个svelte(1)
https://svelte.dev/repl/99aeea705b1e48fe8610b3ccee948280?version=3.23.2 这里可以看到一个最简单的svelte组件的输出结果(JS output), 如下:
Copy /* App.svelte generated by Svelte v3.23.2 */
import {
SvelteComponent ,
detach ,
element ,
init ,
insert ,
noop ,
safe_not_equal
} from "svelte/internal" ;
function create_fragment (ctx) {
let h1;
return {
c () {
h1 = element ( "h1" );
h1 .textContent = "Hello World" ;
} ,
m (target , anchor) {
insert (target , h1 , anchor);
} ,
d (detaching) {
if (detaching) detach (h1);
}
};
}
class App extends SvelteComponent {
constructor (options) {
super ();
init ( this , options , null , create_fragment , safe_not_equal , {});
}
}
export default App;
create_fragment
create_fragment
就是给svelte的组件去构建DOM片段。
他的返回的对象里的方法:
c()
create 的短写
包含创建所有元素片段的指令。
这个例子里,就是创建h1元素
Copy h1 = element ( 'h1' );
h1 .textContent = 'Hello World' ;
// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L21
export function element < K extends keyof HTMLElementTagNameMap >(name : K ) {
return document .createElement < K >(name);
}
m(target, anctor)
mount 短写
就是挂载到对应的target上。
这个例子里就是mount h1到target上。
Copy insert (target , h1 , anchor);
// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.ts
export function insert (target , node , anchor) {
target .insertBefore (node , anchor || null );
}
d(detaching)
destroy 短写
从target中删除某个元素。
这里的例子就是从DOM中移除h1。
Copy detach (h1);
// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L11
export function detach (node : Node ) {
node . parentNode .removeChild (node);
}
export default class App extends SvelteComponent
svelte api
初始化的时候就使用类似create_fragment这些信息来组成。Svelte只会传递需要它的信息,并在不需要的时候删除它们。
试试空的组件,会输出什么信息。
https://svelte.dev/repl/1f29ce52adf446fc9116bb957b7200ec?version=3.19.1
打开上面这个链接看看jsoutput:
Copy /* App.svelte generated by Svelte v3.19.1 */
import { SvelteComponent , init , safe_not_equal } from "svelte/internal" ;
class App extends SvelteComponent {
constructor (options) {
super ();
init ( this , options , null , null , safe_not_equal , {});
// 下面是 hello world 版本
// init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default App;
create_fragment
被改为null
去传递了。
Svelte在init
函数中设置了大多数内部的内容:
component的props, ctx
(后面会解释ctx)和上下文
最后通过create_fragment去创建和挂载元素到DOM上。
并且所有的内部状态(state)和方法都被附加到this.$$
上面。
所以你获取组件的$$
属性,那么就在访问组件的内部了。
Adding data
目前,大致了解了基本的组件行为,现在看下如何添加数据之后编译的输出如何改变的。
Copy <script>
let name = 'World';
</script>
<h1>Hello {name}</h1>
Svelte REPL
这个在编译后的输出里有了改变:
Copy function create_fragment (ctx) {
// ...
return {
c () {
h1 = element ( 'h1' );
h1 .textContent = `Hello ${ name } ` ; } ,
// ...
};
}
let name = 'World' ;
class App extends SvelteComponent {
// ...
}
可以发现内部的内容被移动到了代码的顶层,并且h1元素的文本内容是一个模板字面量。
这有很多的东西发生在幕后。
Updating data
添加一个函数去更新name
变量。
Copy <script>
let name = 'World';
function update() {
name = 'Svelte';
}
</script>
<h1>Hello {name}</h1>
Svelte REPL
然后发现编译后改变的地方:
Copy function create_fragment(ctx) {
return {
c() {
+ h1 = element('h1');
+ t0 = text('Hello ');
+ t1 = text(/*name*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
append(h1, t0);
append(h1, t1);
},
+ p(ctx, [dirty]) {
+ if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
+ },
d(detaching) {
if (detaching) detach(h1);
},
};
}
+ function instance($$self, $$props, $$invalidate) {
+ let name = 'World';
+ function update() {
+ $$invalidate(0, (name = 'Svelte'));
+ }
+ return [name];
+ }
export default class App extends SvelteComponent {
constructor(options) {
super();
+ init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
一些新发现:
create_fragment
返回的对象新增了一个p(ctx, dirty)
方法。
update 短写
<script>
中的内容被移到了instance
函数中。
你是否发现create_fragment
中使用的变量name
被替换成了ctx[0]
,并且也不是模板字符串了。
那么,为什么要改变?
Svelte编译器跟踪所有在<script>
标签中声明的变量,他跟踪变量是否存在以下情况:
可否进行改变(mutated)?,类似 count++
是否可以重新赋值?类似name = 'Svelte'
是否在模板中被引用?类似 Hello {name}
是否可写?类似 const i = 1;
和 let i = 1;
当Svelte编译器意识到可以重新分配变量name
时,(由于name = 'Svelte';
在update
函数中),它将h1
的文本内容分解为若干部分,以便动态更新部分文本。
实际上,可以看到有一个新方法p
来更新文本节点。
p(ctx, dirty)
update 短写
p(ctx, dirty) 包含一些更新元素,他是基于组件中被改变的状态(dirty
)和状态(ctx
)。
instance variable
编译器发现变量name
没有在App的不同组件实例间共享,这就是为什么会把name
声明放到名为instance
的函数中。
在上一个示例中,无论App组件有多少个实例,变量name
的值在这些实例中都是相同的,并且没有变化:
Copy <App />
<App />
<App />
<!-- gives you -->
<h1>Hello world</h1>
<h1>Hello world</h1>
<h1>Hello world</h1>
目前的这个例子里,变量name
在组件的一个实例里是可以改变的,因此变量name
的声明被移到了instance
函数中:
Copy <App />
<App />
<App />
<!-- could possibly be -->
<h1>Hello world</h1>
<h1>Hello Svelte</h1>
<h1>Hello world</h1>
<!-- depending on the inner state of the component -->
instance($$self, $$props, $$invalidate)
instance
函数返回一个instance 变量列表,这些变量是:
可以改变(mutated)或重新分配(在组件的一个实例内改变)
在Svelte中,我们将这个实例变量列表称为ctx 。
init
函数里,Svelte调用instance
函数去创建 ctx ,并使用它去创建组件片段。
Copy // 就概念而言,
const ctx = instance ( /*...*/ );
const fragment = create_fragment (ctx);
// create the fragment
fragment .c ();
// mount the fragment onto the DOM
fragment .m (target);
现在,不是直接在组件外貌访问变量name
,而是直接通过传递的ctx 来访问变量name
。
Copy t1 = text ( /*name*/ ctx[ 0 ]);
$$invalidate
Svelte系统的反应能力是背后秘密是$$invalidate
函数。
存在的这些变量只要:
会在改变或重新赋值之后通过$$invalidate
函数正确的插入:
Copy name = 'Svelte' ;
count ++ ;
foo .a = 1 ;
// 编译成类似于这样
name = 'Svelte' ;
$$invalidate ( /* name */ , name);
count ++ ;
$$invalidate ( /* count */ , count);
foo .a = 1 ;
$$invalidate ( /* foo */ , foo);
$$invalidate
函数会标记变量为dirty并为组件安排更新:
Copy // 概念上来说...
const ctx = instance ( /*...*/ );
const fragment = create_fragment (ctx);
// 跟踪哪些变量被改变
const dirty = new Set ();
const $$invalidate = (variable , newValue) => {
// 更新 ctx (variable对应的数组的下标)
ctx[variable] = newValue;
// 标记变量为dirty
dirty .add (variable);
// 为组件安排更新
scheduleUpdate (component);
};
// 被安排更新时调用
function flushUpdate () {
// update片段
fragment .p (ctx , dirty);
// 清楚dirty标记
dirty .clear ();
}
Adding event listeners
现在来加一个事件监听:
Copy <script>
let name = 'world';
function update() {
name = 'Svelte';
}
</script>
<h1 on:click={update}>Hello {name}</h1>
Svelte REPL
发现与之前的不同点:
Copy function create_fragment(ctx) {
// ...
return {
c() {
h1 = element('h1');
t0 = text('Hello ');
t1 = text(/*name*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
append(h1, t0);
append(h1, t1);
+ dispose = listen(h1, 'click', /*update*/ ctx[1]); },
p(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
},
d(detaching) {
if (detaching) detach(h1);
+ dispose(); },
};
}
function instance($$self, $$props, $$invalidate) {
let name = 'world';
function update() {
$$invalidate(0, (name = 'Svelte'));
}
+ return [name, update];}
// ...
一些发现:
在mount 期间侦听单击事件,并在destroy 中处理它
前面也提到了,instance
函数返回的变量列表是被 在模板中引用 和 可变和重新赋值的 。
因为在模板中引用了update
函数,所以作为在instance
函数返回的 ctx 的一部分。
你如果在模板中不引用update
,这个是不会被加到instance
函数返回的数组里的。
由于Svelte尽可能的去返回简洁的js输出,没有必要是不会返回额外的变量。
listen and dispose
dispose(处理)
每当在Svelte中添加事件侦听器 时,Svelte将注入代码来添加事件侦听器 ,并在从DOM中删除DOM片段时将其删除。
尝试多加几个事件监听:
Copy <h1
on:click={update}
on:mousedown={update}
on:touchstart={update}>
Hello {name}!
</h1>
Svelte REPL
并观察编译后的输出:
Copy // ...
+ dispose = [
+ listen(h1, 'click', /*update*/ ctx[1]),
+ listen(h1, 'mousedown', /*update*/ ctx[1]),
+ listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true }),
+ ];
// ...
+ run_all(dispose);
Svelte并没有声明和创建一个新变量来删除每个事件侦听器,而是将它们全部分配给一个数组:
Copy // instead of
dispose1 = listen (h1 , 'click' , /*update*/ ctx[ 1 ]);
dispose2 = listen (h1 , 'mousedown' , /*update*/ ctx[ 1 ]);
dispose2 = listen (h1 , 'touchstart' , /*update*/ ctx[ 1 ] , { passive : true });
// ...
dispose1 ();
dispose2 ();
dispose3 ();
这样变量名压缩可以做的更好。
同样,这是Svelte试图生成更小的JavaScript输出的另一个很好的例子。当只有一个事件侦听器时,Svelte不会创建dispose
数组。
Summary
Svelte语法是HTML的超集,就像ts是js的超集。
当你写Svelte组件的时候,Svelte编译器会分析你的代码并生成优化过的Js代码输出。
输出大致可以分成三个部分:
1. create_fragment
返回片段,他的内部是关于如何构建组件DOM片段的。
2. instance
返回实例中用到的变量列表(可变和重新赋值的或模板中引用的)
$$invalidate
在实例变量被改变或者重新赋值的时候正确的插入
3. class App extends SvelteComponent
使用create_fragment
和instance
来初始化组件
Svelte尽可能生成简洁的JavaScript输出:
仅当部分文本可以更新时,将h1的文本内容拆分为单独的文本节点
create_fragment
和instance
仅仅在需要的时候才会去定义
根据事件侦听器的数量,以数组或函数的形式生成dispose
变量
结束语
这里介绍了Svelte编译输出的基本结构,而这仅仅是开始。
后面还有,待续
基本就是翻译过来的,原文地址:
https://lihautan.com/compile-svelte-in-your-head-part-1/