什么是signal
signal的本质,是将「对状态的引用」以及「对状态值的获取」分离开。详细解释见
react signal实现
首先来分析signal的功能点
- 状态值的引用和获取分开
- 对于依赖了state变量的memo和effect能够自动触发更新或者执行
对于状态值的和获取的分开可以通过闭包来实现
对于自动触发更新或者执行,我们可以通过发布订阅的方式来实现
function useState(value) {
// 存放依赖于该state变化的消费者
const subs = new Set();
const getter = () => {
return value;
}
const setter = (nextValue) => {
value = nextValue;
subs.forEach(sub => sub());
}
return [getter, setter];
}
function useEffect(callback) {
const effect = () => {
effectStack.push(effect);
try {
callback();
} finally {
effectStack.pop();
}
}
effect();
}
写到这里,我们发现一个问题,如何将对应的effect和state建立关联关系呢,我们如何知道当前哪些effect和state有依赖关系呢。这时候需要借助一个全局变量effectStack来解决这个问题。当执行该effect的时候,如果依赖了某个state,那么就将该effect存入effectStack,然后state从effectStack取出对应的effect,然后建立发布订阅关系
const effectStack = [];
function useState(value) {
const subs = new Set();
const getter = () => {
const effect = effectStack[effectStack.length - 1];
if (effect) {
subs.add(effect);
}
return value;
}
const setter = (nextValue) => {
value = nextValue;
subs.forEach(sub => sub());
}
return [getter, setter];
}
function useEffect(callback) {
const effect = () => {
effectStack.push(effect);
try {
callback();
} finally {
effectStack.pop();
}
}
effect();
}
function useMemo(callback) {
const [s, set] = useState();
useEffect(() => set(callback()));
return s;
}
写到这里,我们感觉是不是写完了,但其实还有一个问题。我通过下面的例子来解释
const [name1, setName1] = useState('Tom');
const [name2, setName2] = useState('Jerry');
const [showAll, setShowAll] = useState(true);
const res = useMemo(() => {
if (!showAll) {
return name1();
}
return `${name1()} and ${name2()}`;
})
setName1('Tom1');
console.log(res()); // Tom1 and Jerry
当前例子是正常的,如果我加几行代码
const [name1, setName1] = useState('Tom');
const [name2, setName2] = useState('Jerry');
const [showAll, setShowAll] = useState(true);
let count = 0;
const res = useMemo(() => {
count++;
if (!showAll()) {
return name1();
}
return `${name1()} and ${name2()}`;
})
setName1('Tom1');
console.log(res()); // Tom1 and Jerry
setShowAll(false);
setName2('Jerry1');
console.log(res()); // Tom1 and Jerry1
console.log(count); // 4
这里可以发现最后一个console的count按理说应该是3,因为showAll为false后,不再依赖name2的结果,所以不应该修改name2出发useMemo的执行
发生这种情况的原因是因为res 的useMemo和name1和name2都建立了订阅关系,所以即使useMemo不再依赖name2的结果,但是发布订阅关系还在,所以修改name2还是会重新执行一次useMemo
为了解决这个问题,我们可以通过useEffect每次执行前先清空订阅的数据源,在执行的时候,再重新建立订阅关系
const effectStack = [];
function subscribe(effect, subs) {
subs.add(effect);
effect.deps.add(subs);
}
function cleanup(effect) {
for (const subs of effect.deps) {
subs.delete(effect);
}
effect.deps.clear();
}
function useState(value) {
const subs = new Set();
const getter = () => {
const effect = effectStack[effectStack.length - 1];
if (effect) {
subscribe(effect, subs);
}
return value;
}
const setter = (nextValue) => {
value = nextValue;
for (const effect of [...subs]) {
effect.execute();
}
}
return [getter, setter];
}
function useEffect(callback) {
const execute = async () => {
cleanup(effect);
effectStack.push(effect);
try {
await callback();
} finally {
effectStack.pop();
}
}
const effect = {
execute,
deps: new Set()
};
execute();
}
function useMemo(callback) {
const [s, set] = useState();
useEffect(() => set(callback()));
return s;
}
const [name1, setName1] = useState('Tom');
const [name2, setName2] = useState('Jerry');
const [showAll, setShowAll] = useState(true);
let count = 0;
const res = useMemo(() => {
count++;
if (!showAll()) {
return name1();
}
return `${name1()} and ${name2()}`;
})
setName1('Tom1');
console.log(res()); // Tom1 and Jerry
setShowAll(false);
setName2('Jerry1');
console.log(res()); // Tom1 and Jerry1
console.log(count); // 3
到这里,这个简易版的react signal的功能就实现了