Vue-js设计与实现-原始值的响应式方案-ref
原始值指的是 Boolean、Number、 String、Symbol、undefined 和 null 等类型的值。Proxy无法直接对原始值进行代理,因此想要将原始值变成响应式数据,就必须对其做一层包裹,也就是使用ref。
ref的引入
由于Proxy 的代理目标必须是非原始值,所以我们没有任何手段拦截对原始值的操作:
1 | let a = 'hello' |
为了解决此问题,我们的唯一办法是,采用一个非原始值去包裹这个原始值,就比如使用一个对象包裹此原始值后传给reactive。但直接这样做既不方便(需要手动包裹对象),也不规范(对应原始值的属性可以随意命名)。
为了规范这类原始值的使用,我们手动封装一个叫做ref的函数,实现以上功能:
1 | function ref(value){ |
我们将原始值封装岛wrapper对象,再将它传给reactive,就可以实现基本的响应了。
但这样的话,我们应该如何区分refVal 到底是原始值包裹对象,还是非原始值响应式数据呢?目前来看没有任何区别。但为了后续ref的完善我们还是需要对其进行区分。
我们可以为这个wrapper对象定义一个特殊的属性__v_isRef
用来标注其为ref:
1 | function ref(value){ |
响应丢失 toRef
ref除了实现原始值的响应式数据,还用来解决响应丢失的问题。
我们来看以下的例子:
1 | <script> |
我们从模板里访问从setup中暴露的数据,但这么做会导致响应丢失,setTimeout
并不会触发重新渲染。
这里的问题在于,我们使用了展开运算符...
。这里的return {...obj}
实际上等价于return{name:'hello',age:'18'}
,返回的其实是一个普通对象,没有响应能力,也就不会触发副作用函数执行。
我们假设这个普通对象为newObj
,那么,有没有办法能够帮助我们实现:在副作用函数内,即使通过普通对象 newObj
来访问属性值,也能够建立响应联系?其实是可以的。让我们重写这个普通对象:
1 | const obj = reactive({foo:1,bar:2}) |
该对象有一个访问器属性 value,当读取 value的值时,最终读 取的是响应式数据 obj 下的同名属性值。也就是说,当在副作用函数内读取 newObj.foo 时,等价于间接读取了 obj.foo 的值。这样响 应式数据自然能够与副作用函数建立响应联系。于是,当我们尝试修改 obj.foo 的值时,能够触发副作用函数重新执行。
我们可以看到这里属性的结构相同,也和ref的结构相似。我们可以把这种结构封装成函数:
1 | function toRef(obj,key){ |
toRef 函数接收两个参数,第一个参数 obj 是响应式对象, 第二个参数是该对象的一个键。该函数返回一个类似于 ref 结构的 wrapper 对象,通过getter访问属性,setter修改属性。并且通过Object.defineProperty
标记为真正的ref
。
我们再封装一个toRefs函数,用于响应式对象属性的批量转换:
1 | function toRefs(obj){ |
调用后可以实现响应:
1 | const obj = reactive({ foo: 1, bar: 2}) |
自动脱ref
我们通过toRefs把响应式数据的第一层属性值成功转换为ref,因此必须通过 value 属性访问值。这样在template
模板中访问数据时也需要访问value属性,很麻烦。
因此我们需要自动脱 ref 的能力。所谓自动脱 ref,指的是属性的访问行为,即如果读取的属性是一个 ref,则直接将该 ref 对应 的 value 属性值返回:newObj.foo
可以直接得到值,而不是newObj.foo.value
。
我们可以结合之前设置的 __v_isRef
属性,针对ref进行代理:
1 | function proxyRefs(target){ |
在上面这段代码中,我们定义了 proxyRefs 函数,该函数接收一个对象作为参数,并返回该对象的代理对象。拦截 get 操作,读取属性是一个 ref 时,直接返回该 ref 的 value ,这样就实现了自动脱 ref。同样地,拦截set操作,如果设置的属性是一个 ref,则间接设置该 ref 的 value 属性的值。
实际上,我们在编写 Vue.js 组件时,组件中的 setup 函数所返回的数据会传递给 proxyRefs函数进行处理,实现在模板直接访问一个 ref 的值:
1 | const MyComponent = { |