TOC
$$invalidate
$$invalidate
在概念上的工作原理如下:
// conceptually...
const ctx = instance(/*...*/);
const fragment = create_fragment(ctx);
// to track which variable has changed
const dirty = new Set();
const $$invalidate = (variable, newValue) => {
// update ctx
ctx[variable] = newValue;
// mark variable as dirty
dirty.add(variable);
// schedules update for the component
scheduleUpdate(component);
};
// gets called when update is scheduled
function flushUpdate() {
// update the fragment
fragment.p(ctx, dirty);
// clear the dirty
dirty.clear();
}
但这并非其确切实现,在本文中,我们将了解$$invalidate
在svelte中是如何实现的。
$$invalidate
是Svelte响应式背后的秘密㊙️。每当一个变量是:
- reassigned(foo = 1)
- mutated(foo.bar = 1)
svelte将用
$$invalidate
函数包装赋值或更新:
name = 'Svelte';
count++;
foo.a = 1;
bar = baz = 3;
// compiled into
$$invalidate('name', (name = 'Svelte'));
$$invalidate('count', count++, count);
$$invalidate('foo', (foo.a = 1), foo);
$$invalidate('bar', (bar = $$invalidate('baz', (baz = 3))));
$$invalidate
函数将:
- 更新
$$ctx
的变量 - 在
$$.dirty
中标记变量 - schedule update
- 返回赋值或更新表达式的值
//https://github.com/sveltejs/svelte/blob/99a3cc93b66bf2c6be551e23101bfdbdb2c6bf72/packages/svelte/src/runtime/internal/Component.js#L124C2-L133C8
// 1. update the variable in $$.ctx
$$.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);
// 2a. mark the variable in $$.dirty
if (ready) make_dirty(component, i);
}
// 4. return the value of the assignment or update expression
return ret;
})
: [];
//https://github.com/sveltejs/svelte/blob/99a3cc93b66bf2c6be551e23101bfdbdb2c6bf72/packages/svelte/src/runtime/internal/Component.js#L78
/** @returns {void} */
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
// 3. schedule an update
schedule_update();
// initialise $$.dirty
component.$$.dirty.fill(0);
}
// 2b. mark the variable in $$.dirty
component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
}
在3.16.0以前,svelte源码使用对象将变量标记为脏:
$$.dirty = { givenName: true, familyName: false };
在之后使用位掩码的技术跟踪更改。 svelte为每个变量分配索引:
givenName -> 0
familyName -> 1
并使用位掩码存储脏信息:
$$.dirty = [0b0000_0011]
// 0和1位标记为true
Bitmask
最紧凑的的方式表示一组true或false的方式,是使用位。如果该位为1,则为true,为0则为false。
数字可以使用二进制表示,5就是二进制的0b0101
如果5用4位二进制表示,那么可以存储4个布尔值,第0和第2位为true,第1位和第3位为false(从右到左,从最低有效位到最高有效位)。
在JavaScript中,数字可以用64位表示。但是,当对数字使用按位运算时,JavaScript会将数字视为32位。 要坚持和修改存储在数字中的布尔值,可以使用按位运算:
// set 1st boolean to true
0b0101 | 0b0010 = 0b0111;
// set 2nd boolean to false
0b0101 & 0b1011 = 0b0001;
// is 2nd boolean true?
((0b0101 & 0b0100) > 0) === true;
// NOTE: You can test multiple boolean values at once
// is 2nd and 3rd boolean true?
((0b0101 & 0b1100) > 0) === true;
在位运算中使用的第二个操作数,就像掩码,允许我们定位第一个数字中存储布尔值的特定位。 在这里称掩码为位掩码。
Bitmask in Svelte
如上所述,为每个变量分配一个索引:
givenName -> 0
familyName -> 1
在instance函数中,返回的ctx作为数组:
function instance($$self, $$props, $$invalidate) {
// ...
return [givenName, familyName];
}
此后访问变量可通过索引,而非变量名:
$$.ctx[0] + $$.ctx[1];
$$invalidate
函数的工作原理相同:
$$invalidate(0, (givenName = 'Li Hau'));
$$.dirty
现在也存储数组,数组每一项携带31个布尔值,每个布尔值指示该索引的变量是否为dirty
.
要将数据设置为脏变量,可直接使用位运算:
$$.dirty[0] |= 1 << 0;
要验证是否位脏,也使用位运算:
if ($dirty[0] & 1) { /* ... */ }
if ($dirty[0] & 3) { /* ... */ }
使用位掩码后,$$.dirty
重置为[-1]
而不是null
响应式声明
svelte允许使用label语句申明响应值:
<script>
export let count = 0;
// `doubled`, `tripled`, `quadrupled` are reactive
$: doubled = count * 2;
$: tripled = count * 3;
$: quadrupled = doubled * 2;
</script>
{doubled} {tripled} {quadrupled}
如果查看编译后的输出,会发现声明语句出现在instance函数中:
function instance($$self, $$props, $$invalidate) {
// ...
$$self.$$.update = () => {
if ($$self.$$.dirty & /*count*/ 8) {
$: $$invalidate(0, doubled = count * 2);
}
if ($$self.$$.dirty & /*count*/ 8) {
$: $$invalidate(1, tripled = count * 3);
}
if ($$self.$$.dirty & /*doubled*/ 1) {
$: $$invalidate(2, quadrupled = doubled * 2);
}
};
return [doubled, tripled, quadrupled, count];
}
- 当存在响应式声明时,svelte定义
$$.update
方法。$$.update
函数默认是无操作函数
- svelte也使用
$$invalidate
更新响应式变量的值 - svelte根据申明与语句之间的依赖关系对响应式声明和语句排序
由于所有响应式声明和语句都分组到$$.update
方法中,而且svelte会根据它们的依赖关系对声明和语法排序,因此最终输出结果与源码中声明的位置和顺序无关。
以下组件是有效的:
<script>
// NOTE: use `count` in a reactive declaration before `count` is declared
$: doubled = count * 2;
let count = 1;
</script>
{count} * 2 = {doubled}
在flush
函数中调用了一个update
函数,其结构如下:
//https://github.com/sveltejs/svelte/blob/99a3cc93b66bf2c6be551e23101bfdbdb2c6bf72/packages/svelte/src/runtime/internal/scheduler.js#L113
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
函数在DOM更新的同一个微任务中被调用,就是调用$$.fragment.p()
更新DOM之前。
从上方update函数可得出:
所有响应式声明和语句都是批处理的
正如DOM更新的批处理方式一样:
<script>
let givenName = '', familyName = '';
function update() {
givenName = 'Li Hau';
familyName = 'Tan';
}
$: name = givenName + " " + familyName;
$: console.log('name', name);
</script>
当update被调用时:
- 与上述流程类似,为givenName和familyName执行
$$invalidate
,并安排更新 - 任务结束
- 微任务开始
flush()
执行时为每个标记为脏的组件调用update
函数- 允许
$$.update
- 由于
givenName
和familyName
被更改,执行name
的$$invalidate
- 由于
name
被更改,执行console.log('name', name);
- 由于
- 调用
$$.fragment.p(...)
更新DOM
如⬆上,即使更新了givenName
和familyName
,也只更新name
和执行console.log('name', name);
一次而不是两次
响应式声明或语句外的响应式变量可能不是最新的
由于反应式声明和语句是在下一个微任务中批量执行的,因此您不能期望值会同步更新。
<script>
let givenName = '', familyName = '';
function update() {
givenName = 'Li Hau';
familyName = 'Tan';
console.log('name', name); // Logs ''
}
$: name = givenName + " " + familyName;
</script>
可在另一个响应式语句中引用响应式变量:
<script>
let givenName = '', familyName = '';
function update() {
givenName = 'Li Hau';
familyName = 'Tan';
}
$: name = givenName + " " + familyName;
$: console.log('name', name); // Logs 'Li Hau Tan'
</script>
响应式声明和语句的排序
Svelte 尝试尽可能保留反应式声明和语句的声明顺序。 但是,如果一个反应式声明或语句引用了另一个反应式声明定义的变量,那么它将被插入到后一个反应式声明之后:
<script>
let count = 0;
// NOTE: refers to `doubled`
$: quadrupled = doubled * 2;
// NOTE: defined `doubled`
$: doubled = count * 2;
// compiles into:
$$self.$$.update = () => {
// ...
$: $$invalidate(/* doubled */, doubled = count * 2);
$: $$invalidate(/* quadrupled */, quadrupled = doubled * 2);
// ...
}
</script>
非响应式变量
svelte编译器会跟踪<script>
脚本中所有变量。如果响应式声明或语句中变量使用了但未发生mutated
或reassigned
,则该响应式声明或语句将不会被添加到$$.update
中。
<script>
let count = 0;
$: doubled = count * 2;
</script>
{ count } x 2 = {doubled}
JavaScript output
function instance($$self, $$props, $$invalidate) {
let doubled;
$: $$invalidate(0, doubled = count * 2);
return [doubled];
}
由于count
未被mutated
或reassigned
,svelte通过不定义$$self.$$.update
来优化输出。
总结
- Svelte在编译阶段追踪被
dirty
的变量并在DOM更新时批量更新 - Svelte使用位掩码技术生成更加紧凑的运行时JavaScript代码
- 响应式声明和语句和DOM更新类似,也是批量执行