Vue3 响应式原理:JavaScript 的程序性
什么是 js 的程序性
js 的程序性指的是:一套固定的,不会发生变化的执行流程。
代码从上往下执行,每一行只在被执行到的那一刻起作用。const total = product.price * product.quantity 这一行做的事情是:读出两个属性此刻的值,算出结果,把这个结果赋给 total。赋值完成之后,total 和 product 之间就再没有任何关系了,它只是一个存着数字 20 的变量,不是一条「等于价格乘数量」的规则。
这一点是理解响应式系统的起点。我们在数学里写 total = price × quantity 表达的是一个恒成立的等式,等号右边变了左边自然跟着变;但在 js 里等号是赋值,表达的是「把右边此刻算出来的值搬到左边」,是一个一次性的动作。程序不会记住这次计算是怎么来的,也不会在 product 变化时回头重算。
所以要让 total 跟着变,只有两条路:要么每次读 total 之前手动重算一遍,要么想办法让「product 被修改」这件事能自动触发重算。前者就是我们平时手写的那种命令式代码,麻烦但直观;后者就是 Vue 响应式系统要做的事,它需要解决两个问题——如何知道 total 的计算依赖了 product 的哪些属性,以及如何在这些属性被改写时收到通知。
示例
如下代码我们想得到 total 的值,按照我们的想法是 product 中的值发生变化了,total 的值也需要一起发生变化, 但是根据下面的示例,我们发现 total 的值在定义之后,无论 product 中的值如何变化,total 的值都不会改变,这就是 js 的程序性
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
let product = {
price: 10,
quantity: 2,
};
const total = product.price * product.quantity;
console.log('总价格:' + total);
product.quantity = 5;
console.log('总价格:' + total);
</script>
</body>
</html>
两次打印都是 总价格:20。这里和 const 没有关系,换成 let 结果也一样——不是变量不允许改,而是根本没有任何代码去改它。product.quantity = 5 只改动了对象上的属性,它不认识 total,也不知道有谁曾经读过 quantity。
这个问题的两个缺口
把上面的现象拆开看,缺的是两样东西。
第一样是「知道谁依赖了谁」。计算 total 时读取了 product.price 和 product.quantity,这个读取动作发生在表达式求值的一瞬间,之后不留痕迹。如果能在读取属性时把「当前正在执行的这段计算」记下来,就建立起了属性到计算的映射关系。这就是依赖收集(track)。
第二样是「改动时能发出通知」。给对象属性赋值是语言内置的行为,默认不给我们插手的机会。要想在赋值时执行自己的逻辑,就得拦截这个动作。ES5 的 Object.defineProperty 能给指定属性装上 getter 和 setter,ES6 的 Proxy 能代理整个对象的读写。有了拦截点,就能在 setter 里把之前收集到的计算重新跑一遍。这就是触发依赖(trigger)。
把这两件事凑齐,写法上就从「算一次存一个结果」变成了「登记一条计算规则,数据变了就重跑」。Vue 3 的 reactive 负责拦截读写,effect 负责承载那段需要重跑的计算,两者通过一张依赖表连起来。下一节我们就用 Object.defineProperty 和 Proxy 分别把这个自动触发的雏形实现出来,顺便看看为什么 Vue 3 最终选了 Proxy。

还没有留言,来发表第一条吧~