SvelteJS 与 ReactJS 渲染差异(重绘/回流)
SvelteJS vs ReactJS rendering difference (repaint / reflow)
这是我对 DOM 和浏览器如何工作的天真理解
只要 DOM(真正的 dom)中的某些内容发生更改,浏览器就会重新绘制或重排 DOM。所以简单来说,每次 DOM 更改时,浏览器都需要重新计算 CSS,进行布局并重新绘制网页。这才是真正需要时间的 dom。
所以 React 带有这个虚拟 DOM,它实际上做的是批量更改并调用将它们一次性应用于真实 dom。因此,最大限度地减少了重新流动和重新绘制。
那么 Svelte 呢?如果它直接操纵 DOM 它如何控制浏览器的 repaint/reflow 。
这两个库都最大限度地减少了需要对 dom 进行的更改数量。不同之处在于他们找出最小变化集是什么的方式。
React 的方法是在内存中表示 dom(虚拟 dom)。当您设置状态时,它会再次运行渲染过程以创建另一个虚拟 dom。它比较之前和之后,发现发生了什么变化,然后任何变化都会被推送到真正的 dom.
Svelte 的方法是,当您设置一个变量时,它会设置一个标记该变量已更改的标志。它知道哪些变量依赖于其他变量,因此它会遍历任何因变量并重新计算它们,从而构建需要更改的列表。然后将这些更改推送到 dom.
除了上面的(正确)答案:Svelte "compiles" 您提供给它的代码,因此最终代码可以在没有库的情况下执行 运行time(与 React 相比).它创建了相当可读的代码,因此绝对有可能理解内部工作原理。
注意:这将是一个更长的答案——并且仍然遗漏了许多关于 Svelte 幕后发生的事情的细节。但我希望它有助于揭开幕后发生的某些方面的神秘面纱。此外,从 v3.16.x 开始,这就是 Svelte 做事的方式。由于这是内部的,它可能会改变。然而,我仍然发现了解真正发生的事情总是有益的。
那么,我们开始吧。
首先: Svelte 教程有一个有用的功能,可以让您看到生成的代码(就在 "Result" 窗格旁边)。一开始它可能看起来有点吓人,但您很快就会掌握它的窍门。
以下代码将基于此示例(但进一步简化):Svelte tutorial - reactivity/assignments
我们的示例组件定义(即 App.svelte)如下所示:
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>{count}</button>
基于这个组件定义,Svelte 编译器创建一个函数,该函数将创建一个 "fragment",它接收 "context" 并与之交互。
function create_fragment(ctx) {
let button;
let t;
let dispose;
return {
c() {
button = element("button");
t = text(/*count*/ ctx[0]);
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t);
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
dispose();
}
};
}
片段 负责与 DOM 交互,并将与组件实例一起传递。简而言之,
里面的代码
- "c" 将是 运行 create(在内存中创建 DOM 元素并设置事件处理程序)
- "m" 将 运行 on mount(将元素附加到 DOM)
- "p" 将 运行 更新 ,即当某些东西(包括道具)发生变化时
- "i" / "o" 与 intro/outro 相关(即转换)
- "d" 运行 销毁
注:像element或者set_data这样的函数其实是很容易上手的。
例如,函数 element 只是 document.createElement:
的包装
function element(name) {
return document.createElement(name);
}
context (ctx) 将包含所有实例变量和函数。它只不过是一个简单的数组。由于 Svelte "knows" 每个索引在编译时的含义,它可以在其他地方对索引进行硬引用。
此代码实质上定义了实例上下文:
function instance($$self, $$props, $$invalidate) {
let count = 0;
function handleClick() {
$$invalidate(0, count += 1);
}
return [count, handleClick];
}
instance 方法和 create_fragment 都将从另一个函数调用中调用 初始化。它有点复杂,所以与其复制粘贴到这里,不如看看这个 link to the source.
$$invalidate 将确保 count 变量设置为脏并安排更新。当下一次更新 运行s 时,它将查看所有 "dirty" 组件并更新它们。这是如何发生的实际上更多的是一个实现细节。如果有兴趣,在 flush function.
中设置断点
事实上,如果你真的想更深入一点,我建议克隆 template 应用程序,然后创建一个简单的组件,编译它然后检查 "bundle.js"。如果删除源映射或停用它们,您还可以调试实际代码。
因此,例如像这样设置 rollup.config.js:
output: {
sourcemap: false,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
dev: false,
注意:如上所示,我建议也将 dev mode 设置为 false 因为这将创建更简洁的代码。
一个简洁的功能: 一旦我们的应用程序 运行ning,您还可以访问 app 变量(它是分配给全局 window 对象,因为它被捆绑为立即调用的函数表达式)。
因此,您可以打开控制台并简单地说
console.dir(app)
这将产生这样的东西
App
$$:
fragment: {c: ƒ, m: ƒ, p: ƒ, i: ƒ, o: ƒ, …}
ctx: (2) [0, ƒ]
props: {count: 0}
update: ƒ noop()
not_equal: ƒ safe_not_equal(a, b)
bound: {}
on_mount: []
on_destroy: []
before_update: []
after_update: []
context: Map(0) {}
callbacks: {}
dirty: [-1]
__proto__: Object
$set: $$props => {…}
一个很棒的功能是您可以自己使用 $set 方法来更新实例。例如像这样:
app.$set({count: 10})
还有一些 Svelte DevTools 试图让 Svelte 的内部结构更加平易近人。不知何故,当我亲自试用它们时,它们似乎会影响我的应用程序的渲染性能,所以我自己不使用它们。但肯定值得一看。
嗯,给你了。我知道这仍然是相当技术性的,但我希望它有助于更好地理解编译后的 Svelte 代码在做什么。
这是我对 DOM 和浏览器如何工作的天真理解
只要 DOM(真正的 dom)中的某些内容发生更改,浏览器就会重新绘制或重排 DOM。所以简单来说,每次 DOM 更改时,浏览器都需要重新计算 CSS,进行布局并重新绘制网页。这才是真正需要时间的 dom。
所以 React 带有这个虚拟 DOM,它实际上做的是批量更改并调用将它们一次性应用于真实 dom。因此,最大限度地减少了重新流动和重新绘制。
那么 Svelte 呢?如果它直接操纵 DOM 它如何控制浏览器的 repaint/reflow 。
这两个库都最大限度地减少了需要对 dom 进行的更改数量。不同之处在于他们找出最小变化集是什么的方式。
React 的方法是在内存中表示 dom(虚拟 dom)。当您设置状态时,它会再次运行渲染过程以创建另一个虚拟 dom。它比较之前和之后,发现发生了什么变化,然后任何变化都会被推送到真正的 dom.
Svelte 的方法是,当您设置一个变量时,它会设置一个标记该变量已更改的标志。它知道哪些变量依赖于其他变量,因此它会遍历任何因变量并重新计算它们,从而构建需要更改的列表。然后将这些更改推送到 dom.
除了上面的(正确)答案:Svelte "compiles" 您提供给它的代码,因此最终代码可以在没有库的情况下执行 运行time(与 React 相比).它创建了相当可读的代码,因此绝对有可能理解内部工作原理。
注意:这将是一个更长的答案——并且仍然遗漏了许多关于 Svelte 幕后发生的事情的细节。但我希望它有助于揭开幕后发生的某些方面的神秘面纱。此外,从 v3.16.x 开始,这就是 Svelte 做事的方式。由于这是内部的,它可能会改变。然而,我仍然发现了解真正发生的事情总是有益的。
那么,我们开始吧。
首先: Svelte 教程有一个有用的功能,可以让您看到生成的代码(就在 "Result" 窗格旁边)。一开始它可能看起来有点吓人,但您很快就会掌握它的窍门。
以下代码将基于此示例(但进一步简化):Svelte tutorial - reactivity/assignments
我们的示例组件定义(即 App.svelte)如下所示:
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>{count}</button>
基于这个组件定义,Svelte 编译器创建一个函数,该函数将创建一个 "fragment",它接收 "context" 并与之交互。
function create_fragment(ctx) {
let button;
let t;
let dispose;
return {
c() {
button = element("button");
t = text(/*count*/ ctx[0]);
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t);
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
dispose();
}
};
}
片段 负责与 DOM 交互,并将与组件实例一起传递。简而言之,
里面的代码- "c" 将是 运行 create(在内存中创建 DOM 元素并设置事件处理程序)
- "m" 将 运行 on mount(将元素附加到 DOM)
- "p" 将 运行 更新 ,即当某些东西(包括道具)发生变化时
- "i" / "o" 与 intro/outro 相关(即转换)
- "d" 运行 销毁
注:像element或者set_data这样的函数其实是很容易上手的。 例如,函数 element 只是 document.createElement:
的包装function element(name) {
return document.createElement(name);
}
context (ctx) 将包含所有实例变量和函数。它只不过是一个简单的数组。由于 Svelte "knows" 每个索引在编译时的含义,它可以在其他地方对索引进行硬引用。
此代码实质上定义了实例上下文:
function instance($$self, $$props, $$invalidate) {
let count = 0;
function handleClick() {
$$invalidate(0, count += 1);
}
return [count, handleClick];
}
instance 方法和 create_fragment 都将从另一个函数调用中调用 初始化。它有点复杂,所以与其复制粘贴到这里,不如看看这个 link to the source.
$$invalidate 将确保 count 变量设置为脏并安排更新。当下一次更新 运行s 时,它将查看所有 "dirty" 组件并更新它们。这是如何发生的实际上更多的是一个实现细节。如果有兴趣,在 flush function.
中设置断点事实上,如果你真的想更深入一点,我建议克隆 template 应用程序,然后创建一个简单的组件,编译它然后检查 "bundle.js"。如果删除源映射或停用它们,您还可以调试实际代码。
因此,例如像这样设置 rollup.config.js:
output: {
sourcemap: false,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
dev: false,
注意:如上所示,我建议也将 dev mode 设置为 false 因为这将创建更简洁的代码。
一个简洁的功能: 一旦我们的应用程序 运行ning,您还可以访问 app 变量(它是分配给全局 window 对象,因为它被捆绑为立即调用的函数表达式)。
因此,您可以打开控制台并简单地说
console.dir(app)
这将产生这样的东西
App
$$:
fragment: {c: ƒ, m: ƒ, p: ƒ, i: ƒ, o: ƒ, …}
ctx: (2) [0, ƒ]
props: {count: 0}
update: ƒ noop()
not_equal: ƒ safe_not_equal(a, b)
bound: {}
on_mount: []
on_destroy: []
before_update: []
after_update: []
context: Map(0) {}
callbacks: {}
dirty: [-1]
__proto__: Object
$set: $$props => {…}
一个很棒的功能是您可以自己使用 $set 方法来更新实例。例如像这样:
app.$set({count: 10})
还有一些 Svelte DevTools 试图让 Svelte 的内部结构更加平易近人。不知何故,当我亲自试用它们时,它们似乎会影响我的应用程序的渲染性能,所以我自己不使用它们。但肯定值得一看。
嗯,给你了。我知道这仍然是相当技术性的,但我希望它有助于更好地理解编译后的 Svelte 代码在做什么。