《Vue.js》阅读之响应式数据与副作用函数

发布于:2025-05-13 ⋅ 阅读:(9) ⋅ 点赞:(0)

Vue.js

《Vue.js设计与实现》(霍春阳)

  • 适合:从零手写Vue3响应式系统,大厂面试源码题直接覆盖。
  • 重点章节:第4章(响应式)、第5章(渲染器)、第8章(编译器)

因为我第一周的任务就是响应式原理(Proxy vs defineProperty),手写简易reactive,所以我是从第四章开始学习的。

4.1 响应式数据与副作用函数

1、副作用函数

函数的执行会直接或间接影响其他函数的执行

比如:

01 function effect() {
02   document.body.innerText = 'hello vue3'
03 }

它的执行会使整个body的值都为hello vue3

2、响应式数据

01 const obj = { text: 'hello world' }
02 function effect() {
03   // effect 函数的执行会读取 obj.text
04   document.body.innerText = obj.text
05 }

如上面的这一段代码,我们希望当text发生变化的时候,effect会自动执行,这就是所谓的响应式数据

下面我们将会讲到这是怎么实现的:

● 当副作用函数 effect 执行时,会触发字段obj.text 的读取操作;
● 当修改 obj.text 的值时,会触发字段 obj.text的设置操作。
实现一个响应式的数据:拦截读和写两个步骤,
读的时候把effect函数放在一个容器里面
修改的时候就把这个effect函数释放出来
拦截一个对象的读取和设置操作:Proxy

  const data = { text: 'hello' }; // 定义数据对象
  const bucket = new Set(); // 设置一个容器用于存储副作用函数

  const obj = new Proxy(data, {
    // 读的时候拦截
    get(target, key) {
        bucket.add(effect); // 将当前活跃的副作用函数添加到容器
      return target[key]; // 返回属性值
    },
    // 拦截设置操作
    set(target, key, newVal) {
      target[key] = newVal;
      bucket.forEach(fn => fn()); // 执行所有存储的副作用函数
      return true;
    }
  });

  const  effect = () => {
    document.body.innerText = obj.text; // 副作用函数依赖于响应式数据
  };
  effect();
  setTimeout(() => {
    obj.text = "world"; // 修改数据,触发响应式更新
  }, 1000); // 延迟1秒修改数据

继续强化->我们现在硬编码了effect,但是如果副作用函数的名字不叫effect的话,这段代码就无法继续工作了

所以我们要提供一个用来注册副作用函数的机制

  // 用一个全局变量存储被注册的副作用函数
  let activeEffect;
  function effect (fn) {
    activeEffect = fn ; 
    fn();
  }
  const bucket = new Set(); //设置一个容器
  const data = { text: 'world' }; // 初始化数据对象
  const obj = new Proxy(data , {
    //读的时候拦截放在容器里面
    get( tartget, key ){
      if(activeEffect){
        bucket.add(activeEffect);
      }
      return tartget[key];//返回属性值
    },
    set(target , key , newVal){
      // 拦截设置操作
      target[key] = newVal;
      bucket.forEach(fn => fn());
      return true;
    }
  })
  effect(() => {
    console.log('run');
    document.body.innerText = obj.text;
  })
  setTimeout(() => {
    obj.text = "hello"
  } , 1000)

当我们为obj添加新的属性的时候

  setTimeout(() => {
    obj.notExist = "hello vue3"
  } , 1000)

匿名副作用函数内没有读取这个新的属性的值,那么在1s之后不会起到写操作(即放出桶里面的所有函数),但是我尝试了一下,发现它执行了的。

我们为了解决这个问题,就要重新设计“桶”这个数据结构:让它无论读取的哪一个属性都会将副作用函数收到桶里面,设置属性的时候,无论设置的是哪一个属性,也都会将副作用函数取出并执行。

  let activeEffect;
  function effect (fn) {
    activeEffect = fn; 
    fn();
  }
  const bucket = new WeakMap();
  const data = { text: 'world' }; // 确保所有属性都已定义
  const obj = new Proxy(data, {
    get(target, key){
      if(!activeEffect){
        return target[key];
      }
      // 根据tartget取来的depsMap,它是一个map类型
      let depsMap = bucket.get(target);
      // 如果不存在
      if(!depsMap){
        // 创建一个
        bucket.set(target, (depsMap = new Map()));
      }
      // 根据key取来的deps,它是一个set类型
      let deps = depsMap.get(key);
      // 如果不存在
      if(!deps){
        // 创建一个
        depsMap.set(key, (deps = new Set()));
      }
      deps.add(activeEffect); // 添加当前活跃的副作用函数
      return target[key];
    },
    set(target, key, newVal){
      target[key] = newVal;
      const depsMap = bucket.get(target);
      if(!depsMap){
        return;
      }
      const effects = depsMap.get(key);
      effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
    }
  });
  effect(() => {
    console.log('run');
    document.body.innerText = obj.text;
  });
  setTimeout(() => {
    obj.text = "hello vue3"; // 修改已定义的属性以触发依赖
  }, 1000);

大家可以发现,我们引用了weakMap(它与map最大的不同就是它对key是弱引用,不影响垃圾回收器的工作,通常存储只有当key所引用的对象存在时,才有价值的信息);

我们要解决的是属性不存在时候的问题,那么

  • 在读的时候判断是否有这个属性,没有就创建一个,有的话就把函数放在桶里面。
  • 在修改的时候也是要判断

最后,我们将一些函数进行封装:

<script setup>
  let activeEffect;
  function effect (fn) {
    activeEffect = fn; 
    fn();
  }
  const bucket = new WeakMap();
  const data = { text: 'world' }; // 确保所有属性都已定义
  const obj = new Proxy(data, {
    get(target, key){
      track(target , key);
      return target[key];
    },
    set(target, key, newVal){
      target[key] = newVal;
      trigger(target , key , newVal);
    }
  });
  // 追踪变化
  function track(target , key){
    if(!activeEffect){
        return target[key];
      }
      // 根据tartget取来的depsMap,它是一个map类型
      let depsMap = bucket.get(target);
      // 如果不存在
      if(!depsMap){
        // 创建一个
        bucket.set(target, (depsMap = new Map()));
      }
      // 根据key取来的deps,它是一个set类型
      let deps = depsMap.get(key);
      // 如果不存在
      if(!deps){
        // 创建一个
        depsMap.set(key, (deps = new Set()));
      }
      deps.add(activeEffect); // 添加当前活跃的副作用函数
  }
  // 触发变化
  function trigger(target , key , newVal){
     const depsMap = bucket.get(target);
      if(!depsMap){
        return;
      }
      const effects = depsMap.get(key);
      effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
    
  }
  effect(() => {
    console.log('run');
    document.body.innerText = obj.text;
  });
  setTimeout(() => {
    obj.text = "hello vue3"; // 修改已定义的属性以触发依赖
  }, 1000);
</script>

网站公告

今日签到

点亮在社区的每一天
去签到