简单说下vue3

Vue Composition API

关于一些vue2与vue3的变化与使用区别在这份RFC(request fom comments)里面可以找到。

具体哪些变化不多说了, 网上vue3的文章有一大半是写这些的。

一段vue3的demo代码:

image.png

单从写法上来看,比2.x版本要引入的东西多了。感觉是一把梭哈与按需引用的区别。

所以对我们vue的使用者来说无非是这样:

  1. 用起来方便,但是build出来的代码size是最大的
  2. 用起来麻烦一点,但可以做到 tree shaking,减少文件大小(目前看我们项目的 vue API 使用情况,不会有啥变化,没鸟用…)

此次重点看下 reactive

从测试用例来看 reactive

test will tell you anything.

一个好的工具/类库/框架,从测试用例上应该就可以看出它有哪些功能。所以建议在阅读质量比较高的项目源码时,可以先从测试用例开始。

以下代码在 packages/reactivity/tests/reactive.spec.ts 中可以找到

这里的测试用例大概分类了一下,从这些测试用例里面,慢慢去发现它有哪些API,并且是干嘛的。

  1. 第一部分对Object与Array测试
    主要测试代理对象的与对象本身的地址引用、代理上的一些方法与属性,比较简单。Array的大概也是这些javascript test('Object', () => { const original = { foo: 1 } const observed = reactive(original) // 第一个API reactive: 返回一个可响应的对象 expect(observed).not.toBe(original) expect(isReactive(observed)).toBe(true) // 第二个API isReactive expect(isReactive(original)).toBe(false) // get expect(observed.foo).toBe(1) // has expect('foo' in observed).toBe(true) // ownKeys expect(Object.keys(observed)).toEqual(['foo']) })

  2. 复制代理对象的测试、嵌套代理是否具有响应
    测试代理对象会不会因为深度而不被代理```javascript test(‘cloned reactive Array should point to observed values’, () => { const original = [{ foo: 1 }] const observed = reactive(original) const clone = observed.slice() expect(isReactive(clone[0])).toBe(true) expect(clone[0]).not.toBe(original[0]) expect(clone[0]).toBe(observed[0]) })

    test(‘nested reactives’, () => { const original = { nested: {

    1. foo: 1

    }, array: [{ bar: 2 }] } const observed = reactive(original) expect(isReactive(observed.nested)).toBe(true) expect(isReactive(observed.array)).toBe(true) expect(isReactive(observed.array[0])).toBe(true) }) ```

  3. 动态増删对象/数组増删```javascript

    test(‘observed value should proxy mutations to original (Object)’, () => { const original: any = { foo: 1 } const observed = reactive(original) // set observed.bar = 1 expect(observed.bar).toBe(1) expect(original.bar).toBe(1) // 直接添加可以响应 // delete delete observed.foo expect(‘foo’ in observed).toBe(false) expect(‘foo’ in original).toBe(false) })

    test(‘observed value should proxy mutations to original (Array)’, () => { const original: any[] = [{ foo: 1 }, { bar: 2 }] const observed = reactive(original) // set const value = { baz: 3 } const reactiveValue = reactive(value) observed[0] = value expect(observed[0]).toBe(reactiveValue) expect(original[0]).toBe(value) // delete delete observed[0] expect(observed[0]).toBeUndefined() expect(original[0]).toBeUndefined() // mutating methods observed.push(value) expect(observed[2]).toBe(reactiveValue) expect(original[2]).toBe(value) })

test(‘setting a property with an unobserved value should wrap with reactive’, () => { const observed = reactive<{ foo?: object }>({}) const raw = {}

  1. observed.foo = raw
  2. expect(observed.foo).not.toBe(raw)
  3. expect(isReactive(observed.foo)).toBe(true)

})

  1. <br />对比2.x,不必使用 `$set` 与 `$delete`了,赞。
  2. 4. 多次代理的测试<br />主要测试是否重复代理,通过结果可以看出来,内部有一个东西在维护这层关系<br />倒腾过来倒腾过去 看看是否符合预期```javascript
  3. test('observing already observed value should return same Proxy', () => {
  4. const original = { foo: 1 }
  5. const observed = reactive(original)
  6. const observed2 = reactive(observed)
  7. expect(observed2).toBe(observed)
  8. })
  9. test('observing the same value multiple times should return same Proxy', () => {
  10. const original = { foo: 1 }
  11. const observed = reactive(original)
  12. const observed2 = reactive(original)
  13. expect(observed2).toBe(observed)
  14. })
  15. test('should not pollute original object with Proxies', () => {
  16. const original: any = { foo: 1 }
  17. const original2 = { bar: 2 }
  18. const observed = reactive(original)
  19. const observed2 = reactive(original2)
  20. observed.bar = observed2
  21. expect(observed.bar).toBe(observed2)
  22. expect(original.bar).toBe(original2)
  23. })
  1. 拆解还原```javascript test(‘unwrap’, () => { const original = { foo: 1 } const observed = reactive(original) expect(toRaw(observed)).toBe(original) // 第三个API: raw表示原始数据 expect(toRaw(original)).toBe(original) })

// Ref 不会被拆解 test(‘should not unwrap Ref‘, () => { const observedNumberRef = reactive(ref(1)) // 第四个API,ref函数 const observedObjectRef = reactive(ref({ foo: 1 }))

expect(isRef(observedNumberRef)).toBe(true) expect(isRef(observedObjectRef)).toBe(true) })

// 计算属性也不被拆解 test(‘should unwrap computed refs’, () => { // readonly const a = computed(() => 1) // 第五个API 计算属性 // writable const b = computed({ get: () => 1, set: () => {} }) const obj = reactive({ a, b }) // check type obj.a + 1 obj.b + 1 expect(typeof obj.a).toBe(number) expect(typeof obj.b).toBe(number) })

  1. <br />观察其返回值:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1654794/1593395895717-d409608f-4d09-4355-b258-d69a1e4c6cc7.png#align=left&display=inline&height=926&margin=%5Bobject%20Object%5D&name=image.png&originHeight=926&originWidth=1652&size=227647&status=done&style=none&width=1652)
  2. 6. 不能被代理的测试```javascript
  3. test('non-observable values', () => {
  4. const assertValue = (value: any) => {
  5. reactive(value)
  6. expect(
  7. `value cannot be made reactive: ${String(value)}`
  8. ).toHaveBeenWarnedLast()
  9. }
  10. // number
  11. assertValue(1)
  12. // string
  13. assertValue('foo')
  14. // boolean
  15. assertValue(false)
  16. // null
  17. assertValue(null)
  18. // undefined
  19. assertValue(undefined)
  20. // symbol
  21. const s = Symbol()
  22. assertValue(s)
  23. // built-ins should work and return same value
  24. const p = Promise.resolve()
  25. expect(reactive(p)).toBe(p) // 不可以响应 直接吐回源对象
  26. const r = new RegExp('')
  27. expect(reactive(r)).toBe(r)
  28. const d = new Date()
  29. expect(reactive(d)).toBe(d)
  30. })
  1. 这个属性不要给我代理javascript test('markNonReactive', () => { const obj = reactive({ foo: { a: 1 }, bar: markNonReactive({ b: 2 }) // 第六个API markNonReactive 会缓存这个对象 }) expect(isReactive(obj.foo)).toBe(true) expect(isReactive(obj.bar)).toBe(false) })
    vue2.x 一直觉得缺少这个方法或者标识,之前说过通过Object.freeze()达到类似的效果

关于reactive的测试用例就这么多,其实reactive文件对应的测试用例是两个文件。还有一个是readonly.spec.ts 。reactive文件中有个函数是readonly,表示代理的这个对象是只读的。

在跑测试的时候 在某个测试用例上増加.only,标示只运行这一个测试用例 e.g. ```typescript test.only(‘should not unwrap Ref‘, () => { const observedNumberRef = reactive(ref(1)) const observedObjectRef = reactive(ref({ foo: 1 }))

expect(isRef(observedNumberRef)).toBe(true) expect(isRef(observedObjectRef)).toBe(true) })

  1. > 然后运行: `./node_modules/.bin/jest packages/reactivity/__tests__/reactive.spec.ts`
  2. > 单独跑这个文件,`only`只在这个文件中生效
  3. <a name="7102f05b"></a>
  4. ### reactive实现原理
  5. 从上面的测试用例可以想到,reactive方法内部应该是有2个集合去做映射。
  6. 代理对象 -> 原始对象
  7. 原始对象 -> 代理对象
  8. > 这里使用了两个WeakMap做关联rawToReactivereactiveToRaw
  9. > 这里回忆一下WeakMap的特性
  10. ```javascript
  11. // 4个 weekmap做缓存映射
  12. const rawToReactive = new WeakMap<any, any>()
  13. const reactiveToRaw = new WeakMap<any, any>()
  14. const rawToReadonly = new WeakMap<any, any>()
  15. const readonlyToRaw = new WeakMap<any, any>()
  16. // 2个做标识的weekmap
  17. // 单独设置某个对象被代理时只能是只读的
  18. const readonlyValues = new WeakSet<any>()
  19. // 标记某个对象不被响应
  20. const nonReactiveValues = new WeakSet<any>()

reactive(target) 函数大概做的事情

  1. 判断 target 是否在 readonlyToRaw 存在, 存在说明 target 是一个只读的代理对象,直接返回 target
  2. 判断 target 是否在 readonlyValues 存在,存在的话调用 readonly(target)
  3. 调用 createReactiveObject. (reactive与readonly函数后面的逻辑一样的所以封装在了这里)
  4. 判断是否是对象,不是的话,直接返回这个值,开发模式下会打印warn信息
  5. 判断 target 是否在 rawToReactive 存在,存在说明 target 是一个已经被代理过的对象了,返回 rawToReactive.get(target)
  6. 判断 target 是否在 reactiveToRaw 存在,存在说明 target 是一个代理对象,直接返回 target
  7. 判断 target 是否可观察,否则直接返回 targettypescript const canObserve = (value: any): boolean => { return ( !value._isVue && !value._isVNode && isObservableType(toRawType(value)) && !nonReactiveValues.has(value) ) }

  8. observed = new Proxy(target, handlers). 数组与对象的handlers处理是两个文件typescript const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers

  9. 往 rawToReactive、reactiveToRaw 里面set

  10. 返回 observed.

大概分这10个步骤吧, 主要的逻辑还是在 handlers 里面处理的。

handlers 里面主要劫持了5个属性(mutableHandlers 是reactive中对象的处理方法):

  1. export const mutableHandlers: ProxyHandler<object> = {
  2. get: createGetter(false),
  3. set,
  4. deleteProperty,
  5. has,
  6. ownKeys
  7. }

思考一下这样的场景:

  1. const a = {
  2. b: {
  3. c: {
  4. d: {
  5. e: {
  6. // ... 一直嵌套
  7. }
  8. }
  9. }
  10. }
  11. }

a 里面的属性嵌套对象,一直嵌好几层(自己脑补这里a可能有个十万八千层)

这个时候如果去代理的话,是不是要自己递归下去?

假如这样的场景呢:

  1. var a = {
  2. b: {
  3. c: {
  4. a
  5. }
  6. }
  7. }

a.b.c.a 死循环了.

|
|
|
V

  1. 第一次调用 reactive() 时,只会代理一层。
  2. get的时候,此时不就触发了get对应的劫持方法嘛,关键的处理在这里```typescript // get 劫持方法 function createGetter(isReadonly: boolean) { return function get(target: object, key: string | symbol, receiver: object) { const res = Reflect.get(target, key, receiver) if (isSymbol(key) && builtInSymbols.has(key)) { return res } if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) return isObject(res) ? isReadonly
    1. ? // need to lazy access readonly and reactive here to avoid
    2. // circular dependency 关键在这里 你啥使用get的时候 我啥时候给你再去给代理
    3. readonly(res)
    4. : reactive(res)
    : res } } ```