链表运用到响应式中
vue响应式实现原理
第一章 vue 简单的effect实现
前言
上一章我们实现了最简单的effect响应式,当时是有很多地方需要改进的,后面的都是在此基础上进行完善
一、链表
在讲解effect后续的内容之前,我们必须先了解一个数据结构:链表,这是一个非常重要的东西,因为源码里面effect就是依靠链表结构来处理的;
当然我们这里只是简单说一下链表的结构,具体的大家可自行查看
1.链表的结构
链表的结构其实不算复杂,一张图就能看懂
这就是一个链表,就这么简单,当然,这是一个单向链表,因为只有一个指向下一节点的指针;
const node1 = {data: 10, // 数据域next: null // 指针域,初始指向null
};const node2 = {data: 20,next: null
};const node3 = {data: 30,next: null
};// 构建链表关系(节点1 → 节点2 → 节点3)
node1.next = node2;
node2.next = node3;// 此时链表结构为:
// node1 → node2 → node3 → null
用js来表示,就是如上所示;
当然看到单向链表那就有双向链表(vue源码是用的是双向链表),那我们再画张图演示一下
这就是双向链表,我们用js来表示一下
const node1 = {data: 10,prev: null, // 前向指针(节点1是头节点,无前驱)next: null // 后向指针
};const node2 = {data: 20,prev: null,next: null
};const node3 = {data: 30,prev: null,next: null
};// 建立双向连接
node1.next = node2; // 节点1的next指向节点2
node2.prev = node1; // 节点2的prev指向节点1
node2.next = node3; // 节点2的next指向节点3
node3.prev = node2; // 节点3的prev指向节点2
以上就是链表的简单概述,具体请自行查阅资料
二、将链表使用到effect中
1.问题
上一篇文章我们编写了一个最简单的effect响应式,细心的小伙伴就会发现有问题
<script type="module">// import {// effect,// ref,// } from "./node_modules/vue/dist/vue.esm-browser.prod.js";import { effect, ref } from "./reactive/effect1.js";const name = ref("vue");effect(() => {const time = new Date().getTime();console.log(time);console.log(name.value);});effect(() => {const time = new Date().getTime();console.log(time);console.log(name.value);});setTimeout(() => {name.value = "react";}, 1000);
如果我effect监听两次,那么修改之后effect有且只有第二个effect执行了,为什么呢;
class RefImpl {_v_isRef = true;subs;constructor(value) {this.value = value;}get value() {// 收集依赖// 第二次get之后subs变了if (activeSub) {this.subs = activeSub;}return this._value;}set value(newVal) {// 触发更新this._value = newVal;console.log(123);this.subs?.();}
}
function ref(val) {return new RefImpl(val);
}
let activeSub = null;
function effect(fn) {activeSub = fn;activeSub();activeSub = null;
}
export { effect, ref };
因为effect执行第二次的时候activeSub变成了最后一次执行的effect,所以导致只执行了一次,那么如何解决这个问题呢
2.解决
在vue3前面的版本中,是用数组的方式去处理的,后面考虑到性能问题,就转成的链表形式;
有一篇文章详细的说明了性能问题(但是我忘了文章的地址了 QAQ,如果后面我找到了会补充的)
那现在我们开始改造之前的依赖收集和派发更新的代码
依赖收集
class RefImpl {_v_isRef = true;/*** 订阅者链表头节点*/subs;/*** 订阅者链表尾节点*/subsTail;constructor(value) {this.value = value;}get value() {if (activeSub) {trackRef(this);}return this._value;}set value(newVal) {// 触发更新this._value = newVal;// this.subs?.();triggerRef(this);}
}
我们在原来的ref的类中添加两个属性,subs和subsTail,代表的是订阅者头节点和尾节点,然后再get方法中执行依赖收集
以下是trackRef的代码
function trackRef(dep) {// 收集依赖if (activeSub) {const newLink = {sub: activeSub,nextSub: null,prevSub: null,};// this.subs = newLink;// 尾插法if (dep.subsTail) {dep.subsTail.nextSub = newLink;newLink.prevSub = dep.subsTail;dep.subsTail = newLink;} else {// 没有的话头尾一样dep.subs = newLink;dep.subsTail = newLink;}}
}
当开始收集依赖的时候,我们创建一个新的节点newLink,既然是双向链表那就肯定有上一个节点和下一个节点这两个参数,在这里分别对应nextSub和prevSub,当然还需要记录当前的订阅者(即effect的回调函数);
然后我们默认是用尾插法,即在最后面插入节点,此时我们判断当前的节点是否有尾节点,如果有我们需要进行双向链表的插入
即将该尾节点的下一个节点指向当前节点,将原本的尾节点指向该节点,将该节点的上一个节点指向原来的尾节点
听起来有点绕啊,我们画个图演示一下就明白了
这是默认的双向链表,此时我们需要在节点3中插入节点4,节点3是原本这个链表的尾节点,在这里就是dep.subsTail,我们最后要变成下面这种链表
那么按照图例所示,是不是需要将节点3(原dep.subsTail 尾节点)的next指向节点4,同时将节点4的prev指向节点3,对应的就是 dep.subsTail.nextSub = newLink;newLink.prevSub = dep.subsTail;
,最后节点4变成这个链表的尾节点,就是将原来的dep.subsTail 指向节点4 ,对应的是 dep.subsTail = newLink;
节点4在代码中就是newLink;
上面这种是如果有尾节点的情况,如果我没有呢,就是第一次收集;
此时头尾的节点都是指向同一个节点,即 dep.subs = newLink; dep.subsTail = newLink;
头尾相同
以上是依赖收集的更新
派发更新
派发更新就是遍历这个链表,分别执行链表的sub
function triggerRef(dep) {if (dep.subs) {propagate(dep.subs); //触发更新}
}
function propagate(subs) {// 依次执行let link = subs; //保存头节点let queuedEffects = []; //保存需要执行的effectwhile (link) {// 注意:这里不能直接执行effect,因为effect可能会再次触发set,导致死循环queuedEffects.push(link.sub); //将effect保存到数组中link = link.nextSub; //指向下一个节点}// 依次执行queuedEffects.forEach((effect) => {effect(); //执行effect});
}
这里需要注意的是,不能在遍历的过程中执行effect的回调函数,为什么呢,给个案例就知道了
function propagate(subs) {// 依次执行let link = subs; //保存头节点let queuedEffects = []; //保存需要执行的effectwhile (link) {// 注意:这里不能直接执行effect,因为effect可能会再次触发set,导致死循环link.sub();link = link.nextSub; //指向下一个节点}// 依次执行// queuedEffects.forEach((effect) => {// effect(); //执行effect// });
}
假设我们在遍历的时候执行effect回调
<script type="module">// import {// effect,// ref,// } from "./node_modules/vue/dist/vue.esm-browser.prod.js";import { effect, ref } from "./reactive/effect2.js";const name = ref("vue");effect(() => {const time = new Date().getTime();console.log(time);name.value = "react";console.log(name.value);});// effect(() => {// const time = new Date().getTime();// console.log(time);// console.log(name.value);// });setTimeout(() => {name.value = "react";}, 1000);
此时我们在effect中修改响应式的值,然后你就会看到
进入无限循环导致栈溢出,所以我们只能等到循环结束的时候才能执行
总结
以上就是链表运用到响应式中,当然,这个还只是很小且比较容易理解的一部分,后面的部分会很难理解;
希望大家结合源码多看几遍