细粒度响应式
细粒度响应式由一些primitive组成。
signal
signal是响应式系统最基本的组件,它由getter,setter,value组成。在一些文章中,也称为Observable,Atom,Subject, Refs。
基本用法如下
// 定义
const [count, setCount] = createSignal(0);
// 读
console.log(count())
// 写
setCount(5)
通过createSignal创建,返回数组,包括两个值getter和setter。通过getter调用获取值,通过setter更改值。这只是一个可以存储任意值的结构,关键在于getter和setter内部逻辑可以是任意代码,因此可以用来传播更新。函数是主要形式,但是也可以通过对象getter或代理。例如
// vue中ref的定义
const count = ref(0)
// 读
console.log(count.value)
// 写
count.value=5;
也可以通过编译加上语法糖
// Svelte 中定义
let count = 0
// 读
console.log(count)
// 写
count = 5;
本质上signal是event emitter,关键区别是如何管理订阅。
Reaction响应
Signal本身并没太大用途,如果没有Reaction的话。Reaction也叫Effect,Autoruns, Watches, computed,观察signal,每次值变化后自动运行。Reaction是一种函数,初始运行后记录下相关的signal,之后每次signal值发生变化,就会重新运行。
const [count,setCount] = createSignal(0);
createEffect(() => { console.log(count())})
setCount(4) //触发上面的effect运行
看上去很神奇,但这是为什么需要getter的原因。每次signal运行,包裹函数检测到会自动订阅signal。重要的是Signal能存储任何类型的数据。signal和Reaction就是细粒度响应式的基石:观察者和被观察者。你可以用这两者创建出大部分行为。但是除这两者之外还有一个核心primitive。
Derivation衍生状态
通常我们需要用不同形式表示数据,在多个reaction里使用同个signal,我们需要提取成一个函数。
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Smith');
const fullName = () => {
console.log('creating/updating fullname')
return `${firstName()} ${lastName()}`
}
createEffect(()=> console.log(fullName()))
createEffect(() => console.log('your name is not', fullName()))
setFirstName('Jacob')
// 'creating/updating fullname'
// John Smith
// 'creating/updating fullname'
// your name is not John Smith
// Set new firstName.....
这里定义了fullName函数,这是因为我们需要在createEffect里面调用signal的getter才能被捕捉到。但是有时候定义衍生值的成本很高,我们不想重复工作。因此我们需要第三个primitive,用来缓存计算结果。通常称为memos, Computed, Pure Computed。
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Smith');
const fullName = createMemo(() => {
console.log('creating/updating fullname')
return `${firstName()} ${lastName()}`
})
console.log("3. Create Reactions");
createEffect(()=> console.log(fullName()))
createEffect(() => console.log('your name is not', fullName()))
setFirstName('Jacob')
// 'creating/updating fullname'
// 3. Create Reactions
// John Smith
// your name is not John Smith
// Set new firstName.....
使用memo后,区别是第一提前计算了memo,后面调用时不再重新运行。当更新signal后,重新运行。虽然上面的例子中,名字并不是一个昂贵的操作,但是使用Derivation将值缓存起来,同时Derivation本身也可追踪。通过Derivation可以保证时同步的,在任何时刻,我们可以根据它的依赖判断值是否过期,而使用effect向表示衍生值的signal去写的方式模拟Derivation却不能保证,因为我们无法看到衍生signal的依赖(signal没用依赖)。
有些库延迟计算Derivation,因为它们只在需要时才计算。
console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);
const displayName = createMemo(() => {
if (!showFullName()) return firstName();
return `${firstName()} ${lastName()}`
});
createEffect(() => console.log("My name is", displayName()));
console.log("2. Set showFullName: false ");
setShowFullName(false);
console.log("3. Change lastName");
setLastName("Legend");
console.log("4. Set showFullName: true");
setShowFullName(true);
// 输出
// 1. Create
// My name is John Smith
// 2. Set showFullName: false
// My name is John
// 3. Change lastName
// 4. Set showFullName: true
// My name is John Legend
需要注意:第三步,改lastName时没有触发effect,那是因为此刻没有任何node依赖它,当修改showFullName后,重构了依赖图。每次运行时都会重新构建订阅和依赖关系。
console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);
const displayName = createMemo(() => {
console.log("### executing displayName");
onCleanup(() =>
console.log("### releasing displayName dependencies")
);
if (!showFullName()) return firstName();
return `${firstName()} ${lastName()}`
});
createEffect(() => console.log("My name is", displayName()));
console.log("2. Set showFullName: false ");
setShowFullName(false);
console.log("3. Change lastName");
setLastName("Legend");
console.log("4. Set showFullName: true");
setShowFullName(true);
// 1. Create
// ### executing displayName
// My name is John Smith
// 2. Set showFullName: false
// ### releasing displayName dependencies
// ### executing displayName
// My name is John
// 3. Change lastName
// 4. Set showFullName: true
// ### releasing displayName dependencies
// ### executing displayName
// My name is John Legend
加上oncleanup后可以看到每次运行时清理原有依赖图的时刻。
同步
细粒度响应系统每次更新都是同步操作,我们可以使用batch将多个更新操作包裹起来,当所有都更新完后在执行reaction。
console.log("1. Create");
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
const c = createMemo(() => {
console.log("### read c");
return b() * 2;
});
createEffect(() => {
console.log("### run reaction");
console.log("The sum is", a() + c());
});
console.log("2. Apply changes");
batch(() => {
setA(2);
setB(3);
});
//输出
// 1. Create
// ### read c
// ### run reaction
// The sum is 5
// 2. Apply changes
// ### run reaction
// ### read c
// The sum is 8
需要注意,apply changes后的打印,A和b更新后,必须选一个依赖执行,这里选了从a开始,先执行reaction,当执行时读取c发现是stale,运行c。
实现
signal
export function createSignal(value) {
const read = () => value;
const write = (nextValue) => value = nextValue;
return [read, write]
}
const [count, setCount] = createSignal(3);
console.log("Initial Read", count());
setCount(5);
console.log("Updated Read", count());
setCount(count() * 2);
console.log("Updated Read", count());
// 输出
// Initial Read 3
// Updated Read 5
// Updated Read 10
先如上实现createSignal函数,它接受value,然后通过闭包方式返回getter和setter函数。getter获取value的值,setter更改值。这样实现后已经具备了读写能力,可以获取signal的值,也可以通过setter更新。但是还缺乏依赖订阅的能力,signal是event emitter。
增加依赖追踪的能力,在getter中建立依赖关系,setter遍历所有订阅更新函数。
const context = []
function subscribe(running, subscriptions) {
subscriptions.add(running);
running.dependencies.add(subscriptions)
}
function createSignal(value) {
const subscriptions = new Set();
const read = () => {
const running = context[context.length - 1];
if (running) {
subscribe(running, subscriptions)
}
return value
};
const write = (nextValue) => {
value = nextValue;
for(const sub of [...subscriptions]) {
sub.execute()
}
};
return [read, write]
}
通过全局变量context记录运行中的Reaction栈,每个signal有一个订阅列表记录了观察者。为了实现自动依赖追踪,每个reaction或者Derivation在执行时将自己压入context栈中,它会被加到signal的订阅列表中。反过来,我们也将signal加到了依赖列表里,建立了双向关联,这会有助于cleanup的实现。
最后在signal发生更新时,执行所有的订阅。这里复制了订阅列表,可以避免在执行过程中订阅列表发生变化。
Reaction与Derivation
// 清理关联
function cleanup(running) {
// 每一个dep时siganl的订阅列表集合
for(const dep of running.dependencies) {
dep.delete(running) // 从订阅列表里删除自己
}
running.dependencies.clear() // 清空当前的reaction的依赖
}
function createEffect(fn) {
// 表示订阅的对象,
const running = {
dependencies: new Set(),
execute: function() {
cleanup(this)
context.push(this);
try {
fn()
} finally {
context.pop();
}
}
}
// 初始运行
running.execute()
}
createEffect的接受一个函数,内部创建一个订阅对象,有两个重要属性dependencies记录依赖,execute执行函数。execute包裹传入的函数,在函数调用前后设置恢复当前的context栈,由于每次调用都会创建依赖关系,所以在调用开始先首先清理旧的关系。到了这里,已经可以做点东西了。
运行下面示例
function cleanup(running) {
for (const dep of running.dependencies) {
dep.delete(running);
}
running.dependencies.clear();
}
export function createEffect(fn) {
const execute = () => {
cleanup(running);
context.push(running);
try {
fn();
} finally {
context.pop();
}
};
const running = {
execute,
dependencies: new Set()
};
execute();
}
// 1. Create Signal
// 2. Create Reaction
// The count is 0
// 3. Set count to 5
// The count is 5
// 4. Set count to 10
// The count is 10
简单实现里,Derivation可以通过reaction和signal这两者结合实现。
function createMemo(fn) {
const [s, set] = createSignal();
createEffect(() => set(fn()))
return s;
}
在上面实现中,我们可以将memo看作是一种特殊的signal,它是可以依赖其他signal的。因为memo是signal所以可以被reaction追踪,同时memo也具有reaction,当依赖的signal变化,memo会更新合成的signal。
原文链接:https://juejin.cn/post/7240636255973638200 作者:Tomarsh