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

引言

今年,对于从事前端开发的同窗而言,非常期待的一件事就是 Vue3.0 的发布。可是,Vue3.0 离发布仍是有点时间的,而且正式发布也不表明咱们就立刻就能够用于业务开发。它还须要完善相应的生态工具。不过正式使用是一码事,咱们本身玩又是一码事(hh)。javascript

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

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

1、定义及优势

1.1 定义

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

首先,咱们须要知道在 Vue3.0 中完全废掉了原先的 Options API,而改用 Composition API,简易版的 Composition API 看起来会是这样的:java

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

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }

能够看到,没有了咱们熟悉的datacomputedmethods等等。看起来,彷佛有点 React风格,这个提出确实当时社区中引起了不少讨论,说Vue愈来愈像React....不少人并非很能接受,具体细节你们能够去阅读 RFC 的介绍react

1.2 优势

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

首先,在 Vue 2x 中数据的响应式处理是基于 Object.defineProperty() 的,可是它只会侦听对象的属性,并不能侦听对象。因此,在添加对象属性的时候,一般须要这样:git

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

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

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

1.3 注意点

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

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 一块儿讲解

2、源码实现

首先,相信你们都有所耳闻,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 去阅读。固然,须要说的是此次的源码只是尝鲜版的,不排除以后正式的会作诸多优化,可是主体确定是保持不变的。

推荐阅读下一篇《4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)》

写做不易,若是你以为有收获的话,能够帅气三连击!!!
相关文章
相关标签/搜索