五分钟看懂 alien signals 依赖收集原理

众所周知,Vue 3.6 计划引入一个全新的响应式机制:alien-signals,用来进一步优化 Vue 的响应式系统。

目前 vue3.6 还没正式发布,但可以先通过以下命令打包alien-signals源码:

esbuild src/index.ts --bundle --format=esm --outfile=esm/index.mjs

打包后的代码还不到 500 行,体积小、结构也比较清晰。趁现在还不那么 复杂,我正在尝试解析一下 alien-signals 的源码,顺便记录一些理解过程

首先我们先有一个 2x2 的单元测试,其中 fn1 和 fn2 分别有两个依赖 count1、count2

ts 复制代码
test("debugger 2*2", () => {
  const count1 = signal(1);
  const count2 = signal(100);
  effect(function fn1() {
    console.log(`effect1-> count1 is: ${count1()}`);
    console.log(`effect1-> count2 is: ${count2()}`);
  });
  effect(function fn2() {
    console.log(`effect2-> count1 is: ${count1()}`);
    console.log(`effect2-> count2 is: ${count2()}`);
  });
  count1(2);
  count2(200);
});

这是 signal 的源码(build 后)如下,重点关心这个 this,也就是 dep,后续我们用蓝色表示dep

js 复制代码
function signal(initialValue) {
  return signalGetterSetter.bind({
    currentValue: initialValue,
    subs: void 0,
    subsTail: void 0,
  });
}

这是 effect 的源码(build 后)如下,重点关心这个 e,也就是 sub,后续我们用黄色表示sub

js 复制代码
function effect(fn) {
  // sub
  const e = { fn, subs: void 0, subsTail: void 0, deps: void 0, depsTail: void 0, flags: 2 /* Effect */
  };
  ... 省略部分与当前单元测试无关的代码
  const prevSub = activeSub;
  activeSub = e;
  try {
    e.fn();
  } finally {
    activeSub = prevSub;
  }
  ...省略部分与当前单元测试无关的代码
}

在 effect 中会默认执行一次 fn 进行初始的依赖收集,当执行 fn1 时,我们可以得到这几个数据

在 fn1 中访问 count1()时就会link(this, activeSub),将当前的依赖和订阅关联起来

diff 复制代码
function signalGetterSetter<T>(this: Signal<T>, ...value: [T]): T | void {
  if (activeSub !== undefined) {
+   关注这个link
    link(this, activeSub);
  }
  return this.currentValue;
}

link这个函数会复用节点,如果无法复用,说明这是一个新的link,当前是第一次执行依赖收集,当然是新的一个link,所以会执行linkNewDep(dep1,sub1,undefined,undefined)

ts 复制代码
function link(dep: Dependency, sub: Subscriber): Link | undefined {
  // 获取当前这个sub的最后一个依赖
  const currentDep = sub.depsTail; 
  ...
  // 获取currentDep的下一个依赖,如果depsTail不存在,就是当前这个sub的第一个依赖
  // 这段逻辑主要在依赖触发后重新依赖收集有关,暂时不会执行这个if里面的逻辑,主要用于复用节点
  const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps;
  if (nextDep !== undefined && nextDep.dep === dep) {
    sub.depsTail = nextDep;
    return;
  }
  ...
  return linkNewDep(dep, sub, nextDep, currentDep);
}

linkNewDep会创建一个newLink节点,用于关联dep和sub

ts 复制代码
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
): Link {
  const newLink: Link = {
    dep,
    sub,
    nextDep,
    prevSub: undefined,
    nextSub: undefined,
  };
  // 没有depsTail,表示currentDep不存在,表示这是一个新的sub,那么sub1的deps就指向dep1
  if (depsTail === undefined) {
    sub.deps = newLink;
  } else {
    depsTail.nextDep = newLink;
  }
  // 如果当前的dep没有订阅,那么dep1的subs指向第一个订阅sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
  return newLink;
}

第一次linkNewDep后的依赖收集如下

开始收集count2了,又进行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep2, sub1, undefined, dep1.depsTail)

ts 复制代码
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> dep1-> depsTail的newLink
  if (depsTail === undefined) {
    // 不会执行
    sub.deps = newLink;
  } else {
    // 这次执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的dep2没有被订阅,那么dep2的subs指向第一个订阅sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    // 不会执行
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

第二次linkNewDep后的依赖收集如下

第一个effect就依赖收集完成了,现在准备开始第二个effect的依赖收集,根据effect的源码,我们知道会创建一个sub2的订阅,现在的依赖关系图如下图所示,就单纯的多了个sub2

ts 复制代码
effect(function fn2() {
  console.log(`effect2-> count1 is: ${count1()}`);
  console.log(`effect2-> count2 is: ${count2()}`);
});

执行fn2,正式进行依赖收集

  • 访问count1(),同样依次执行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep1, sub2, undefined, undefined)
ts 复制代码
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> undefined
  if (depsTail === undefined) {
    // 这次执行这个
    sub.deps = newLink;
  } else {
    // 不会执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的dep1已经被订阅,subs指向newLink-sub->sub1
  if (dep.subs === undefined) {
    dep.subs = newLink;
  } else {
    // 执行这个
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

这次依赖收集后,最新的关系图如下:

  • 访问count2(),同样依次执行link和linkNewDep这两个函数,根据上一次的依赖关系图,可以知道 linkNewDep(dep2, sub2, undefined, dep1.depsTail)
ts 复制代码
function linkNewDep(
  dep: Dependency,
  sub: Subscriber,
  nextDep: Link | undefined,
  depsTail: Link | undefined
) {
  // 根据上述可知,depsTail -> dep1-> depsTail的newLink
  if (depsTail === undefined) {
    // 不会执行
    sub.deps = newLink;
  } else {
    // 这次执行这个
    depsTail.nextDep = newLink;
  }
  // 当前的已经被sub1订阅了
  if (dep.subs === undefined) {
    // 不会执行
    dep.subs = newLink;
  } else {
    // 这次执行这个
    const oldTail = dep.subsTail!;
    newLink.prevSub = oldTail;
    oldTail.nextSub = newLink;
  }
  // 更新尾部指针
  sub.depsTail = newLink;
  // 更新尾部指针
  dep.subsTail = newLink;
}

至此所有的依赖收集都完成了。

下面这是一个3x3的依赖收集关系图,头都画大了🤯

相关推荐
吴永琦(桂林电子科技大学)42 分钟前
HTML5
前端·html·html5
爱因斯坦乐1 小时前
【HTML】纯前端网页小游戏-戳破彩泡
前端·javascript·html
恋猫de小郭1 小时前
注意,暂时不要升级 MacOS ,Flutter/RN 等构建 ipa 可能会因 「ITMS-90048」This bundle is invalid 被拒绝
android·前端·flutter
大莲芒5 小时前
react 15-16-17-18各版本的核心区别、底层原理及演进逻辑的深度解析--react17
前端·react.js·前端框架
木木黄木木7 小时前
html5炫酷3D文字效果项目开发实践
前端·3d·html5
Li_Ning217 小时前
【接口重复请求】axios通过AbortController解决页面切换过快,接口重复请求问题
前端
胡八一8 小时前
Window调试 ios 的 Safari 浏览器
前端·ios·safari
Dontla8 小时前
前端页面鼠标移动监控(鼠标运动、鼠标监控)鼠标节流处理、throttle、限制触发频率(setTimeout、clearInterval)
前端·javascript
再学一点就睡8 小时前
深拷贝与浅拷贝:代码世界里的永恒与瞬间
前端·javascript
CrimsonHu8 小时前
B站首页的 Banner 这么好看,我用原生 JS + 三大框架统统给你复刻一遍!
前端·javascript·css