简单、好懂的Svelte实现原理

开发 前端
本文会围绕一张流程图和两个Demo讲解,正确的食用方式是用电脑打开本文,跟着流程图、Demo一边看、一边敲、一边学

[[434242]]

大家好,我卡颂。

Svelte问世很久了,一直想写一篇好懂的原理分析文章,拖了这么久终于写了。

本文会围绕一张流程图和两个Demo讲解,正确的食用方式是用电脑打开本文,跟着流程图、Demo一边看、一边敲、一边学。

让我么开始吧。

Demo1

Svelte的实现原理如图:

图中Component是开发者编写的组件,内部虚线部分是由Svelte编译器编译而成的。图中的各个箭头是运行时的工作流程。

首先来看编译时,考虑如下App组件代码:

<h1>{count}</h1> 
 
<script> 
  let count = 0; 
</script> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

完整代码见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; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

create_fragment

首先来看create_fragment方法,他是编译器根据App的UI编译而成,提供该组件与浏览器交互的方法,在上述编译结果中,包含3个方法:

  • c,代表create,用于根据模版内容,创建对应DOM Element。例子中创建H1对应DOM Element:
h1 = element("h1"); 
h1.textContent = `${count}`; 
  • 1.
  • 2.
  • m,代表mount,用于将c创建的DOM Element插入页面,完成组件首次渲染。例子中会将H1插入页面:
insert(target, h1, anchor); 
  • 1.

insert方法会调用target.insertBefore:

function insert(target, node, anchor) { 
  target.insertBefore(node, anchor || null); 

  • 1.
  • 2.
  • 3.
  • d,代表detach,用于将组件对应DOM Element从页面中移除。例子中会移除H1:
if (detaching) detach(h1); 
  • 1.

detach方法会调用parentNode.removeChild:

function detach(node) { 
  node.parentNode.removeChild(node); 

  • 1.
  • 2.
  • 3.

仔细观察流程图,会发现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, {}); 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

总结一下,流程图中虚线部分在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> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

完整代码见Demo2 repl[2]

编译产物发生变化,ctx的变化如下:

// 从module顶层的声明语句 
let count = 0; 
 
// 变为instance方法 
function instance($$self, $$props, $$invalidate) { 
  let count = 0; 
 
  function update() { 
    $$invalidate(0, count++, count); 
  } 
 
  return [countupdate]; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

count从module顶层的声明语句变为instance方法内的变量。之所以产生如此变化是因为App可以实例化多个:

// 模版中定义3个App 
<App/> 
<App/> 
<App/> 
 
// 当count不可变时,页面渲染为:<h1>0</h1> 
<h1>0</h1> 
<h1>0</h1> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

当count不可变时,所有App可以复用同一个count。但是当count可变时,根据不同App被点击次数不同,页面可能渲染为:

<h1>0</h1> 
<h1>3</h1> 
<h1>1</h1> 
  • 1.
  • 2.
  • 3.

所以每个App需要有独立的上下文保存count,这就是instance方法的意义。推广来说,Svelte编译器会追踪<script>内所有变量声明:

  • 是否包含改变该变量的语句,比如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); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

从流程图可知,$$invalidate方法会执行如下操作:

  • 更新ctx中保存状态的值,比如Demo2中count++
  • 标记dirty,即标记App UI中所有和count相关的部分将会发生变化
  • 调度更新,在microtask中调度本次更新,所有在同一个macrotask中执行的$$invalidate都会在该macrotask执行完成后被统一执行,最终会执行组件fragment中的p方法

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(); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

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> 
  • 1.
  • 2.
  • 3.
  • 4.

 对应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]); 
}, 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

Demo2完整的更新步骤如下:

  1. 点击H1触发回调函数update
  2. update内调用$$invalidate,更新ctx中的count,标记count为dirty,调度更新
  3. 执行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

 

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2021-10-31 23:57:33

Eslint原理

2023-06-02 16:28:01

2016-12-26 18:05:00

单点登录原理简单实现

2022-05-06 09:22:25

Go泛型

2022-03-16 22:24:50

ReactstateHooks

2022-03-11 19:54:07

Svelte应用程序JavaScript

2023-06-13 18:24:26

TypeScriptJSDoc开发

2010-06-21 10:42:50

BitTorrent协

2024-10-28 00:01:00

2010-09-01 11:43:06

KickstartPXE无人值守

2009-11-06 09:22:46

WCF应用

2021-09-06 05:59:17

Svelte前端框架

2014-06-06 09:01:07

DHCP

2017-12-06 16:28:48

Synchronize实现原理

2023-06-24 22:14:23

2024-12-23 15:05:29

2010-08-31 19:53:25

DHCP功能

2012-05-10 13:42:26

Java网络爬虫

2021-08-16 09:59:52

ReactSvelte开发

2011-06-19 11:48:27

百度蜘蛛
点赞
收藏

51CTO技术栈公众号