链表运用到响应式中

vue响应式实现原理

第一章 vue 简单的effect实现


前言

上一章我们实现了最简单的effect响应式,当时是有很多地方需要改进的,后面的都是在此基础上进行完善

一、链表

在讲解effect后续的内容之前,我们必须先了解一个数据结构:链表 ,这是一个非常重要的东西,因为源码里面effect就是依靠链表结构来处理的;

当然我们这里只是简单说一下链表的结构,具体的大家可自行查看

1.链表的结构

链表的结构其实不算复杂,一张图就能看懂

这就是一个链表,就这么简单,当然,这是一个单向链表,因为只有一个指向下一节点的指针;

javascript 复制代码
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来表示一下

javascript 复制代码
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响应式,细心的小伙伴就会发现有问题

javascript 复制代码
 <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执行了,为什么呢;

javascript 复制代码
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,如果后面我找到了会补充的)

那现在我们开始改造之前的依赖收集和派发更新的代码

依赖收集

javascript 复制代码
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的代码

javascript 复制代码
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

javascript 复制代码
function triggerRef(dep) {
  if (dep.subs) {
    propagate(dep.subs); //触发更新
  }
}
function propagate(subs) {
  //  依次执行
  let link = subs; //保存头节点
  let queuedEffects = []; //保存需要执行的effect
  while (link) {
    // 注意:这里不能直接执行effect,因为effect可能会再次触发set,导致死循环
    queuedEffects.push(link.sub); //将effect保存到数组中
    link = link.nextSub; //指向下一个节点
  }
  // 依次执行
  queuedEffects.forEach((effect) => {
    effect(); //执行effect
  });
}

这里需要注意的是,不能在遍历的过程中执行effect的回调函数,为什么呢,给个案例就知道了

javascript 复制代码
function propagate(subs) {
  //  依次执行
  let link = subs; //保存头节点
  let queuedEffects = []; //保存需要执行的effect
  while (link) {
    // 注意:这里不能直接执行effect,因为effect可能会再次触发set,导致死循环
  
    link.sub();
    link = link.nextSub; //指向下一个节点
  }
  // 依次执行
  // queuedEffects.forEach((effect) => {
  //   effect(); //执行effect
  // });
}

假设我们在遍历的时候执行effect回调

javascript 复制代码
  <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中修改响应式的值,然后你就会看到

进入无限循环导致栈溢出,所以我们只能等到循环结束的时候才能执行

总结

以上就是链表运用到响应式中,当然,这个还只是很小且比较容易理解的一部分,后面的部分会很难理解;

希望大家结合源码多看几遍

相关推荐
前端农民工ws3 小时前
Vue 框架的 markdown 渲染组件,针对 AI 的 markdown 流式传输场景
前端·javascript·vue.js·ai
百思可瑞教育3 小时前
Vue 生命周期详解:从初始化到销毁的全过程剖析
前端·javascript·vue.js·前端框架·uni-app·北京百思可瑞教育·百思可瑞教育
靠近彗星3 小时前
2.3单链表
数据结构
徐子童4 小时前
优选算法---链表
数据结构·算法·链表·面试题
CYH&JK4 小时前
数据结构---链式队列
数据结构
星语卿5 小时前
Vuetify:构建优雅Vue应用的Material Design组件库
前端·javascript·vue.js
wangbing11255 小时前
界面规范11-对话框
javascript·vue.js·elementui
shan&cen5 小时前
Day02 集合 | 30. 串联所有单词的子串、146. LRU 缓存、811. 子域名访问计数
java·数据结构·算法·缓存