solid.js响应式createSignal源码解析
正文
www.solidjs.com/docs/latest…
createSignal
用来创建响应式数据,它可以跟踪单个值的变化。
solid.js 的响应式实现参考了 S.js,它是一个体积超小的 reactive 库,支持自动收集依赖和简单的响应式编程。
createSignal
createSignal
首先我们来看下 createSignal
的声明:
// packages/solid/class="lazy" data-src/reactive/signal.ts
export interface BaseOptions {
name?: string;
}
export interface EffectOptions extends BaseOptions {}
export interface MemoOptions<T> extends EffectOptions {
equals?: false | ((prev: T, next: T) => boolean);
}
export type Accessor<T> = () => T;
export type Setter<T> = (undefined extends T ? () => undefined : {}) &
(<U extends T>(value: (prev: T) => U) => U) &
(<U extends T>(value: Exclude<U, Function>) => U) &
(<U extends T>(value: Exclude<U, Function> | ((prev: T) => U)) => U);
// packages/solid/class="lazy" data-src/reactive/signal.ts
export type Signal<T> = [get: Accessor<T>, set: Setter<T>];
export interface SignalOptions<T> extends MemoOptions<T> {
internal?: boolean;
}
export function createSignal<T>(): Signal<T | undefined>;
export function createSignal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
可以看到 createSignal
支持两个参数,分别是 value 和 options,然后返回一个包含 setter 和 getter 的数组。
参数:
- value:初始值,默认值为
undefiend
- options
- equals:自定义比较器,用于新旧值比较或触发强制更新,允许传递函数或者 false;
- internal(可选):标识是否为内置属性,应用于开发环境,生产环境会移除掉相关逻辑;
- name(可选):自定义属性对象名称,应用于开发环境,生产环境会移除掉相关逻辑。
返回值:
- getter:返回当前值,以函数形式调用
- 自动进行依赖收集。例如在
createEffect
中调用 getter, state 对象会与 effect 建立依赖关系。
- 自动进行依赖收集。例如在
- setter:设置值,以函数形式调用
- 如果存在依赖当前 state 对象的观察者,循环执行观察者数组。
了解 createSignal
声明之后,下面我们来看下具体实现。
// packages/solid/class="lazy" data-src/reactive/signal.ts
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,
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.tValue);
else value = value(s.value);
}
return writeSignal(s, value);
};
return [readSignal.bind(s), setter];
}
如果用户传入 options,会对 options 和默认的 options 进行合并,否则使用默认 options。
// packages/solid/class="lazy" data-src/reactive/signal.ts
export const equalFn = <T>(a: T, b: T) => a === b;
const signalOptions = { equals: equalFn };
默认配置只有一个 equals
属性,值为 equalFn
,用于比较两个值是否相同。
由于这里比较的是引用地址,所以当你改变一个对象的某个属性,重新赋值时,相关订阅并不会被触发,所以这时我们可以在传入的 options 配置中配置 equals
为 false 或者自定义其他比较逻辑。
例如下面的案例:
const [object, setObject] = createSignal({ count: 0 });
createEffect(() => {
console.log(object());
});
object().count = 2;
setObject(object);
setObject(current => {
current.count += 1;
current.updated = new Date();
return current;
});
// { count: 0 }
上述代码在运行时 effect 中代码只会触发一次,这可能与我们的预期不符,所以我们可以传入自定义 options。
const [object, setObject] = createSignal({ count: 0 }, { equals: false });
// { count: 0 }
// { count: 2 }
// { count: 3, updated: 2022-09-11T08:21:44.258Z }
当我们设置 equals 属性为 false,effect 就会被触发 3 次。
除此之外,我们还可以使用该配置作为触发器来使用,这里就不展开阐述了。感兴趣可以查看官方提供的案例,createSignal。
下面让我们继续查看代码:
// packages/solid/class="lazy" data-src/reactive/signal.ts
export interface SignalState<T> {
value?: T;
observers: Computation<any>[] | null;
observerSlots: number[] | null;
tValue?: T;
comparator?: (prev: T, next: T) => boolean;
name?: string;
}
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,
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.tValue);
else value = value(s.value);
}
return writeSignal(s, value);
};
return [readSignal.bind(s), setter];
}
在 createSignal
定义了 s
对象,它有四个属性,分别是:
- value:传入的值
- observers:观察者数组
- observerSlots:观察者对象在数组的位置
- comparator:比较器
// packages/solid/class="lazy" data-src/reactive/signal.ts
if ("_SOLID_DEV_" && !options.internal)
s.name = registerGraph(options.name || hashValue(value), s as { value: unknown });
这段代码为 state 对象设置了 name 属性,不过它只作用于开发环境,生产环境打包时 _SOLID_DEV_
变量会被替换为 false,然后会作为 decode 被移除掉。
// packages/solid/rollup.config.js
export default [
{
input: "class="lazy" data-src/index.ts",
// ...
plugins: [
replace({
'"_SOLID_DEV_"': false,
preventAssignment: true,
delimiters: ["", ""]
})
].concat(plugins)
}
]
接下来定义 setter 函数:首先会对 value 的值进行判断,如果传递的 setter 是一个 函数:
- 如果发现
Transition
存在,并且Transition.sources
中存在当前 state,会使用s.tValue
属性值; - 如果上述条件不满足,会使用当前 state 的 value 属性值。
然后调用 wrtieSignal
,并返回其结果。
// packages/solid/class="lazy" data-src/reactive/signal.ts
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> {
// ...
const setter: Setter<T | undefined> = (value?: unknown) => {
if (typeof value === "function") {
if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue);
else value = value(s.value);
}
return writeSignal(s, value);
};
return [readSignal.bind(s), setter];
}
最后返回操作数组:第一个参数为 readSignal 函数,用来返回 s 中的 value 值,第二个参数就是 setter。
总结一下,createSignal
首先会合并用户 options,其次会定义 state 对象,用来记录当前值和依赖关系,然后定义 setter 函数,用来设置值,最后返回一个数组,分别是 readSignal
函数和 setter
函数。
readSignal
readSignal
看完 createSignal 定义,接着我们再来看下 readSignal,这个方法非常重要。solid.js 依赖关系的建立就发生在这个方法中。
// packages/solid/class="lazy" data-src/reactive/signal.ts
// Internal
export function readSignal(this: SignalState<any> | Memo<any>) {
const runningTransition = Transition && Transition.running;
if (
(this as Memo<any>).sources &&
((!runningTransition && (this as Memo<any>).state) ||
(runningTransition && (this as Memo<any>).tState))
) {
if (
(!runningTransition && (this as Memo<any>).state === STALE) ||
(runningTransition && (this as Memo<any>).tState === STALE)
)
updateComputation(this as Memo<any>);
else {
const updates = Updates;
Updates = null;
runUpdates(() => lookUpstream(this as Memo<any>), false);
Updates = updates;
}
}
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;
}
函数内部首先判断是否正在 transition,我们暂时不需要关心这段逻辑,直接跳到下面这段逻辑:
// packages/solid/class="lazy" data-src/reactive/signal.ts
export function readSignal(this: SignalState<any> | Memo<any>) {
// ...
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;
}
首先会判断 Listener 是否存在,如果存在才会执行这段代码。那么这个 Listener 是什么时候被定义并赋值的呢?
// packages/solid/class="lazy" data-src/reactive/signal.ts
let Listener: Computation<any> | null = null;
let Updates: Computation<any>[] | null = null;
let Effects: Computation<any>[] | null = null;
Listener 是一个全局变量,默认值是 null。同时还定义了 Updates
、Effectes
数组,它们都是 Computation 类型。
export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next;
export interface SignalState<T> {
value?: T;
observers: Computation<any>[] | null;
observerSlots: number[] | null;
tValue?: T;
comparator?: (prev: T, next: T) => boolean;
name?: string;
}
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 interface Computation<Init, Next extends Init = Init> extends Owner {
fn: EffectFunction<Init, Next>;
state: number;
tState?: number;
sources: SignalState<Next>[] | null;
sourceSlots: number[] | null;
value?: Init;
updatedAt: number | null;
pure: boolean;
user?: boolean;
suspense?: SuspenseContextType;
}
可以看到 Computation 是一个对象,定义了很多属性,基本都不知道啥作用。不过其中一个 sources 属性,你是否也感觉很眼熟?
对,它就是一个普通的 signal 对象,也就是我们调用 createSignal
方法时,内部创建的 s 对象。
另外可以看到,上面 SignalState
接口声明中的 observers 就是一个 Computation 类型的数组,这时我们已经知道 state 和 computation 互相依赖,并且是多对多的关系。
接下来再回到代码:
// packages/solid/class="lazy" data-src/reactive/signal.ts
export function readSignal(this: SignalState<any> | Memo<any>) {
// ...
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;
}
当 Listener 存在时,首先会获取当前 observers 的数量,如果不存在就是 0,这里的 this 就是 s
对象。
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> {
// ...
return [readSignal.bind(s), setter];
}
接下来分别判断 Listener.sources 和 this.observers 是否存在,如果不存在会创建数组,并建立依赖关系。最后将 s 对象的 value 返回。这里的 value 就是我们调用 createSignal 传入的初始值。
不同于 vue 中 通过 Proxy 或者 Object.defineProperty 进行属性劫持,solid.js 中的依赖关系建立是通过函数调用实现的,例如在 createEffect
中调用 getter
函数,这时就会建立依赖关系。
writeSignal
writeSignal
我们已经知道,通过 getter 可以建立 compulation 和 state 之间的依赖关系。setter 函数其实就是用来触发依赖。
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;
}
writeSignal 接收两个参数,第一个参数就是 state 对象,第二个参数就是我们传入的值。
首先获取 current,我们暂时忽略 Transition 相关的判断逻辑,这里的 current 就是 state 的值,也就是旧值。
当 node.comparator
为 false,或者新值和旧值不同时,才会进行赋值和触发更新。
comparator 可以被赋值为 false 或者一个函数,默认 comparator 只会比较新值和旧值引用是否相同,这里我们后面再去分析。
当传入的值与旧值不同,将新值赋值 node.value
。然后判断 node 是否存在观察者,如果存在会循环遍历 observers 数组,根据不同逻辑放入 Updates 或者 Effects 数组,不过最终都会执行 observer 对象,即 computation 对象。
案例分析
我们可以通过源码调试的方式对代码进行分析。
我们来看下面这个例子。
const { createSignal } = require("../../solid/dist/solid.cjs");
const [count, setCount] = createSignal(0);
console.log(count());
setCount(count() + 1);
console.log(count());
例子很简单,就是创建一个响应式数据,打印它,改变值后继续对其进行打印。
当 createSignal 函数被执行完毕之前,我们可以可以看到 s 对象已经被创建,value 值为 0,observers 为 null。
接下来执行第一次打印,这时会触发 readSignal 函数。
可以看到,this 其实就是 state 对象,此时 runningTransition 和 Listerner 都为空,什么都不会执行,直接返回 s.value。
当执行到 setCount(count() + 1)
这段代码时,首先会取到 state 的 value 值,然后再进行计算,并将结果传给 setter 函数,触发 writeSignal
函数。
可以看到,current 的值是 0,此时 comparator 肯定是存在的,并且两个值并不相等,由于 Transition 不存在,所以会将 value 赋值给 node.value
,此时 state 的 value 值已经变为 1。由于 node.observers` 也不存在,所以会直接返回传入的 value ,函数执行完毕。
接下来执行最后一次打印,和之前的过程一样,这里只是做了一次取值操作,打印出改变后的结果 1。
我们还可以调试其他案例,比如给 createSignal
传递第二个参数,配置 name 和 equals 属性然后查看代码的变化。
相关案例
总结
createSignal 用于创建响应式数据,其内部定义了一个 s 对象,保存当前值和依赖关系,并返回 getter 函数和 setter 函数。
当调用 getter 函数读取值时,如果存在 Listener,双方会建立依赖关系,即将 Listener 添加到 state 的 observers 数组中,将 state 添加到 Listener 的 sources 数组中, 并返回当前值。
当调用 settter 函数赋值时,如果存在 observers,会遍历 observers 数组,并根据逻辑加入 Updates 或 Effects 数组中,最后去执行它们,触发副作用函数执行。
以上就是solid.js响应式createSignal 源码解析的详细内容,更多关于solid.js createSignal源码的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341