Svelte 响应式原理

722 阅读2分钟

vue中与Svelte响应式写法

计数器

vue

var vm = new Vue({
  data: {
     count: 0
  },
  computed: {
    double: function () {
      return this.count * 2
    }
  }
})

Svelte

  let count = 0;

  function handleClick() {
    count += 1;
  }
	
  $: double = count * 2
</script>

<button on:click={handleClick}>
  Clicked {double} times
</button>

上面例子中每次点击视图都会更新并增加2

Svelte 响应式原理

  • 实现一个todolist

截屏2023-05-17 下午2.06.14.png

  • 原生怎么去实现

然后每次新增/删除/修改任务时,除了修改 tasks 数据,都需要手动触发重新渲染 tasks(当然这样的实现并不好,每次删除/插入太多 DOM 节点性能会有问题

  • react 页面操作后 有 三步曲
  1. 首先是调度器,这里主要是为了处理优先级(用户点击事件属于高优先级)和合成事件
  2. 第二个部分是 Render 阶段,这里主要是遍历节点,找到需要更新的 Fiber Node,执行 Diff 算法计算需要执行那种类型的操作,打上 effectTag,生成一条带有 effectTag 的 Fiber Node 链表。常说的异步可中断也是发生在这个阶段。
  3. 第三个阶段是 Commit,这一步要做的事情是遍历第二步生成的链表,依次执行对应的操作(是新增,还是删除,还是修改...)
  • vue

截屏2023-05-17 上午11.37.27.png 大致过程是编译过程中收集依赖,基于 Proxy(3.x) ,defineProperty(2.x) 的 getter,setter 实现在数据变更时通知 Watcher。Vue 的实现很酷,每次修改 data 上的数据都像在施魔法。

  • Svelte Svelte 源代码主要分成 compiler 和 runtime 两部分

截屏2023-05-17 下午2.20.59.png

例子

<script>
  let name = "world";
  function setName() {
    name = "fesky";
  }
</script>

<h1 on:click={setName}>Hello {name}!</h1>

经过编译之后

import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/App.svelte");/* src/App.svelte generated by Svelte v3.59.1 */
import {
	SvelteComponentDev,
	add_location,
	append_dev,
	detach_dev,
	dispatch_dev,
	element,
	init,
	insert_dev,
	listen_dev,
	noop,
	safe_not_equal,
	set_data_dev,
	text,
	validate_slots
} from "/node_modules/.vite/deps/svelte_internal.js?v=d1baa22a";

const file = "src/App.svelte";

function create_fragment(ctx) {
	let h1;
	let t0;
	let t1;
	let t2;
	let mounted;
	let dispose;

	const block = {
		c: function create() {
			h1 = element("h1");
			t0 = text("Hello ");
			t1 = text(/*name*/ ctx[0]);
			t2 = text("!");
			add_location(h1, file, 13, 0, 164);
		},
		l: function claim(nodes) {
			throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
		},
		m: function mount(target, anchor) {
			insert_dev(target, h1, anchor);
			append_dev(h1, t0);
			append_dev(h1, t1);
			append_dev(h1, t2);

			if (!mounted) {
				dispose = listen_dev(h1, "click", /*setName*/ ctx[1], false, false, false, false);
				mounted = true;
			}
		},
		p: function update(ctx, [dirty]) {
			if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
		},
		i: noop,
		o: noop,
		d: function destroy(detaching) {
			if (detaching) detach_dev(h1);
			mounted = false;
			dispose();
		}
	};

	dispatch_dev("SvelteRegisterBlock", {
		block,
		id: create_fragment.name,
		type: "component",
		source: "",
		ctx
	});

	return block;
}

function instance($$self, $$props, $$invalidate) {
	let { $$slots: slots = {}, $$scope } = $$props;
	validate_slots('App', slots, []);
	let name = "world";

	function setName() {
		$$invalidate(0, name = "fesky");
	}

	const writable_props = [];

	Object.keys($$props).forEach(key => {
		if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console.warn(`<App> was created with unknown prop '${key}'`);
	});

	$$self.$capture_state = () => ({ name, setName });

	$$self.$inject_state = $$props => {
		if ('name' in $$props) $$invalidate(0, name = $$props.name);
	};

	if ($$props && "$$inject" in $$props) {
		$$self.$inject_state($$props.$$inject);
	}

	return [name, setName];
}

class App extends SvelteComponentDev {
	constructor(options) {
		super(options);
		init(this, options, instance, create_fragment, safe_not_equal, {});

		dispatch_dev("SvelteRegisterComponent", {
			component: this,
			tagName: "App",
			options,
			id: create_fragment.name
		});
	}
}

前面例子tolist 中 我们在每次修改数据之后,都要手动重新渲染 DOM!我们不提倡这么写法,因为难以维护 而 Svelte Compile 实际上就是在代码编译阶段帮我们实现了这件事!把需要数据变更之后做的事情都分析出来生成原生 JS 代码,运行时就不需要像 Vue Proxy 那样的运行时代码了

Fragment—— DOM 操作

create_fragment里面都有什么

export interface Fragment {
  key: string | null;
  first: null;
	/* create  */ c: () => void;
	/* claim   */ l: (nodes: any) => void;
	/* hydrate */ h: () => void;
	/* mount   */ m: (target: HTMLElement, anchor: any) => void;
	/* update  */ p: (ctx: T$$['ctx'], dirty: T$$['dirty']) => void;
	/* measure */ r: () => void;
	/* fix     */ f: () => void;
	/* animate */ a: () => void;
	/* intro   */ i: (local: any) => void;
	/* outro   */ o: (local: any) => void;
	/* destroy */ d: (detaching: 0 | 1) => void;
}

主要看以下四个钩子方法:
c(create) :在这个钩子里面创建 DOM 节点,创建完之后保存在每个 fragment 的闭包内。
m(mount) :挂载 DOM 节点到 target 上,在这里进行事件的板顶。
p(update) :组件数据发生变更时触发,在这个方法里面检查更新。
d(destroy) :移除挂载,取消事件绑定。

编译结果会从 svelte/internal 中引入 text,element,append,detach,listen 等等的方法。源码中可以看到,都是一些非常纯粹的 DOM 操作。

export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
  return document.createElement<K>(name);
}

export function text(data: string) {
  return document.createTextNode(data);
}

export function append(target: Node, node: Node) {
  if (node.parentNode !== target) {
    target.appendChild(node);
  }
}

export function detach(node: Node) {	
  node.parentNode.removeChild(node);
}

export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
  node.addEventListener(event, handler, options);
  return () => node.removeEventListener(event, handler, options);
}
  • $$invalidate

	$$.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;
		  })
		: [];
                
                
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;
}   
//`schedule_update` 很简单,在 `Promise.resolve`(microTask) 中调用 `flush` 方法。
export function schedule_update() {
  if (!update_scheduled) {
    update_scheduled = true;
    resolved_promise.then(flush);
  }
}

//`flush` 方法其实就是消费前面的 `dirty_components`,调用每个需要更新组件的 `update` 方法。
function flush() {
   			while (flushidx < dirty_components.length) {
				const component = dirty_components[flushidx];
				flushidx++;
				set_current_component(component);
				update(component.$$);
			}
                  }
                  
 // 又回到了每个 fragment 的 `p(update)` 方法      
   function update($$) {
	if ($$.fragment !== null) {
		$$.update();
		run_all($$.before_update);
		const dirty = $$.dirty;
		$$.dirty = [-1];
		$$.fragment && $$.fragment.p($$.ctx, dirty);

		$$.after_update.forEach(add_render_callback);
	}
}

整体思路

  1. 修改数据,调用 $$invalidate 方法
  2. 判断是否相等,标记脏数据,make_dirty
  3. 在 microTask 中触发更新,遍历所有 dirty_component, 更新 DOM 节点
  4. 重置 Dirty