Vue3中的响应式API是怎么实现的(ref篇、下)

313 阅读4分钟

在上一篇文章中,我们知道了ref作为原始值的响应式方案是怎么实现的,也就是getter和setter。然后我们还知道了什么时副作用函数,知道了响应式数据是通过在读取时收集相关联的副作用函数和设置时重新运行副作用函数来实现响应式效果。然后还实现了实现这两个效果的函数track和trigger。

在这一篇中,我们来说一下ref剩下的一些问题

如何区分ref包装的原始值对象和非原始值的响应式数据:

Vue3中非原始值API的响应式包装是通过reactive()这个API,它可以把对象类型的数据转换为响应式数据,关于他的原理我们暂且不表,现在知道怎么用就可以了:

const obj = reactive({ count: 0 })
//会触发依赖于obj.count的所有副作用函数重新执行
obj.count++

所以现在的问题是,如果我们使用reactive包装了一个key为value的对象,访问时也是通过访问value来得到数据:

const val1 = ref(1)
const val2 = reactive({ value: 1 })

此时这两个响应式数据val1和val2使用value的方式一模一样,那么怎么样才能区分出是用户包装的非原始值响应式对象还是ref包装的原始值响应式对象?

vue是这么解决的:

function ref(val) { 
   const refValue =  new RefImp(val) 
   //使用Object.defineProperty()在refValue上定义一个不可枚举的“_v_isRef”
   Object.defineProperty(refValue, '_v_isRef', {
      value: true
   })
   
   return refValue  
}

在这里我把上一篇中的ref函数拿了过来,我们上篇知道了ref是通过RefImp这个类来创建原始值的响应式包裹对象的,所以在返回对象前,使用Object.defineProperty在对象上定义了一个_v_isRef属性。Object.defineProperty这个API定义对象的属性的时候,如果没有显式的在属性描述符对象中将writableenumerableconfigurable设置为true的话,这三个属性就会被默认设置为false,也就是说我们得到的“_v_isRef” 是一个不可写,不可枚举,不可配置的属性。所以这就相当于在这个属性上做了一个永远不会被改变的标记,方便我们知道这是原始值的响应式包裹对象。

脱ref:

聪明的小伙伴一定都注意到过,在模板中使用ref数据时是不用在.value中访问的,直接使用ref的包装对象就可以,所以这是怎么做到的呢?

我们上文介绍了‘_v_ref’这个标识,脱ref就是通过它来实现的,假设我们使用的时vue3的setupAPI:

const Mycomponent = {
    setup() {
       const count = ref(0)
       // 返回包含ref的对象
       return { count }
    }  
}

在setupAPI中,setup的返回值会最终被传递给proxyRefs:

function proxyRef(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      // 如果是ref对象,那么直接设置value.value
      if (value._v_ref) {
        return value.value
      }
      return value
    },
    set(target, key, newValue, receiver) {
      const oldvalue = Reflect.get(target, key, receiver)
      // set亦然
      if (oldvalue._v_ref) {
        oldvalue.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

通过proxyRef这个函数,我们就能够对setup的返回值进行代理,这样我们在setup中如果定义了原始值的响应式refValue,那么就会被代理发现这个响应式值上有_v_ref这个标记,这样就可以绕过value这个getter,从而返回它真正的值。

讲到这里我们知道了,访问setup返回值的代理是可以实现脱ref的,但是问题在于模板中使用ref和我setup有什么关系?

这个问题也是一个宏大的话题,因为涉及到Vue的运行时在浏览器中的逻辑,但是我简单提一嘴:

虽然vue一般来说使用模板语法来书写视图逻辑,但是vue中的模板最终会被编译为返回虚拟dom的函数,就像这样:

function render(ctx) {
  return {
    type: 'div',
    props: {
      class: 'foo'
    },
    children: ctx.message
  }
}

就像这样,props中的class绑定了render的ctx中的message,每次运行render时ctx中的message不同,就实现了动态绑定。

相信你也猜到了,我们的setup代理对象就可以在ctx中访问到,因为虚拟DOM中的上下文就是这个ctx,而模板生成虚拟dom,所以这个上下文也就是模板的上下文。而虚拟dom负责挂载为真实dom,所以他也被称为渲染上下文。

说了几句题外话,不过到这里就是ref篇的结束了。接下来会介绍非原始值的响应式原理,敬请期待。