type
status
date
slug
summary
tags
category
icon
password
Edited
Nov 18, 2024 02:20 PM
Created
Nov 9, 2024 02:45 PM
在上一篇文章中,深入的了解了 props 相关的一些内容,还涉及到了一点点组件编译相关的。
Solid 之旅 —— 为什么 props 被解构后会导致响应式丢失通过这篇文章,主要了解两个方面:
- Solid 的组件编译
- Solid 是如何实现 Signal 的插入和细颗粒度更新的?
先从一个最简单的案例来说,不添加任何响应式数据,先了解组件编译后的内容。
Solid 官方提供了 playground 可以进行调试
组件编译
编译后
我们一个个来看,
<Counter>
组件内的 html 标签被使用一个名叫 template
函数调用了template
来看一下 template 函数的源码:
这个函数的主要目的就是根据 html 字符串创建一个模板元素。
create
函数用来根据 html 参数创建一个模板
fn
用来根据这个模板克隆出来一份 DOM
可以直接拿到控制台实践一下,效果就是这样:
对于
探索HTML中的<template>
这一块内容,如果不熟悉的话,可以看一下我之前的文章。<template>
标签结束标签的省略问题
对于编译后的内容没有结束标签,算是一种小优化。
通过省略闭合标签,编译器可以生成更简洁的代码,从而减少内存使用和提高性能。
本质上是利用了浏览器渲染来进行兜底。
可以拿一个 html 来进行测试:
最终渲染出来的结果:
这时候浏览器会自动修复错误。
但不是所有情况都可以省略结束标签了,像下面这种情况:
浏览器渲染的效果就是:
像这种情况,浏览器的自动修复就出现了问题。因为浏览器会外向内对标签进行匹配,最后再进行修复。
最终得的结论,就是最后一个元素的结束标签是可以省略的,即:
最终就会将所有的结束标签加上:
template
函数的原理就讲完了,再回过头来看编译后的函数,我们接着往下看,Counter 组件这里没什么说的了,返回了克隆出来的模板。createComponent
往下接着看
<Counter />
被编译成了 createComponent(Counter, {})
,这个在之前讲《Solid 之旅 —— 为什么 props 被解构后会导致响应式丢失》的时候涉及到过,这里再提一下。来看一下
createComponent
函数:很简单,单纯的执行了一下这个组件,因为通过前面的响应式原理那边,我们也知道,Solid 的更新靠的是
Signal
、Effect
等等响应式函数实现了细颗粒度更新。render
再回头来看最后的
render
函数:这里面涉及一个
root
→ createRoot
函数,我们先通过官网文档看一下它的用处:Creates a new non-tracked owner scope that doesn't auto-dispose. This is useful for nested reactive scopes that you do not wish to release when the parent re-evaluates.All Solid code should be wrapped in one of these top level as they ensure that all memory/computations are freed up. Normally you do not need to worry about this as createRoot is embedded into all render entry functions.
即:
创建一个不自动释放的新非跟踪所有者范围。这对于你不希望在父级上下文重新计算时释放的嵌套响应式作用域很有用。 所有 Solid 代码都应该包装在这些顶级代码之一中,因为它们确保所有内存/计算都被释放。通常,您无需担心这一点,因为 createRoot 已嵌入到所有渲染入口函数中。
也就是说
createRoot
用于创建一个独立的响应式上下文。- “这里的不自动释放”指的是会返回一个手动清理的函数给你
- “不希望在父级上下文重新计算时释放的嵌套响应式”指的就是响应式的重新计算之后在当前上下文中进行执行,不会影响其他响应式上下文,也就是独立的。
我们在到
createRoot
内部看一下:这里的处理是有点类似于
runComputation
的,具体的不在过多介绍,可以看一下响应式原理那一篇文章。这里的
cleanNode
也就是清理函数,在响应式原理那边也讲过,会在 completeUpdates
之后进行自动清理;也就是上面官网说的会在上层更新的时候自动清理响应式。后面就是加入到
Updates
进行根据优先级进行更新执行 fn 了。接着往下看,在 root 里面执行了
insert(element, code(), element.firstChild ? null : undefined, init)
。insert
函数很重要,不仅仅在这里,这后续实现 Signal
的插入和更新都至关重要。insert
函数内部接着调用了 insertExpression(parent, accessor, initial, marker);
实现最终的插入逻辑。这里
accesor
即 <Counter />
组件是一个 DOM 元素,不是函数,所以走上面这个 insertExpression
。insertExpression
整个函数很长,又很多类型的判断逻辑和实现逻辑,这里只截取了部分用到的地方。现在我们暂时只需要知道几个参数:- parent:父元素
- value:指
<Counter>
组件的 DOM元素
其他参数暂时用不到,这里就会走
value.nodeType
这一块的逻辑,然后忽略 hydrating
和 current
等等,最终就会走到 parent.appendChild(value);
这里,实现最终的插入。到这里,我们大概知道了
render
函数所作的事情:- 创建一个独立的响应式上下文
- 将跟组件插入到页面元素上进行渲染
如何实现 Signal 的插入和细颗粒度更新的?
接下来,我们给案例添加
Signal
,看看 Solid
是如何实现 Signal
的插入和更新的。以官方的源案例为例:
编译后:
想比较之前的,这里主要变化的在于 <Counter> 内部实现了一个 Signal 的插入逻辑;先暂且忽略 Solid 对 事件的处理。
原理
这里注意一个细节,尽管我们在组件内给
button
的值是 count()
,但编译后,还是还原成了 count
,这对实现后面的响应式很关键,因为如果直接变成 count()
的话,那么就是简简单单的一个字面量了,不要再说后续的东西了。先来看一下
_$insert(_el$, count);
,为什么一个简单的插入就能实现后续的细颗粒度更新。我们知道 count 实际是一个函数,所以这次会走下面的
insertExpression
,相较于上面,这里多了个 effect,也就是 createEffect
,这就是最最最关键的一点。Signal 凭什么实现响应式,就是和 Computation(Effect)结合,然后内部进行依赖收集,相互关联,最终实现响应式。这里也是同一个道理。
我们先不往里面看,单单看这一行
effect(current => insertExpression(parent, accessor(), current, marker), initial);
。这里我们知道了 effect 和 count 之间做了依赖收集,那么,在后续的 count 更新的时候,就会找到该 effect,也就是重新执行了一遍
insertExpression
,这不就实现了只有 Signal 更新的细颗粒度嘛!!! 如果对于响应式这一块忘了或者不熟悉,可以重温一下之前响应式那一篇文章。
回过来再想想,其实发现很简单。
接下来,在重新看一下
insertExpression
的剩余代码:再来解释一下几个参数的含义:
parent
:父节点
value
:最新值
current
:当前值,这里会从effect
上拿取
t
:value 的类型
insertExpression
主要就是对于不同类型进行的特殊处理,实现最后的插入和更新。事件处理
最后再提一嘴 Solid 是如何处理事件的,回到上面的代码,可以看到 Solid 在最后调用了
delegateEvents(["click"]);
直接来看一下这个函数:
用于将事件名称列表的事件委托添加到
document
中。它在 $$EVENTS
中维护一组委托事件。其实就是正常的事件代理到
document
上,Solid 将其抽出来的原因是只对用到的事件进行处理,进行了一些优化。总结
到这里,差不多对Solid 的组件编译有个大概了解了,最主要的是学习到了Solid 是如何实现 Signal 的插入和细颗粒度更新的,了解了它是如何实现的之后,也发现原理是如此的简单
参考链接
- 作者:JinSo
- 链接:https://jinso365.top/article/139cee95-0faf-80e6-aa41-e47fa1bfba54
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。