likes
comments
collection
share

深入浅出 solid.js 源码 (八)—— 从 S.js开始

作者站长头像
站长
· 阅读数 23

查看 solid.js 中 reactive 部分的源码,会发现上面的注释。Inspired by S.js by Adam Haile, github.com/adamhaile/S,这部分的实现是参考了 S.js 这个库,相对而言这个库的知名度并不高,我们首先先来了解一下 S.js 是做什么的。

S.js 是一个很轻量的响应式库,github.com/adamhaile/S…

let greeting = S.data("Hello"),
    name = S.data("world");

S(() => document.body.textContent = `${greeting()}, ${name()}!`);

name("reactivity");

这里使用 S 创建了一个 name 函数,在页面上读取 name 的值,当调用 name 为其设置新值时,页面上的内容也会随之更新,这就是一个最简单的响应式更新系统。那么这里的 S.data 是如何实现的呢?我们来简单阅读一下 S 的源码。

我们之前调用的是 S.data,这里就先来看 data 的实现:

S.data = function data<T>(value : T) : (value? : T) => T {
    var node = new DataNode(value);

    return function data(value? : T) : T {
        if (arguments.length === 0) {
            return node.current();
        } else {
            return node.next(value);
        }
    }
};

传入的 value 会被封装为 DataNode 对象,最终返回的是一个函数,这个函数根据参数个数进行处理,不传参数是 getter 函数,传参数变为 setter 函数,这种用法在早期 js 中也很常用,jQuery 中就有大量的这种用法。在这里,getter 调用的是 DataNode 的 current 方法,setter 调用的是 DataNode 的 next 方法。如果熟悉 Rxjs 这里一定会感到很熟悉,这里 DataNode 的行为是不是和 BehaviorSubject 很像:

const node = new BehaviorSubject();
// getter
node.getValue();
// setter
node.next(value);

Rxjs 是一个响应式的库,这里的原理本质上是一样的,响应式的效果都是基于发布订阅的方式实现的,这里想自己实现一个 DataNode 也不难,感兴趣不妨尝试一下,我曾经在面试中也遇到过手写过类似的实现效果的问题。这里我们还是来看 DataNode 的实现:

class DataNode {
    pending = NOTPENDING as any;   
    log     = null as Log | null;
    
    constructor(
        public value : any
    ) { }

    current() {
        if (Listener !== null) {
            logDataRead(this);
        }
        return this.value;
    }

    next(value : any) {
        if (RunningClock !== null) {
            if (this.pending !== NOTPENDING) { // value has already been set once, check for conflicts
                if (value !== this.pending) {
                    throw new Error("conflicting changes: " + value + " !== " + this.pending);
                }
            } else { // add to list of changes
                this.pending = value;
                RootClock.changes.add(this);
            }
        } else { // not batching, respond to change now
            if (this.log !== null) {
                this.pending = value;
                RootClock.changes.add(this);
                event();
            } else {
                this.value = value;
            }
        }
        return value!;
    }

    clock() {
        return RootClockProxy;
    }
}

这里的收集变和触发更新的逻辑本身很简单,读值和取值本身也没有上面特殊的地方,重点需要关注的还是如何通知更新。每次 next 调用后,数据值发生了刷新,我们要通知订阅数据的位置重新取最新值。在 Rxjs 中,数据的订阅时通过 subscribe 来添加的,变更时只需要触发 subscribe 的回调函数即可。而在 S 中,每次在取值时调用 logDataRead,会为其添加 log 记录,这里的 log 命名容易被误导,这里不是日志,而是观察记录的意思,log 表示的是观察者信息。

整体数据更新的调度是通过 RootClock 来完成的,每次 next 触发变化,会添加一个 changes 记录,这个 changes 会在 event 触发时进行统一调度处理。而 event 中执行的是 run 函数:

function run(clock : Clock) {
    var running = RunningClock,
        count = 0;
        
    RunningClock = clock;

    clock.disposes.reset();
    
    // for each batch ...
    while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) {
        if (count > 0) // don't tick on first run, or else we expire already scheduled updates
            clock.time++;

        clock.changes.run(applyDataChange);
        clock.updates.run(updateNode);
        clock.disposes.run(dispose);

        // if there are still changes after excessive batches, assume runaway            
        if (count++ > 1e5) {
            throw new Error("Runaway clock detected");
        }
    }

    RunningClock = running;
}

run 函数是一个 while 循环,就是不断地执行未执行的更新动作。首先原始的更新行为来自 changes,这个是在 next 中添加的,因此只要有更新调度产生就会进入 while 逻辑。changes 中调用 applyDataChange 函数,这个函数的关键动作是 markNodeStale,它的作用是标记需要关联更新的节点,即从 log 中获取订阅数据的节点,把这些节点添加到 updates 中。这样一来 updates 中就有数据了,接下来执行 updates,这里会执行 fn 逻辑,fn 就是最初调用 S 函数时传入的函数,这样重新取值就完成了一次更新动作。

以上就是 S.data 的简单原理,这部分内容很好理解,S.js 本身源码也比较短,对 S 其他 API 感兴趣可以自行阅读相关实现。我们这里阅读 S.js 还是为了后面的 solid.js 学习,接下来我们回到 solid.js 本身,来看这部分逻辑在 solid.js 中是如何应用的。