这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
前言
Solid 是一个冷门的 web 框架。类似于 Vue、React、Svelte。它具有以下特点:
- 使用了 React Hooks 的 DSL(写法)
- 执行效率高于其他框架(Vue、React …),因为通过预编译将代码转换为 DOM 的原生操作,跳过了现代 web 框架的 diff 环节
- 支持 Typescript、SSR 等一系列现代 web 必备特性
介绍
Solid 本文的重点在于讲述 Solid 的响应式原理。
Solid 响应式的核心是 createSignal 和 createEffect。我们可以通过 createSignal 创建响应式变量,通过 createEffect 创建监听响应式变量的函数。
我们先看一下关于 Solid 响应式的一段代码。
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log('latest count is ', count())
})
setInterval(() => {
setCount(count() + 1)
}, 1000)
复制代码
首先,我们通过 createSignal 传入一个基本数据类型 0,然后返回了一个数组,数组前两项分别用来 获取 count、更新 count。
然后,通过 createEffect 创建了一个监听函数,当 count 的值变更时,监听函数会被重新执行。
这时候大家可能会好奇,当 count 更新的时候,Solid 是如何自动执行 createEffect 中传入的监听函数的呢?
分析
我们先分析一下 createEffect 都做了哪些工作。
首先,我们可以观察到 createSignal 返回的数组成员都是函数类型。
createEffect 肯定会执行传入的函数,除此之外,它还会将函数缓存起来,我们来写一段模拟代码:
function createEffect(fn) {
const lastListener = Listener // 缓存监听函数
Listener = fn // 设置当前监听函数
fn() // 执行监听函数
Listener = lastListener // 还原监听函数
}
复制代码
可以看到,这段代码主要做了两件事:
-
缓存上次监听函数
-
执行当前监听函数
在【执行监听函数】的过程中会执行到 count() 、console.log(‘latest count is ‘) 。
这里我们看看 count() 都做了什么?
由于 count 是 createSignal 返回数组的第一项,所以我们排除掉无关代码,重点放在 createSignal 返回的数组第一项上。
function createSignal(init) {
const getter = () => {
if (Listener) {
// 创建 `当前变量` 和 `Listener` 间的关联关系
}
return node.value
}
// ...
return [getter, ]
}
复制代码
这里我们可以看到,每一个通过 createSignal 创建的响应式变量,在 getter 触发时都会判断是否存在 Listener,如果存在则将 响应式变量 和 Listener 产生关联。
这时候大家可能会想,传入的 init 值是基本数据类型 0,它是如何与 Listener 产生关联的呢?
翻开 Solid 源码,我们可以看到,Solid 会将 初始值 和 Listener 都保存在同一个对象中,自始至终都在维护这个对象。
function createSignal(init) {
+ const node = {
+ value: init,
+ Listeners: []
+ }
const getter = () => {
if (Listener) {
// 创建 `当前变量` 和 `Listener` 间的关联关系
+ node.Listeners.push(Listener)
}
return node.value
}
// ...
return [getter, ]
}
复制代码
通过创建了 node 对象,可以把 变量的值 和 Listener 产生关联,同时可以看到,这里的 node.Listeners 是一个数组类型,因为可能会有多个 Listener 都监听了同一个响应式变量。
那么接下来还有个问题,在更新变量的时候都发生了什么呢?
其实大家可以应该猜到,在更新变量时,应该需要通知之前缓存过的所有监听函数,即 遍历 Listeners 并依次执行。
我们继续完善 createSignal 的代码。
function createSignal(init) {
const node = {
value: init,
Listeners: []
}
const getter = () => {
if (Effect) {
node.Listeners.push(Listener)
}
return node.value
}
+ const setter = (newValue) => {
+ node.value = newValue
+ node.Listeners.forEach(fn => fn())
+ }
return [getter, setter]
}
复制代码
至此,Solid 的响应式原理已经介绍完了。
现在,我们把上述代码综合一下:
let Listener = null
function createSignal(init) {
const node = {
value: init,
Listeners: []
}
const getter = () => {
if (Listener) {
node.Listeners.push(Listener)
}
return node.value
}
const setter = (newValue) => {
node.value = newValue
node.Listeners.forEach(fn => fn())
}
return [getter, setter]
}
function createEffect(fn) {
Listener = fn
fn()
Listener = null
}
// 测试代码
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log('latest count is ', count())
})
setInterval(() => {
setCount(count() + 1)
}, 1000)
复制代码
将上述代码粘贴到 Chrome 控制台,即可看到浏览器的输出:
最后,我们梳理一下执行流程:
—- 初始时 —->
-
执行 createEffect
-
设置 Listener 为 createEffect 传入的监听函数
-
执行 count()
-
触发 count 的 getter
-
绑定 count 值 和 Listener
-
还原 Listener
—- 更新 count 时 —->
-
触发 count 的 setter
-
执行 count 的 Listener
总结
在 Solid 中,响应式的核心就是 createSignal。它通过创建一个 node 对象,把变量值、Listener 监听函数保存其中,然后在变量的 getter 时 保存监听函数、 在 setter 时 触发监听函数。
我们知道 Vue 其实也有响应式更新,但和 Vue 不同的点在于,createSignal 的 getter 是通过函数调用触发的,我们可以在 getter 函数内去绑定监听函数。而 Vue 可以直接通过类似 obj.name 的方式来触发 getter,所以 Vue 会用到 Object.defineProperty 或 new Proxy 来监听 getter。但除此之外,它们的实现方案大致都是相同的。