简单、好懂的Svelte实现原理
大家好,简单我卡颂。好懂
Svelte问世很久了,现原一直想写一篇好懂的简单原理分析文章,拖了这么久终于写了。好懂
本文会围绕一张流程图和两个Demo讲解,现原正确的简单食用方式是用电脑打开本文,跟着流程图、好懂Demo一边看、现原一边敲、简单一边学。好懂
让我么开始吧。现原
Demo1
Svelte的简单实现原理如图:
图中Component是开发者编写的组件,内部虚线部分是好懂由Svelte编译器编译而成的。图中的现原各个箭头是运行时的工作流程。
首先来看编译时,考虑如下App组件代码:
<h1>{ count}</h1> <script> let count = 0; </script>完整代码见Demo1 repl[1]
浏览器会显示:
这段代码经由编译器编译后产生如下代码,包括三部分:
create_fragment方法 count的声明语句 class App的声明语句 // 省略部分代码… function create_fragment(ctx) { let h1; return { c() { h1 = element("h1"); h1.textContent = `${ count}`; }, m(target, anchor) { insert(target, h1, anchor); }, d(detaching) { if (detaching) detach(h1); } }; } let count = 0; class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, create_fragment, safe_not_equal, { }); } } export default App;create_fragment
首先来看create_fragment方法,他是编译器根据App的UI编译而成,云服务器提供商提供该组件与浏览器交互的方法,在上述编译结果中,包含3个方法:
c,代表create,用于根据模版内容,创建对应DOM Element。例子中创建H1对应DOM Element: h1 = element("h1"); h1.textContent = `${ count}`; m,代表mount,用于将c创建的DOM Element插入页面,完成组件首次渲染。例子中会将H1插入页面: insert(target, h1, anchor);insert方法会调用target.insertBefore:
function insert(target, node, anchor) { target.insertBefore(node, anchor || null); } d,代表detach,用于将组件对应DOM Element从页面中移除。例子中会移除H1: if (detaching) detach(h1);detach方法会调用parentNode.removeChild:
function detach(node) { node.parentNode.removeChild(node); }仔细观察流程图,会发现App组件编译的产物没有图中fragment内的p方法。
这是因为App没有「变化状态」的逻辑,所以相应方法不会出现在编译产物中。
可以发现,create_fragment返回的c、m方法用于组件首次渲染。那么是谁调用这些方法呢?
SvelteComponent
每个组件对应一个继承自SvelteComponent的class,实例化时会调用init方法完成组件初始化,create_fragment会在init中调用:
class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, create_fragment, safe_not_equal, { }); } }总结一下,流程图中虚线部分在Demo1中的服务器租用编译结果为:
fragment:编译为create_fragment方法的返回值 UI:create_fragment返回值中m方法的执行结果 ctx:代表组件的上下文,由于例子中只包含一个不会改变的状态count,所以ctx就是count的声明语句可以改变状态的Demo
现在修改Demo,增加update方法,为H1绑定点击事件,点击后count改变:
<h1 on:click="{ update}">{ count}</h1> <script> let count = 0; function update() { count++; } </script>完整代码见Demo2 repl[2]
编译产物发生变化,ctx的变化如下:
// 从module顶层的声明语句 let count = 0; // 变为instance方法 function instance($$self, $$props, $$invalidate) { let count = 0; function update() { $$invalidate(0, count++, count); } return [count, update]; }count从module顶层的声明语句变为instance方法内的变量。之所以产生如此变化是因为App可以实例化多个:
// 模版中定义3个App <App/> <App/> <App/> // 当count不可变时,页面渲染为:<h1>0</h1> <h1>0</h1> <h1>0</h1>当count不可变时,所有App可以复用同一个count。但是当count可变时,根据不同App被点击次数不同,页面可能渲染为:
<h1>0</h1> <h1>3</h1> <h1>1</h1>所以每个App需要有独立的上下文保存count,这就是instance方法的意义。推广来说,Svelte编译器会追踪<script>内所有变量声明:
是否包含改变该变量的云南idc服务商语句,比如count++ 是否包含重新赋值的语句,比如count = 1 等等情况一旦发现,就会将该变量提取到instance中,instance执行后的返回值就是组件对应ctx。
同时,如果执行如上操作的语句可以通过模版被引用,则该语句会被$$invalidate包裹。
在Demo2中,update方法满足:
包含改变count的语句 —— count++ 可以通过模版被引用 —— 作为点击回调函数所以编译后的update内改变count的语句被$$invalidate方法包裹:
// 源代码中的update function update() { count++; } // 编译后instance中的update function update() { $$invalidate(0, count++, count); }从流程图可知,$$invalidate方法会执行如下操作:
p方法是Demo2中新的编译产物,除了p之外,create_fragment已有的方法也产生相应变化:
c() { h1 = element("h1"); // count的值变为从ctx中获取 t = text(/*count*/ ctx[0]); }, m(target, anchor) { insert(target, h1, anchor); append(h1, t); // 事件绑定 dispose = listen(h1, "click", /*update*/ ctx[1]); }, p(ctx, [dirty]) { // set_data会更新t保存的文本节点 if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]); }, d(detaching) { if (detaching) detach(h1); // 事件解绑 dispose(); }p方法会执行$$invalidate中标记为dirty的项对应的更新函数。
在Demo2中,App UI中只引用了状态count,所以update方法中只有一个if语句,如果UI中引用了多个状态,则p方法中也会包含多个if语句:
// UI中引用多个状态 <h1 on:click="{ count0++}">{ count0}</h1> <h1 on:click="{ count1++}">{ count1}</h1> <h1 on:click="{ count2++}">{ count2}</h1>对应p方法包含多个if语句:
p(new_ctx, [dirty]) { ctx = new_ctx; if (dirty & /*count*/ 1) set_data(t0, /*count*/ ctx[0]); if (dirty & /*count1*/ 2) set_data(t2, /*count1*/ ctx[1]); if (dirty & /*count2*/ 4) set_data(t4, /*count2*/ ctx[2]); },Demo2完整的更新步骤如下:
点击H1触发回调函数update update内调用$$invalidate,更新ctx中的count,标记count为dirty,调度更新 执行p方法,进入dirty的项(即count)对应if语句,执行更新对应DOM Element的方法总结
Svelte的完整工作流程会复杂的多,但是核心实现便是如此。
我们可以直观的感受到,借由模版语法的约束,经过编译优化,可以直接建立「状态与要改变的DOM节点的对应关系」。
在Demo2中,状态count的变化直接对应p方法中一个if语句,使得Svelte执行「细粒度的更新」时对比使用虚拟DOM的框架更有性能优势。
上述性能分析中第四行「select row」就是一个「细粒度的更新」。想比较之下,React(倒数第三列)性能就差很多。
参考资料
[1]Demo1 repl:
https://svelte.dev/repl/9945d189204a4168b4c23890f1d92a3a?version=3.19.1[2]Demo2 repl:
https://svelte.dev/repl/bf22a31a0eff4875b5b3084aa2b85fc3?version=3.19.1