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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章