作为后起之秀,Svelte
到底是怎么俘获大批开发者的呢?我们先从它的特性开始说起。
虚拟DOM
官网给出了一个三大框架的同样功能的例子作比较
import React, { useState } from 'react';
export default () => {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
function handleChangeA(event) {
setA(+event.target.value);
}
function handleChangeB(event) {
setB(+event.target.value);
}
return (
<div>
<input type="number" value={a} onChange={handleChangeA}/>
<input type="number" value={b} onChange={handleChangeB}/>
<p>{a} + {b} = {a + b}</p>
</div>
);
};
<template>
<div>
<input type="number" v-model.number="a">
<input type="number" v-model.number="b">
<p>{{a}} + {{b}} = {{a + b}}</p>
</div>
</template>
<script>
export default {
data: function() {
return {
a: 1,
b: 2
};
}
};
</script>
<script>
let a = 1;
let b = 2;
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {a + b}</p>
结果一目了然了。
Svelte
认为,你写的代码越多,造成更多的bug
的概率越大。
Svelte
放弃了流行的虚拟DOM
方案,虽然虚拟DOM
足够的快,但是虚拟DOM
最致命的问题是不管状态是否发生变化,都会被重新计算并且更新。
React
会从应用根节点开始重新加载,Vue
会从所在组件开始重新加载。
Svelte
回归到了原生JavaScript
,在Svelte
中,每个组件都有一个对应的JavaScript
类,称为“组件实例”。当组件状态发生变化时,Svelte
会生成一个新的组件实例,并使用差异算法比较新旧组件实例的DOM
结构,然后更新需要更改的部分。
Svelte
使用的差异算法与传统的虚拟DOM
实现类似,都是将新旧DOM树
进行比较,找出需要更新的部分。但是,Svelte
使用了一些优化技巧来减少比较的复杂性和DOM
操作的数量。
使用“key”
属性来帮助Svelte
识别相同类型的元素。当Svelte
在比较新旧DOM
树时遇到相同类型的元素时,它会使用“key”
属性来判断这些元素是否相同,并避免进行不必要的更新。这可以减少比较的复杂性和DOM
操作的数量,从而提高性能。
Svelte还使用了一种称为“移位”算法的技巧来进一步优化差异算法。移位算法是一种将多个连续的DOM操作合并为单个操作的技术,从而减少DOM操作的数量和复杂性。
另外,还针对{{#if}}指令做了优化,Svelte
会使用DOM
元素的插入和移除来隐藏或显示元素,而不是使用CSS
的display:none
等方式。这种方法也可以减少DOM操作的数量和复杂性。
Svelte
还使用了一种称为“可变长度缓存”(VLC
)的技术来进一步优化差异算法。可变长度缓存是一种将最近使用的元素缓存起来,以便它们可以更快地被访问和使用的技术。当Svelte
比较新旧DOM树
时,它可以使用VLC
缓存来快速查找和访问最近使用的元素,从而减少比较的复杂性和时间复杂度。
所以,Svelte虽然没有虚拟DOM,但是它的性能反而更好。
什么是响应式?就是当一个值发生改变时,使用这个值的地方做出相应的改变。
如果不同的人设计响应式的功能,它的使用方案也会不尽相同。
例如,早期的Svelte
写法如下:
const { count } = this.get();
this.set({
count: count + 1
});
React
的写法
const { count } = this.state;
this.setState({
count: count + 1
});
hook
const [count, setCount] = useState(props.count)
setCount(count + 1)
Vue3
写法
const { count } = defineProps(props)
count ++
这些方案都是基于一些响应式的Api实现的响应式功能。
Svelte
意识到最好的API
就是根本没有 API
。我们可以直接使用。
let count = 0
count +=1
以上就是Svelte
的主要特性。总结下:
Svelte
拥有接近原生JavaScript
的写法Svelte
没有虚拟DOM
,使用原生DOM
描述组件Svelte
没有Api
既然Svelte
没有Api
,那到底是怎么追踪变量变化的呢?
接下来我们由简单到复杂,来看看Svelte
的编译结果。
首先我们看看,下面的代码会被编译成啥样的:
<h1>Hello world!</h1>
/* App.svelte generated by Svelte v3.59.1 */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
let h1;
return {
c() {
h1 = element("h1");
h1.textContent = "Hello world!";
},
m(target, anchor) {
insert(target, h1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(h1);
}
};
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default App;
很明显,组件编译之后,会返回一个继承了SvelteComponent
的类,并且在构造函数中执行了init
方法,它的其中一个参数为在组件中定义的create_fragment
函数。
这个函数会返回一个对象,包含组件对应的的create``mount``update``delete
操作。由于上面的代码中是个静态的字符串,所以p
对应的值为noop
即no operate
没有操作。
接下来,我们修改下代码如下:
<script>
let count = 0
</script>
<h1>Hello world!</h1>
此时组件编译之后,仅仅出现了
let count = 0
其余没有变化,(所以代码里没有用到的变量,我们应该即时删除)
接着,我们新增代码如下:
<script>
let count = 0
</script>
<h1 on:click={() => count++}>Hello world!</h1>
function create_fragment(ctx) {
let h1;
let mounted;
let dispose;
return {
c() {
h1 = element("h1");
h1.textContent = "Hello world!";
},
m(target, anchor) {
insert(target, h1, anchor);
if (!mounted) {
dispose = listen(h1, "click", /*click_handler*/ ctx[1]);
mounted = true;
}
},
// ...
d(detaching) {
if (detaching) detach(h1);
mounted = false;
dispose();
}
};
}
function instance($$self, $$props, $$invalidate) {
let count = 0;
const click_handler = () => $$invalidate(0, count++, count);
return [count, click_handler];
}
我们可以看到在mounted
之后使用listen
方法新增了一个针对h1
的click
方法的监听事件,并且在delete
阶段移除监听事件。
同时多了个实例方法instance
,它的返回值是count
的实际值,以及修改count
的处理函数。请记住这里,后面还会提到。
值得注意的是h1
的click
事件的参数是/*click_handler*/ ctx[1])
function instance($$self, $$props, $$invalidate) {
let count = 0;
const click_handler = () => $$invalidate(0, count++, count);
return [count, click_handler];
}
此时,init方法也发生了改变
// 之前
init(this, options, null, create_fragment, safe_not_equal, {});
// 之后
init(this, options, instance, create_fragment, safe_not_equal, {});
最关键的来了,此时我们继续修改代码如下
<script>
let count = 0
</script>
<h1 on:click={() => count++}>Hello world!{count}</h1>
再去查看编译结果,create_fragment
发生了重大变化
function create_fragment(ctx) {
let h1;
let t0;
let t1;
let mounted;
let dispose;
return {
c() {
h1 = element("h1");
h1.textContent = "Hello world!";
t0 = text("Hello world!");
t1 = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
append(h1, t0);
append(h1, t1);
if (!mounted) {
dispose = listen(h1, "click", /*click_handler*/ ctx[1]);
mounted = true;
}
},
// ...
d(detaching) {
if (detaching) detach(h1);
mounted = false;
dispose();
}
};
}
值得注意的是,t1
的值为/* count */ ctx[0]
instance
的返回值为[count, click_handler]
,
结合前面的内容,得出一个明显的结论:instance
的返回值就是create_fragment
的参数!
好了,啰里吧嗦这么多,我们终于可以讨论开头的问题了
既然Svelte没有Api,那到底是怎么追踪变量变化的呢?
svelte
在编译时,会检测所有变量的赋值行为,并将变化后的值和赋值的行为,作为创建片段的参数。
这就是svelte
朴素的编译原理。
现在我们又有了一个新的问题。我们已经可以感知到值的变化,那是怎么将值得变化更新到页面中的了。
你可能马上想到的是create_fragment
返回的updata
方法啊。这里仅仅是提供了更新页面DOM的方法,那是什么样的时机调用这个更新方法的呢?
init
方法其实,svelte的编译结果是运行时运行的代码。在进入运行时,首先执行init
方法,该方法大致流程如下:
instance
方法,在回调函数中标记脏组件
beforeUpdate
生命周期的函数create_fragment
函数create_fragement
返回的m(mounted)
方法flush
方法你可以跳过这段代码,不影响阅读
export function init(
component,
options,
instance,
create_fragment,
not_equal,
props,
append_styles,
dirty = [-1]
) {
const parent_component = current_component;
set_current_component(component);
const $$: T$$ = component.$$ = {
fragment: null,
ctx: [],
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
on_disconnect: [],
before_update: [],
after_update: [],
context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
// everything else
callbacks: blank_object(),
dirty,
skip_bound: false,
root: options.target || parent_component.$$.root
};
append_styles && append_styles($$.root);
let ready = false;
$$.ctx = instance
? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i);
}
return ret;
})
: [];
$$.update();
ready = true;
run_all($$.before_update);
// `false` as a special case of no DOM component
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
start_hydrating();
const nodes = children(options.target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.l(nodes);
nodes.forEach(detach);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.c();
}
if (options.intro) transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor, options.customElement);
end_hydrating();
flush();
}
set_current_component(parent_component);
}
看起来,flush
方法很可能才是我们需要的答案。
flush
方法flush
的方法主要做了一件事:
遍历需要更新的组件(dirty_components
),然后更新它,并且调用afterUpdate
方法。
export function flush() {
// Do not reenter flush while dirty components are updated, as this can
// result in an infinite loop. Instead, let the inner flush handle it.
// Reentrancy is ok afterwards for bindings etc.
if (flushidx !== 0) {
return;
}
const saved_component = current_component;
do {
// first, call beforeUpdate functions
// and update components
try {
while (flushidx < dirty_components.length) {
const component = dirty_components[flushidx];
flushidx++;
set_current_component(component);
update(component.$$);
}
} catch (e) {
// reset dirty state to not end up in a deadlocked state and then rethrow
dirty_components.length = 0;
flushidx = 0;
throw e;
}
set_current_component(null);
dirty_components.length = 0;
flushidx = 0;
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
// ...so guard against infinite loops
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
seen_callbacks.clear();
set_current_component(saved_component);
}
我们再来看看具体的更新操作update
函数做了啥
before_update
方法create_fragment
返回的p(update)
方法function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
好了,我们总结下:在运行时
instance
方法,在回调函数中标记脏组件
beforeUpdate
生命周期的函数create_fragment
函数create_fragement
返回的m(mounted)
方法flush
方法before_update
方法create_fragment
返回的p(update)
方法afterUpdate
方法好了,今天的分享就这些了,总的来说,Svelte
的响应式原理虽然很朴素,但是却拥有了更好的性能,同时也降低了开发者的记忆负担。我觉得这是svelte
最成功的地方。
如果你发现文章有错误的地方,请及时告诉我,十分感谢。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。