Svelte 的工作原理是什么?(第一部分)
第一步
从底部开始
应用程序:第一个假设
一切都始于create_fragment
消费地点在哪里block?
app行动中
下次
散记
这是第二部分:
今年夏天,一位朋友向我介绍了 Svelte。他并没有吹嘘 Svelte 相对于当时其他框架的性能,而是强调了它编译后生成的 JavaScript 代码的简洁性和可读性。
我正在编写一门使用 Svelte(以及 FastAPI 和其他一些炫酷的东西)的课程,并且意识到我需要对 Svelte 的运行方式有更深入的了解:具体来说,就是 Svelte 编译成的代码是如何工作的。
我会随时分享我的见解,这是第一部分x。
第一步
我使用了 Svelte 项目提供的模板,方法是npx degit sveltejs/template my-svelte-project; cd $_; npm install:
然后我运行命令npm run dev编译包含的组件并启动开发服务器。
由此产生了build/bundle.js,我们将要剖析的这头野兽。
从底部开始
// build/bundle.js (all code blocks are from this file unless otherwise specified)
...
const app = new App({
target: document.body,
props: {
name: 'world'
}
});
return app;
}());
//# sourceMappingURL=bundle.js.map
我之前不知道什么是源图,但是通过谷歌搜索并bundle.js.map稍作研究后,我决定暂时不去尝试解读它!
末尾的括号告诉我,app第 3 行的变量是bundle.js
...
var app = (function () {
...
存储结果return app,因为👆👆右侧的所有内容=都是一个匿名函数,它会立即调用自身。
那么,上述以 开头的代码块const app与 中的逻辑相同main.js。
// src/main.js
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'world',
}
});
export default app;
main.js我在该示例应用程序附带的 Rollup 配置文件中搜索,发现
// rollup.config.js
...
input: 'src/main.js',
...
好的,我想起来了,这里定义了 Svelte 应用,配置如下rollup.config.js。
应用程序:第一个假设
看起来该类App有get方法set,每个方法都名为name。
...
class App extends SvelteComponentDev {
constructor(options) {
super(options);
init(this, options, instance, create_fragment, safe_not_equal, { name: 0 });
dispatch_dev("SvelteRegisterComponent", {
component: this,
tagName: "App",
options,
id: create_fragment.name
});
const { ctx } = this.$$;
const props = options.props || ({});
if (/*name*/ ctx[0] === undefined && !("name" in props)) {
console.warn("<App> was created without expected prop 'name'");
}
}
get name() {
throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
set name(value) {
throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
}
...
我假设,如果我再给App它一个属性,也会有一对属性get和set对应的属性。
检验假设1
<!-- src/App.svelte -->
<script>
export let name;
export let number; // new
</script>
果不其然,这些方法已经出现了:
...
get name() {
throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
set name(value) {
throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
get number() {
throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
set number(value) {
throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
...
原来是这样。我对JS类中的getter/setter机制了解不多,但我猜它和Python类似:当你尝试获取或设置实例属性时,它们会被触发。
然后,在构造函数中还有这个App:
if (/*name*/ ctx[0] === undefined && !("name" in props)) {
console.warn("<App> was created without expected prop 'name'");
}
if (/*number*/ ctx[1] === undefined && !("number" in props)) {
console.warn("<App> was created without expected prop 'number'");
}
这ctx东西很神秘,而且它还是从更神秘的东西上蹦出来的this.$$。
class App extends SvelteComponentDev {
constructor(options) {
...
const { ctx } = this.$$;
...
我们稍后会再讨论这些问题。
在继续之前,让我们更新一下main.js,给这个number属性赋一个值。
// src/main.js
...
const app = new App({
target: document.body,
props: {
name: 'world',
number: 42
}
});
一切都始于create_fragment
function create_fragment(ctx) {
let main;
let h1;
let t0;
let t1;
let t2;
let t3;
let p;
let t4;
let a;
let t6;
const block = {
c: function create() {
main = element("main");
h1 = element("h1");
t0 = text("Hello ");
t1 = text(/*name*/ ctx[0]);
t2 = text("!");
t3 = space();
p = element("p");
t4 = text("Visit the ");
a = element("a");
a.textContent = "Svelte tutorial";
t6 = text(" to learn how to build Svelte apps.");
attr_dev(h1, "class", "svelte-1tky8bj");
add_location(h1, file, 5, 1, 46);
attr_dev(a, "href", "https://svelte.dev/tutorial");
add_location(a, file, 6, 14, 83);
add_location(p, file, 6, 1, 70);
attr_dev(main, "class", "svelte-1tky8bj");
add_location(main, file, 4, 0, 38);
},
l: function claim(nodes) {
throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
},
m: function mount(target, anchor) {
insert_dev(target, main, anchor);
append_dev(main, h1);
append_dev(h1, t0);
append_dev(h1, t1);
append_dev(h1, t2);
append_dev(main, t3);
append_dev(main, p);
append_dev(p, t4);
append_dev(p, a);
append_dev(p, t6);
},
p: function update(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
},
i: noop,
o: noop,
d: function destroy(detaching) {
if (detaching) detach_dev(main);
}
};
dispatch_dev("SvelteRegisterBlock", {
block,
id: create_fragment.name,
type: "component",
source: "",
ctx
});
return block;
}
create_fragment是一个接受单个参数的函数ctx,它的主要作用是创建和渲染 DOM 元素;它返回block。
block
block是一个对象,其最重要的属性是c(创建)、m(挂载)、p(更新)、d(销毁)。
c(创造)
block.c的值是一个名为 的工厂函数create,该函数
c: function create() {
main = element("main");
h1 = element("h1");
t0 = text("Hello ");
t1 = text(/*name*/ ctx[0]);
t2 = text("!");
t3 = space();
p = element("p");
t4 = text("Visit the ");
a = element("a");
a.textContent = "Svelte tutorial";
t6 = text(" to learn how to build Svelte apps.")
...
1)创建大量 DOM 元素和文本节点;
2)将它们分别赋值给在开头声明的变量。create_fragment
然后它
...
attr_dev(h1, "class", "svelte-1tky8bj");
add_location(h1, file, 5, 1, 46);
attr_dev(a, "href", "https://svelte.dev/tutorial");
add_location(a, file, 6, 14, 83);
add_location(p, file, 6, 1, 70);
attr_dev(main, "class", "svelte-1tky8bj");
add_location(main, file, 4, 0, 38);
}
3) 设置元素的属性(例如“class”和“href”)。
4) 为每个属性设置触发一个事件(稍后会详细介绍:我们可以一直忽略这些事件)。5
) 为每个元素添加元数据__svelte_meta,详细说明其在src模块中的定义位置。
m(山)
block.m's value 是一个名为 的工厂函数mount,你知道,它会将每个元素和文本节点添加到 DOM 的适当位置。
m: function mount(target, anchor) {
insert_dev(target, main, anchor);
append_dev(main, h1);
append_dev(h1, t0);
append_dev(h1, t1);
append_dev(h1, t2);
append_dev(main, t3);
append_dev(main, p);
append_dev(p, t4);
append_dev(p, a);
append_dev(p, t6);
},
p(更新)
block.p's 的值不是一个工厂函数,而是一个普通的函数,它似乎是
p: function update(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
},
1) 处理一些我不理解的位,但可能只是检查是否有需要更新的内容(dirty)
2) 如果新值( )与的值(默认值)ctx[0]不同, 3) 更新的值——提醒一下,它是一个文本节点t1undefinedt1
假设二
我注意到,我们在第一个假设中添加的属性number并没有出现在update函数中。我认为这是因为它在组件中没有被使用:这是一个未使用的属性。
检验假设2
<!-- src/App.svelte -->
...
<main>
<h1>Hello {name}!</h1>
<p>Your lucky number is {number}.</p> <!-- 👈👈👈 new -->
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>
...
// build/bundle.js
...
p: function update(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
if (dirty & /*number*/ 2) set_data_dev(t5, /*number*/ ctx[1]);
},
...
叮叮叮!我对这if (dirty & 2)事儿还不太确定;咱们先把这事儿搁置一下吧。
d(破坏)
block.d's value 是一个函数,它——令人震惊的是——从 DOM 中删除一个元素。
d: function destroy(detaching) {
if (detaching) detach_dev(main);
消费地点在哪里block?
create_fragment只在代码中调用一次bundle.js,这使得侦查变得相当容易:
...
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
...
这是在怪兽init函数内部,而怪兽函数本身只在class App定义的构造函数中被调用。这个create_fragment ? ...三元运算符是什么意思?鉴于它的存在,它似乎create_fragment总是为真?更有意义的问题可能是它在哪里以及如何$$.fragment使用?在哪里?结果发现,在三个地方。如何使用?
init
...
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const prop_values = options.props || {};
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty
};
let ready = false;
$$.ctx = instance
? instance(component, prop_values, (i, ret, value = ret) => {
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if ($$.bound[i])
$$.bound[i](value);
if (ready)
make_dirty(component, i);
}
return ret;
})
: [];
$$.update();
ready = true;
run_all($$.before_update);
// `false` as a special case of no DOM component
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment.l(children(options.target));
}
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment.c();
}
if (options.intro)
transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor);
flush();
}
set_current_component(parent_component);
}
...
$$.fragment在创建之后,它被直接引用了三次init。由于示例应用程序的target中只有,我们将忽略除第二次之外的所有其他引用。与上一步类似,我不理解这里对的布尔检查,但值得注意的是,调用了的方法,该方法将创建(但不会挂载)所有元素和文本节点,并为元素提供有关其在中的预编译位置的元数据。options$$.fragment && $$.fragment.c();$$.fragment && ...fragmentcApp.svelte
由于init在构造函数内部调用App,我们知道上述操作将在运行时执行。
回溯:那又如何呢$$?
简单来说:$$在早期就已定义init。
...
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty
};
...
谜团解开了!
update
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
$$.fragment && $$.fragment.p($$.ctx, $$.dirty);
$$.dirty = [-1];
$$.after_update.forEach(add_render_callback);
}
}
我们可以忽略几乎所有这些。`is`$$.update被赋值给noop一个实际上什么也不做的变量。我们还要假设$$.fragment`is` 不为空(怎么可能呢?)。然后,` $$.before_updateis` 目前是一个空数组,所以我们要等到应用程序复杂度更高一些才会去研究它run_all($$.before_update)。类似地,$$.after_update.forEach(add_render_callback)我们也可以忽略 `is`,因为$$.after_update它也是一个空数组。
这样就只剩下
$$.fragment && $$.fragment.p($$.ctx, $$.dirty);
$$.dirty = [-1];
环顾四周,bundle.js我相当肯定这$$.dirty = [-1]意味着应用程序的状态没有待处理的更改。也就是说,在上一行更新 DOM 之后,$$.fragment.p($$.ctx, $$.dirty)我们表明所有必要的更改都已完成。
这样就只剩下一行包含大量操作的代码$$.fragment.p($$.ctx, $$.dirty),用于更新 DOM 以反映任何更改$$.ctx。
$$.ctx
$$.ctx这里似乎存放着应用程序的状态信息。它的计算过程有点复杂:
$$.ctx = instance
? instance(component, prop_values, (i, ret, value = ret) => {
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if ($$.bound[i])
$$.bound[i](value);
if (ready)
make_dirty(component, i);
}
return ret;
})
该instance函数负责生成它:
function instance($$self, $$props, $$invalidate) {
let { name } = $$props;
let { number } = $$props;
const writable_props = ["name", "number"];
Object.keys($$props).forEach(key => {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<App> was created with unknown prop '${key}'`);
});
$$self.$set = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
$$self.$capture_state = () => {
return { name, number };
};
$$self.$inject_state = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
return [name, number];
}
instance解构我们的属性,name并将number它们原封不动地传递给$$.ctx。
因此,$$.ctx等于["world", 42]:没有我想象的那么复杂;我们稍后会讨论在看似传递属性之间发生的所有这些副作用。
如前所述,$$.fragment.p($$.ctx, $$.dirty)它正在调用此函数:
function update(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
if (dirty & /*number*/ 2) set_data_dev(t5, /*number*/ ctx[1]);
}
好了,是时候弄清楚这项dirty & x业务到底是做什么的了。它似乎dirty包含了需要更新的元素索引,但为什么不弄清楚具体细节呢?
p: function update(ctx, [dirty]) {
if (dirty & /*name*/ 1) {
console.log(`dirty 1 was dirty: ${dirty}`)
set_data_dev(t1, /*name*/ ctx[0]);
} else {
console.log(`dirty 1 wasn't dirty: ${dirty}`)
}
if (dirty & /*name*/ 2) {
console.log(`dirty 2 was dirty: ${dirty}`)
set_data_dev(t5, /*name*/ ctx[0]);
} else {
console.log(`dirty 2 wasn't dirty: ${dirty}`)
}
console.log(typeof dirty)
},
为了在update不构建任何用户界面的情况下触发这些信息console.log提示,我们需要手动操作应用程序的状态:
app行动中
回到instance函数本身,它执行的更有意义的工作(“副作用”)是将三个方法$set——、、$capture_state和$inject_state——绑定到$$self,即App。
我有没有提到我们可以在控制台中检查我们的App实例app?这是 Svelte 的另一个优点:由于它最终会被编译成纯 JavaScript,因此app它处于浏览器渲染的全局作用域中,无需任何特殊插件或其他额外操作!掌握了这些知识,让我们在 JavaScript 控制台中体验一下这些新方法:
>> app.$capture_state()
► Object { name: "world", number: 42 }
>> app.$set({name: "Whirl"})
undefined
dirty 1 was dirty: 1
dirty 2 wasn't dirty: 1
number
>> app.$capture_state()
► Object { name: "Whirl", number: 42 }
>> app.$inject_state({number: 24})
undefined
undefined
dirty 1 wasn't dirty: 2
dirty 2 was dirty: 2
number
>> app.$capture_state()
► Object { name: "Whirl", number: 24 }
页面现在看起来是这样的:
这里有几项发现:
1)$capture_state以对象形式返回应用程序的当前状态。2
)$set和$inject_state似乎都通过对象更新应用程序的状态。3
)dirty当不等于 时[-1],是一个正整数,似乎通过从 1 开始的索引引用 props。4
) 这些 props 在渲染后的页面中更新。
还有一个谜团待解:
>> app.name
Error: <App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or
'<svelte:options accessors/>'
>> app.name = 'hi'
Error: <App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or
'< svelte:options accessors/>'
set这就是前面提到的 ` and`方法的目的get:强制编译后的代码不能直接在App实例上设置和获取 props,而是使用……内置的机制?
下次
下次请继续关注我们,一起揭开……的神秘面纱。
1) `$($($($($($($($($($($( app.$set$ ($($($($($($($( ))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ) ) ) app.$inject_state) ) ) )))))))))))))))))))))))))))))))))))))))))) ) ) ' ' 是 ' 是 ' 是 ' 是”)))`)))))) ' 是 ' 是 ' 是 ' 是))`))))))))))))))))))))))))))))) ' 是 ...bundle.js__svelte_metamountdirtyupdate
散记
根据 Svelte 在推特上对我的回复,在各个阶段触发的事件bundle.js仅供开发工具使用。因此我们可以忽略它们。
