Svelte响应式原理

lxf2023-04-13 19:57:01

前置知识:二进制的操作

假设我们有以下的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包裹起来,invalidate包裹起来,invalidate函数有两个参数,一个标记是第几个变量,另外一个赋值语句的右值表达式也就是=号右边的内容。然后用一个dirty数据记录了,那几个变量发生了更新。最后在调用生成的p函数,通过&运算检查发生更新的变量的对应的dom即可。