createRoot 源码分析

https://www.solidjs.com/docs/latest/api#createroot

官方的概念其实有些难以理解,个人认为 createRoot 的作用就是会创建没有依赖收集的 owner 作用域,并提供缓存功能(Effects 缓存),可以延迟 computation 执行,直至赋值时才会触发 computation 执行。除此之外,官方建议所有的 solid.js 代码都应该被包裹在 createRoot 中,这样可以确保内部的变量可以被释放。

同样一段代码,直接运行与包裹在createRoot 中运行结果是不同的。

const [count, setCount] = createSignal(0);

createEffect(() => {
  console.log("count effect01 =", count());
});
createEffect(() => {
  console.log("count effect02 =", count());
});

setCount(1);
typescript

例如上面这段代码,直接在模块全局中运行,打印结果如下:

count effect01 = 0
count effect02 = 0
count effect01 = 1
count effect02 = 1

但是如果放到 createRoot 函数中,打印结果如下:

count effect01 = 1
count effect02 = 1

可以看到,在 createRoot 中使用 effect 不是立即执行的,而是在首次渲染之后才会执行。在 render 函数和 runWithOwner 中具有相同的执行效果。

因为前面我们已经分析过 createEffect 的源码,这次重点主要分析 createRoot 是如何使 effect 延迟执行的。

createRoot

createRoot

createRoot 没有定义单独的声明,我们直接来看它是如何实现的。

export interface Owner {
  owned: Computation<any>[] | null;
  cleanups: (() => void)[] | null;
  owner: Owner | null;
  context: any | null;
  sourceMap?: Record<string, { value: unknown }>;
  name?: string;
  componentName?: string;
}

export type RootFunction<T> = (dispose: () => void) => T;

export function createRoot<T>(fn: RootFunction<T>, detachedOwner?: Owner): T {
  const listener = Listener,
    owner = Owner,
    unowned = fn.length === 0,
    root: Owner =
      unowned && !"_SOLID_DEV_"
        ? UNOWNED
        : { owned: null, cleanups: null, context: null, owner: detachedOwner || owner },
    updateFn = unowned
      ? "_SOLID_DEV_"
        ? () =>
            fn(() => {
              throw new Error("Dispose method must be an explicit argument to createRoot function");
            })
        : fn
      : () => fn(() => untrack(() => cleanNode(root)));

  if ("_SOLID_DEV_") {
    if (owner) root.name = `${owner.name}-r${rootCount++}`;
    globalThis._$afterCreateRoot && globalThis._$afterCreateRoot(root);
  }

  Owner = root;
  Listener = null;

  try {
    return runUpdates(updateFn as () => T, true)!;
  } finally {
    Listener = listener;
    Owner = owner;
  }
}
typescript

createRoot 可以接收两个参数,分别是 fn 和 detachedOwner。

  • fn:用户自定义函数
  • detachedOwner:owner 上下文对象
    • 针对开发环境, solid.js 会优先使用用户传入的 detachedOwner;
    • 如果不存在或者是生产环境,会使用全局中的 Owner 对象。

首先缓存全局的 Listerner、Owner,还是一样的套路,缓存现有变量,等程序执行完毕,对变量进行恢复。

我们在讲解 createEffect 源码时,分析过 updateComputation 方法,也是类似的用法。

其次定义 unowned 变量,标识 fn 的参数是否为 0,定义 root 和 updateFn:

  • root:因为 unowned 为真,并且处于开发环境
{ owned: null, cleanups: null, context: null, owner: detachedOwner || owner }
typescript
  • updateFn,因为 unowned 为真,并且处于开发环境
() =>
  fn(() => {
    throw new Error("Dispose method must be an explicit argument to createRoot function");
  })
typescript

然后还是开发环境的逻辑,会给 owner 对象设置名称,如果 globalThis._$afterCreateRoot 存在,会调用该方法传入 owner 对象。

if ("_SOLID_DEV_") {
  if (owner) root.name = `${owner.name}-r${rootCount++}`;
  globalThis._$afterCreateRoot && globalThis._$afterCreateRoot(root);
}
typescript

将 root 赋值给 Owner(本次流程调试 Owner 是有值的),Listener 赋值为 null。

然后返回 runUpdates 方法调用,传入 updateFn 函数,并传入第二个参数为 true。这里使用 try 语句包裹函数,我们在自己定义库的时候也可以这么做,并且还可以自定义全局的错误函数,因为用户传入的任何配置都是不可靠的,我们必须对错误进行处理。

最后恢复 Listener 和 Owner。

runUpdates

function runUpdates<T>(fn: () => T, init: boolean) {
  if (Updates) return fn();
  let wait = false;
  if (!init) Updates = [];
  if (Effects) wait = true;
  else Effects = [];
  ExecCount++;
  try {
    const res = fn();
    completeUpdates(wait);
    return res;
  } catch (err) {
    if (!Updates) Effects = null;
    handleError(err);
  }
}
typescript

还是这个方法,我们已经看过很多次,这里的不同点在于 init 参数是 true,所以不会给 Updates 赋值。

Effects 赋值为空数组,ExecCount 自增。然后执行我们传入的自定义方法。

() =>
  fn(() => {
    throw new Error("Dispose method must be an explicit argument to createRoot function");
  })

// fn
createRoot(() => {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log("count effect01 =", count());
  });
  createEffect(() => {
    console.log("count effect02 =", count());
  });

  setCount(1);
});
typescript

首先调用 createSignal 创建响应式数据(SignalState),然后依次调用 createEffect 方法。

export function createEffect<Next, Init = Next>(
  fn: EffectFunction<Init | Next, Next>,
  value: Init,
  options?: EffectOptions
): void;
export function createEffect<Next, Init>(
  fn: EffectFunction<Init | Next, Next>,
  value?: Init,
  options?: EffectOptions
): void {
  runEffects = runUserEffects;
  const c = createComputation(fn, value!, false, STALE, "_SOLID_DEV_" ? options : undefined),
    s = SuspenseContext && lookup(Owner, SuspenseContext.id);
  if (s) c.suspense = s;
  c.user = true;
  Effects ? Effects.push(c) : updateComputation(c);
}
typescript

首先将 runUserEffects 赋值给 runEffects,然后调用 createComputation 方法创建 computation 对象。

因为此时 Effects 存在,并且是空数组,所以不会执行 updateComputation 方法,而是仅将 computation 对象添加到 Effects 数组中。

如果不用 createRoot 包裹代码,这里的 Effects 数组是空,所以会立即执行更新,添加到 Effects 数组相当于延迟更新。

createComputation

function createComputation<Next, Init = unknown>(
  fn: EffectFunction<Init | Next, Next>,
  init: Init,
  pure: boolean,
  state: number = STALE,
  options?: EffectOptions
): Computation<Init | Next, Next> {
  const c: Computation<Init | Next, Next> = {
    fn,
    state: state,
    updatedAt: null,
    owned: null,
    sources: null,
    sourceSlots: null,
    cleanups: null,
    value: init,
    owner: Owner,
    context: null,
    pure
  };
	
  // Transition
  // ...

  if (Owner === null)
    "_SOLID_DEV_" &&
      console.warn(
        "computations created outside a `createRoot` or `render` will never be disposed"
      );
  else if (Owner !== UNOWNED) {
    if (Transition && Transition.running && (Owner as Memo<Init, Next>).pure) {
      if (!(Owner as Memo<Init, Next>).tOwned) (Owner as Memo<Init, Next>).tOwned = [c];
      else (Owner as Memo<Init, Next>).tOwned!.push(c);
    } else {
      if (!Owner.owned) Owner.owned = [c];
      else Owner.owned.push(c);
    }
    if ("_SOLID_DEV_")
      c.name =
        (options && options.name) ||
        `${(Owner as Computation<any>).name || "c"}-${
          (Owner.owned || (Owner as Memo<Init, Next>).tOwned!).length
        }`;
  }
	
  // ExternalSourceFactory
  // ...

  return c;
}
typescript

createComputation 方法执行也有所不同,当前存在 Owner 对象,Transition 相关的逻辑仍然不存在。

if (!Owner.owned) Owner.owned = [c];
else Owner.owned.push(c);
typescript

因为 Owener.owned 为空,所以会创建一个数组赋值给 Owner.owned,并将当前 computation 对象作为第一个元素。如果是开发环境会给 computation 设置一个名称。

c.name =
  (options && options.name) ||
  `${(Owner as Computation<any>).name || "c"}-${
    (Owner.owned || (Owner as Memo<Init, Next>).tOwned!).length
  }`;
typescript

这里的名称就是 c-1,取默认字符串 c ,拼接 ownned 的长度 1,最后将 computation 对象返回。

此时不仅 Effects 数组中存在 computation 对象,并且 Owner 的 owned 数组中也存在当前 computation 对象。

writeSignal

我们定义了两个副作用函数,所以最终 Effects 和 Owner.owned 中会存在两个 computation 对象。

createRoot(() => {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log("count effect01 =", count());
  });
  createEffect(() => {
    console.log("count effect02 =", count());
  });

  setCount(1);
});
typescript

执行 setCount,会触发 writeSignal 函数。

export function writeSignal(node: SignalState<any> | Memo<any>, value: any, isComp?: boolean) {
  let current =
    Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
  if (!node.comparator || !node.comparator(current, value)) {
    if (Transition) {
      const 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];
          const TransitionRunning = Transition && Transition.running;
          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;
}
typescript

Transition 相关逻辑仍然不存在,直接将最新值赋值给 node.value,此时值为 1。此时 node.observers 也不存在,因为我们并没有触发 readSignal 函数收集 computation 依赖。所以这里返回最新值就执行结束了。

completeUpdates

function runUpdates<T>(fn: () => T, init: boolean) {
  if (Updates) return fn();
  let wait = false;
  if (!init) Updates = [];
  if (Effects) wait = true;
  else Effects = [];
  ExecCount++;
  try {
    const res = fn();
    completeUpdates(wait);
    return res;
  } catch (err) {
    if (!Updates) Effects = null;
    handleError(err);
  }
}
typescript

然后回到 runUpdates 函数中,此时已经执行完 fn() 函数,接下来触发 completeUpdates

function completeUpdates(wait: boolean) {
  if (Updates) {
		// ...
  }
  if (wait) return;
  let res;
  if (Transition && Transition.running) {
    // ...
  }
  const e = Effects!;
  Effects = null;
  if (e!.length) runUpdates(() => runEffects(e), false);
  else if ("_SOLID_DEV_") globalThis._$afterUpdate && globalThis._$afterUpdate();
  if (res) res();
}
typescript

此时 Effects 存在,赋值给 e 后将 Effects 清空,重新调用 runUpdates ,执行 runEffects

runUpdates

runUpdates 方法还是一样的逻辑,只不过由于传递第二个参数为 false,会初始化 Updates 数组,ExecCount 继续自增。

function runUpdates<T>(fn: () => T, init: boolean) {
  if (Updates) return fn();
  let wait = false;
  if (!init) Updates = [];
  if (Effects) wait = true;
  else Effects = [];
  ExecCount++;
  try {
    const res = fn();
    completeUpdates(wait);
    return res;
  } catch (err) {
    if (!Updates) Effects = null;
    handleError(err);
  }
}
typescript

runEffects(runUserEffects)

function runUserEffects(queue: Computation<any>[]) {
  let i,
    userLength = 0;
  for (i = 0; i < queue.length; i++) {
    const e = queue[i];
    if (!e.user) runTop(e);
    else queue[userLength++] = e;
  }
  if (sharedConfig.context) setHydrateContext();
  for (i = 0; i < userLength; i++) runTop(queue[i]);
}
typescript

因为此时都是用户 computation,所以最终会执行两次 runTop 方法。

runTop

function runTop(node: Computation<any>) {
	// ...
  for (let i = ancestors.length - 1; i >= 0; i--) {
    node = ancestors[i];
    if (runningTransition) {
      let top = node,
        prev = ancestors[i + 1];
      while ((top = top.owner as Computation<any>) && top !== prev) {
        if (Transition!.disposed.has(top)) return;
      }
    }
    if (
      (!runningTransition && node.state === STALE) ||
      (runningTransition && node.tState === STALE)
    ) {
      updateComputation(node);
    } else if (
      (!runningTransition && node.state === PENDING) ||
      (runningTransition && node.tState === PENDING)
    ) {
      const updates = Updates;
      Updates = null;
      runUpdates(() => lookUpstream(node, ancestors[0]), false);
      Updates = updates;
    }
  }
}
typescript

runTop 我们已经分析过,在这里只会执行 updateComputation 方法。

function updateComputation(node: Computation<any>) {
  if (!node.fn) return;
  cleanNode(node);
  const owner = Owner,
    listener = Listener,
    time = ExecCount;
  Listener = Owner = node;
  runComputation(
    node,
    Transition && Transition.running && Transition.sources.has(node as Memo<any>)
      ? (node as Memo<any>).tValue
      : node.value,
    time
  );
	// ...
  Listener = listener;
  Owner = owner;
}
typescript

updateComputation 首先会清理 computation 依赖关系,此时这里并没有依赖相关的 SignalState。然后执行 runComputation

function runComputation(node: Computation<any>, value: any, time: number) {
  let nextValue;
  try {
    nextValue = node.fn(value);
  } catch (err) {
    if (node.pure) Transition && Transition.running ? (node.tState = STALE) : (node.state = STALE);
    handleError(err);
  }
  if (!node.updatedAt || node.updatedAt <= time) {
    if (node.updatedAt != null && "observers" in (node as Memo<any>)) {
      writeSignal(node as Memo<any>, nextValue, true);
    } else if (Transition && Transition.running && node.pure) {
      Transition.sources.add(node as Memo<any>);
      (node as Memo<any>).tValue = nextValue;
    } else node.value = nextValue;
    node.updatedAt = time;
  }
}
typescript

在 runComputation 中会执行 node.fn,即 createEffect 的内部逻辑,在打印 count() 时,会触发依赖收集,然后打印结果。

两次 runTop 执行完毕后,控制台已经打印出:

count effect01 = 1
count effect02 = 1
typescript

总结

createRoot 函数提供缓存 Effects 的功能,可以延迟执行 computation。

副作用函数在定义时并不会执行,只会在设置值的时候触发执行,这种表现其实才是我们想要的。

除此之外,不仅 createRoot 具有这个功能,使用 render 函数或者 runWithOwner 都可以达到这样的效果。