这是我参与「掘金日新计划 · 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 的底层实现是相悖的,这也是为什么作者没有把这部分内容集成编译能力上去,而是选择提供两个辅助方法。当然如果你喜欢,可以尝试使用这类插件。