likes
comments
collection
share

深入浅出 solid.js 源码 (九)—— 从 S 到 solid

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

上一节看了 S.js 的源码实现,solid.js 的响应式更新的实现方式是参考的 S.js,现在再去阅读 solid.js 的源码就会很容易,我们回到 solid 的 signal.ts 文件,查看 createSignal 的实现:

export function createSignal<T>(): Signal<T | undefined>;
export function createSignal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> {
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;

  const s: SignalState<T> = {
    value,
    observers: null,
    observerSlots: null,
    pending: NOTPENDING,
    comparator: options.equals || undefined
  };

	if ("_SOLID_DEV_" && !options.internal)
    s.name = registerGraph(options.name || hashValue(value), s as { value: unknown });

  const setter: Setter<T | undefined> = (value?: unknown) => {
    if (typeof value === "function") {
      if (Transition && Transition.running && Transition.sources.has(s))
        value = value(s.pending !== NOTPENDING ? s.pending : s.tValue);
      else value = value(s.pending !== NOTPENDING ? s.pending : s.value);
    }
    return writeSignal(s, value);
  };

  return [readSignal.bind(s), setter];
}

我们实际的数值是被封装到了一个 s 对象中,作为其 value 属性。回忆一下我们调用 createSignal 的场景,这个函数会返回两个函数,第一个函数为 getter 函数,调用之后可以获取 value 最新值,第二个是 setter 函数,调用可以修改 value 值。在这里对应的返回值就是 getter 和 setter 两个函数,实际上调用的是 readSignal 和 writeSignal 两个函数,这里我们先忽略 Memo 相关的逻辑。

export function readSignal(this: SignalState<any> | Memo<any>) {
  const runningTransition = Transition && Transition.running;
  if (Listener) {
    const sSlot = this.observers ? this.observers.length : 0;
    if (!Listener.sources) {
      Listener.sources = [this];
      Listener.sourceSlots = [sSlot];
    } else {
      Listener.sources.push(this);
      Listener.sourceSlots!.push(sSlot);
    }
    if (!this.observers) {
      this.observers = [Listener];
      this.observerSlots = [Listener.sources.length - 1];
    } else {
      this.observers.push(Listener);
      this.observerSlots!.push(Listener.sources.length - 1);
    }
  }
  if (runningTransition && Transition!.sources.has(this)) return this.tValue;
  return this.value;
}

按照之前 S.js 源码中的逻辑,读取时需要做的事情是添加监听者信息,这里会把对当前 state 的全部监听者放在当前 signal state 的 observers 数组中,每一个监听者是一个 Listener 对象,其中 Listener 的 sources 中存储了当前 signal state 对象信息,这里是一个双向引用的关系。

那么 Listener 具体是一个什么呢?Listener 的创建向上可以追溯到 createRoot 方法。还记得 render 的源码吗,在其中内部调用了 root 函数,这个函数对应的就是从 solid.js 中导出的 createRoot 函数。createRoot 的主要逻辑是调用 runUpdates,这里是触发了一次更新,即首次渲染其实也是走了一次更新逻辑。runUpdates 本身设计到的内容比较多,这部分我们后面详细分析更新流程时还会阅读,这里只需要知道,内部影响到 Listener 的是 updateComputation 方法,每次更新时 Listener 都会被赋值成当前更新的节点。

这样回到上面的逻辑,每次更新时,如果当前节点调用了某个 signal 的 getter,代表这个节点需要监听该 signal 的变化,这时该节点就会被加入这个 signal state 的 observers 数组中,同时这个 signal state 也会被加入到当前节点的 sources 数组中,这样就完成了注册监听的逻辑。

接下来 writeSignal 的实现,这里需要根据 UI 状态做一些调度策略,我们暂时忽略 Pending 的处理:

export function writeSignal(node: SignalState<any> | Memo<any>, value: any, isComp?: boolean) {
  if (node.comparator) {
    if (Transition && Transition.running && Transition.sources.has(node)) {
      if (node.comparator(node.tValue, value)) return value;
    } else if (node.comparator(node.value, value)) return value;
  }
  let TransitionRunning = false;
  if (Transition) {
    TransitionRunning = Transition.running;
    if (TransitionRunning || (!isComp && Transition.sources.has(node))) {
      Transition.sources.add(node);
      node.tValue = value;
    }
    if (!TransitionRunning) node.value = value;
  } else node.value = value;
  if (node.observers && node.observers.length) {
    runUpdates(() => {
      for (let i = 0; i < node.observers!.length; i += 1) {
        const o = node.observers![i];
        if (TransitionRunning && Transition!.disposed.has(o)) continue;
        if ((TransitionRunning && !o.tState) || (!TransitionRunning && !o.state)) {
          if (o.pure) Updates!.push(o);
          else Effects!.push(o);
          if ((o as Memo<any>).observers) markDownstream(o as Memo<any>);
        }
        if (TransitionRunning) o.tState = STALE;
        else o.state = STALE;
      }
      if (Updates!.length > 10e5) {
        Updates = [];
        if ("_SOLID_DEV_") throw new Error("Potential Infinite Loop Detected.");
        throw new Error();
      }
    }, false);
  }
  return value;
}

这里首先做一个比较,只有设置新值时才会触发更新,Transition 是用来做时机控制的,当正处于更新中时设置为 Transition.running 状态,此时 value 赋新值但是更新还在进行中,这时的临时新值会保存在 tValue 中,正式的更新流程调用位于 runUpdates 中,在这个过程中会去遍历当前添加的 observers 数组,待更新的内容会根据是否为副作用被分别放在对应的 Update 或 Effects 队列中,在 completeUpdates 阶段会对队列进行遍历,逐一去执行更新。

从原理上来看,createSignal 的实现和上一篇的 S.data 的实现基本上是一致的,solid.js 更复杂的地方在变化之后的更新逻辑上,由于需要进行 UI 更新处理,因此这里需要有一套调度更新的机制,在更新过程中使用了 runUpdates 的实现方式。