HTML 优先,JavaScript 最后:网络速度的秘密!
所有框架都需要保存状态。框架通过执行模板来构建状态。大多数框架以引用和闭包的形式将状态保存在 JavaScript 堆中。Qwik 的独特之处在于,状态以属性的形式保存在 DOM 中。(请注意,引用和闭包都不可序列化,但 DOM 属性(字符串)可以。这是可恢复性的关键!)
在 DOM 中保持状态有许多独特的好处,包括:
- DOM 使用 HTML 作为其序列化格式。通过以字符串属性的形式在 DOM 中保存状态,应用程序可以随时序列化为 HTML。HTML 可以通过网络发送,并在不同的客户端上反序列化为 DOM。然后可以恢复反序列化的 DOM。
- 每个组件都可以独立于其他组件恢复。这种无序 rehydration 机制仅允许整个应用程序的某个子集进行 rehydration,并限制了响应用户操作所需下载的代码量。这与传统框架截然不同。
- Qwik 是一个无状态框架(所有应用程序状态都以字符串的形式存储在 DOM 中)。无状态代码易于序列化、网络传输和恢复。这也使得组件之间可以独立地进行 rehydrate 操作。
- 应用程序可以在任何时间点序列化(不仅仅是在初始渲染时),并且可以多次序列化。
让我们看一个简单的Counter
组件示例,了解状态序列化的工作原理。(请注意,这是服务器端渲染的 HTML 的输出,不一定是开发人员手动编写的具体代码。)
<div ::app-state="./AppState"
app-state:1234="{count: 321}">
<div decl:template="./Counter_template"
on:q-render="./Counter_template"
::.="{countStep: 5}"
bind:app-state="state:1234">
<button on:click="./MyComponent_increment">+5</button>
321.
<button on:click="./MyComponent_decrrement">-5</button>
</div>
</div>
::app-state
(应用程序状态代码):指向可下载应用程序状态变更代码的 URL。仅当状态需要变更时,才会下载状态更新代码。app-state:1234
(应用程序状态实例):指向特定应用程序实例的指针。通过序列化状态,应用程序可以从中断处继续执行,而无需重新执行状态重建。decl:template
(declare template):指向可下载组件模板的 URL。只有当 Qwik 确定组件状态发生改变并需要重新渲染时,才会下载组件模板。on:q-render
(组件已安排渲染):框架需要跟踪哪些组件需要重新渲染。这通常通过存储内部无效组件列表来实现。在 Qwik 中,无效组件列表以属性的形式存储在 DOM 中。然后,这些组件等待事件q-render
广播。::.="{countStep: 5}"
(组件实例的内部状态):组件可能需要在 rehydration 后保留其内部状态。它可以将状态保留在 DOM 中。当组件 rehydration 后,它已拥有继续运行所需的所有状态。它无需重建其状态。bind:app-state="state:1234"
(对共享应用程序状态的引用):这允许多个组件引用相同的共享应用程序状态。
querySelectorAll
是我们的朋友
框架需要做的一件常见事情是,在应用程序状态发生变化时识别哪些组件需要重新渲染。这种情况可能由多种原因导致,例如某个组件被显式地失效(markDirty()
),或者由于应用程序共享状态发生变化而导致某个组件被隐式地失效。
在上面的例子中,count
被保存在应用程序状态中,键为app-state:1234
。如果状态更新,则需要使依赖于该应用程序状态的组件失效(加入队列等待重新渲染)。框架应该如何知道哪些组件需要更新?
在大多数框架中,解决方案是从根组件开始重新渲染整个应用程序。这种策略的缺点是需要下载所有组件模板,这会对用户交互的延迟产生负面影响。
有些框架是响应式的,会跟踪在给定状态发生变化时需要重新渲染的组件。然而,这种记录是以闭包的形式进行的(参见“闭包致死”),闭包会将模板封闭起来。其结果是,在应用程序引导时,当初始化响应式连接时,需要下载所有模板。
Qwik 是组件级响应式的。由于它是响应式的,因此无需从根节点开始渲染。然而,它并没有将响应式监听器保存在闭包中,而是以属性的形式保存在 DOM 中,这使得 Qwik 能够恢复。
如果count
得到更新,Qwik 可以通过执行此操作在内部确定哪些组件需要失效querySelectorAll
。
querySelectorAll('bind\\:app-state\\:1234').forEach(markDirty);
上述查询允许 Qwik 确定哪些组件依赖于该状态,并针对每个组件调用markDirty()
该状态。markDirty()
使该组件失效,并将其添加到需要重新渲染的组件队列中。这样做是为了将多个markDirity
调用合并为一个渲染过程。渲染过程使用 进行调度requestAnimationFrame
。但是,与大多数框架不同,Qwik 也以 属性的形式将此队列保存在 DOM 中。
<div on:q-render="./Counter_template" ... >
requestAnimationFrame
用于调度渲染。逻辑上,这意味着requestAnimationFrame
广播q-render
组件正在等待的事件。再次querySelectorAll
起到了作用。
querySelectorAll('on\\:q-render').forEach(jsxRender);
浏览器没有广播事件(事件冒泡的逆向),但querySelectorAll
可以用来识别应该接收事件广播的所有组件。jsxRender
然后使用函数重新渲染 UI。
请注意,Qwik 无需将状态保存在 DOM 之外。任何状态都以属性的形式存储在 DOM 中,这些属性会自动序列化为 HTML。换句话说,应用程序可以随时快照为 HTML,通过网络发送,然后反序列化。应用程序将自动从中断处恢复。
Qwik 是无状态的,这使得 Qwik 应用程序可以恢复。
好处
将所有框架状态存储在 DOM 元素中,其显著优势在于应用程序的可恢复性。然而,它还有其他一些优势,乍一看可能并不明显。
跳过可见视口之外的组件的渲染。当q-render
广播事件以确定组件是否需要渲染时,可以轻松判断组件是否可见,并直接跳过该组件的渲染。跳过渲染还意味着无需下载任何模板或任何其他代码。
无状态的另一个好处是,由于应用程序已在运行,HTML 可以延迟加载。例如,服务器可以发送用于渲染初始视图的 HTML,但跳过不可见视图的 HTML。用户可以开始与初始视图交互并使用应用程序。一旦用户开始滚动,应用程序就可以获取更多 HTML 并将innerHTML
其添加到 DOM 的末尾。由于 Qwik 是无状态的,因此可以直接插入额外的 HTML,而不会对已在运行的应用程序造成任何问题。Qwik 在有人与其交互之前并不知道新的 HTML,只有在交互之后才会进行延迟加载。上述用例对于当前的框架来说非常难以实现。
我们对 Qwik 的未来以及它所开辟的用例类型感到非常兴奋。
- 在StackBlitz上尝试
- 在github.com/builderio/qwik上为我们加星
- 在@QwikDev和@builderio上关注我们
- 在Discord上与我们聊天
- 加入builder.io
目前就是这样,但请继续关注,因为我们将在接下来的几周内继续撰写有关 Qwik 和前端框架的未来的文章!
文章来源:https://dev.to/builderio/html-first-javascript-last-the-secret-to-web-speed-4ic9