svelte的源码很简单是由两大部分组成,compiler和runtime。
compiler就是一个编译器将svelte模版语法转换为浏览器能够识别的代码。而runtime则是在浏览器中帮助业务代码运作的运行时函数。所以说runtime是svelte框架最核心的部分,它也解释了svelte是如何在没有virtual dom的情况下也照样运行的。今天我们review一下runtime代码。
svelte的runtime主要由fragment和component组成,而component是包含了fragment。它们有着独立的生命周期,将逻辑层和渲染层分离。
Fragment
Svelte官方example提供了compile出来的Js output。这些output就是运行在浏览器的源码,根据内容知道svelte的基本运作,让开发者很清除它内部的每一步运作。下面这个栗子很简单,就是对hello的一个字符串插值。而name是一个变量。
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
编译出来的结果:
/* App.svelte generated by Svelte v3.24.0 */
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 ${name}!`;
},
m(target, anchor) {
insert(target, h1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(h1);
}
};
}
let name = "world";
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default App;
编译出来的结果就是有一个初始化函数,叫create_fragment,它是用于dom的初始挂载。它使用了element函数,通过查阅源码src/runtime/internal/dom
,我们知道它的作用就是用来创建h1标签实例,并且填入可变内容。除了element
之外,还有space
,text
,svg_element
等都是用于生成真实dom,分别是对空格,纯文本,svg进行生成处理。
export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
return document.createElement<K>(name);
}
export function text(data: string) {
return document.createTextNode(data);
}
export function space() {
return text(' ');
}
create_fragment的过程还包含有c,m,p,i,o,d
等特殊名称的函数,这些函数并非编译混淆,而是Fragment内部的生命周期缩写。Fragment指得是真实dom的节点,它拥有着独立的生命周期和属性。源码中src/runtime/internal/Component
介绍了它的定义,它是一个真实的dom元素集合,它的属性并非组件属性(如下方ts类型定义),分别包含了create
,claim
,hydrate
,mount
,update
,mesure
,fix
,animate
,intro
,outro
,destory
,组件的真实变化会影响Fragment的变化,Fragment的变化影响真实的dom,从上面例子看在create的过程中它创建了h1标签,在mount的过程将刚才创建的h1挂载到页面中,在update的过程没有任何操作,在detach的过程销毁该Fragment。
interface Fragment {
key: string|null;
first: null;
/* create */ c: () => void;
/* claim */ l: (nodes: any) => void;
/* hydrate */ h: () => void;
/* mount */ m: (target: HTMLElement, anchor: any) => void;
/* update */ p: (ctx: any, dirty: any) => void;
/* measure */ r: () => void;
/* fix */ f: () => void;
/* animate */ a: () => void;
/* intro */ i: (local: any) => void;
/* outro */ o: (local: any) => void;
/* destroy */ d: (detaching: 0|1) => void;
}
Component
SvelteComponent则是包含了svelte组件内置的属性和生命周期,它们与Fragment的属性和生命周期是息息相关,SvelteComponent是依赖于Fragment,组件的变化会触发Fragment的变化。它是一个相辅相成的组合。源码中还有SvelteComponent和SvelteElement的细分,不同点在于Web Component的组件的支持,这里就不再展开。
Component拥有四个生命周期,分别是mount,beforeUpdate, afterUpdate,destory。没有create阶段是因为svelte没有virtual dom。所以在组件层面,它没有像vue那么复杂。
数据流
react的单向数据流,vue的双向绑定,那么svelte是怎么样实现数据流的呢?
下面是我们业务中经常见到的代码,点击按钮请求数据然后设置到变量,触发dom内容的变化。svelte的写法形似vue的写法,但是它的runtime原理并没有双向绑定。编译后的代码除了有上面所说的create和mount等fragment生命周期属性外,其他代码更多表现了数据流的形式。
<script>
let num = 1;
async function handleClick() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
if (res.ok) {
num = text;
return text;
} else {
throw new Error(text);
}
}
</script>
<button on:click={handleClick}>
generate random number
</button>
<p>The number is {num}</p>
编译出来的结果:
/* App.svelte generated by Svelte v3.24.0 */
import {
SvelteComponent,
append,
detach,
element,
init,
insert,
listen,
noop,
safe_not_equal,
set_data,
space,
text
} from "svelte/internal";
function create_fragment(ctx) {
let button;
let t1;
let p;
let t2;
let t3;
let mounted;
let dispose;
return {
c() {
button = element("button");
button.textContent = "generate random number";
t1 = space();
p = element("p");
t2 = text("The number is ");
t3 = text(/*num*/ ctx[0]);
},
m(target, anchor) {
insert(target, button, anchor);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
if (!mounted) {
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*num*/ 1) set_data(t3, /*num*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
if (detaching) detach(t1);
if (detaching) detach(p);
mounted = false;
dispose();
}
};
}
function instance($$self, $$props, $$invalidate) {
let num = 1;
async function handleClick() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
if (res.ok) {
$$invalidate(0, num = text);
return text;
} else {
throw new Error(text);
}
}
return [num, handleClick];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
代码中,handleClick函数被封装在一个名为instance
的方法当中,而它的入参当中有个$$invalidate
的回调函数,用于变量的设置,把接口异步获取的数据设置回调函数当中。而它在组件的调用如下,重点在于回调函数当中,instance
只会在初始化的时候调用,但是回调函数$$invalidate
可以在各种异步情况调用。它会触发make_dirty
的方法,而它触发了schedule_update
,在一个微任务当中,触发flush
将一段时间内的变量操作都执行掉。实现变量的处理,flush函数的具体实现请查看源码(src/runtime/internal/Component.ts
)。flush的过程中会触发Fragment的update以及Component的update。
$$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i);
}
return ret;
})
: [];
由此可见,svelte是单向数据流,很多数据工作已经在compile的过程当中已经完成。runtime更多是服务于浏览器层面的数据流转化。
题外话
shopee,又称虾皮,是一家腾讯投资的跨境电商平台。这里加班少,技术氛围好。如果想和我并肩作战一起学习,可以找我内推。邮箱weiping.xiang@shopee.com,非诚勿扰。