svelte有多火:
看看state of js 2022就知道大家对于svelte有多感兴趣了,至于usage现在也是仅次于react,vue,angular的第四大框架了。
天下三分分久必合合久必分,svelte号称自己去掉了virtual dom,没有了virtual dom的diff,那他是怎么实现响应式的呢?
写个demo看下:
我们直接进入正题,在svelte的官方 playground 写一个demo康康:
<div>变量var1是: {var1}</div>
<button on:click={clickEvent}>add1</button>
<script>
let var1 = 1;
let clickEvent = () => {
var1++;
}
</script>
页面效果如下:
编译后的.svelte文件:
对于响应式的依赖收集部分,在编译阶段是可以分析出来的,本文我们着重看一下在“干掉”virtual dom的前端框架中,是如何实现对比得知监听的变量变化并更新dom的。
svelte将我们的代码编译之后的结果如下:
/* App.svelte generated by Svelte v3.55.1 */
import {
...
} from "svelte/internal";
function create_fragment(ctx) {
let div;
let t0;
let t1;
let t2;
let button;
let mounted;
let dispose;
return {
c() {
div = element("div");
t0 = text("变量var1是: ");
t1 = text(/*var1*/ ctx[0]);
t2 = space();
button = element("button");
button.textContent = "add1";
},
m(target, anchor) {
insert(target, div, anchor);
append(div, t0);
append(div, t1);
insert(target, t2, anchor);
insert(target, button, anchor);
if (!mounted) {
dispose = listen(button, "click", /*clickEvent*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*var1*/ 1) set_data(t1, /*var1*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div);
if (detaching) detach(t2);
if (detaching) detach(button);
mounted = false;
dispose();
}
};
}
function instance($$self, $$props, $$invalidate) {
let var1 = 1;
let clickEvent = () => {
$$invalidate(0, var1++, var1);
};
return [var1, clickEvent];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
简单看下编译后的产物,分为三个大块:
-
function create_fragment,前面定义了一堆变量,后面return了几个方法。
方法c对应create,创建dom的引用变量。
方法m对应mount,作用是将c中创建的dom给加到dom上面。
方法p对应update,更新变量会调用该方法(和响应式相关,后面会讲到)。
方法d对应detach,组件销毁会调用的方法。 -
class App的定义(这部分就是我们写的自定义组件的class,没什么要关注的)。
-
function instance,能够看出来该方法包裹了我们在点击事件中对变量修改的方法。当有点击事件发生时,会调用clickEvent,然后调用其中的invalidate方法的内容为
$$invalidate的内容
(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;
}
从上面的.svelte编译产物可以得出方法$$invalidate的入参ret是点击事件之前的变量var1的值“1”,rest[0]是点击事件发生之后的变量var1的值"2",当前后值`not_equal`的话,会调用`make_dirty`方法。
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));
}
svelte有个约定,当dirty数组为[-1]代表着组件为干净的,我们直接看最后一行,也是svelte响应式的最关键的一行代码:component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); 这一看是一个位运算啊!其中i代表着修改的变量的序号。
Bitmask标记变量的状态
方便理解,我们在点击事件中多加几个变量var1 --- var7,并且打个debugger看一下:
- component.$$.ctx的值是一个数组,里面保存了所有的变量 [var1, var2, var3, var4, var5, var6, var7]。
- component.$$.dirty也是个数组,且其现在的值是[63],因为这里涉及到js的位运算,位运算中所有的数字都是符合
IEEE-754标准的64位双精度浮点类型。而所有的位运算都只会保留32结果的整数(这段话引用自 硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准),数字“63”的32位2进制表示位:0000, 0000, 0000, 0000, 0000, 0000, 0011, 1111
再来看下component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)),我们分为两部分理解:
-
左侧部分为被赋值的变量component.$$.dirty,其下标为:
(i / 31) | 0,就是变量序号除以31再向下取整,意为取dirty数组中的第几个'32位'的变量。在我们的例子中变量没有超过32个,所以取的就是这个dirty[0]。(这里理解下来就是dirty这个数组中的每一个数字都代表着32个变量的状态,如果我们的组件中有33个变量的话,即var1到var33,dirty这个数组的长度就会变为2了,dirty.length === 2) -
右侧部分代表着这32个变量中的哪一位需要变dirty,
1 << (i % 31)把0001向左移动 i % 31位,代表该位的变量是dirty的。(比方说var4变量需要发生变化变为dirty的,那么右侧部分就是把0000, 0000, 0000, 0000, 0000, 0000, 0000, 0001向左移动三位,变成了0000, 0000, 0000, 0000, 0000, 0000, 0000, 1000) -
|=两个部分取“或”,意思是说如果原来第i个变量dirty了,那就是dirty的,不然的话给他设置成为dirty。(比方说原来只有var2是dirty的,现在var4也dirty了,那么就是0000, 0000, 0000, 0000, 0000, 0000, 0000, 0010 | 0000, 0000, 0000, 0000, 0000, 0000, 0000, 1000,结果是0000, 0000, 0000, 0000, 0000, 0000, 0000, 1010也就是10)
schedule_update的内容
dirty完成之后,当然就是更新组件了
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
export function flush() {
...
while (flushidx < dirty_components.length) {
const component = dirty_components[flushidx];
flushidx++;
update(component.$$);
}
...
}
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);
}
}
这里在方法update中就会调用组件在create_fragment中返回的p方法了。
p(ctx, [dirty]) {
if (dirty & /*var1*/ 1) set_data(t1, /*var1*/ ctx[0]);
},
因为这里使用的变量的dom节点是一个text,所以对应的更新方法是set_data,如果是input元素的话,其方法就会是`set_input_value`等等。。。
set_data中的内容
export function set_data(text, data) {
data = '' + data;
if (text.wholeText !== data) text.data = data;
}
入参text是对应的dom节点,这里直接将其的内容修改成改变之后的value了。
至此,变量更新到dom更新的流程就结束了。
reference: juejin.cn/post/696574…