Vue3.0 的 reactive API 定义和源码实现

引言

今年,对于从事前端开发的同学而言,很是期待的一件事就是 Vue3.0的发布。但是,Vue3.0离发布还是有点时间的,并且正式发布也不代表我们就马上就可以用于业务开发。它还需要完善相应的生态工具。不过正式使用是一码事,我们自己玩又是一码事(hh)。

Vue3.0特地准备了一个尝鲜版的项目供大家体验 Vue3.0即将会出现的一些 API,例如 setupreactivetoRefsreadonly等等, 顺带附上Composition API文档 的地址,还没看过的同学赶紧去 Get,别等到发布才知道(笨鸟要先飞,聪明鸟那更要先飞是吧)。

同样地,我也 Clone了下来玩了一会,对这个 reactive API颇感兴趣。所以,今天我们就来看看 reactive API是什么(定义)怎么实现的(源码实现)?

一、定义及优点

1.1 定义

reactive API的定义为传入一个对象并返回一个基于原对象的响应式代理,即返回一个 Proxy,相当于 Vue2x版本中的 Vue.observer

首先,我们需要知道在 Vue3.0中彻底废掉了原先的 Options API,而改用 Composition API,简易版的 Composition API看起来会是这样的:

  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }

可以看到,没有了我们熟悉的datacomputedmethods等等。看起来,似乎有点 React风格,这个提出确实当时社区中引发了很多讨论,说Vue越来越像React…很多人并不是很能接受,具体细节大家可以去阅读 RFC 的介绍

1.2 优点

回到本篇文章所关注的,很明显 reactive API对标 data选项,那么相比较 data选项有哪些优点?

首先,在 Vue 2x中数据的响应式处理是基于 Object.defineProperty()的,但是它只会侦听对象的属性,并不能侦听对象。所以,在添加对象属性的时候,通常需要这样:

    // vue2x添加属性
    Vue.$set(object, 'name', wjc)

reactive API是基于 ES2015 Proxy实现对数据对象的响应式处理,即在 Vue3.0可以往对象中添加属性,并且这个属性也会具有响应式的效果,例如:

    // vue3.0中添加属性
    object.name = 'wjc'

1.3 注意点

使用 reactive API需要注意的是,当你在 setup中返回的时候,需要通过对象的形式,例如:

    export default {
      setup() {
          const pos = reactive({
            x: 0,
            y: 0
          })

          return {
             pos: useMousePosition()
          }
      }
    }

或者,借助 toRefs API包裹一下导出,这种情况下我们就可以使用展开运算符或解构,例如:

    export default {
      setup() {
          let state = reactive({
            x: 0,
            y: 0
          })
        
          state = toRefs(state)
          return {
             ...state
          }
      }
    } 

toRefs() 具体做了什么,接下来会和 reactive 一起讲解

二、源码实现

首先,相信大家都有所耳闻,Vue3.0TypeScript重构了。所以,大家可能会以为这次会看到一堆 TypeScript的类型之类的。出于各种考虑,本次我只是讲解编译后,转为 JS 的源码实现(没啥子门槛,大家放心 hh)。

2.1 reactive

1.先来看看 reactive函数的实现:

function reactive(target) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (readonlyToRaw.has(target)) {
        return target;
    }
    // target is explicitly marked as readonly by user
    if (readonlyValues.has(target)) {
        return readonly(target);
    }
    if (isRef(target)) {
        return target;
    }
    return createReactiveObject(target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers);
}

可以,看到先有 3 个逻辑判断,对 readonlyreadonlyValuesisRef分别进行了判断。我们先不看这些逻辑,通常我们定义 reactive会直接传入一个对象。所以会命中最后的逻辑 createReactiveObject()

2.那我们转到 createReactiveObject()的定义:

function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
    if (!isObject(target)) {
        if ((process.env.NODE_ENV !== 'production')) {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // target already has corresponding Proxy
    let observed = toProxy.get(target);
    if (observed !== void 0) {
        return observed;
    }
    // target is already a Proxy
    if (toRaw.has(target)) {
        return target;
    }
    // only a whitelist of value types can be observed.
    if (!canObserve(target)) {
        return target;
    }
    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers;
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target);
    return observed;
}

createReactiveObject()传入了四个参数,它们分别扮演的角色:

  • target是我们定义 reactive时传入的对象
  • toProxy是一个空的 WeakSet
  • toProxy是一个空的 WeakSet
  • baseHandlers是一个已经定义好 getset的对象,它看起来会是这样:
    const baseHandlers = {
        get(target, key, receiver) {},
        set(target, key, value, receiver) {},
        deleteProxy: (target, key) {},
        has: (target, key) {},
        ownKey: (target) {}
    };
  • collectionHandlers是一个只包含 get 的对象。

然后,进入 createReactiveObject(), 同样地,一些分支逻辑我们这次不会去分析。

看源码时需要保持的一个平常心,先看主逻辑

所以,我们会命中最后的逻辑,即:

    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers;
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target);

它首先判断 collectionTypes中是否会包含我们传入的 target的构造函数,而 collectionTypes是一个 Set集合,主要包含 Set, Map, WeakMap, WeakSet等四种集合的构造函数。

如果 collectionTypes包含它的构造函数,那么将 handlers赋值为只有 getcollectionHandlers对象,否则,赋值为 baseHandlers对象。

这两者的区别就在于前者只有 get,很显然这个是留给不需要派发更新的变量定义的,例如我们熟悉的 props它就只实现了 get

然后,将 targethandlers传入 Proxy,作为参数实例化一个 Proxy对象。这也是我们看到一些文章常谈的 Vue3.0ES2015 Proxy取代了 Object.defineProperty

最后的两个逻辑,也是非常重要,toProxy()将已经定义好 Proxy对象的 target和 对应的 observed作为键值对塞进 toProxy这个 WeakMap中,用于下次如果存在相同引用的 target 需要 reactive,会命中前面的分支逻辑,返回定义之前定义好的 observed,即:

    // target already has corresponding Proxy target 是已经有相关的 Proxy 对象
    let observed = toProxy.get(target);
    if (observed !== void 0) {
        return observed;
    }

toRaw()则是和 toProxy相反的键值对存入,用于下次如果传进的 target已经是一个 Proxy对象时,返回这个 target,即:

    // target is already a Proxy target 已经是一个 Proxy 对象
    if (toRaw.has(target)) {
        return target;
    }

2.2 toRefs

前面讲了使用 reactive需要关注的点,提及 toRefs可以让我们方便地使用解构和展开运算符,其实是最近 Vue3.0 issue也有大神讲解过这方面的东西。有兴趣的同学可以移步 When it’s really needed to use toRefs in order to retain reactivity of reactive value了解。

我当时也凑了一下热闹,如下图:

可以看到,toRefs是在原有 Proxy对象的基础上,返回了一个普通的带有 getset的对象。这样就解决了 Proxy对象遇到解构和展开运算符后,失去引用的情况的问题。

结语

好了,对于 reactive API的定义和大致的源码实现就如上面文章中描的述。而分支的逻辑,大家可以自行走不同的 case去阅读。当然,需要说的是这次的源码只是尝鲜版的,不排除之后正式的会做诸多优化,但是主体肯定是保持不变的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值