svelte响应式原理

  • svelte响应式原理已关闭评论
  • 82 次浏览
  • A+
所属分类:Web前端
摘要

源代码:编译后的js代码结构再来看看invalidate函数的定义,invalidate函数就是在init时调用instance的时候传入的第三个参数


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