我对Svelte 的看法
我在很早前已经听说过 Svelte
在开发的过程中我对
你需要有使用过任意一个前端框架的经验才能读懂本文。这篇文章不是一篇
Svelte 是什么?
简单来说
let a = 1let b = 2let c = a + bconsole.log(c) //=> 3a = 2console.log(c) //=> 3, not 4
let c = a + b
a
或 b
的值之后,c
不会因此而改变。真正的
拿C1
如果是=SUM(A1:B1)
C1
值会随着 A1
或 B1
的值而重新计算和改变。
如果你用过defineProperty
或者Proxy
const reactive = {}Object.defineProperty(reactive, 'a', { set(value) { console.log('a was updated') }})
reactive.a = 'changed' //=> a was updated
这是一种「运行时」的手段,它需要在运行时改变了赋值行为,所以在用data
里,上文的例子用
const yourData = { data() { return { a: 1, b: 2 } }, computed: { // 还有 computed c() { return this.a + this.b } }}
const reactive = new Vue(yourData)console.log(reactive.c) //=> 3reactive.a = 2console.log(reactive.c) //=> 4, not 3
但是我们可以想一想,如果不用defineProperty
当然可以:
let a = 1let b = 2let c = a + b
function update() { c = a + b}
console.log(c) //=> 3
a = 2; update()console.log(c) //=> 4
b = 5; update()console.log(c) //=> 6
我们只要每次在赋值的时候,手动触发一个 update
函数,那么 c
的值就会重新计算,不就实现了
但是这样做未免太蠢,要写太多的代码,而且很容易漏掉。不过,我们可以借助update
方法的语句。
这就是
以一个
function Counter () { const [ count, setCount ] = React.useState(0) return ( <> <div>{count}</div> <button onClick={_ => setCount(count + 1) }>+</button> <button onClick={_ => setCount(count - 1) }>-</button> </> )}
我在之前的文章已经谈过
UI = f(state)
在 setState
的时候,这个函数会重新执行,因为是新的state
Counter
会在每次状态变化的时候被重新执行。这使得写
function Counter () { const [ count, setCount ] = React.useState(0)
doSomethingHeavy()
return ( <> <div>{count}</div> <button onClick={_ => setCount(count + 1) }>+</button> <button onClick={_ => setCount(count - 1) }>-</button> </> )}
在「古典」useCallback
和 useMemo
,手动地缓存函数,来避免性能问题。
同样是使用
如果你是
Vue 用户,而不太清楚React 的机制, 你可以把一个React 组件函数想象成是一个Vue 的computed
里的成员函数, 你一定知道在computed
的成员函数里做耗时计算的后果是什么。
现代前端框架倾向于使用
Virtual DOM 可以port 到任何除了Web 以外的宿主环境。Virtual DOM diff 算法足够快,框架把DOM diff 和DOM 修改的工作交给了算法,可以把精力花在实现框架的其它功能上。
const target = document.querySelector('#app')
// statelet count = 0
// viewconst div = document.createElement('div')const countText = document.createTextNode(`${count}`)div.appendChild(countText)
const button1 = document.createElement('button')const button1Text = document.createTextNode(`+`)button1.appendChild(button1Text)
const button2 = document.createElement('button')const button2Text = document.createTextNode(`-`)button2.appendChild(button2Text)
target.appendChild(div)target.appendChild(button1)target.appendChild(button2)
// eventbutton1.addEventListener('click', () => { count += 1})button2.addEventListener('click', () => { count -= 1})
上面的程序生成了count
的值。但是显然update
函数,让状态在变化的时候,触发特定的
const target = document.querySelector('#app')
// statelet state = { count: 0}
// viewconst div = document.createElement('div')const countText = document.createTextNode(`${state.count}`)div.appendChild(countText)
const button1 = document.createElement('button')const button1Text = document.createTextNode(`+`)button1.appendChild(button1Text)
const button2 = document.createElement('button')const button2Text = document.createTextNode(`-`)button2.appendChild(button2Text)
target.appendChild(div)target.appendChild(button1)target.appendChild(button2)
// eventbutton1.addEventListener('click', () => { update('count', state.count + 1)})button2.addEventListener('click', () => { update('count', state.count - 1)})
// updatefunction update(key, value) { state[key] = value countText.nodeValue = state[key]}
现在点击按钮,div
显示的 count
就会变化了,因为我们在 update
函数指明了
我敢保证上面的程序性能一定比
但没人愿意这样写程序:
- 这样的代码完全丧失了可读性,无法一眼看出
UI 树的结构。 UI 只要一调整,就需要写大量的代码。- 每当有元素依赖一个状态值,就要手动在
update
函数中加上UI 更新的逻辑。和传统的MVC 没区别。
<div>hello world</div>
会被编译成:
const div = document.createElement('div')const text = document.createTextNode('hello world')div.appendChild(text)
这并不是
Svelte 编译出来的代码,真实的代码经过了封装。这里只是为了方便讲解,但本质上是一致的。
加一个变量:
<script> let count = 0</script><div> {count}</div>
会被编译成:
let count = 0const div = document.createElement('div')const text = document.createTextNode(`${count}`)div.appendChild(text)
update() { text.nodeValue = count}
再次强调,这并非
Svelte 编译出来的真实代码。如果你对Svelte 真实编译出来的代码有兴趣,可以在官方的REPL https://svelte.dev/repl 写一个简单的Svelte 组件然后看JS output. 然后推荐进一步阅读 https://lihautan.com/compile-svelte-in-your-head/
编译器在遇到 {count}
的时候,就可以收集到在
一个完整的
<script> let count = 0</script>
<div> {count}</div><button on:click={_ => { count += 1 }}>+</button><button on:click={_ => { count -= 1 }}>-</button>
你可以在这里打开这个程序 https://svelte.dev/repl/cfd45cdafb8a48a88edab6921c69ac0c?version=3
在编译的阶段,只要遇到赋值语句,就可以插入一个语句来安排
到这里,已经解释了什么是「在编译时实现了
Svelte 的特殊语法
回到最初的例子:
let a = 1let b = 2let c = a + b
c
依赖了其它变量,如果其中的依赖发生了改变,它应该会被重新计算。在computed
实现:
const reactive = new Vue({ data() { return { a: 1, b: 2 } }, computed: { c() { return this.a + this.b } }})
<script> let a = 1 let b = 2 $: c = a + b</script>
<span>{c}</span>
:
其实是一个合法的JavaScript 语法,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
当然背后同样是在编译时实现的,它在更新视图的函数前会加入执行 a + b
并赋值给
Svelte 的跨组件通讯
状态管理和组件通讯是前端框架设计比较重要的一块,通常的做法是 Lifting State Upstore
import { wrtiable } from 'svelte/store'export let count = writable(0)
// A.svelte<script>import { count } from './store.js'
let count_valueconst unsub = count.subscribe((newValue) => { count_value = newValue})</script><sapn>{count_value}</sapn>
// B.svelte<script>import { count } from './store.js'</script><button on:click={_ => count.set(2) }>mutate</button>
每个subscribe
他的变化,然后更新到自己组件里的状态。在另一个组件里可以调用 set
更新这个状态的值。
敏锐的读者可能已经发现,上面的代码没有处理组件销毁时onDestroy()
里调用unsub()
import { wrtiable } from 'svelte/store'export let count = writable(0)
// A.svelte<script>import { count } from './store.js'
</script><sapn>{$count}</sapn>
// B.svelte<script>import { count } from './store.js'</script><button on:click={_ => $count = 2 }>mutate</button>
$
.set()
的方法。
不要以为 svelte/store
的$
为首的变量做了一些特殊处理。比如:
<script> console.log($name)</script>
会编译成:
let name_valueconst unsub = name.subscribe((newValue) => { name_value = newValue})console.log(name_value)onDestroy(() =>{ unsub()})
同样这不是
Svelte 实际生成的代码,这里是为了讲解,但本质和Svelte 的逻辑一致
对一个
<script> $name = 'new'</script>
会被编译成:
name_value.set('name')
看到这里,你可能已经知道了,其实
subscribe
. 返回一个unsubscribe 方法set
只要任何对象有实现两个方法,就可以用writable
如果你用
RxJS, 你会发现RxJS 天生就兼容store 协议
我对Svelte 的看法
我用
- 实现
Reactivity 的原理都是依赖收集,但Svelte 是在编译时完成了,Vue 在运行时收集。 Vue 用了Virtual DOM, Svelte 在编译时就知道它应该操作哪个DOM
因为所有的功能都是在编译时实现的,所以用this
我认为 Write less code 是重要的,在前端开发的领域,我们花了太多精力在处理像
所以无论是对比哪个框架,我个人觉得
function Timer () {
const [ time, setTime ] = React.useState(0)
React.useEffect(() => { const interval = setInterval(() => { setTime(time + 1) }, 1000)
return () => clearInterval(interval) }, [])
return ( <div>{time}</div> )}
这个 time
在视图里一直是
同样的逻辑在
<script> let time = 0
const interval = setInterval(() => { time = time + 1 }, 1000)
onDestroy(() => { clearInterval(interval) })</script>
<div> {time}</div>
当然可能有人会说,
我没有办法提出一个杀手级的功能吸引没有用过
至于有人说,

这只是一个大概的趋势,图中的斜率不是一个准确的值。详细在
Github 看相关的讨论 https://github.com/sveltejs/svelte/issues/2546
Svelte 的适用场景
生态是技术选型一个很重要的考虑因素,
我认为
另外一个很好的用法是用
结论
我很喜欢
最后想说的是,学习一个框架或者一个语言,不一定是非要把它用到生产环境才算是有用。我很喜欢看新的技术和学不同的语言,更多地是因为想看看在面对同一个问题的时候,不同的人解决问题的思路是怎么样的,这才是框架和语言真正的魅力。比如说你不一定非要用 Elm
延伸链接
- 知乎
- 如何看待svelte 这个前端框架? - Rethinking Reactivity (YouTube)
- Making setInterval Declarative with React Hooks
- Compile Svelte in your head
- Svelte for Sites, React for Apps
