ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Vue 3 深入响应式原理 - 聊一聊响应式构建的那些经历

2021-11-24 14:34:04  阅读:139  来源: 互联网

标签:Vue 运算 depsMap dep 响应 聊一聊 let key target


图片描述

vue 的响应性

当我第一次使用 vue 进行项目开发时,响应式数据渲染 是让我感到最惊奇的一个功能,我们来看下面这段代码:

<body>
  <div id="app">
    <div>
      修改商品的数量: <input type="number" v-model="product.quantity">
    </div>
    <div>
      修改商品的价格: <input type="number" v-model="product.price">
    </div>

    <p>
      总价格:{{ total }}
    </p>
  </div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script>
  const component = {
    data() {
      return {
        // 定义一个商品对象,包含价格和数量
        product: {
          price: 10,
          quantity: 2
        },
      }
    },
    computed: {
      // 计算总价格
      total() {
        return this.product.price * this.product.quantity
      }
    }
  }

  const app = Vue.createApp(component)
  app.mount('#app')
</script>

这是一段标准的 vue3 的代码,当你在输入框中输入内容的时候,totla 永远会被重新计算,就像下面这样。

图片描述

这样的功能我们称它为 响应式

响应式的数据渲染 是现在前端非常重要的机制。但是这种机制它究竟是 怎么被一步一步的构建出来的呢? 这就是这篇博客想要说的内容。

如果你想要了解 vue3的 响应系统 及 构建历程 ,那么你就应该看下去。

js 的程序性

想要了解 响应性,那么你需要先了解 程序性。我们来看下面这段普通的 js代码:

  // 定义一个商品对象,包含价格和数量
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = product.price * product.quantity;
  // 第一次打印
  console.log(`总价格:${total}`); 
  // 修改了商品的数量
  product.quantity = 5;
  // 第二次打印
  console.log(`总价格:${total}`); 

想一下,上面的代码第一次应该打印什么内容?第二次应该打印什么内容?

恭喜你!答对了,因为它们只是普通的 js 代码,所以两次的打印结果应该都是:

总价格:20

但是你有没有想过,当我们去进行第二次打印的时候,你真的希望它还是 20 吗?

你有没有过冒出来这么一个想法:商品数量发生变化了,如果总价格能够自己跟随变化,那就太好了! 这是 人性,从  的角度考虑,确实应该这个样子。但是 程序 并不会如此 ”智能“。 那么怎么能够让程序变得更加 “聪明” 呢?

这个 让程序变 ”聪明“ 的过程,就是响应式构建的过程

你希望:当数据变化时,重新执行运算

你为了让你的程序变得更加 “聪明” , 所以你开始想:”如果数据变化了,重新执行运算就好了“。

想到就去做,为了达到这个目的,你开始对运算函数进行了封装。

你定义了一个匿名函数 effect,用来计算 商品总价格。并且 当打印总价格前,让 effect 执行 。所以你得到了下面的代码:

  // 定义一个商品对象,包含价格和数量
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = 0;
  // 计算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
  };
  // 第一次打印
  effect();
  console.log(`总价格:${total}`); // 总价格:20
  // 修改了商品的数量
  product.quantity = 5;
  // 第二次打印
  effect();
  console.log(`总价格:${total}`); // 总价格:50

在这样的一个代码中,你得到了一个想要的结果:数据变化了,运算也重新执行了

但是,你很快发现了一个新的问题:这样的代码只能维护单一的总价格运算 。 你希望让它可以支持更多的运算,那怎么办呢?

你希望:当数据变化时,重新执行多个运算

你的代码只支持单一运算,你希望让它支持更多。

为了达到这个目的,你开始对代码进行了简单的封装,你做了以下三件事情:

  1. 创建 Set 数组([点击了解 Set]) ,用来存放多个运算函数
  2. 创建 track 函数,用来向 Set 中存放运算函数
  3. 创建 trigger 函数,用来执行所有的运算函数

这样,你得到了下面的代码,并且把这样的一套代码称之为 响应式

  // -------------创建响应式-------------
  // set 数组,用作保存所有的运算函数
  let deps = new Set();
  // 保存运算函数
  function track() {
    deps.add(effect);
  }
  // 触发器,执行所有的运算函数
  function trigger() {
    deps.forEach((effect) => effect());
  }

  // -------------创建数据源-------------
  // 声明商品对象,为数据源
  let product = {
    price: 10,
    quantity: 2
  };
  // 声明总价格
  let total = 0;
  // 运算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
  };

  // -------------执行响应式-------------
  // 保存运算函数
  track();
  // 运算 总价格
  effect();
  console.log(`总价格:${total}`); // 总价格:20
	
  // 修改数据源
  product.quantity = 5;
  // 数据源被修改,执行触发器,重新运算所有的 total
  trigger();
  console.log(`总价格:${total}`); // 总价格:50

你对你的 创造 非常骄傲,并且开始把它推荐给周边的朋友进行使用。但是很快,就有人提出了问题:我 希望把响应式作用到对象的具体属性中 ,而不是 一个属性改变,全部计算重新执行

你希望:使每个属性具备单独的响应性

响应性绑定对象,导致 一个属性改变,全部计算重新执行。所以你希望把响应式作用到对象的具体属性中,只 重新运算该属性相关的内容

为了实现这个功能,你需要借助 [Map 对象]。

Map 以 key:val 的形式存储数据,你希望以 属性为 key,以该属性相关的运算方法集合为 val。以此你构建了一个 depsMap 对象,用来达到你的目的:

// -------------创建响应式-------------
  // Key:Val 结构的集合
  let depsMap = new Map();
  // 为每个属性单独保存运算函数,从而让每个属性具备自己独立的响应式
  function track(key, eff) {
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(eff)
  }
  // 触发器,执行指定属性的运算函数
  function trigger(key) {
    // 获取指定函数的 dep 数组
    const dep = depsMap.get(key);
    // 遍历 dep,执行指定函数的运算函数
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // -------------创建数据源-------------
  // 声明商品对象,为数据源
  let product = {
    price: 10,
    quantity: 2
  };
  // 声明总价格
  let total = 0;
  // 运算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
  };

  // -------------执行响应式-------------
  // 保存运算函数
  track('quantity', effect);
  // 运算 总价格
  effect();
  console.log(`总价格:${total}`); // 总价格:20

  // 修改数据源
  product.quantity = 5;
  // quantity 被修改,仅仅触发 quantity 的响应式
  trigger('quantity');
  console.log(`总价格:${total}`); // 总价格:50
</script>

你的客户总是非常挑剔的,很快他们抛出了新的问题:我的程序不可能只有一个对象!你需要让所有的对象都具备响应式!

你希望:使不同对象的不同属性具备单独的响应性

你的响应式需要覆盖程序中的所有对象,否则你的代码将毫无意义!

为了达到这个目的,你需要将 对象、属性、运算方法 进行分别的缓存,现有的 depsMap 已经没有办法满足你了。你需要更加强大的 Map ,让 每个对象 都有一个 Map 。它就是 WeakMap。

WeakMap 对象是一组键/值对的集合。其键必须是对象,而值可以是任意的。

借助 WeakMap 你让每个对象都拥有了一个 depsMap

  // -------------创建响应式-------------
  // weakMap:key 必须为对象,val 可以为任意值
  const targetMap = new WeakMap()
  // 为不同对象的每个属性单独保存运算函数,从而让不同对象的每个属性具备自己独立的响应式
  function track(target, key, eff) {
    // 获取对象所对应的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取 depsMap 对应的属性
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    // 保存不同对象,不同属性的 运算函数
    dep.add(eff)
  }
  // 触发器,执行指定对象的指定属性的运算函数
  function trigger(target, key) {
    // 获取对象所对应的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 获取指定函数的 dep 数组
    const dep = depsMap.get(key);
    // 遍历 dep,执行指定函数的运算函数
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // -------------创建数据源-------------
  // 声明商品对象,为数据源
  let product = {
    price: 10,
    quantity: 2
  };
  // 声明总价格
  let total = 0;
  // 运算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
  };


  // -------------执行响应式-------------
  // 保存运算函数
  track(product, 'quantity', effect);
  // 运算 总价格
  effect();
  console.log(`总价格:${total}`); // 总价格:20

  // 修改数据源
  product.quantity = 5;
  // quantity 被修改,仅仅触发 quantity 的响应式
  trigger(product, 'quantity');
  console.log(`总价格:${total}`); // 总价格:50

每次数据改变,我都需要重新执行 trigger , 这样太麻烦了!万一我忘了怎么办? 。客户总是会提出一些 改(wu)进(li) 的要求,没办法,谁让人家是客户呢?

你希望:使不同对象的不同属性具备自动的响应性

每次数据改变,我都需要重新执行 trigger ,你的客户发出了这样的抱怨。

如果想要达到这样的目的,那么你需要了解 “数据的行为” , 即:你需要知道,数据在什么时候被赋值,在什么时候被输出

此时你需要借助两个新的对象:

  • [Proxy]
  • [Reflect]

借助 Proxy + Reflect 你成功实现了对数据的监听:

  // -------------创建响应式-------------
  // weakMap:key 必须为对象,val 可以为任意值
  const targetMap = new WeakMap()
  // 为不同对象的每个属性单独保存运算函数,从而让不同对象的每个属性具备自己独立的响应式
  function track(target, key, eff) {
    // 获取对象所对应的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取 depsMap 对应的属性
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    // 保存不同对象,不同属性的 运算函数
    dep.add(eff)
  }
  // 触发器,执行指定对象的指定属性的运算函数
  function trigger(target, key) {
    // 获取对象所对应的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 获取指定函数的 dep 数组
    const dep = depsMap.get(key);
    // 遍历 dep,执行指定函数的运算函数
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // 使用 proxy 代理数据源,以达到监听的目的
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key, effect)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
  }

  // -------------创建数据源-------------

  // 声明商品对象,为数据源
  let product = reactive({ price: 10, quantity: 2 })
  // 声明总价格
  let total = 0;
  // 运算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
  };


  // -------------执行响应式-------------
  effect()
  console.log(`总价格:${total}`); // 总价格:20
  // 修改数据源
  product.quantity = 5;
  console.log(`总价格:${total}`); // 总价格:50

你心满意足,觉得你的代码无懈可击。突然耳边响起客户 赏(bu)心(he)悦(shi)目(yi) 的声音:你不觉得每次执行 effect 很反人类吗?

你希望:让运算自动执行

自动化!自动化!所有的操作都应该自动运行!

为了可以让运算自动运行,你专门设计了一个 effect 函数,它可以 接收运算函数,并自动执行

  // -------------创建响应式-------------
  // weakMap:key 必须为对象,val 可以为任意值
  const targetMap = new WeakMap()
  // 运算函数的对象
  let activeEffect = null;
  // 为不同对象的每个属性单独保存运算函数,从而让不同对象的每个属性具备自己独立的响应式
  function track(target, key) {
    if (activeEffect) {
      // 获取对象所对应的 depsMap
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      // 获取 depsMap 对应的属性
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      // 保存不同对象,不同属性的 运算函数
      dep.add(activeEffect)
    }

  }
  // 触发器,执行指定对象的指定属性的运算函数
  function trigger(target, key) {
    // 获取对象所对应的 depsMap
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      return
    }
    // 获取指定函数的 dep 数组
    const dep = depsMap.get(key);
    // 遍历 dep,执行指定函数的运算函数
    if (dep) {
      dep.forEach((eff) => eff());
    }
  }

  // 使用 proxy 代理数据源,以达到监听的目的
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
  }

  // 接收运算函数,执行运算函数
  function effect(eff) {
    activeEffect = eff;
    activeEffect();
    activeEffect = null;
  }

  // -------------创建数据源-------------
  // 声明商品对象,为数据源
  let product = reactive({ price: 10, quantity: 2 })
  // 声明总价格
  let total = 0;
  // 通过 effect 运算总价格
  effect(() => {
    total = product.price * product.quantity;
  })


  // -------------执行响应式-------------
  console.log(`总价格:${total}`); // 总价格:20
  // 修改数据源
  product.quantity = 5;
  console.log(`总价格:${total}`); // 总价格:50

总结

vue 的响应性让人惊奇,我们希望了解它,更希望知道它的发展历程。

我们从 JS 的程序性 开始,站在 人性 开始思考,程序应该是什么样子的?

我们经历了 6 个大的阶段,最终得到了我们想要的 响应式 系统,而这个也正是 vue3 的响应式在构建时,所经历的 ”过程“。


作者:Sunday
链接:https://www.imooc.com/article/320186
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

标签:Vue,运算,depsMap,dep,响应,聊一聊,let,key,target
来源: https://blog.csdn.net/qq_27009517/article/details/121514654

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有