Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

Introduction

From Svelte's core principles, it can be understood that Svelte aims to extract necessary information from the compilation process to minimize dynamic overhead. The previous article explained how Svelte works from compilation to code generation. Today, let's take a look at how the generated code from Svelte works.

Let's start by examining a simple Svelte component:

<script>
  import { onMount } from 'svelte';
  let count = 1;
  
  onMount(() => {
    setInterval(() => count++, 1000);
  })
</script>

{#if count != 100}
	<span>{count}</span>
{/if}

<p>
  this is text
</p>

The syntax of Svelte components is similar to regular HTML, except that it includes template-like syntax (such as if, await). Basically, it is fully compatible with HTML. However, the generated component is in JavaScript. For example, the above component will be compiled into:

// Generated by Svelte, omitted some code
import { onMount } from "svelte";

function create_if_block(ctx) {
	let span;
	let t;

	return {
		c() {
			span = element("span");
			t = text(/*count*/ ctx[0]);
		},
		m(target, anchor) {
			insert(target, span, anchor);
			append(span, t);
		},
		p(ctx, dirty) {
			if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
		},
		d(detaching) {
			if (detaching) detach(span);
		}
	};
}

function create_fragment(ctx) {
	let t0;
	let p;
	let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);

	return {
		c() {
			if (if_block) if_block.c();
			t0 = space();
			p = element("p");
			p.textContent = "this is text";
		},
		m(target, anchor) {
			if (if_block) if_block.m(target, anchor);
			insert(target, t0, anchor);
			insert(target, p, anchor);
		},
		p(ctx, [dirty]) {
			if (/*count*/ ctx[0] != 100) {
				if (if_block) {
					if_block.p(ctx, dirty);
				} else {
					if_block = create_if_block(ctx);
					if_block.c();
					if_block.m(t0.parentNode, t0);
				}
			} else if (if_block) {
				if_block.d(1);
				if_block = null;
			}
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (if_block) if_block.d(detaching);
			if (detaching) detach(t0);
			if (detaching) detach(p);
		}
	};
}

function instance($$self, $$props, $$invalidate) {
	let count = 1;

	onMount(() => {
		setInterval(() => $$invalidate(0, count++, count), 1000);
	});

	return [count];
}

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

Svelte also supports SSR (Server-Side Rendering), so if you use Svelte's SSR feature to compile the above code, it will create a function that generates an HTML string.

// Generated by Svelte, omitted some code
import { onMount } from "svelte";

const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
	let count = 1;

	onMount(() => {
		setInterval(() => count++, 1000);
	});

	return `${count != 100 ? `<span>${escape(count)}</span>` : ``}

<p>this is text
</p>`;
});

export default App;

Observing the Generated Code (dom)

For the sake of explanation, let's focus on the generated code for dom and skip the SSR part.

The generated code can be mainly divided into three parts: create_fragment function, instance function, and SvelteComponent class.

create_fragment

Let's start by looking at create_fragment:

function create_fragment(ctx) {
	let t0;
	let p;
	let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);

	return {
		c() {
			if (if_block) if_block.c();
			t0 = space();
			p = element("p");
			p.textContent = "this is text";
		},
		m(target, anchor) {
			if (if_block) if_block.m(target, anchor);
			insert(target, t0, anchor);
			insert(target, p, anchor);
		},
		p(ctx, [dirty]) {
			if (/*count*/ ctx[0] != 100) {
				if (if_block) {
					if_block.p(ctx, dirty);
				} else {
					if_block = create_if_block(ctx);
					if_block.c();
					if_block.m(t0.parentNode, t0);
				}
			} else if (if_block) {
				if_block.d(1);
				if_block = null;
			}
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (if_block) if_block.d(detaching);
			if (detaching) detach(t0);
			if (detaching) detach(p);
		}
	};
}

create_fragment returns an object with several single-letter properties that represent different lifecycle hooks:

  • c: Represents create, which means the function to be executed when the component is initially created.
  • m: Represents mount, which means the function to be executed when the component is mounted to the DOM.
  • p: Represents patch, which means the function to be executed when the component is updated.
  • i: Represents intro, which means the function to be executed when the component is transitioning in.
  • o: Represents outro, which means the function to be executed when the component is transitioning out.
  • d: Represents destroy or detach, which means the function to be executed when the component is unmounted.

Knowing the meaning of each letter, their respective tasks become clearer:

  • Assign the result of the conditional expression (if count != 100) to if_block.
  • In the create phase:
    • Create the p element.
    • Set p.textContent to "this is text".
  • In the mount phase:
    • If if_block is true, call if_block.m() (the mount function).
    • Insert t0 into the anchor.
    • Insert p into the anchor.
  • In the patch phase:
    • If the condition count != 100 is true:
      • If if_block already exists, call if_block.p().
      • If not, call create_if_block once and then execute if_block.m().
    • If the condition count != 100 is false:
      • It means that the content inside if_block needs to be removed, so call if_block.m().

instance

Next, let's look at the instance function:

function instance($$self, $$props, $$invalidate) {
	let count = 1;

	onMount(() => {
		setInterval(() => $$invalidate(0, count++, count), 1000);
	});

	return [count];
}

The code inside the <script> tag is placed inside the instance function. Here are a few notable points:

  • The original code setInterval(() => count++, 1000) is transformed into setInterval(() => $$invalidate(0, count++, count), 1000).
  • The return value is an array containing the value of count.

Svelte can infer variable dependencies during static analysis, allowing it to track changes. The $$invalidate function here works somewhat similar to setState in React. The difference is that in Svelte, you have to manually add it, whereas in React, it is automatically detected and handled for you.

Here's how $$invalidate is implemented (omitting some code):

// If the variable value is different, mark the component as dirty (indicating that it needs to be updated)
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
  if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
  if (ready) make_dirty(component, i);
}

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));
}

Every time setInterval triggers count++, $$invalidate is called. It compares the old and new values of count and marks the component as dirty if there is a change. Then, it calls the make_dirty function, which adds the component to dirty_components and schedules an update. Svelte also implements a batch update mechanism, where it tries to update as much as possible within a single frame.

SvelteComponent

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

The implementation of SvelteComponent is quite simple. It calls the init function, which initializes the Svelte component. It invokes the create_fragment function and executes the instance function to mount the component to the DOM.

Summary

The generated code from Svelte consists of three main parts: create_fragment, instance, and SvelteComponent.

  • create_fragment: Specifies how each lifecycle of the Svelte component should be handled.
  • instance: Executes the code inside the <script> tag and returns the context (props, variables, etc.).
  • SvelteComponent: Initializes the Svelte component through the init function.

This article explains how to interpret the generated code from Svelte and briefly describes how the reactive mechanism behind it is achieved (there are many other mechanisms not mentioned here, which will be explained in future articles). Now everyone can understand the generated code from Svelte!

For more Svelte-related articles, you can refer to this link.

Prev

4 Reasons to Learn Svelte in 2022

Next

The value of this thing

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.