当我第一次使用 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
代码:
// 定义一个商品对象,包含价格和数量 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
在这样的一个代码中,你得到了一个想要的结果:数据变化了,运算也重新执行了
但是,你很快发现了一个新的问题:这样的代码只能维护单一的总价格运算 。 你希望让它可以支持更多的运算,那怎么办呢?
你的代码只支持单一运算,你希望让它支持更多。
为了达到这个目的,你开始对代码进行了简单的封装,你做了以下三件事情:
Set
数组([点击了解 Set]) ,用来存放多个运算函数track
函数,用来向 Set
中存放运算函数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
你成功实现了对数据的监听:
// -------------创建响应式------------- // 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
的响应式在构建时,所经历的 ”过程“。