前置知识:二进制的操作
假设我们有以下的4位的二进制0000,这样子我们就可以使用每个位置上的0代表不存在,1代表存在,又因为二进制进行|运算的时候,只要有一位为1则为1,因此我们可以使用|运算来设置我们想要的对应位置为1,又因为&运算的时候对应位置主要有一个为0,就是为0,我们可以使用&运算检查相应位置是否为1。我们来看一下具体的例子。
const VIP = 1 << 0 // 0001
const SVIP = 1 << 1 // 0010
const SSVIP = 1 << 2 // 0100
const SSSVIP = 1 << 3 // 1000
这样子我们就表示用户拥有的权限了,例如const user = VIP | SVIP | SSVIP 拥有了3中权限,如果我们想要检查用户拥有某些权限,只需用&来检查即可,例如user & VIP 输出 1代表有, user & SVIP 输出 1,user & SSVIP 输出还是1,但是 user & SSSVIP 输出的就是0了,表明它没有SSSVIP的权限,因为我们没有用|来打开权限。
svelte的响应原理
<script>
let count = 0
</script>
<button on:click={() => count += 1}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
对于上面的组件,我们只要点击一下button,{count}就会更新到最新的值,svelte是怎么做到的呢?
我们先来看一下svelte编译输出的代码。
function create_fragment(ctx) {
let button;
let t0;
let t1;
let t2;
let t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "";
let t3;
let mounted;
let dispose;
return {
c() {
button = element("button");
t0 = text("Clicked ");
t1 = text(/*count*/ ctx[0]);
t2 = space();
t3 = text(t3_value);
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t0);
append(button, t1);
append(button, t2);
append(button, t3);
if (!mounted) {
dispose = listen(button, "click", /*click_handler*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0]);
if (dirty & /*count*/ 1 && t3_value !== (t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "")) set_data(t3, t3_value);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
mounted = false;
dispose();
}
};
}
function instance($$self, $$props, $$invalidate) {
let count = 0;
const click_handler = () => $$invalidate(0, count += 1);
return [count, click_handler];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
其他的代码我删除掉了,我们暂时不用关心。我们只关注$$invalidate(0, count += 1) 和if (dirty & /count/ 1) set_data(t1, /count/ ctx[0])。
我们先来看一下$$invalidate函数的两个参数,第一位为变量索引,意思为是这个component的第几个变量,具体是什么意思呢?我们来看一下组下面的代码。
假设你在script便签内写了这样子的代码并且在tempalte中引用到了他们。
<script>
let count1 = 0
let count2 = 0
</script>
就会生成以下代码
$$invalidate(0, count1 += 1)
$$invalidate(1, count2 += 1)
0就代表count1, 1就代表count2,这些索引标志最终用来标记template引用到的变量是否有更新。接下我们来看一下,init函数有什么内容。
function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
...
$$.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;
})
: [];
}
我们可以看到$$invalidate,就是一个匿名函数,其中i就是我们刚刚说的变量索引,ret就是要更新的值。其中最关键的是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));
}
(i / 31)| 0 作用就是每段31的倍数一段的数字映射为同一数组下标,例如i = 1,数组下标为0, i = 30 数组下标也是为0,i = 35 数组下标为1,i = 60数组下标为1,以此类推。右边的|= (1 << (i % 31))表达式就是我们在位标志说的打开位标志。
例如我们在component声明了let count1 = 0, let count2 = 0, 如果我们同时给这两个变量更新的话,这时候dirty数组dirty[0]里面的值就是3二进制就是0011,代表第一位和第二位的变量发生了更新,需要重新更新tempalte。
上面的schedule_udpate的作用的就是用一个微任务队列,来更新组件,具体做法就是调用生成的代码中有关更新的代码。
function create_fragment(ctx) {
return {
...
p(ctx, [dirty]) {
if (dirty & /*count1*/ 1) set_data(t0, /*count1*/ ctx[0]);
if (dirty & /*count2*/ 2) set_data(t2, /*count2*/ ctx[1]);
}
};
}
从第六行和第七行代码我们就是看出,用dirty & 对应变量索引,如果make_dirty的过程中有设置过标志的话,这时候就会调用set_data,来更新对应的dom。
总结
svelte会编译我们的代码,发现赋值语句,就会用invalidate函数有两个参数,一个标记是第几个变量,另外一个赋值语句的右值表达式也就是=号右边的内容。然后用一个dirty数据记录了,那几个变量发生了更新。最后在调用生成的p函数,通过&运算检查发生更新的变量的对应的dom即可。