在 Vue3 的响应式系统中,双向链表是一个非常重要的数据结构。相比 Vue2 使用数组来存放依赖,Vue3 选择链表的原因在于效率更高,尤其是在频繁收集和清理依赖时,链表可以显著优化性能。本文将通过讲解和示例代码,帮助你理解这一点。
为什么要用双向链表
在响应式依赖收集过程中,Vue 需要完成两件事:
- 收集依赖:当访问响应式数据时,要记录当前副作用函数(
effect)。 - 清理依赖:当副作用函数重新运行或失效时,需要把它从依赖集合里移除。
如果依赖集合使用数组:
- 删除某个依赖需要遍历整个数组才能找到它,复杂度是 O(n)。
- 当依赖数量多、更新频繁时,性能就会下降。
使用双向链表:
- 删除节点只需要修改前后两个指针,复杂度是 O(1)。
- 插入节点同样是 O(1)。
- 非常适合依赖频繁增删的场景。
双向链表的基本结构
每个链表节点包含:
effect:保存副作用函数prev:指向前一个节点next:指向后一个节点
通过 prev 和 next,节点可以快速被插入或删除。
示例:
1A <-> B <-> C 2删除 B: 3A.next = C 4C.prev = A 5
代码示例
下面的示例展示了如何通过双向链表来管理依赖的添加、删除和触发。
1// 链表节点:存储副作用函数和前后指针 2class EffectNode { 3 constructor(effect) { 4 this.effect = effect; // 要执行的副作用函数 5 this.prev = null; // 前一个节点 6 this.next = null; // 后一个节点 7 this.dep = null; // 当前节点所属的依赖集合 8 } 9} 10 11// 依赖集合(Dep):使用双向链表存储多个 effect 12class Dep { 13 constructor() { 14 this.head = null; // 链表头 15 this.tail = null; // 链表尾 16 } 17 18 // 添加依赖:O(1),插入到链表尾部 19 add(effect) { 20 const node = new EffectNode(effect); 21 node.dep = this; 22 if (!this.head) { 23 // 如果链表为空,头尾都指向当前节点 24 this.head = this.tail = node; 25 } else { 26 // 否则插入到尾部 27 this.tail.next = node; 28 node.prev = this.tail; 29 this.tail = node; 30 } 31 return node; // 返回节点,方便以后删除 32 } 33 34 // 删除依赖:O(1),只需修改相邻节点指针 35 remove(node) { 36 if (node.prev) node.prev.next = node.next; 37 if (node.next) node.next.prev = node.prev; 38 if (node === this.head) this.head = node.next; 39 if (node === this.tail) this.tail = node.prev; 40 // 清理引用,帮助垃圾回收 41 node.prev = node.next = node.dep = null; 42 } 43 44 // 触发依赖:遍历链表,执行所有副作用函数 45 trigger() { 46 let cur = this.head; 47 while (cur) { 48 cur.effect(); 49 cur = cur.next; 50 } 51 } 52} 53 54// 模拟响应式数据 55const state = { count: 0 }; 56// 每个属性都可能有一个依赖集合,这里用一个 dep 演示 57const dep = new Dep(); 58 59// 注册副作用函数,并收集依赖 60function effect(fn) { 61 const runner = () => fn(); 62 const node = dep.add(runner); 63 runner(); // 立即执行一次 64 return node; // 返回节点,方便后续删除 65} 66 67// 使用:收集依赖 68const node = effect(() => { 69 console.log("副作用执行: count =", state.count); 70}); 71 72// 修改数据时触发依赖 73state.count++; 74dep.trigger(); // 输出: 副作用执行: count = 1 75 76// 删除依赖,再次触发时不会执行 77dep.remove(node); 78state.count++; 79dep.trigger(); // 无输出 80
输出结果
1副作用执行: count = 0 2副作用执行: count = 1 3
在移除依赖后,再次修改数据时不会有输出,说明链表删除操作成功。
总结
- Vue3 使用双向链表来存储依赖,是为了提升收集和清理的效率。
- 相比数组,链表的插入和删除复杂度更低,尤其适合依赖数量多且频繁变化的场景。
- 通过示例可以看到,依赖的收集、触发和清理都能高效完成。
这就是 Vue3 响应式系统比 Vue2 更快的一个重要原因。
本文部分内容借助 AI 辅助生成,并由作者整理审核。
《Vue3 中的双向链表依赖管理详解与示例》 是转载文章,点击查看原文。
