五分钟看懂 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的依赖收集关系图,头都画大了🤯

相关推荐
孤水寒月3 小时前
基于HTML的悬窗可拖动记事本
前端·css·html
祝余呀3 小时前
html初学者第一天
前端·html
速易达网络5 小时前
RuoYi、Vue CLI 和 uni-app 结合构建跨端全家桶方案
javascript·vue.js·低代码
耶啵奶膘6 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
视频砖家6 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能
lyj1689977 小时前
vue-i18n+vscode+vue 多语言使用
前端·vue.js·vscode
小白变怪兽8 小时前
一、react18+项目初始化(vite)
前端·react.js
ai小鬼头8 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
墨菲安全9 小时前
NPM组件 betsson 等窃取主机敏感信息
前端·npm·node.js·软件供应链安全·主机信息窃取·npm组件投毒
GISer_Jing9 小时前
Monorepo+Pnpm+Turborepo
前端·javascript·ecmascript