這篇文章將爲你們介紹前端圈「新」寵 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,作大綱,寫文章),若是以爲對你有收穫,歡迎點贊 ❤️ + 收藏 + 評論 + 關注,這樣我會更有動力產出好文章。
時間倉促,水平有限,不免會有紕漏,歡迎指正。