发布于 2026-01-06 4 阅读
0

Svelte 的工作原理是什么?第一部分:从底层开始 应用:第一个假设 一切都始于 create_fragment 代码块在哪里被消耗? 应用运行中 下次见 随机笔记

Svelte 的工作原理是什么?(第一部分)

第一步

从底部开始

应用程序:第一个假设

一切都始于create_fragment

消费地点在哪里block

app行动中

下次

散记

这是第二部分:

今年夏天,一位朋友向我介绍了 Svelte。他并没有吹嘘 Svelte 相对于当时其他框架的性能,而是强调了它编译后生成的 JavaScript 代码的简洁性和可读性。

我正在编写一门使用 Svelte(以及 FastAPI 和其他一些炫酷的东西)的课程,并且意识到我需要对 Svelte 的运行方式有更深入的了解:具体来说,就是 Svelte 编译成的代码是如何工作的。

我会随时分享我的见解,这是第一部分x

第一步

我使用了 Svelte 项目提供的模板,方法是
npx degit sveltejs/template my-svelte-project; cd $_; npm install

然后我运行命令npm run dev编译包含的组件并启动开发服务器。

由此产生了build/bundle.js,我们将要剖析的这头野兽。

从底部开始

// build/bundle.js (all code blocks are from this file unless otherwise specified)
...
const app = new App({
    target: document.body,
    props: {
        name: 'world'
    }
});

return app;

}());
//# sourceMappingURL=bundle.js.map
Enter fullscreen mode Exit fullscreen mode

我之前不知道什么是源图,但是通过谷歌搜索并bundle.js.map稍作研究后,我决定暂时不去尝试解读它!

末尾的括号告诉我,app第 3 行的变量是bundle.js

...
var app = (function () {
...
Enter fullscreen mode Exit fullscreen mode

存储结果return app,因为👆👆右侧的所有内容=都是一个匿名函数,它会立即调用自身。

那么,上述以 开头的代码块const app与 中的逻辑相同main.js

// src/main.js

import App from './App.svelte';

const app = new App({
    target: document.body,
    props: {
    name: 'world',
    }
});

export default app;
Enter fullscreen mode Exit fullscreen mode

main.js我在该示例应用程序附带的 Rollup 配置文件中搜索,发现

// rollup.config.js
...
    input: 'src/main.js',
...
Enter fullscreen mode Exit fullscreen mode

好的,我想起来了,这里定义了 Svelte 应用,配置如下rollup.config.js

应用程序:第一个假设

看起来该类Appget方法set,每个方法都名为name

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

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

        const { ctx } = this.$$;
        const props = options.props || ({});

        if (/*name*/ ctx[0] === undefined && !("name" in props)) {
            console.warn("<App> was created without expected prop 'name'");
        }
    }

    get name() {
        throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
    }

    set name(value) {
        throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
    }
}

...
Enter fullscreen mode Exit fullscreen mode

我假设,如果我再给App它一个属性,也会有一对属性getset对应的属性。

检验假设1

<!-- src/App.svelte -->

<script>
    export let name; 
    export let number; // new
</script>

Enter fullscreen mode Exit fullscreen mode

果不其然,这些方法已经出现了:

...
get name() {
    throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}

set name(value) {
    throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}

get number() {
    throw new Error("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}

set number(value) {
    throw new Error("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
...
Enter fullscreen mode Exit fullscreen mode

原来是这样。我对JS类中的getter/setter机制了解不多,但我猜它和Python类似:当你尝试获取或设置实例属性时,它们会被触发。

然后,在构造函数中还有这个App

if (/*name*/ ctx[0] === undefined && !("name" in props)) {
    console.warn("<App> was created without expected prop 'name'");
}

if (/*number*/ ctx[1] === undefined && !("number" in props)) {
    console.warn("<App> was created without expected prop 'number'");
}
Enter fullscreen mode Exit fullscreen mode

ctx东西很神秘,而且它还是从更神秘的东西上蹦出来的this.$$

class App extends SvelteComponentDev {
    constructor(options) {
        ...
        const { ctx } = this.$$;
...
Enter fullscreen mode Exit fullscreen mode

我们稍后会再讨论这些问题。

在继续之前,让我们更新一下main.js,给这个number属性赋一个值。

// src/main.js
...
const app = new App({
    target: document.body,
    props: {
        name: 'world',
        number: 42
    }
});
Enter fullscreen mode Exit fullscreen mode

一切都始于create_fragment

function create_fragment(ctx) {
    let main;
    let h1;
    let t0;
    let t1;
    let t2;
    let t3;
    let p;
    let t4;
    let a;
    let t6;

    const block = {
        c: function create() {
            main = element("main");
            h1 = element("h1");
            t0 = text("Hello ");
            t1 = text(/*name*/ ctx[0]);
            t2 = text("!");
            t3 = space();
            p = element("p");
            t4 = text("Visit the ");
            a = element("a");
            a.textContent = "Svelte tutorial";
            t6 = text(" to learn how to build Svelte apps.");
            attr_dev(h1, "class", "svelte-1tky8bj");
            add_location(h1, file, 5, 1, 46);
            attr_dev(a, "href", "https://svelte.dev/tutorial");
            add_location(a, file, 6, 14, 83);
            add_location(p, file, 6, 1, 70);
            attr_dev(main, "class", "svelte-1tky8bj");
            add_location(main, file, 4, 0, 38);
        },
        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, main, anchor);
            append_dev(main, h1);
            append_dev(h1, t0);
            append_dev(h1, t1);
            append_dev(h1, t2);
            append_dev(main, t3);
            append_dev(main, p);
            append_dev(p, t4);
            append_dev(p, a);
            append_dev(p, t6);
        },
        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(main);
        }
    };

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

    return block;
}



Enter fullscreen mode Exit fullscreen mode

create_fragment是一个接受单个参数的函数ctx,它的主要作用是创建和渲染 DOM 元素;它返回block

block

block是一个对象,其最重要的属性是c(创建)、m(挂载)、p(更新)、d(销毁)。

c(创造)

block.c的值是一个名为 的工厂函数create,该函数

    c: function create() {
        main = element("main");
        h1 = element("h1");
        t0 = text("Hello ");
        t1 = text(/*name*/ ctx[0]);
        t2 = text("!");
        t3 = space();
        p = element("p");
        t4 = text("Visit the ");
        a = element("a");
        a.textContent = "Svelte tutorial";
        t6 = text(" to learn how to build Svelte apps.")
        ...
Enter fullscreen mode Exit fullscreen mode

1)创建大量 DOM 元素和文本节点;
2)将它们分别赋值给在开头声明的变量。create_fragment

然后它

    ...
    attr_dev(h1, "class", "svelte-1tky8bj");
    add_location(h1, file, 5, 1, 46);
    attr_dev(a, "href", "https://svelte.dev/tutorial");
    add_location(a, file, 6, 14, 83);
    add_location(p, file, 6, 1, 70);
    attr_dev(main, "class", "svelte-1tky8bj");
    add_location(main, file, 4, 0, 38);
}
Enter fullscreen mode Exit fullscreen mode

3) 设置元素的属性(例如“class”和“href”)。
4) 为每个属性设置触发一个事件(稍后会详细介绍:我们可以一直忽略这些事件)。5
) 为每个元素添加元数据__svelte_meta,详细说明其在src模块中的定义位置。

m(山)

block.m's value 是一个名为 的工厂函数mount,你知道,它会将每个元素和文本节点添加到 DOM 的适当位置。

    m: function mount(target, anchor) {
        insert_dev(target, main, anchor);
        append_dev(main, h1);
        append_dev(h1, t0);
        append_dev(h1, t1);
        append_dev(h1, t2);
        append_dev(main, t3);
        append_dev(main, p);
        append_dev(p, t4);
        append_dev(p, a);
        append_dev(p, t6);
    },

Enter fullscreen mode Exit fullscreen mode

p(更新)

block.p's 的值不是一个工厂函数,而是一个普通的函数,它似乎是

    p: function update(ctx, [dirty]) {
        if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
    },
Enter fullscreen mode Exit fullscreen mode

1) 处理一些我不理解的位,但可能只是检查是否有需要更新的内容(dirty
2) 如果新值( )与的值(默认值)ctx[0]不同, 3) 更新的值——提醒一下,它是一个文本节点t1undefined
t1

假设二

我注意到,我们在第一个假设中添加的属性number并没有出现在update函数中。我认为这是因为它在组件中没有被使用:这是一个未使用的属性。

检验假设2

<!-- src/App.svelte -->
...
<main>
    <h1>Hello {name}!</h1>
    <p>Your lucky number is {number}.</p> <!-- 👈👈👈 new -->
    <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>
...
Enter fullscreen mode Exit fullscreen mode
// build/bundle.js
...
    p: function update(ctx, [dirty]) {
        if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
        if (dirty & /*number*/ 2) set_data_dev(t5, /*number*/ ctx[1]);
    },
...
Enter fullscreen mode Exit fullscreen mode

叮叮叮!我对这if (dirty & 2)事儿还不太确定;咱们先把这事儿搁置一下吧。

d(破坏)

block.d's value 是一个函数,它——令人震惊的是——从 DOM 中删除一个元素。

    d: function destroy(detaching) {
        if (detaching) detach_dev(main);
Enter fullscreen mode Exit fullscreen mode

消费地点在哪里block

create_fragment只在代码中调用一次bundle.js,这使得侦查变得相当容易:

    ...
    $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
    ...
Enter fullscreen mode Exit fullscreen mode

这是在怪兽init函数内部,而怪兽函数本身只在class App定义的构造函数中被调用。这个create_fragment ? ...三元运算符是什么意思?鉴于它的存在,它似乎create_fragment总是为真?更有意义的问题可能是它在哪里以及如何$$.fragment使用?在哪里?结果发现,在三个地方。如何使用?

init

...
   function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
    const parent_component = current_component;
    set_current_component(component);
    const prop_values = options.props || {};
    const $$ = component.$$ = {
        fragment: null,
        ctx: null,
        // state
        props,
        update: noop,
        not_equal,
        bound: blank_object(),
        // lifecycle
        on_mount: [],
        on_destroy: [],
        before_update: [],
        after_update: [],
        context: new Map(parent_component ? parent_component.$$.context : []),
        // everything else
        callbacks: blank_object(),
        dirty
    };
    let ready = false;
    $$.ctx = instance
        ? instance(component, prop_values, (i, ret, value = ret) => {
            if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
                if ($$.bound[i])
                    $$.bound[i](value);
                if (ready)
                    make_dirty(component, i);
            }
            return ret;
        })
        : [];
    $$.update();
    ready = true;
    run_all($$.before_update);
    // `false` as a special case of no DOM component
    $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
    if (options.target) {
        if (options.hydrate) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            $$.fragment && $$.fragment.l(children(options.target));
        }
        else {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            $$.fragment && $$.fragment.c();
        }
        if (options.intro)
            transition_in(component.$$.fragment);
        mount_component(component, options.target, options.anchor);
        flush();
    }
    set_current_component(parent_component);
}

...
Enter fullscreen mode Exit fullscreen mode

$$.fragment在创建之后,它被直接引用了三次init。由于示例应用程序的target中只有,我们将忽略除第二次之外的所有其他引用。与上一步类似,我不理解这里对的布尔检查,但值得注意的是,调用了的方法,该方法将创建(但不会挂载)所有元素和文本节点,并为元素提供有关其在中的预编译位置的元数据options$$.fragment && $$.fragment.c();$$.fragment && ...fragmentcApp.svelte

由于init在构造函数内部调用App,我们知道上述操作将在运行时执行。

回溯:那又如何呢$$

简单来说:$$在早期就已定义init

...
        const $$ = component.$$ = {
            fragment: null,
            ctx: null,
            // state
            props,
            update: noop,
            not_equal,
            bound: blank_object(),
            // lifecycle
            on_mount: [],
            on_destroy: [],
            before_update: [],
            after_update: [],
            context: new Map(parent_component ? parent_component.$$.context : []),
            // everything else
            callbacks: blank_object(),
            dirty
        };
...

Enter fullscreen mode Exit fullscreen mode

谜团解开了!

update

    function update($$) {
        if ($$.fragment !== null) {
            $$.update();
            run_all($$.before_update);
            $$.fragment && $$.fragment.p($$.ctx, $$.dirty);
            $$.dirty = [-1];
            $$.after_update.forEach(add_render_callback);
        }
    }
Enter fullscreen mode Exit fullscreen mode

我们可以忽略几乎所有这些。`is`$$.update被赋值给noop一个实际上什么也不做的变量。我们还要假设$$.fragment`is` 不为空(怎么可能呢?)。然后,` $$.before_updateis` 目前是一个空数组,所以我们要等到应用程序复杂度更高一些才会去研究它run_all($$.before_update)。类似地,$$.after_update.forEach(add_render_callback)我们也可以忽略 `is`,因为$$.after_update它也是一个空数组。

这样就只剩下

    $$.fragment && $$.fragment.p($$.ctx, $$.dirty);
    $$.dirty = [-1];
Enter fullscreen mode Exit fullscreen mode

环顾四周,bundle.js我相当肯定这$$.dirty = [-1]意味着应用程序的状态没有待处理的更改。也就是说,在上一行更新 DOM 之后,$$.fragment.p($$.ctx, $$.dirty)我们表明所有必要的更改都已完成。

这样就只剩下一行包含大量操作的代码$$.fragment.p($$.ctx, $$.dirty),用于更新 DOM 以反映任何更改
$$.ctx

$$.ctx

$$.ctx这里似乎存放着应用程序的状态信息。它的计算过程有点复杂:

    $$.ctx = instance
        ? instance(component, prop_values, (i, ret, value = ret) => {
            if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
                if ($$.bound[i])
                    $$.bound[i](value);
                if (ready)
                    make_dirty(component, i);
            }
            return ret;
        })
Enter fullscreen mode Exit fullscreen mode

instance函数负责生成它:

    function instance($$self, $$props, $$invalidate) {
        let { name } = $$props;
        let { number } = $$props;
        const writable_props = ["name", "number"];

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

        $$self.$set = $$props => {
            if ("name" in $$props) $$invalidate(0, name = $$props.name);
            if ("number" in $$props) $$invalidate(1, number = $$props.number);
        };

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

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

        return [name, number];
    }
Enter fullscreen mode Exit fullscreen mode

instance解构我们的属性,name并将number它们原封不动地传递给$$.ctx

因此,$$.ctx等于["world", 42]:没有我想象的那么复杂;我们稍后会讨论在看似传递属性之间发生的所有这些副作用。

如前所述,$$.fragment.p($$.ctx, $$.dirty)它正在调用此函数:

    function update(ctx, [dirty]) {
      if (dirty & /*name*/ 1) set_data_dev(t1, /*name*/ ctx[0]);
      if (dirty & /*number*/ 2) set_data_dev(t5, /*number*/ ctx[1]);
    }
Enter fullscreen mode Exit fullscreen mode

好了,是时候弄清楚这项dirty & x业务到底是做什么的了。它似乎dirty包含了需要更新的元素索引,但为什么不弄清楚具体细节呢?

    p: function update(ctx, [dirty]) {
        if (dirty & /*name*/ 1) {
            console.log(`dirty 1 was dirty: ${dirty}`)
            set_data_dev(t1, /*name*/ ctx[0]);
        } else {
            console.log(`dirty 1 wasn't dirty: ${dirty}`)
        }
        if (dirty & /*name*/ 2) {
            console.log(`dirty 2 was dirty: ${dirty}`)
            set_data_dev(t5, /*name*/ ctx[0]);
        } else {
            console.log(`dirty 2 wasn't dirty: ${dirty}`)
        }
        console.log(typeof dirty)
    },
Enter fullscreen mode Exit fullscreen mode

为了在update不构建任何用户界面的情况下触发这些信息console.log提示,我们需要手动操作应用程序的状态:

app行动中

回到instance函数本身,它执行的更有意义的工作(“副作用”)是将三个方法$set——、、$capture_state$inject_state——绑定到$$self,即App

我有没有提到我们可以在控制台中检查我们的App实例app?这是 Svelte 的另一个优点:由于它最终会被编译成纯 JavaScript,因此app它处于浏览器渲染的全局作用域中,无需任何特殊插件或其他额外操作!掌握了这些知识,让我们在 JavaScript 控制台中体验一下这些新方法:

>> app.$capture_state()
    Object { name: "world", number: 42 }
>> app.$set({name: "Whirl"})
   undefined
   dirty 1 was dirty: 1
   dirty 2 wasn't dirty: 1
   number
>> app.$capture_state()
   ► Object { name: "Whirl", number: 42 }
>> app.$inject_state({number: 24})
   undefined
   undefined
   dirty 1 wasn't dirty: 2 
   dirty 2 was dirty: 2
   number
>> app.$capture_state()
    Object { name: "Whirl", number: 24 }
Enter fullscreen mode Exit fullscreen mode

页面现在看起来是这样的:

屏幕截图显示,更新后的属性也在渲染后的页面中发生了变化。

这里有几项发现:

1)$capture_state以对象形式返回应用程序的当前状态。2
)$set$inject_state似乎都通过对象更新应用程序的状态。3
)dirty当不等于 时[-1],是一个正整数,似乎通过从 1 开始的索引引用 props。4
) 这些 props 在渲染后的页面中更新。

还有一个谜团待解:

>> app.name
   Error: <App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or
   '<svelte:options accessors/>'
>> app.name = 'hi'
   Error: <App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or
   '<  svelte:options accessors/>'
Enter fullscreen mode Exit fullscreen mode

set这就是前面提到的 ` and`方法的目的get:强制编译后的代码不能直接在App实例上设置和获取 props,而是使用……内置的机制?

下次

下次请继续关注我们,一起揭开……的神秘面纱。

1) `$($($($($($($($($($($( app.$set$ ($($($($($($($( ))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ) ) ) app.$inject_state) ) ) )))))))))))))))))))))))))))))))))))))))))) ) ) ' ' 是 ' 是 ' 是 ' 是”)))`)))))) ' 是 ' 是 ' 是 ' 是))`))))))))))))))))))))))))))))) ' 是 ...
bundle.js
__svelte_meta
mount
dirtyupdate

散记

根据 Svelte 在推特上对我的回复,在各个阶段触发的事件bundle.js仅供开发工具使用。因此我们可以忽略它们。

文章来源:https://dev.to/zev/how-does-svelte-actually-work-part-1-j9m