<script>
let a = 1;
let b = 2;
$: total = a+b
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {total}</p>
//Name.svelte
<script lang='typescript'>
export let name = "yuxl"
</script>
<span>
{name}
</span>
//Age.svelte
<script lang='typescript'>
export let age = 18
</script>
<span>
{age}
</span>
//index.svelte
<script>
import Name from './Name.svelte'
import Age from './Age.svelte'
</script>
<div>
<Name name="some name"/>
<Age age = {20} />
</div>
<script lang='typescript'>
const promise = new Promise((resolve)=>{
setTimeout(()=>{
resolve("success")
},2000)
})
</script>
<div>
{#await promise}
<!-- promise is pending -->
<p>waiting for the promise to resolve...</p>
{:then value}
<!-- promise was fulfilled -->
<p>The value is {value}</p>
{/await}
</div>
<script>
let m = { x: 0, y: 0 };
function handleMousemove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
svelte编译后的代码与hello world相比增加的代码:
function create_fragment(ctx) {
let div;
let t0;
let t1_value = /*m*/ ctx[0].x + "";
let t1;
let t2;
let t3_value = /*m*/ ctx[0].y + "";
let t3;
return {
...
p(ctx, [dirty]) {
if (dirty & /*m*/ 1 && t1_value !== (t1_value = /*m*/ ctx[0].x + "")) set_data(t1, t1_value);
if (dirty & /*m*/ 1 && t3_value !== (t3_value = /*m*/ ctx[0].y + "")) set_data(t3, t3_value);
},
...
};
}
function instance($$self, $$props, $$invalidate) {
let m = { x: 0, y: 0 };
function handleMousemove(event) {
$$invalidate(0, m.x = event.clientX, m);
$$invalidate(0, m.y = event.clientY, m);
}
return [m, handleMousemove];
}
深入浅出svelte.js
最近有一个官网页,打算用svelte体验一下,顺便学习了一下svelte(发音:[svelt]),整体来说,svelte是比较简洁的,上手很快。不过与其说是一个前端框架,不如说是一个“dom操作编译器”。svelte的开发代码,在编译阶段会被编译成一系列的dom操作的代码,运行时的代码很少。因此svelte.js的体积很小(只保留了脏值检测更新和封装dom操作API等core代码)。本文从一下几个方面聊一聊对于svelte的认识。
一、svelte初体验
我们直接来看官网的例子:
实现的功能也很简单,就是两个Input的值求和,然后展示出来。用svelte编写的代码为:
上述代码很简洁,像vue一样也是类似style dom script的三段式写法,不过比vue更加简洁一点,比如dom不需要template包裹等等。
同样的上述的例子的代码如果用react书写:
上述react的写法,必须要先弄懂useState的含义等,此外缺少了默认的双向数据绑定,代码有一点冗余。
同样的上述的例子的代码如果用vue书写:
三者对比:
单纯的说,svelte编码只需要145个字符,比vue和react少,因此得出说svelte的编码体积更小,这样是不对的,因为svelte会在编译阶段将代码编译到更加贴近dom操作的代码,上述例子的代码,编译后的结果为:
在编译后生成的代码其实代码量也不小,是远远大于145个字符的,也不能说因为编译后的代码量大,所以说svelte有点名不副实,并不能减少运行时代码的体积。要考虑到svelte的运行时代码是很少的.我们来对比一下:
从上述对比中可以看出,svelte的体积很少,虽然其业务代码在编译后会生产较多的代码。得益于较少的运行时代码。虽然svelte代码的随着业务的编写增量速度比较快,得益于其很小的包体积1.6k,对于一般中小型项目而言,整体运行的代码(编译后的代码+包体积)还是比较小的,所以可以说svelte项目的代码较小。不过对于大型项目而言,因为svelte随着业务的进行,运行时代码增量陡峭,大型项目体积并不会比react、vue等小,因此需要辩证看待。
此外虽说svelte的代码在编译后体积很大,但是在编译前的代码,其实很简洁,这种简洁,一定程度上,可以增强开发体验。
二、 svelte的语法
svelte的写法跟vue有点类似,是指令式和响应式的。
基本用法
这是一个最简单的hello world的例子,上述代码中很简洁。在编译后的代码分为js编译和css编译。
svelte/internal包中是一些封装了dom操作的函数。
css编译结果:
css是通过创建style标签引入到最后的dom中的。
指令形式和数据绑定
还是以上面的例子为例,上述就是一个指令形式+数据绑定的形式。跟vue的写法很相似,改例子绑定了input和a, input和b.效果如下:
这里的$total: 就是reactive statement. 类似vue中的计算属性。
组件compose
在svelte中的组件的compose也是跟react中类似的,不同的是在react中export的属性就是组件的props,写法上比较简洁,此外,export const 和export function、export class这3个组件的props是只读的,不可写。
模版语法
在svelte中,html相关的场景适用于模版语法,最简单的模版语法为:
这里介绍几个在svelte中几个比较有趣的模版语法。
运行debugger的结果为: @debug 在后面跟的参数name发生变化的时候会进行debugger,从上图我们看到debugger的地方上下文的代码是编译后运行时,跟编码的时候有一点区别,也进一步说明,svelte可以看作是一个前端的编译框架,真正运行时的代码是编译后的结果。
await
用法为:{#await expression}...{:then name}...{:catch name}...{/await}
动画效果
在svelte中,对于原始的dom元素,自带了一些动画指令,在一般的官网或者活动页中,场景最多的就是动画效果,svelte自带的动画指令,因此在写官网的时候方便了不少。
以transition:fly为例:
最后的结果为:
当然在svelte中也支持自定义动画指令。
组件的生命周期
svelte组件也提供了完整的生命周期。onMount、
beforeUpdate
、afterUpdate
、onDestroy
等。见名思意,这里不一一介绍,跟react & vue的组件生命周期近似。除了上述之外,svelte还支持自定义元素(custom element), store以及context等等。
三、Virtual Dom和Dom
这个其实可以,比较客观的去看待,svelte的作者认为,Virtual Dom的性能并没有太大的问题,不管是diff算法还是render的过程都没有什么性能问题,不过作者认为,svelte不需要diff,还是有一点优势的。虽然**diff**很快,但是没有diff的话,显然会更快的得到渲染结果。
svelte的编译后的结果来看,所有的dom的变动都变为了直接的dom操作行为,是不需要做diff的,这种方法,没有diff/patch,因此从速度来看,肯定更快一些。 比如:
上述这个例子中,修改了visible,编译后的代码知道这个行为,这是一个确定的会如何影响dom的行为,编译后的结果部分为:
可以看到,state的改变如何影响dom在svelte的编译结果中都是很确定的。
除了性能问题,svelte的作者认为,因为virtualDom的存在,需要保存new object和old object的虚拟dom对象,在react的编程中,每一次渲染都有这两个对象,这两个对象,在正常的开发中,很容易添加一些冗余代码:
在这个例子中,为每一个li都绑定了一个事件,这是不过度优化情况下的正常下发,因为virtualDom虚拟dom的存在,每一次state更新的时候,每一个new object和old object都包含了每一个li的绑定函数,这些是冗余的代码,增加了代码的体积等。
四、优缺点
个人归纳了一下几个优缺点:
优点:
缺点
五、源码阅读
首先svelte的源码分为两部分,compiler和runtime,compiler主要的作用是将开发代码编译成运行时的代码,具体如何编译不是本文所要关注的代码。本文主要关注的是编译后的运行时的代码runtime。
dom操作相关core api
我们以最简单的hello world为例: svelte编译前源码:
svelte编译后的代码:
这里的App就可以直接使用了,比如渲染到一个父dom中可以这样使用:
上述方法就可以把App这个编译后的运行时组件渲染到body中,我们来看编译后的代码。
在svelte组件中,与dom相关的部分封装在了create_fragment中,该函数创建了一个Fragment, 该函数返回一个包含dom操作的对象:
在上述的例子中,c对应创建一个子dom元素,m表示创建元素要渲染元素时需要执行的函数,d表示删除元素时的操作。上述的例子中:
在m中的intert和d中的detach方法,都是原生的dom操作方法,上述Fragment的意思是创建了h1这个dom,并在渲染的时候插入到目标dom节点中,在Fragment这个组件元素被销毁的时候,销毁被创建的子dom元素 h1。
element、insert、detach等方法都是原生的dom操作,具体源码如下所示:
SvelteComponent组件定义了如何销毁组件以及如何设置组件的属性,以及如何增加监听函数,其中最重要的是定义了组件的实例属性 .
发现SvelteComponent组件确实包含了ctx上下文内容,以及组件的生命周期属性,以及组件的脏值检测等相关的属性。
init函数 `js export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
init函数在SvelteComponent组件内部调用,用于实例属性的初始化。这里最重要的是$$.ctx的赋值部分,后续会用来做脏值检测。ctx中保存了所有的再多次渲染中都存在的值,包含了内部的state以及监听处理函数等等。
脏值检测和更新部分
这里我们以一个带有鼠标时间的svelte组件为例,
编译前的代码:
svelte编译后的代码与hello world相比增加的代码:
这里多了一个instance函数,而这个instance函数在svelteComponent的init函数中就是用作脏值检测和更新的。
如果值发生了变动,就触发make_dirty函数:
make_dirty标记了哪一些脏组件,然后对脏组件执行schedule_update方法来更新组件:
schedule_update在需要更新时候,在下一个微任务重执行flush:
简化后的flush方法如上所示,就是遍历整个脏组件,执行所有的脏组件中的更新方法update.update方法的定义为:
update方法标记自身组件为脏,并且制定自身组件fragment中的p(全名:update)也就是前面的fragment中的:
在p方法中,直接操作dom改变UI。
总结来看,组件更新的步骤为以下几步: