信号(Signals)与行为(Reactions)构成了响应式系统的基础,而在此基础之上的派生(Derivations)、依赖收集与同步进一步增强了整个系统的健壮性。
难以置信的是所有的这些核心的理念能通过不超过50行的核心代码完整的实现,在真正理解了响应式系统的核心部分设计思想之后,我们会发现这些概念的代码实现并没有那么复杂。而之所以让人觉得迷惑,往往是由于正确实现的细粒度响应式系统能够良好的适配大量的动态场景。其实这正是声明式结构所带来的优势,只要遵守了正确的约定,底层的实现以及变更对于调用者而言已完全被隔离。
这里极简的响应式实现并没有完整的包含现代成熟框架例如Mobx、Vue、SolidJs等所提供的所有的功能点,但这对于理解响应式系统的实现已经足够。
信号(Signal)的实现
信号(Signal)作为任何响应式系统的核心,提供响应式系统最根本的数据元,然而借助于JavaScript提供的闭包特性,其实现极其简单。
export function createSignal(value = null) {
const getter = () => value
const setter = (next) => value = next;
return [getter, setter];
}
以上就是信号(Signal)的全部核心实现,它包括了一个内部的值,以及暴露的getter
和setter
函数。当然,你可以有更好的实现方法,丰俭由人。无论你如何去实现它的内部结构,对于调用者而言,他们获得了一个良好的约束,这是声明式接口的魔力。
const [count, setCount] = createSignal(0);
// 可以通过信号存储任意类型的值
const [name, setName] = createSignal("Mike");
为了添加对行为和派生的依赖双向收集的支持,我们还需要对信号的实现增加更多的特性。
const context = [];
function subscribe(running, subscriptions) {
subscriptions.add(running);
running.dependencies.add(subscriptions);
}
export function createSignal(value = null) {
const subscriptions = new Set();
const getter = () => {
const running = context[context.length - 1]
if (running) {
subscribe(running, subscriptions)
}
return value;
}
const setter = (next) => {
value = next;
for (const sub of [...subscriptions]) {
sub.execute();
}
}
return [getter, setter]
}
这里我们增加了额外的两组管理对象,分别是一个全局的context
数组,它起到了一个运行时堆栈的作用,它将用于跟踪管理正在执行阶段的所有行为与派生。由于JavaScript语言本身的单线程特性,这里采用全局的数组实现并不会影响线程安全。另外,每个独立的信号对象中维护了自身的subscriptions
列表,提供双向依赖追踪。
context
与subscriptions
两者共同构成了响应式系统自动依赖收集与跟踪的基础。当行为或者派生开始执行时,他们首先将自身推入全局的context
数组,在此阶段任何信号的getter
函数被调用也会将当前的行为或派生推入信号内部维护的subscriptions
数组(信号对象同时也被当前执行上下文running
对象捕获,为了给单个的行为或派生提供主动清理的能力)。
最后,当任意信号对象的值获得更新时,除了更新信号内部维护的变量,同时也会遍历信号对象内部持有的subscriptions
数组以通知变更。
这里之所以首先对数组进行复制再遍历,其原因在于之前所谈论的同步,任意的行为或是派生都可能在执行期间再次读取信号,为了避免变更蔓延,新的订阅会延迟到下一次更新时触发。
行为(Reaction)与派生(Derivation)的实现
通过以上的信号实现,我们很容易推导出行为的具体实现,以下是关于行为(Reaction)的核心部分。
function cleanup(running) {
for (const dep of running.dependencies) {
dep.delete(running);
}
running.dependencies.clear();
}
export function createEffect(fn) {
const execute = () => {
cleanup(running)
context.push(running);
try {
fn();
} finally {
context.pop();
}
}
const running = {
execute,
dependencies: new Set(),
}
// 初始化时立即执行一次
execute();
}
这里行为函数内部生成的最重要的对象是被推入全局context
中的执行上下文。它内部包含了当前行为函数的依赖项dependencies(Signals)
列表,以及执行依赖收集和计算的函数体表达式。
在每个执行周期开始阶段,首先会执行清理程序将当前行为函数从所有依赖的信号中移除,并重新开始收集此次执行过程中新的依赖。这是在执行上下文中保存信号对象反向链接的作用,可以非常便捷的在每次执行过程中重新生成依赖项的列表。
以上就是一个极简的响应式系统的全部实现,尽管看起来仍然简陋,但借助这段实现,我们已经可以验证完整的响应式细节。
基于以上的实现扩展一个派生实现并没有太多复杂的细节,甚至可以完全基于行为来实现。
在成熟的响应式框架中,例如Vue、MobX、SolidJS,采用了混合的
push/pull
机制来优化实现,并持续的追踪图的变更以避免额外的计算。
export function createMemo(fn) {
const [s, setS] = createSignal()
createEffect(() => {
setS(fn())
});
return s;
}
我们可以采用以上的实现来搭建测试以验证响应式系统的正确性。上述的实现中,没有包含例如批处理、自定义析构函数以及无限循环安全守卫等高阶的特性,如果有兴趣可以进一步探索这些特性的实现。
通过这一段简短的实现,已经足够让我们了解现代的响应式框架中的基础以及运行原理。接下来,让我们看看ArcGIS Maps SDK for JavaScript中是如何实现这一系统的。
原文链接:https://juejin.cn/post/7337512140662603803 作者:戈伊