你为何看不懂源码之Vue 3.0 面面俱到【2】

先唠会儿嗑

距离上一篇结束已通过去了整整一天,上一篇大部分讲读源码前的准备,以及粗略的顺了便响应式的流程,戳我看上一篇 这篇主要讲,如何让测试用例跑起来,而且辅助咱们解决看不懂的地方。vue

是骡子是马,牵出来溜溜

熟悉一个源码/工具的方法就是让它跑起来,更快速的熟悉一个源码/工具的方法就是让它的测试用例跑起来。
先到根目录安装下包 npm ireact

再运行下 reactive 的测试用例
jest packages/reactivity/__tests__/reactive.spec.tstypescript

命令行输出了让人赏心悦目的结果。npm

PASS  packages/reactivity/__tests__/reactive.spec.ts
  reactivity/reactive
    ✓ Object (6ms)
    ✓ Array (1ms)
    ✓ cloned reactive Array should point to observed values (1ms)
    ✓ nested reactives (5ms)
    ✓ observed value should proxy mutations to original (Object) (1ms)
    ✓ observed value should proxy mutations to original (Array) (1ms)
    ✓ setting a property with an unobserved value should wrap with reactive
    ✓ observing already observed value should return same Proxy (1ms)
    ✓ observing the same value multiple times should return same Proxy
    ✓ should not pollute original object with Proxies
    ✓ unwrap (1ms)
    ✓ non-observable values (1ms)
    ✓ markNonReactive (1ms)
复制代码

一应俱全

为何要从测试用例看源码呢,由于它就像咱们的产品经理,它会告诉咱们输入什么,预期什么。它会考虑边界状况,基本上源码难懂的地方都是边界状况,因此这个阶段,咱们能够跑用例来理解。api

为了支持单个测试用例运行,在 Vscode 商店中安装 Jest-Runner 插件,这个插件可让咱们更简单的运行用例和调试。如下是它的用法。bash

咱们先选一个测试用例,花几分钟,看看jest的基本用法。
这里我选择了 reactive.spec.js 用例文件。函数

import { reactive, isReactive
  , toRaw, markNonReactive 
} from '../src/reactive'
import { mockWarn } from '@vue/runtime-test'
test('Object', () => {
  const original = { foo: 1 }
  // 用 reactive 包装 original,original变成了响应式数据
  const observed = reactive(original)
  // 这句很明显了吧,observed 不等于 original
  expect(observed).not.toBe(original)
  // observed 是响应式数据
  expect(isReactive(observed)).toBe(true)
  // original 不是响应式数据
  expect(isReactive(original)).toBe(false)
  // 经过响应数据 observed 拿到的值与原数据相等
  expect(observed.foo).toBe(1)
  // foo 这个key 值,存在于 observed 中
  expect('foo' in observed).toBe(true)
  // observed 的健集合与原数据相等,toEqual 是深度比较,它会比较值,而非地址
  expect(Object.keys(observed)).toEqual(['foo'])
})
复制代码

读懂 Jest 语法就和读懂白话文的难度同样吧。你能够看到 test 的第一个参数是语义化的,基本上能经过这个参数,猜出每一个用例想干什么,咱们将 reactive.spec.js中的参数 列举出来。工具

  • Object
  • Array
  • cloned reactive Array should point to observed values
  • nested reactives
  • observed value should proxy mutations to original (Object)
  • observed value should proxy mutations to original (Array)
  • setting a property with an unobserved value should wrap with reactive
  • observing already observed value should return same Proxy
  • observing the same value multiple times should return same Proxy
  • should not pollute original object with Proxies
  • unwrap
  • non-observable values
  • markNonReactive

到此为止,接下来咱们再跳入代码中,寻找上个文章中留下的问题。post

优化!优化!仍是TMD优化!

在 reactive.ts 中的 createReactiveObject 方法里,为何要 set 两次 toProxy.set(target, observed) toRaw.set(observed, 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
  }
复制代码

很明显,这两个Set是用来优化代码用的,当 target 存在于时,返回便可。不一样的是 toProxy 的key值为 target,toRaw 的 key 值 为 observed。

大胆猜想下,假如 createReactiveObject 运行了两次,第二次的 target 刚好是 第一次包装后的 observed。

若是是以上状况,那测试用例确定存在这种状况。稍微看一眼就是这个case: observing already observed value should return same Proxy

test('observing already observed value should return same Proxy', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    const observed2 = reactive(observed)
    expect(observed2).toBe(observed)
  })
复制代码

该用例将包装好的 observed 再次做为参数传给了 reactive。 咱们把断点打上。验证猜测。
当 reactive 运行第二次,到 toRaw 判断语句的时候便返回了。


到这里,为何 reactive 内部会 set 两次的缘由已经清晰了:为了优化包装后的对象再次被传入的状况,防止屡次proxy。
以上,是经过测试用例分析的过程。咱们再看看其余用例。

使人困扰的 Ref

上一篇咱们是从 Ref 开始阅读源码的,只是大致顺了下来,知道了 Ref 对象是怎么建立的,以及它的 getset 过程。以后,咱们看到 reactive.ts,知道它是响应式的核心,而且实现了一个简单的demo,那 Ref 存在的意义是什么?

相信我,此时此刻我和你同样困惑。让咱们打开 ref.spec.ts 测试用例看看他会告诉咱们什么。

it('should hold a value', () => {
  const a = ref(1)
  expect(a.value).toBe(1)
  a.value = 2
  expect(a.value).toBe(2)
})
复制代码

第一个测试用例就给 ref 传入一个基本类型number。那就该想到,reactive 传入基本类型会怎么样?
让咱们试试!在 reactive.ts 编写对应测试用例。

Typescript 的优点体现出来了,入参只支持对象,不支持基本类型! reactive.ts 核心api 是 Proxy,Proxy 的传参只能是对象。若是传基本类型的话,会console

Cannot create proxy with a non-object as target or handler at proxyMethod
因此,ref 是为了使基本类型也能成为响应式数据存在的,让咱们回到第一个测试用例: should hold a value

const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
  // 若是是对象,则用 reactive 方法 包装 raw
  raw = convert(raw)
  // 返回一个 v 对象,在 取value 值时,调用 track 方法,在存 value 值时,调用 trigger方法
  const v = {
    [refSymbol]: true,
    get value() {
      track(v, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}
复制代码

若是 ref 入参是基本类型的话,这个函数就很容易看懂了,返回值是一个被包装过的对象。这个对象在 get 时调用 track 方法,在 set时,调用 trigger 方法走更新 view 层逻辑。所以它是经过这种方式,实现基本类型的数据绑定的。

为了对 ref 有更详细的认识,咱们须要更复杂的的用例。

我截取了一部分 toRefs 的用例,这部分代码不依赖其余模块。

test('toRefs', () => {
    const a = reactive({
      x: 1,
      y: 2
    })

    const { x, y } = toRefs(a)

    expect(isRef(x)).toBe(true)
    expect(isRef(y)).toBe(true)
    expect(x.value).toBe(1)
    expect(y.value).toBe(2)

    // source -> proxy
    a.x = 2
    a.y = 3
    expect(x.value).toBe(2)
    expect(y.value).toBe(3)

    // proxy -> source
    x.value = 3
    y.value = 4
    expect(a.x).toBe(3)
    expect(a.y).toBe(4)
}
复制代码

能够看到,这个用例是来测试 toRefs 方法的。
若是用例没有 toRefs(a),而是

const { x, y } = reactive({
    x: 1,
    y: 2
  })
复制代码

毫无疑问,xy 不是响应式的,两者都是基本类型。咱们指望它是响应式数据,因此须要转化成 Ref 对象。视线再转回 ref.ts

export function toRefs<T extends object>( object: T ): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>( object: T, key: K ): Ref<T[K]> {
  return {
    [refSymbol]: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  }
}
复制代码

奇怪,前面的 toRefs 能够看懂,遍历了 object,并用 toProxyRef 包装后从新赋值。 但 toProxyRef 内部,仅仅用 get set 包装下,没有咱们可爱的 triggertrack,这是为何的?

由于 object 自己就是响应式数据。

其实这里只须要用存取器包装成对象,让基本类型变为引用类型,当执行 expect(x.value).toBe(1) 时,会调用 object[key],因此它也会触发 object 的 get 方法。

一样的,当执行 x.value = 3 语句时,会调用 set 方法,执行 object[key] = newVal 后也会触发 object 的 set 方法。

其实 toRefs 解决的问题就是,开发者在函数中错误的解构 reactive,来返回基本类型。 const { x, y } = = reactive({ x: 1, y: 2 }),这样会使 x, y 失去响应式,因而官方提出了 toRefs 方案,在函数返回时,将 reactive 转为 refs,来避免这种状况。

到此为止

若是咱们接着探究的话,不得不涉及到其余模块,好比 computed effect......, 而这块儿又有些庞大,只能后续更新,因此 refreactive 部分探究到此为止。

未完待续

vue-next 的源码正在不断更新中,小伙伴们在看源码的过程当中,要时不时pull一下,防止源码版本滞后呦......

从单测看起的灵感来自: Vue3响应式系统源码解析(上),老规矩,先点赞。

我会持续更新,敬请关注。😁 (千万别关注我呦)

相关文章
相关标签/搜索