深入浅出 solid.js 源码 (十六)—— 属性处理

428 阅读3分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

props 是我们在组件之间传递信息的重要渠道,在 react 中我们经常会使用解构赋值的方式操作 props 对象。在 solid.js 中也是使用 props 做属性传递,不过有一个重要的区别就是在 solid 中,props 本身是响应式对象,我们直接对 props 对象进行解构处理会使得对象中的内容丧失响应能力,在 solid.js 中,下面的写法是有问题的:

props = { { name: "Smith" }, ...props };
const { children, ...rest } = props;

这样处理后会丧失响应能力,上面的情况需要使用 mergeProps 和 splitProps 来处理:

props = mergeProps({ name: "Smith" }, props);
const [local, rest] = splitProps(props, ["children"]); // local.children

在 solid 开发中 props 的处理是一个需要时刻关注的问题,这一节看一下 mergeProps 和 splitProps的处理逻辑。

这两个函数同样位于 compoent 文件下,先来看 mergeProps:

export function mergeProps<T extends [unknown, ...unknown[]]>(...sources: T): MergeProps<T> {
  return new Proxy(
    {
      get(property: string | number | symbol) {
        for (let i = sources.length - 1; i >= 0; i--) {
          const v = resolveSource(sources[i])[property];
          if (v !== undefined) return v;
        }
      },
      has(property: string | number | symbol) {
        for (let i = sources.length - 1; i >= 0; i--) {
          if (property in resolveSource(sources[i])) return true;
        }
        return false;
      },
      keys() {
        const keys = [];
        for (let i = 0; i < sources.length; i++)
          keys.push(...Object.keys(resolveSource(sources[i])));
        return [...new Set(keys)];
      }
    },
    propTraps
  ) as unknown as MergeProps<T>;
}

mergeProps 的效果是把前面全部的内容都合并到最后一个对象上,可以看到这里创建的是一个 proxy 对象,也就是说经过 mergeProps 之后我们得到的其实是一个 proxy 而非原始的 props,对这个 proxy 的读取会被代理到原始的 props 上。

这里的 resolveSource 很简单:

function resolveSource(s: any) {
  return (s = typeof s === "function" ? s() : s) == null ? {} : s;
}

再来看 splitProps:

export function splitProps<T, K extends [readonly (keyof T)[], ...(readonly (keyof T)[])[]]>(
  props: T,
  ...keys: K
): SplitProps<T, K> {
  const blocked = new Set<keyof T>(keys.flat());
  const descriptors = Object.getOwnPropertyDescriptors(props);
  const res = keys.map(k => {
    const clone = {};
    for (let i = 0; i < k.length; i++) {
      const key = k[i];
      Object.defineProperty(
        clone,
        key,
        descriptors[key]
          ? descriptors[key]
          : {
              get() {
                return props[key];
              },
              set() {
                return true;
              }
            }
      );
    }
    return clone;
  });
  res.push(
    new Proxy(
      {
        get(property: string | number | symbol) {
          return blocked.has(property as keyof T) ? undefined : props[property as keyof T];
        },
        has(property: string | number | symbol) {
          return blocked.has(property as keyof T) ? false : property in props;
        },
        keys() {
          return Object.keys(props).filter(k => !blocked.has(k as keyof T));
        }
      },
      propTraps
    )
  );
  return res as SplitProps<T, K>;
}

这里面的逻辑比较多,还是创建 proxy,拆分之后变成的几个部分也都是 proxy 对象。为什么要用 proxy,这其实和 solid 的实现有关。

在 solid 中一切变化通知都是由基础的响应式系统来构建的,props 本身只是一个抽象载体,实际的变化还是由于 signal 变化触发的。我们监听 props.xxx 本质上还是引用的原始的 xxx signal,因此可以订阅 signal 更新,一旦我们解构处理,将会切断和原始的 signal 之间的引用联系,这样 signal 的更新就收不到了。这里创建了 proxy,用 proxy 把操作代理回原始的 props 上,这样对新的 props 的操作也会回到最初的 props.xxx 上面,这样就仍旧保持响应式的能力,就可以监听更新。

如果你一定要解构属性,可以借助一些 babel 插件在编译时完成这部分工作,github.com/orenelbaum/… 这样可以在 solid 中编写普通的解构逻辑。当然这其实只是提供了一个语法糖的效果,本质上来说 solid 的一切都是构建在响应式系统之上的,理解了响应式就可以理解这里一定不应该这样写,我们使用插件也只是想和自己使用其他库时形成的习惯统一起来,这和 solid 的底层实现是相悖的,这也是为什么作者没有把这部分内容集成编译能力上去,而是选择提供两个辅助方法。当然如果你喜欢,可以尝试使用这类插件。