svelte响应式原理
svelte文件编译为js后的结构
源代码:
<script lang="ts">
let firstName = '张'
let lastName = '三'
let age = 18
function handleChangeName() {
firstName = '王'
lastName = '二'
}
function handleChangeAge() {
age = 28
}
</script>
<div>
<p>fullName is {firstName} {lastName}</p>
<p>age is {age}</p>
<div>
<button on:click={handleChangeName}>change name</button>
<button on:click={handleChangeAge}>change age</button>
</div>
</div>
编译后的js代码结构
function create_fragment(ctx) {
const block = {
c: function create() {
// ...
},
m: function mount(target, anchor) {
// ...
},
p: function update(ctx, [dirty]) {
// ...
},
d: function destroy(detaching) {
// ...
}
};
return block;
}
function instance($$self, $$props, $$invalidate) {
let firstName = '张';
let lastName = '三';
let age = 18;
function handleChangeName() {
$$invalidate(0, firstName = '王');
$$invalidate(1, lastName = '二');
}
function handleChangeAge() {
$$invalidate(2, age = 28);
}
return [firstName, lastName, age, handleChangeName, handleChangeAge];
}
class Name extends SvelteComponentDev {
constructor(options) {
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
初始化调用init方法
function init(component, options, instance, create_fragment, ...,dirty = [-1]) {
// $$属性为组件的实例
const $$ = component.$$ = {
...
// dirty的作用是标记哪些变量需要更新,
// 在update生命周期的时候将那些标记的变量和对应的dom找出来,更新成最新的值。
dirty,
// fragment字段为一个对象,对象里面有create、mount、update等方法
fragment: null,
// 实例的ctx属性是个数组,存的是组件内的顶层变量、方法等。按照定义的顺序存储
ctx: [],
...
}
// ctx属性的值为instance方法的返回值。
// instance方法就是svelte文件编译script标签代码生成的。
// instance方法的第三个参数为名字叫$$invalidate的箭头函数,
// 在js中修改变量的时候就会自动调用这个方法
$$.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)) {
make_dirty(component, i);
}
return ret;
})
: [];
// 调用create_fragment方法
// 并且在后续对应的生命周期里面调用create_fragment方法返回的create、mount、update等方法
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
}
点击change name按钮,修改firstName和lastName的值
let firstName = '张'
let lastName = '三'
let age = 18
function handleChangeName() {
// firstName变量第一个定义,所以这里是0,并且将新的firstName的值传入$$invalidate方法
$$invalidate(0, firstName = '王');
// lastName变量第二个定义,所以这里是1,并且将新的firstName的值传入$$invalidate方法
$$invalidate(1, lastName = '二');
}
// ...
再来看看invalidate函数的定义,invalidate函数就是在init时调用instance的时候传入的第三个参数
(i, ret, ...rest) => {
// 拿到更新后的值
const value = rest.length ? rest[0] : ret;
// 判断更新前和更新后的值是否相等,不等就调用make_dirty方法
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
// 第一个参数为组件对象,第二个参数为变量的index。
// 当更新的是firstName变量,firstName是第一个定义的,所以这里的i等于0
// 当更新的是lastName变量,lastName是第二个定义的,所以这里的i等于1
make_dirty(component, i);
}
return ret;
}
make_dirty方法的定义
function make_dirty(component, i) {
// dirty初始化的时候是由-1组成的数组,dirty[0] === -1说明是第一次调用make_dirty方法。
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
// 在下一个微任务中调用create_fragment方法生成对象中的update方法。
schedule_update();
// 将dirty数组的值全部fill为0
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
二进制运算 demo
// 有采购商权限
purchaser= 1 << 2 => 100
// 有供应商商权限
supplier = 1 << 1 => 010
// 有运营权限
admin = 1 << 0 => 001
user1 = purchaser | supplier | admin => 111
user2 = purchaser | supplier => 110
// 用户是否有admin的权限
user1 & admin = 111 & 001 = true
user2 & admin = 110 & 001 = false
再来看看component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
。dirty数组中每一位能够标记31个变量是否为dirty。
(i / 31) | 0
就是i/31然后取整。
- 比如i=0,计算结果为0。
- i=1,计算结果为0。
- i=32,计算结果为1。
(1 << (i % 31))
,1左移的位数为i和31求余的值。
- 比如i=0,计算结果为1<<0 => 01。
- i=1,计算结果为1 << 1 => 10。
- i=32,计算结果为1<<1 => 10。
当i=0时这行代码就变成了component.$$.dirty[0] |= 01
,由于dirty数组在前面已经被fill为0了,所以代码就变成了component.$$.dirty[0] = 0 | 01
=> component.$$.dirty[0] = 01
。说明从右边数第一个变量被标记为dirty。
同理当i=1时这行代码就变成了component.$$.dirty[0] |= 10
=>component.$$.dirty[0] = 0 | 10
=> component.$$.dirty[0] = 10
。说明从右边数第二个变量被标记为dirty。
create_fragment函数
function create_fragment(ctx) {
let div1;
let p0;
let t0;
let t1;
let t2;
let t3;
let t4;
let p1;
let t5;
let t6;
let t7;
let div0;
let button0;
let t9;
let button1;
let mounted;
let dispose;
const block = {
// create生命周期时调用,调用浏览器的dom方法生成对应的dom。
// element、text这些方法就是浏览器的
// document.createElement、document.createTextNode这些原生方法
c: function create() {
div1 = element("div");
p0 = element("p");
t0 = text("fullName is ");
t1 = text(/*firstName*/ ctx[0]);
t2 = space();
t3 = text(/*lastName*/ ctx[1]);
t4 = space();
p1 = element("p");
t5 = text("age is ");
t6 = text(/*age*/ ctx[2]);
t7 = space();
div0 = element("div");
button0 = element("button");
button0.textContent = "change name";
t9 = space();
button1 = element("button");
button1.textContent = "change age";
},
l: function claim(nodes) {
// ...
},
// 将create生命周期生成的dom节点挂载到target上面去
m: function mount(target, anchor) {
insert_dev(target, div1, anchor);
append_dev(div1, p0);
append_dev(p0, t0);
append_dev(p0, t1);
append_dev(p0, t2);
append_dev(p0, t3);
append_dev(div1, t4);
append_dev(div1, p1);
append_dev(p1, t5);
append_dev(p1, t6);
append_dev(div1, t7);
append_dev(div1, div0);
append_dev(div0, button0);
append_dev(div0, t9);
append_dev(div0, button1);
if (!mounted) {
dispose = [
// 添加click事件监听
listen_dev(button0, "click", /*handleChangeName*/ ctx[3], false, false, false),
listen_dev(button1, "click", /*handleChangeAge*/ ctx[4], false, false, false)
];
mounted = true;
}
},
// 修改变量makedirty后,下一次微任务时会调用update方法
p: function update(ctx, [dirty]) {
if (dirty & /*firstName*/ 1) set_data_dev(t1, /*firstName*/ ctx[0]);
if (dirty & /*lastName*/ 2) set_data_dev(t3, /*lastName*/ ctx[1]);
if (dirty & /*age*/ 4) set_data_dev(t6, /*age*/ ctx[2]);
},
i: noop,
o: noop,
d: function destroy(detaching) {
// ...
mounted = false;
// 移除事件监听
run_all(dispose);
}
};
return block;
}
再来看看update
方法里面的 if (dirty & /*firstName*/ 1) set_data_dev(t1, /*firstName*/ ctx[0]);
。
当firstName的值被修改时,firstName是第一个定义的变量,i=0。按照上面的二进制计算component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
,此时dirty[0]= 0 |(1<<0)=01
。
if (dirty & /*firstName*/ 1) set_data_dev(t1, /*firstName*/ ctx[0]);
就变成了if (01 & /*firstName*/ 1) set_data_dev(t1, /*firstName*/ ctx[0]);
。此时if条件满足,执行set_data_dev(t1, /*firstName*/ ctx[0]);
。这里的t1就是t1 = text(/*firstName*/ ctx[0]);
,使用firstName
变量的dom。
同理当lastName的值被修改时,lastName是第二个定义的变量,i=1。按照上面的二进制计算component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
,此时dirty[0]= 0 |(1<<1)=10
。
if (dirty & /*lastName*/ 2) set_data_dev(t3, /*lastName*/ ctx[1]);
就变成了if (10 & /*lastName*/ 2) set_data_dev(t3, /*lastName*/ ctx[1]);
。此时if条件满足,执行set_data_dev(t3, /*lastName*/ ctx[1]);
。这里的t3就是t3 = text(/*lastName*/ ctx[1]);
,使用lastName
变量的dom。
set_data_dev方法
function set_data_dev(text2, data) {
data = "" + data;
if (text2.wholeText === data)
return;
text2.data = data;
}
这个方法很简单,判断dom里面的值和新的值是否相等,如果不等直接修改dom的data属性,将最新值更新到dom里面去。