svelte响应式原理

发布时间 2023-12-28 20:13:22作者: 欧阳码农

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里面去。