这篇文章将为你们介绍前端圈“新”宠 Svelte ,以及其背后的响应式原理。对于 Svelte 你还没用过,但大几率会在一些技术周刊,社区,或者前端年度报告上听到这个名字。若是你使用掘金写文章的话,那其实已经在使用 Svelte 了,由于掘金新版的编辑器 bytemd 就是使用 Svelte 写的 👀 。html
(:对于一些讯息源比较广的同窗来讲,Svelte 可能不算新事物,由于其早在 2016 就开始动工,是我落后了。前端
这篇文章发布与掘金:https://juejin.cn/post/696574...vue
一个前端框架,轮子哥 Rich Harris 搞的,你可能对这我的字不太熟悉,但 rollup 确定听过,同一个做者。node
新的框(轮)架(子)意味着要学习新的语法,好像每隔几个月就要学习新的“语言”,不由让我想晒出那个旧图。react
吐槽归吐槽,该学的仍是要学,否则就要被淘汰了👻 。Svelte 这个框架的主要特色是:git
决定是否使用某个框架,须要有一些事实依据,下面咱们将从 Star 数,下载趋势,代码体积,性能,用户满意度,等几个维度来对比一下 React、Vue、Angular、Svelte 这几个框架。github
React | Vue | @angular/core | Svelte | |
---|---|---|---|---|
Star 数🌟 | 168,661 | 183,540 | 73,315 | 47,111 |
代码体积 🏋️♀️ | 42k | 22k | 89.5k | 1.6k |
Star 数上看,Svelte 只有 Vue(yyds)的四分之一(Svelte(2016) 比 Vue(2013) 慢起步三年)。不过 4.7w Star 数也不低。web
代码体积(minizipped)上,Svelte 只有 1.6k !!!可别忘了轮子哥另外一个做品是 rollup,打包优化很在行。不过随着项目代码增长,用到的功能多了,Svelte 编译后的代码体积增长的速度会比其余框架快,后面也会提到。算法
下载量差距很是明显,Svelte(231,262) 只有 React(10,965,933) 的百分之二。光看这边表面数据还不够,跑个分 看看。
越绿表示分越高,从上图能够看到 Svelte 在性能,体积,内存占用方面表现都至关不错。再看看用户满意度如何。
一样地,Svelte 排到了第一!!!(Interest 也是)。
经过以上的数据对比,咱们大体能获得的结论是:Svelte 代码体积小,性能爆表,将来可期,值得深刻学习。
<script> let count = 0; function handleClick() { count += 1; } </script> <button on:click={handleClick}> Clicked {count} {count === 1 ? 'time' : 'times'} </button> <style> button { color: black; } </style>
以上就是一个 Svelte 组件,能够看到和 Vue 的写法基本一致,一个 .svelte
文件包含了 JS,HTML,CSS,而且使用相似的模板语法,指令绑定。
不同的点多是 Style 默认是 scoped
的,HTML 不须要用 <template></template>
包裹,以及没有 new Vue
,data
的初始化步骤。直接定义一个变量,直接用就好了。(背后发生了什么放到 Reactivity
章节再讲)
Vue 的写法:
var vm = new Vue({ data: { count: 0 } })
$:
语法须要在依赖数据变动时触发运算,在 Vue
中一般是使用 computed
来实现。
var vm = new Vue({ data: { count: 0 }, computed: { double: function () { // `this` 指向 vm 实例 return this.count * 2 } } })
在 Svelte
也有相似的实现,咱们使用 $:
关键字来声明 computed
变量。
<script> let count = 0; function handleClick() { count += 1; } $: double = count * 2 </script> <button on:click={handleClick}> Clicked {double} times </button>
上面的例子中,每次点击按钮,double 都会从新运算并更新到 DOM Tree 上。这是什么黑科技?是原生 JS 代码吗?
还别说,确实是,这里的使用的是 Statements and declarations 语法,冒号:
前能够是任意合法变量字符,定义一个 goto
语句。不过语义不同,这里 Svelte 只是讨巧用了这个被废弃的语法来声明计算属性(仍是原生 JS 语法,👻 没有引入黑科技。
做为一个前端框架,Svelte 该有的功能同样很多,例如模板语法,条件渲染,事件绑定,动画,组件生命周期,Context,甚至其余框架没有的它也有,好比自带 Store,Motion 等等很是多,因为这些 API 的学习成本并不高,用到的时候看一下代码就能够了。
接下来进入本篇文章的核心,Svelte 如何实现响应式(Reactivity) 或者说是数据驱动视图的方式和 Vue、React 有什么区别。
高中化学的课堂咱们接触过不少实验,例如使用紫色石蕊试液来鉴别酸碱。酸能使紫色石蕊溶液变成红色,碱能使紫色石蕊溶液变成蓝色。实验的原理是和分子结构有关,分子结构是连接,添加酸/碱是动做,而分子结构变化呈现出的结果就是反应 Reactivity。
利用好 Reactivity 每每能事半功倍,例如在 Excel/Number 里面的函数运算。
上例咱们定义 E11
单元格的内容为 =SUM(D10, E10)
(创建链接),那么每次 D10
,E10
的数据发生变动时(动做),应用自动帮咱们执行运算(反应),不用笨笨地手动用计算器运算。
为了更清晰地认识 Reactvity 对编码的影响,设想一下开发一个 Todo 应用,其功能有新增任务,展现任务列表,删除任务,切换任务 DONE
状态等。
首先须要维护一个 tasks 的数据列表。
const tasks = [ { id: 'id1', name: 'task1', done: false } ]
使用 DOM 操做遍历列表,将它渲染出来。
function renderTasks() { const frag = document.createDocumentFragment(); tasks.forEach(task => { // 省略每一个 task 的渲染细节 const item = buildTodoItemEl(task.id, task.name); frag.appendChild(item); }); while (todoListEl.firstChild) { todoListEl.removeChild(todoListEl.firstChild); } todoListEl.appendChild(frag); }
而后每次新增/删除/修改任务时,除了修改 tasks 数据,都须要手动触发从新渲染 tasks
(固然这样的实现并很差,每次删除/插入太多 DOM 节点性能会有问题)。
function addTask (newTask) { tasks.push(newTask) renderTaks() } function updateTask (payload) { tasks = //... renderTaks() } function deleteTask () { tasks = //... renderTaks() }
注意到问题了吗,每次咱们修改数据时,都须要手动更新 DOM 来实现 UI 数据同步。(在 jQuery 时代,咱们确实是这么作的,开发成本高,依赖项多了之后会逐渐失控)
而有了 Reactvity,开发者只须要修改数据便可,UI 同步的事情交给 Framework 作,让开发者完全从繁琐的 DOM 操做里面解放出来。
// vue this.tasks.push(newTask)
在讲解 Svelte 如何实现 Reactivity
以前,先简单说说 React 和 Vue 分别是怎么作的。
React 开发者使用 JSX
语法来编写代码,JSX
会被编译成 ReactElement
,运行时生成抽象的 Virtual DOM。
而后在每次从新 render 时,React 会从新对比先后两次 Virtual DOM,若是不须要更新则不做任何处理;若是只是 HTML 属性变动,那反映到 DOM 节点上就是调用该节点的 setAttribute
方法;若是是 DOM 类型变动、key 变了或者是在新的 Virtual DOM 中找不到,则会执行相应的删除/新增 DOM 操做。
除此以外,抽象 Virtual DOM 的好处还有方便跨平台渲染和测试,好比 react-native, react-art。
使用 Chrome Dev Tool 的 Performance 面板,咱们看看一个简单的点击计数的 DEMO 背后 React 都作了哪些事情。
import React from "react"; const Counter = () => { const [count, setCount] = React.useState(0); return <button onClick={() => setCount((val) => val + 1)}>{count}</button>; }; function App() { return <Counter />; } export default App;
大体能够将整个流程分为三个部分,首先是调度器,这里主要是为了处理优先级(用户点击事件属于高优先级)和合成事件。
第二个部分是 Render 阶段,这里主要是遍历节点,找到须要更新的 Fiber Node,执行 Diff 算法计算须要执行那种类型的操做,打上 effectTag,生成一条带有 effectTag 的 Fiber Node 链表。常说的异步可中断也是发生在这个阶段。
第三个阶段是 Commit,这一步要作的事情是遍历第二步生成的链表,依次执行对应的操做(是新增,仍是删除,仍是修改...)
因此对咱们这个简单的例子,React 也有大量的前置工做须要完成,真正修改 DOM 的操做是的是红框中的部分。
前置操做完成,计算出原来是 nodeValue 须要更新,最终执行了 firstChild.nodeValue = text
。
演示使用的 React 版本是 17.0.2
,已经启用了 Concurrent Mode
。
每次 setState
React 都 Schedule Update,而后会遍历发生变动节点的全部子孙节点,因此为了不没必要要的 render,写 React 的时候须要特别注意使用 shouldComponentUpdate
,memo
,useCallback
,useMemo
等方法进行优化。
写了半天,发现还没写到重点。。。为了控制篇幅 Demo 就不写了(介绍 Vue 响应式原理的文章很是多)。
大体过程是编译过程当中收集依赖,基于 Proxy(3.x) ,defineProperty(2.x) 的 getter,setter 实如今数据变动时通知 Watcher。Vue
的实现很酷,每次修改 data
上的数据都像在施魔法。
不管 React, Vue 都在达到目的(数据驱动 UI 更小)的过程当中都多作了一些事情(Vue 也用了 Virtual DOM)。而 Svelte 是怎么作到减小运行时代码的呢?
秘密就藏在 Compiler 里面,大部分工做都在编译阶段都完成了。
Svelte 源代码分红 compiler 和 runtime 两部分。
那 Compiler 怎么收集依赖的呢?其实代码中的依赖关系在编译时是能够分析出来的,例如在模板中渲染一个 {name}
字段,若是发现 name 会在某些时刻修改(例如点击按钮以后),那就在每次name
被赋值以后尝试去触发更新视图。若是 name 不会被修改,那就什么也不用作。
这篇文章不会介绍 Compiler 具体如何实现,来看看通过 Compiler 以后的代码长什么样。
<script> let name = 'world'; </script> <h1>Hello {name}!</h1>
会被编译成以下代码,为了方便理解,我把无关的代码暂时删除了。
/* App.svelte generated by Svelte v3.38.2 */ import { SvelteComponent, append, detach, element, init, insert, listen, noop, safe_not_equal, set_data, text } 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); } } let name = "world";
create_fragment
方法是和每一个组件 DOM 结果相关的方法,提供一些 DOM 的钩子方法,下一小结会介绍。
对比一下若是变量会被修改的代码
<script> let name = 'world'; function setName () { name = 'fesky' } </script> <h1 on:click={setName}>Hello {name}!</h1>
编译后
import { SvelteComponent, append, detach, element, init, insert, listen, noop, safe_not_equal, set_data, text } from "svelte/internal"; function create_fragment(ctx) { let h1; let t0; let t1; let t2; let dispose; return { c() { h1 = element("h1"); t0 = text("Hello "); t1 = text(/*name*/ ctx[0]); t2 = text("!"); }, m(target, anchor) { insert(target, h1, anchor); append(h1, t0); append(h1, t1); append(h1, t2); if (!mounted) { // 增长了绑定事件 dispose = listen(h1, "click", /*handleClick*/ ctx[1]); } }, // 多一个 p (update)方法 p(ctx, [dirty]) { if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]); } }; } // 多了 instance 方法 function instance($$self, $$props, $$invalidate) { let name = "world"; function setName() { $$invalidate(0, name = "fesky"); } return [name, setName]; }
这种状况下编译结果的代码多了一些,简单介绍一下,首先是 fragment
中原来的 m
方法内部增长了 click
事件;多了一个 p
方法,里面调了 set_data
;新增了一个 instance
方法,这个方法返回每一个组件实例中存在的属性和修改这些属性的方法(name, 和 setName),若是有其余属性和方法也是在同一个数组中返回(不要和 Hooks 搞混了)。
一些细节还不太了解不要紧,后面都会介绍。重点关注赋值的代码原来的 name = 'fesky'
被编译成了 $$invalidate(0, name = "fesky")
。
还记得前面咱们使用原生代码实现 Todo List 吗?咱们在每次修改数据以后,都要手动从新渲染 DOM!咱们不提倡这么写法,由于难以维护。
function addTask (newTask) { tasks.push(newTask) renderTaks() }
而 Svelte Compile 实际上就是在代码编译阶段帮咱们实现了这件事!把须要数据变动以后作的事情都分析出来生成原生 JS 代码,运行时就不须要像 Vue Proxy
那样的运行时代码了。
Selve 提供了在线的实时编译器,能够动动小手试一下。 https://svelte.dev/repl/hello...
接下来的部分将是从源码角度来看看 Svelte
总体是如何 run
起来的。
每一个 Svelte 组件编译后都会有一个 create_fragment
方法,这个方法返回一些 DOM 节点的声明周期钩子方法。都是单个字母很差理解,从 源码 上能够看到每一个缩写的含义。
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; }
主要看如下四个钩子方法:
c(create):在这个钩子里面建立 DOM 节点,建立完以后保存在每一个 fragment 的闭包内。
m(mount):挂载 DOM 节点到 target 上,在这里进行事件的板顶。
p(update):组件数据发生变动时触发,在这个方法里面检查更新。
d(destroy):移除挂载,取消事件绑定。
编译结果会从 svelte/internal
中引入 text
,element
,append
,detach
,listen
等等的方法。源码中能够看到,都是一些很是纯粹的 DOM 操做。
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 append(target: Node, node: Node) { if (node.parentNode !== target) { target.appendChild(node); } } export function detach(node: Node) { node.parentNode.removeChild(node); } export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) { node.addEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options); }
咱们能够确信 Svelte 没有 Virtual DOM 了~
前面说了,Compiler 会把赋值的代码通通使用 $$invalidate
包裹起来。例如 count ++
,count += 1
,name = 'fesky'
等等。
这个方法干了什么?看看 源码,(删减了部分不重要的代码)
(i, ret, ...rest) => { const value = rest.length ? rest[0] : ret; if (not_equal($$.ctx[i], $$.ctx[i] = value)) { make_dirty(component, i); } return ret; }
第一个参数 i
是什么?代码中运行起来赋值给 ctx 又是怎么回事 $$.ctx[i] = value
?,编译结果传入了一个 0???
$$invalidate(0, name = "fesky");
实际上,instance
方法会返回一个数组,里面包括组件实例的一些属性和方法。Svelte 会把返回 instance
方法的返回值赋到 ctx
上保存。因此这里的 i
就是 instance
返回的数组下标。
$$.ctx = instance ? instance(component, options.props || {}, (i, ret, ...rest) => { //... }) : [];
在编译阶段,Svelte 会按照属性在数组中的位置,生成对应的数字。例如如今有两个变量,
<script> let firsName = ''; let lastName = ''; function handleClick () { firsName = 'evan' lastName = 'zhou'; } </script> <h1 on:click={handleClick}>Hello {firsName}{lastName}!</h1>
invalidate 部分代码编译结果就会变成:
function handleClick() { // 对应数组下标 0 $$invalidate(0, firsName = "evan"); // 对应数组下标 1 $$invalidate(1, lastName = "zhou"); } return [firsName, lastName, handleClick];
好了,接着往下,$$invalidate
中判断赋值以后不相等时就会调用 make_dirty
。
function make_dirty(component, i) { if (component.$$.dirty[0] === -1) { dirty_components.push(component); schedule_update(); component.$$.dirty.fill(0); } component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); }
这个方法里面的主流程是把调用 make_dirty
的组件添加到 dirty_components
中,而后调用了 schedule_update
方法。(dirty 字段的细节延后)
export function schedule_update() { if (!update_scheduled) { update_scheduled = true; resolved_promise.then(flush); } }
schedule_update
很简单,在 Promise.resolve
(microTask) 中调用 flush
方法。
(看源码有点单调无聊,坚持住,立刻结束了)
export function flush() { for (let i = 0; i < dirty_components.length; i += 1) { const component = dirty_components[i]; set_current_component(component); update(component.$$); } }
flush
方法其实就是消费前面的 dirty_components
,调用每一个须要更新组件的 update
方法。
function update($$) { if ($$.fragment !== null) { $$.update(); const dirty = $$.dirty; $$.dirty = [-1]; $$.fragment && $$.fragment.p($$.ctx, dirty); } }
而 Update 方法呢,又回到了每一个 fragment 的 p(update)
方法。这样整个链路就很清晰了。再整理如下思路:
$$invalidate
方法make_dirty
dirty_component
, 更新 DOM 节点上一小结中还有很重要的细节没有解释,就是 dirty
到底是怎么标记的。
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
看到 31
,看到 <<
右移符号,那铁定是位运算没跑了。首先咱们要知道,JS 中全部的数字都是符合 IEEE-754
标准的 64
位双精度浮点类型。而全部的位运算都只会保留 32
结果的整数。
将这个语句拆解一下: (i / 31) | 0
:这里是用数组下标 i
属于 31,而后向下取整(任何整数数字和 | 0
的结果都是其自己,位运算有向下取整的功效)。 (1 << (i % 31))
:用 i
对 31
取模,而后作左移操做。
这样咱们就知道了,dirty 是个数组类型,存放了多个 32
位整数,整数中的每一个 bit
表示换算成 instance
数组下标的变量是否发生变动。
为了方便理解,咱们用四位整数。
[1000] => [8] 表示 instance 中的第一个变量是 dirty。 [1001] => [9] 表示 instance 中的第一个变量和第四个变量是 dirty。 [1000, 0100] => [9, 4] 表示 instance 中的第一个变量和第六个变量是 dirty。
对这些基础知识不太熟悉的朋友能够翻我之前写的另外两篇文章
硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准
硬核基础二进制篇(二)位运算
再回头看 p
方法,每次调用时都会判断依赖的数据是否发生变动,只有发生变动了,才更新 DOM。
p(ctx, [dirty]) { if (dirty & /*firsName*/ 1) set_data(t1, /*firsName*/ ctx[0]); if (dirty & /*lastName*/ 2) set_data(t2, /*lastName*/ ctx[1]); }
对了,还有个约定,若是 dirty 第一个数字存储的是 -1
表示当前组件是干净的。
$$.dirty = [-1];
能够在 Github Issue 中找到相关的讨论,这样实现的好处是,编译后代码体积更小,二进制运算更快一点点。
最后写个 DEMO 一样使用 Performance 面板记录代码运行信息和 React 对比一下。
<script> let count = 1; function handleClick () { count += 1 } </script> <button on:click={handleClick}>{count}</button>
(因为实在过高效了,以致于我不得不单独为它作张放大图)💰钱都花在刀刃上。
但愿看到这里你已经完全掌握了 Svelte 响应式背后的全部逻辑。我把整个流程画了个草图,能够参考。总体看下来,Svelte 运行时的代码是很是精简,也很好理解的,有时间的话推荐看源码。
决定是否使用某框架还有很打一个因数是框架生态怎么样,我在网上搜集了一部分,列出来供参考。
整体上看,整个生态还不太够强大,有很大空间。若是使用 Svelte 来开发管理后台,可能没有像使用 Antd 那样顺滑,而若是是开发 UI 高度自定义的 H5 活动页就彻底不在话下。
之前你们选 Vue 而不是 React 的理由,理由听到最多的是说 Vue
体积小,上手快。如今 Svelte 更小(针对小项目)更快更适合用来作活动页,你会上手吗?
Anyways,不管如何武器库又丰富了 💐💐💐,下次作技术选型的时候多了一种选择,了解了不用和没据说过因此不用仍是有很大区别的。
对于我而言,Svelte 实现 Reactivity 确实特立独行,了解完实现原理也从中学到了不少知识。这篇文章花了我三天时间(找资料、看源码、写 DEMO,作大纲,写文章),若是以为对你有收获,欢迎点赞 ❤️ + 收藏 + 评论 + 关注,这样我会更有动力产出好文章。
时间仓促,水平有限,不免会有纰漏,欢迎指正。