Vue 3 响应式原理初探——从零开始实现响应式 API
响应式是指变量的值发生改变时,用到这个变量的其他变量的值会被重新计算。
在下面的例子中,sum
是 num1
和 num2
的和。无论哪个加数被修改,都要重新计算 sum
。
1 | let num1 = 2 |
要在一个变量发生变化时,重新计算另一个变量的值,就必须知道哪些计算过程用到了这个变量,以及具体的计算过程,以便在变量发生变化时重新执行这些计算。
具体的计算过程可以用一个函数来表示。但是要知道变量被哪些函数所使用,就必须在变量的值被读取时,记住当前正在读取该变量的函数。可惜,JavaScript 并没有提供一种可以满足上述需求的机制。
访问器属性
庆幸的是,如果把变量换成对象的属性的话,情况就大不相同了。利用对象的 访问器属性 就可以实现上述需求。这就是为什么在 Vue 3 中,原始类型必须用 ref()
方法包装成对象类型。
访问器是对象的特殊方法,用关键字 get
或 set
定义而不是 function
。
1 | const obj = { |
get value()
方法是读取对象的 value
属性(实际上不存在)时会被调用的函数,称为 getter,它的返回值将被当作该属性的值。
1 | console.log(obj.value) // Hello World! |
set value()
方法是设置对象的 value
属性时会被调用的函数,称为 setter,它的参数是希望赋予该属性的值。
1 | obj.value = 'JavaScript' |
实现 ref()
函数
为了把前面的例子中提到的变量包装成对象,需要定义一个包装函数:
1 | function ref(val) { |
此处的 ref()
方法还不完善,它仅仅创建一个对象,并用 _value
属性来保存实际值,没有额外功能。
1 | const num1 = ref(2) |
在 getter
中追踪
利用 getter 很容易在属性的值被读取时执行一些操作,比如记住正在读取该属性的函数。为了做到这一点,必须要求每个函数在读取属性前,将自身的引用赋予一个全局变量。
1 | let activeEffect = null |
这样,当 getter 被调用时,就可以通过 activeEffect
得知当前是哪个函数正在读取属性。为了记住这个函数以及它正在操作的对象,定义一个追踪函数 track(obj)
,在 getter 中调用:
1 | let activeEffect = null |
追踪函数用一个对象来保存相关信息并推送到数组 effects
中(推送之前先检查是否已存在重复元素,不存在才推送)。
在 setter
中触发
在属性的值被读取时,已经将相关函数都记录下来。这些函数可能用该属性修改了一些变量。如果现在这个属性的值变了,自然就要重新调用这些函数,以更新相关变量的值。
定义一个触发函数 trigger(obj)
,在 setter 中调用:
1 | function trigger(obj) { |
至此,一组极其简陋的响应式 API 已经基本实现。
实现 watchEffect()
函数
要求每个函数在读取属性前,将自身的引用赋予一个全局变量,显然太繁琐。可以增加一个侦听函数负责此动作:
1 | function watchEffect(fn) { |
watchEffect()
在调用目标函数 fn()
之前,先将目标函数 fn()
包裹在箭头函数 effect()
中。这个箭头函数在调用目标函数之前,会被赋予全局变量 activeEffect
。
完整代码
响应式 API 的完整代码如下:
1 | let activeEffect = null |
测试
定义 3 个响应式变量,用 watchEffect()
调用求和函数:
1 | const num1 = ref(2) |
现在,修改其中一个加数的值:
1 | num1.value = 4 |
可以看到,sum
的值已经被重新计算。