实现一个简易版react signal
发布于 1 年前 作者 qiangwen 619 次浏览 来自 分享

什么是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的功能就实现了

1 回复
回到顶部