🚀Svelte原理和进阶看这篇就够了🚀
作为后起之秀,Svelte
到底是怎么俘获大批开发者的呢?我们先从它的特性开始说起。
Svelte特性
- 🚀简洁的语法
- ✈无
虚拟DOM
- 🚗正在的响应式
🚀简洁的语法
官网给出了一个三大框架的同样功能的例子作比较
React写法
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>
);
};
Vue写法
<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>
Svelte写法
<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
的概率越大。
✈无虚拟DOM
Svelte
放弃了流行的虚拟DOM
方案,虽然虚拟DOM
足够的快,但是虚拟DOM
最致命的问题是不管状态是否发生变化,都会被重新计算并且更新。
React
会从应用根节点开始重新加载,Vue
会从所在组件开始重新加载。
Svelte
回归到了原生JavaScript
,在Svelte
中,每个组件都有一个对应的JavaScript
类,称为“组件实例”。当组件状态发生变化时,Svelte
会生成一个新的组件实例,并使用差异算法比较新旧组件实例的DOM
结构,然后更新需要更改的部分。
Svelte
使用的差异算法与传统的虚拟DOM
实现类似,都是将新旧DOM树
进行比较,找出需要更新的部分。但是,Svelte
使用了一些优化技巧来减少比较的复杂性和DOM
操作的数量。
- 💎使用key标记DOM
- 💎合并DOM(移位算法)、减少DOM
- 💎缓存DPOM(可变长缓存)
💎使用key标记DOM
使用“key”
属性来帮助Svelte
识别相同类型的元素。当Svelte
在比较新旧DOM
树时遇到相同类型的元素时,它会使用“key”
属性来判断这些元素是否相同,并避免进行不必要的更新。这可以减少比较的复杂性和DOM
操作的数量,从而提高性能。
💎合并DOM(移位算法)
Svelte还使用了一种称为“移位”算法的技巧来进一步优化差异算法。移位算法是一种将多个连续的DOM操作合并为单个操作的技术,从而减少DOM操作的数量和复杂性。
另外,还针对{{#if}}指令做了优化,Svelte
会使用DOM
元素的插入和移除来隐藏或显示元素,而不是使用CSS
的display:none
等方式。这种方法也可以减少DOM操作的数量和复杂性。
💎缓存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编译原理
既然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
朴素的编译原理。
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
最成功的地方。
如果你发现文章有错误的地方,请及时告诉我,十分感谢。
转载自:https://juejin.cn/post/7235628080219078693